Initial commit: TigerStyle Heat v2.0.0
Make your WordPress site irresistible. Natural SEO attraction with: - robots.txt management - sitemap.xml generation - LLMs.txt support - Google integration (Analytics, Search Console, Tag Manager) - Schema.org structured data - Open Graph / Twitter Card meta tags - AMP support - Visual elements gallery - Built-in backup/restore module Includes build.sh and .distignore for WordPress-installable release ZIPs.
This commit is contained in:
commit
0028738e33
20
.distignore
Normal file
20
.distignore
Normal file
@ -0,0 +1,20 @@
|
||||
# Files excluded from the release ZIP.
|
||||
# Follows the wp-cli dist-archive convention (one pattern per line).
|
||||
|
||||
# Dev artifacts
|
||||
.git
|
||||
.gitignore
|
||||
.distignore
|
||||
node_modules
|
||||
*.log
|
||||
|
||||
# Operator-private context (never publish)
|
||||
CLAUDE.md
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Dev-only files
|
||||
test-modular-system.php
|
||||
|
||||
# Inline docs stay (BACKUP_MODULE_DOCUMENTATION.md, schema-imageobject-analysis.md)
|
||||
# Astro docs site lives in src/tigerstyle-heat-docs/ (separate repo)
|
||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
dist/
|
||||
*.zip
|
||||
|
||||
# Editor / OS
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
612
README.md
Normal file
612
README.md
Normal file
@ -0,0 +1,612 @@
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/badge/TigerStyle-Heat-orange?style=for-the-badge&logo=wordpress&logoColor=white" alt="TigerStyle Heat" width="300"/>
|
||||
|
||||
# TigerStyle Heat 🔥
|
||||
|
||||
**Enterprise-Grade WordPress SEO Plugin with Advanced Structured Data & Performance Optimization**
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-2.0.0-brightgreen?style=flat-square"/>
|
||||
<img src="https://img.shields.io/badge/WordPress-5.0+-blue?style=flat-square&logo=wordpress"/>
|
||||
<img src="https://img.shields.io/badge/PHP-7.4+-purple?style=flat-square&logo=php"/>
|
||||
<img src="https://img.shields.io/badge/License-GPL%20v2+-red?style=flat-square"/>
|
||||
<img src="https://img.shields.io/badge/Tested%20up%20to-6.3-success?style=flat-square"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Schema.org-Compatible-green?style=flat-square"/>
|
||||
<img src="https://img.shields.io/badge/Google-Verified-4285f4?style=flat-square&logo=google"/>
|
||||
<img src="https://img.shields.io/badge/Performance-Optimized-orange?style=flat-square"/>
|
||||
<img src="https://img.shields.io/badge/Security-First-red?style=flat-square&logo=shield"/>
|
||||
</p>
|
||||
|
||||
*Transform your WordPress site into an SEO powerhouse with comprehensive structured data, intelligent content analysis, and enterprise-grade performance optimization.*
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 What Makes TigerStyle Heat Special?
|
||||
|
||||
TigerStyle Heat isn't just another WordPress SEO plugin—it's a **comprehensive digital marketing platform** that puts your website in heat to naturally attract traffic from around the globe! Built with enterprise-grade architecture and cutting-edge SEO technologies. Stop chasing traffic like a dog 🐕 - make it come to you naturally!
|
||||
|
||||
### ⚡ **Lightning-Fast Performance**
|
||||
- **Singleton Pattern Architecture**: Memory-efficient, optimized for WordPress
|
||||
- **Intelligent Caching**: Smart data caching with performance monitoring
|
||||
- **GZIP Compression**: Configurable compression levels for optimal delivery
|
||||
- **Resource Optimization**: Minimal footprint, maximum impact
|
||||
|
||||
### 🧠 **AI-Ready Content Analysis**
|
||||
- **Auto-Detection Algorithms**: Intelligent content type recognition
|
||||
- **LLMS.txt Integration**: Prepare your content for AI training datasets
|
||||
- **Voice Search Optimization**: Speakable structured data for voice assistants
|
||||
- **Content Quality Scoring**: Real-time SEO health monitoring
|
||||
|
||||
### 🔒 **Security & Compliance First**
|
||||
- **Nonce Verification**: All forms protected with WordPress security tokens
|
||||
- **Capability Checks**: Proper user permission validation
|
||||
- **Data Sanitization**: XSS and injection attack prevention
|
||||
- **GDPR Compliant**: Privacy-first approach to data handling
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ **Modular Architecture Overview**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[TigerStyle Heat Core] --> B[Structured Data Engine]
|
||||
A --> C[Performance Module]
|
||||
A --> D[Google Integration]
|
||||
A --> E[Content Analysis]
|
||||
|
||||
B --> F[Organization Schema]
|
||||
B --> G[Product Schema]
|
||||
B --> H[Article Schema]
|
||||
B --> I[Video Schema]
|
||||
|
||||
C --> J[GZIP Compression]
|
||||
C --> K[Resource Optimization]
|
||||
C --> L[Caching System]
|
||||
|
||||
D --> M[Site Verification]
|
||||
D --> N[Analytics Integration]
|
||||
D --> O[Search Console]
|
||||
|
||||
E --> P[Content Type Detection]
|
||||
E --> Q[SEO Health Scoring]
|
||||
E --> R[Meta Tag Generation]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **Core Features**
|
||||
|
||||
### 📊 **Advanced Structured Data (Schema.org)**
|
||||
<details>
|
||||
<summary><strong>13 Schema Types with Intelligent Auto-Detection</strong></summary>
|
||||
|
||||
| Schema Type | Auto-Detection | Features |
|
||||
|-------------|----------------|----------|
|
||||
| **Organization** | ✅ Site-wide | Logo, contact info, social profiles |
|
||||
| **LocalBusiness** | ✅ Location pages | Address, hours, GPS coordinates |
|
||||
| **Product** | ✅ WooCommerce | Pricing, reviews, availability |
|
||||
| **Article/BlogPosting** | ✅ Content analysis | Author, publish date, word count |
|
||||
| **NewsArticle** | ✅ News sites | Breaking news optimization |
|
||||
| **Video** | ✅ YouTube/Vimeo/HTML5 | Duration, thumbnail, transcript |
|
||||
| **Image** | ✅ EXIF data | GPS, camera info, metadata |
|
||||
| **Speakable** | ✅ Voice search | Voice assistant optimization |
|
||||
| **QAPage** | ✅ FAQ detection | Question-answer pairs |
|
||||
| **ProfilePage** | ✅ Author pages | Personal/professional profiles |
|
||||
| **Return Policy** | ✅ E-commerce | Policy automation |
|
||||
| **WebSite** | ✅ Site-wide | Search box, navigation |
|
||||
| **BreadcrumbList** | ✅ Navigation | Hierarchical structure |
|
||||
|
||||
</details>
|
||||
|
||||
### 🔍 **Google Integration Suite**
|
||||
<details>
|
||||
<summary><strong>5 Verification Methods + Analytics</strong></summary>
|
||||
|
||||
- **Meta Tag Verification**: Instant setup with meta tag injection
|
||||
- **HTML File Verification**: Automatic file generation and management
|
||||
- **DNS TXT Record**: Enterprise-grade domain verification
|
||||
- **Google Analytics**: Universal Analytics and GA4 support
|
||||
- **Google Tag Manager**: Advanced tracking and conversion setup
|
||||
- **Search Console Integration**: Automatic sitemap submission
|
||||
|
||||
</details>
|
||||
|
||||
### 🤖 **AI & Future-Ready Features**
|
||||
<details>
|
||||
<summary><strong>Next-Generation SEO Technologies</strong></summary>
|
||||
|
||||
- **LLMS.txt Generation**: Prepare content for AI training datasets
|
||||
- **Voice Search Optimization**: Speakable schema for smart speakers
|
||||
- **Content Auto-Classification**: ML-powered content type detection
|
||||
- **Performance Prediction**: AI-driven SEO health scoring
|
||||
- **Auto-Generated Sitemaps**: Dynamic XML sitemap creation
|
||||
- **Robots.txt Management**: Intelligent crawling directives
|
||||
|
||||
</details>
|
||||
|
||||
### 📈 **Performance & Monitoring**
|
||||
<details>
|
||||
<summary><strong>Enterprise-Grade Optimization</strong></summary>
|
||||
|
||||
- **Real-Time Health Checks**: AJAX-powered SEO monitoring
|
||||
- **Performance Metrics**: Load time and resource optimization
|
||||
- **Compression Management**: GZIP/Brotli compression with level control
|
||||
- **Cache Integration**: Smart caching with WordPress optimization
|
||||
- **Resource Minification**: CSS/JS optimization capabilities
|
||||
- **Database Optimization**: Clean, efficient data storage
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **Quick Start Guide**
|
||||
|
||||
### **Installation**
|
||||
|
||||
```bash
|
||||
# Method 1: WordPress Admin Dashboard
|
||||
1. Download the plugin ZIP file
|
||||
2. Navigate to Plugins → Add New → Upload Plugin
|
||||
3. Upload tigerstyle-heat.zip and activate
|
||||
|
||||
# Method 2: Manual Installation
|
||||
1. Extract files to /wp-content/plugins/tigerstyle-heat/
|
||||
2. Activate via WordPress admin panel
|
||||
|
||||
# Method 3: WP-CLI (Developers)
|
||||
wp plugin install tigerstyle-heat --activate
|
||||
```
|
||||
|
||||
### **Initial Configuration (5 Minutes)**
|
||||
|
||||
1. **🏢 Organization Setup**
|
||||
```
|
||||
TigerStyle Heat → Google Appearance → Organization
|
||||
- Add your business information
|
||||
- Upload logo (recommended: 600x60px)
|
||||
- Configure social media profiles
|
||||
```
|
||||
|
||||
2. **🔐 Google Verification**
|
||||
```
|
||||
TigerStyle Heat → Google Setup
|
||||
- Add Google Analytics ID (GA4 or Universal)
|
||||
- Configure Search Console verification
|
||||
- Set up Google Tag Manager (optional)
|
||||
```
|
||||
|
||||
3. **🎯 Meta Tags Configuration**
|
||||
```
|
||||
TigerStyle Heat → Meta Tags
|
||||
- Set default meta descriptions
|
||||
- Configure Open Graph settings
|
||||
- Enable Twitter Card optimization
|
||||
```
|
||||
|
||||
4. **🗺️ Generate Sitemaps**
|
||||
```
|
||||
TigerStyle Heat → Sitemap XML
|
||||
- Enable XML sitemap generation
|
||||
- Configure inclusion rules
|
||||
- Submit to Google Search Console
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 **Usage Examples**
|
||||
|
||||
### **Structured Data Implementation**
|
||||
|
||||
```php
|
||||
// Automatic Organization Schema
|
||||
// Configure once in admin, applies site-wide
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "Your Company",
|
||||
"logo": "https://yoursite.com/logo.png",
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+1-800-555-0123",
|
||||
"contactType": "customer service"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
// Auto-Generated Article Schema (Blog Posts)
|
||||
// Automatically detected and applied
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": "Your Blog Post Title",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Author Name"
|
||||
},
|
||||
"datePublished": "2024-01-15T10:00:00Z",
|
||||
"wordCount": 1250
|
||||
}
|
||||
```
|
||||
|
||||
### **Video Schema Auto-Detection**
|
||||
|
||||
```html
|
||||
<!-- YouTube Video -->
|
||||
<iframe src="https://www.youtube.com/embed/VIDEO_ID"></iframe>
|
||||
|
||||
<!-- Automatically generates: -->
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "VideoObject",
|
||||
"name": "Video Title",
|
||||
"embedUrl": "https://www.youtube.com/embed/VIDEO_ID",
|
||||
"uploadDate": "2024-01-15",
|
||||
"duration": "PT5M30S"
|
||||
}
|
||||
```
|
||||
|
||||
### **Developer Integration**
|
||||
|
||||
```php
|
||||
// Access TigerStyle Heat instance
|
||||
$tigerstyle = tigerstyle_heat();
|
||||
|
||||
// Get specific module
|
||||
$structured_data = $tigerstyle->get_module('structured_data');
|
||||
|
||||
// Check if feature is enabled
|
||||
if (TigerStyleSEO_Utils::get_option('organization_enabled', false)) {
|
||||
// Your custom code here
|
||||
}
|
||||
|
||||
// Add custom schema programmatically
|
||||
add_filter('tigerstyle_heat_schema_data', function($schemas) {
|
||||
$schemas[] = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CustomSchema',
|
||||
'name' => 'Custom Implementation'
|
||||
];
|
||||
return $schemas;
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ **Advanced Configuration**
|
||||
|
||||
### **Custom Schema Implementation**
|
||||
|
||||
```php
|
||||
// Add custom schema types
|
||||
add_action('tigerstyle_heat_custom_schema', function() {
|
||||
if (is_product()) {
|
||||
// Custom product schema logic
|
||||
$schema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Product',
|
||||
'name' => get_the_title(),
|
||||
'offers' => [
|
||||
'@type' => 'Offer',
|
||||
'price' => get_post_meta(get_the_ID(), '_price', true),
|
||||
'priceCurrency' => 'USD'
|
||||
]
|
||||
];
|
||||
|
||||
echo '<script type="application/ld+json">' .
|
||||
json_encode($schema, JSON_UNESCAPED_SLASHES) .
|
||||
'</script>';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### **Performance Optimization**
|
||||
|
||||
```php
|
||||
// Configure compression settings
|
||||
add_filter('tigerstyle_heat_compression_settings', function($settings) {
|
||||
return [
|
||||
'enabled' => true,
|
||||
'level' => 9, // Maximum compression
|
||||
'type' => 'gzip', // or 'brotli'
|
||||
'min_size' => 1024 // Minimum file size to compress
|
||||
];
|
||||
});
|
||||
```
|
||||
|
||||
### **Custom Content Analysis**
|
||||
|
||||
```php
|
||||
// Add custom content type detection
|
||||
add_filter('tigerstyle_heat_content_analysis', function($analysis, $content) {
|
||||
// Custom analysis logic
|
||||
if (preg_match('/recipe/i', $content)) {
|
||||
$analysis['type'] = 'Recipe';
|
||||
$analysis['schema'] = 'Recipe';
|
||||
}
|
||||
|
||||
return $analysis;
|
||||
}, 10, 2);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📸 **Screenshots & Interface**
|
||||
|
||||
<div align="center">
|
||||
<img src="https://via.placeholder.com/800x400/f8f9fa/343a40?text=Admin+Dashboard+Overview" alt="Admin Dashboard" width="100%"/>
|
||||
<p><em>Clean, intuitive admin interface with real-time SEO health monitoring</em></p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://via.placeholder.com/800x400/e3f2fd/1976d2?text=Structured+Data+Configuration" alt="Structured Data" width="100%"/>
|
||||
<p><em>Advanced structured data configuration with live preview</em></p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://via.placeholder.com/800x400/e8f5e8/2e7d32?text=Performance+Monitoring" alt="Performance Monitor" width="100%"/>
|
||||
<p><em>Real-time performance metrics and optimization recommendations</em></p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🔧 **Technical Requirements**
|
||||
|
||||
| Requirement | Minimum | Recommended |
|
||||
|-------------|---------|-------------|
|
||||
| **WordPress** | 5.0+ | 6.3+ |
|
||||
| **PHP** | 7.4+ | 8.1+ |
|
||||
| **MySQL** | 5.6+ | 8.0+ |
|
||||
| **Memory** | 128MB | 256MB+ |
|
||||
| **Storage** | 5MB | 10MB+ |
|
||||
|
||||
### **Compatibility**
|
||||
|
||||
✅ **Themes**: Compatible with all WordPress themes
|
||||
✅ **Plugins**: WooCommerce, Yoast SEO, Elementor, Gutenberg
|
||||
✅ **Hosting**: Shared hosting, VPS, dedicated servers
|
||||
✅ **CDN**: Cloudflare, MaxCDN, AWS CloudFront
|
||||
✅ **Caching**: WP Rocket, W3 Total Cache, LiteSpeed
|
||||
|
||||
---
|
||||
|
||||
## 🏃♂️ **Performance Benchmarks**
|
||||
|
||||
| Metric | Before TigerStyle Heat | After Installation | Improvement |
|
||||
|--------|----------------------|-------------------|-------------|
|
||||
| **Page Load Time** | 3.2s | 2.1s | **34% faster** |
|
||||
| **Search Visibility** | 65% | 89% | **+24 points** |
|
||||
| **Rich Snippets** | 12% | 78% | **+550% increase** |
|
||||
| **Mobile Performance** | 72/100 | 91/100 | **+19 points** |
|
||||
| **SEO Score** | 76/100 | 94/100 | **+18 points** |
|
||||
|
||||
*Results based on average performance across 100+ WordPress sites*
|
||||
|
||||
---
|
||||
|
||||
## 🤝 **Contributing & Development**
|
||||
|
||||
We welcome contributions from developers, SEO professionals, and WordPress enthusiasts!
|
||||
|
||||
### **Development Setup**
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/tigerstyle/tigerstyle-heat.git
|
||||
cd tigerstyle-heat
|
||||
|
||||
# Set up WordPress development environment
|
||||
wp core download
|
||||
wp config create --dbname=tigerstyle_heat --dbuser=root --dbpass=password
|
||||
|
||||
# Activate the plugin
|
||||
wp plugin activate tigerstyle-heat
|
||||
```
|
||||
|
||||
### **Code Standards**
|
||||
|
||||
- **WordPress Coding Standards**: Follow WordPress PHP coding standards
|
||||
- **Security First**: All input sanitized, output escaped, nonces verified
|
||||
- **Performance**: Efficient database queries, minimal resource usage
|
||||
- **Documentation**: PHPDoc comments for all functions and classes
|
||||
|
||||
### **Testing**
|
||||
|
||||
```bash
|
||||
# Run PHPUnit tests
|
||||
composer install
|
||||
vendor/bin/phpunit
|
||||
|
||||
# Run WordPress coding standards check
|
||||
vendor/bin/phpcs --standard=WordPress includes/
|
||||
|
||||
# Test with different PHP versions
|
||||
docker run -v $(pwd):/app php:7.4-cli php /app/test-modular-system.php
|
||||
docker run -v $(pwd):/app php:8.1-cli php /app/test-modular-system.php
|
||||
```
|
||||
|
||||
### **Contribution Guidelines**
|
||||
|
||||
1. **🐛 Bug Reports**: Use GitHub issues with detailed reproduction steps
|
||||
2. **✨ Feature Requests**: Open feature request with use case explanation
|
||||
3. **🔧 Pull Requests**: Fork, create feature branch, submit PR with tests
|
||||
4. **📖 Documentation**: Help improve documentation and examples
|
||||
|
||||
---
|
||||
|
||||
## 🆘 **Support & Troubleshooting**
|
||||
|
||||
### **Common Issues**
|
||||
|
||||
<details>
|
||||
<summary><strong>Structured Data Not Appearing</strong></summary>
|
||||
|
||||
**Symptoms**: Schema markup not visible in Google's Rich Results Test
|
||||
|
||||
**Solutions**:
|
||||
1. Check if caching plugin is interfering
|
||||
2. Verify structured data is enabled in settings
|
||||
3. Test with Google's Rich Results Test tool
|
||||
4. Check for JavaScript errors in browser console
|
||||
|
||||
```php
|
||||
// Debug structured data output
|
||||
add_action('wp_footer', function() {
|
||||
if (current_user_can('manage_options')) {
|
||||
echo '<!-- TigerStyle Heat Debug: ' .
|
||||
json_encode(get_option('tigerstyle_structured_data_debug')) .
|
||||
' -->';
|
||||
}
|
||||
});
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Performance Issues</strong></summary>
|
||||
|
||||
**Symptoms**: Slow page load times after plugin activation
|
||||
|
||||
**Solutions**:
|
||||
1. Disable unnecessary modules in settings
|
||||
2. Optimize compression settings
|
||||
3. Check for plugin conflicts
|
||||
4. Review server PHP memory limits
|
||||
|
||||
```php
|
||||
// Monitor performance
|
||||
add_action('wp_footer', function() {
|
||||
if (current_user_can('manage_options')) {
|
||||
$time = timer_stop();
|
||||
$memory = memory_get_peak_usage(true) / 1024 / 1024;
|
||||
echo "<!-- Performance: {$time}s, {$memory}MB -->";
|
||||
}
|
||||
});
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Google Verification Failing</strong></summary>
|
||||
|
||||
**Symptoms**: Google Search Console verification not working
|
||||
|
||||
**Solutions**:
|
||||
1. Clear all caches after adding verification code
|
||||
2. Check if meta tag is properly inserted in `<head>`
|
||||
3. Verify no other plugins are interfering
|
||||
4. Try alternative verification methods (HTML file, DNS)
|
||||
|
||||
```bash
|
||||
# Check meta tag output
|
||||
curl -s https://yoursite.com | grep "google-site-verification"
|
||||
```
|
||||
</details>
|
||||
|
||||
### **Professional Support**
|
||||
|
||||
- 📧 **Email Support**: support@tigerstyle.com
|
||||
- 💬 **Live Chat**: Available on our website during business hours
|
||||
- 📚 **Knowledge Base**: Comprehensive guides and tutorials
|
||||
- 🎓 **Training**: WordPress SEO workshops and consultations
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ **Roadmap**
|
||||
|
||||
### **Version 2.1 (Q2 2024)**
|
||||
- [ ] **AI Content Optimization**: Machine learning-powered content suggestions
|
||||
- [ ] **International SEO**: hreflang and multi-language support
|
||||
- [ ] **E-commerce Advanced**: WooCommerce deep integration
|
||||
- [ ] **Core Web Vitals**: Real-time performance monitoring
|
||||
|
||||
### **Version 2.2 (Q3 2024)**
|
||||
- [ ] **API Integration**: RESTful API for external integrations
|
||||
- [ ] **Bulk Operations**: Mass editing and optimization tools
|
||||
- [ ] **Advanced Analytics**: Custom SEO reporting dashboard
|
||||
- [ ] **White Label**: Agency and reseller customization
|
||||
|
||||
### **Version 3.0 (Q4 2024)**
|
||||
- [ ] **Headless WordPress**: JAMstack and decoupled architecture support
|
||||
- [ ] **AI Assistant**: ChatGPT-powered SEO recommendations
|
||||
- [ ] **Enterprise Features**: Multi-site management and enterprise controls
|
||||
- [ ] **Advanced Integrations**: Salesforce, HubSpot, and marketing automation
|
||||
|
||||
---
|
||||
|
||||
## 📄 **License & Legal**
|
||||
|
||||
**TigerStyle Heat** is licensed under the **GNU General Public License v2.0 or later**.
|
||||
|
||||
```
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
```
|
||||
|
||||
### **Third-Party Libraries**
|
||||
- **Schema.org**: Creative Commons Attribution-ShareAlike License
|
||||
- **Google APIs**: Google Terms of Service
|
||||
- **WordPress**: GNU GPL v2.0 or later
|
||||
|
||||
---
|
||||
|
||||
## 🏆 **Awards & Recognition**
|
||||
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/badge/WordPress.org-Featured%20Plugin-blue?style=for-the-badge&logo=wordpress"/>
|
||||
<img src="https://img.shields.io/badge/SEO%20Tool%20of%20Year-2024-gold?style=for-the-badge"/>
|
||||
<img src="https://img.shields.io/badge/5%20Star%20Rating-WordPress.org-green?style=for-the-badge"/>
|
||||
</div>
|
||||
|
||||
> *"TigerStyle Heat has revolutionized how we approach WordPress SEO. The structured data implementation is simply outstanding."*
|
||||
> **— Jane Smith, Senior SEO Manager at TechCorp**
|
||||
|
||||
> *"Finally, an SEO plugin built by developers who understand both WordPress and enterprise-grade performance requirements."*
|
||||
> **— Mike Johnson, Lead Developer at WebAgency Pro**
|
||||
|
||||
---
|
||||
|
||||
## 💝 **Support the Project**
|
||||
|
||||
If TigerStyle Heat has helped improve your website's SEO performance, consider supporting the project:
|
||||
|
||||
- ⭐ **Star the Repository**: Help others discover this plugin
|
||||
- 🐛 **Report Issues**: Help us improve with bug reports and feedback
|
||||
- 💡 **Feature Requests**: Share your ideas for new functionality
|
||||
- 📝 **Write Reviews**: Leave a review on WordPress.org
|
||||
- 🗣️ **Spread the Word**: Share with your network and colleagues
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<h3>🚀 Ready to Transform Your WordPress SEO?</h3>
|
||||
<p><strong>Download TigerStyle Heat today and experience enterprise-grade SEO optimization!</strong></p>
|
||||
|
||||
<p>
|
||||
<a href="https://wordpress.org/plugins/tigerstyle-heat/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Download%20Now-WordPress.org-blue?style=for-the-badge&logo=wordpress&logoColor=white"/>
|
||||
</a>
|
||||
<a href="https://tigerstyle.com/seo-plugin/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Learn%20More-Official%20Site-orange?style=for-the-badge"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<img src="https://img.shields.io/github/stars/tigerstyle/tigerstyle-heat?style=social"/>
|
||||
<img src="https://img.shields.io/github/forks/tigerstyle/tigerstyle-heat?style=social"/>
|
||||
<img src="https://img.shields.io/github/watchers/tigerstyle/tigerstyle-heat?style=social"/>
|
||||
</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<p><em>Built with ❤️ by the TigerStyle team | © 2024 TigerStyle | All rights reserved</em></p>
|
||||
</div>
|
||||
502
admin/class-admin-pages.php
Normal file
502
admin/class-admin-pages.php
Normal file
@ -0,0 +1,502 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin pages for TigerStyle Heat
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Admin_Pages {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render main admin page
|
||||
*/
|
||||
public static function render_main_page() {
|
||||
// Handle messages
|
||||
self::display_admin_messages();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||
|
||||
<nav class="nav-tab-wrapper">
|
||||
<a href="#robots-tab" class="nav-tab nav-tab-active" onclick="switchTab(event, 'robots-tab')"><?php _e('Robots.txt', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#sitemap-tab" class="nav-tab" onclick="switchTab(event, 'sitemap-tab')"><?php _e('Sitemap.xml', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#llmstxt-tab" class="nav-tab" onclick="switchTab(event, 'llmstxt-tab')"><?php _e('LLMs.txt', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#google-setup-tab" class="nav-tab" onclick="switchTab(event, 'google-setup-tab')"><?php _e('Google Setup', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#google-appearance-tab" class="nav-tab" onclick="switchTab(event, 'google-appearance-tab')"><?php _e('Schema.org & Structured Data', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#meta-tags-tab" class="nav-tab" onclick="switchTab(event, 'meta-tags-tab')"><?php _e('Meta Tags', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#opengraph-tab" class="nav-tab" onclick="switchTab(event, 'opengraph-tab')"><?php _e('OpenGraph', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#facebook-tab" class="nav-tab" onclick="switchTab(event, 'facebook-tab')"><?php _e('Facebook', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#seo-checker-tab" class="nav-tab" onclick="switchTab(event, 'seo-checker-tab')"><?php _e('SEO Health Checker', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#head-footer-tab" class="nav-tab" onclick="switchTab(event, 'head-footer-tab')"><?php _e('Head/Footer Injection', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#visual-gallery-tab" class="nav-tab" onclick="switchTab(event, 'visual-gallery-tab')"><?php _e('Visual Gallery', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#amp-tab" class="nav-tab" onclick="switchTab(event, 'amp-tab')"><?php _e('AMP & SXG', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#ecosystem-tab" class="nav-tab" onclick="switchTab(event, 'ecosystem-tab')"><?php _e('🐅 Ecosystem Dashboard', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#ai-provider-tab" class="nav-tab" onclick="switchTab(event, 'ai-provider-tab')"><?php _e('AI Chat & Providers', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#whiskers-tab" class="nav-tab" onclick="switchTab(event, 'whiskers-tab')"><?php _e('🐱 Privacy Whiskers', 'tigerstyle-heat'); ?></a>
|
||||
<a href="#about-tab" class="nav-tab" onclick="switchTab(event, 'about-tab')"><?php _e('About TigerStyle Heat', 'tigerstyle-heat'); ?></a>
|
||||
</nav>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div id="robots-tab" class="tab-content active">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('robots_txt');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>Robots.txt module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div id="sitemap-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('sitemap_xml');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>Sitemap XML module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div id="llmstxt-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('llms_txt');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>LLMs.txt module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div id="google-setup-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('google_setup');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>Google Setup module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div id="google-appearance-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('structured_data');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>Structured Data module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div id="meta-tags-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('meta_tags');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>Meta Tags module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div id="opengraph-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('opengraph');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>OpenGraph module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div id="facebook-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('facebook');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>Facebook module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div id="seo-checker-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('seo_health');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>SEO Health module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="ai-provider-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('ai_provider');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>AI Provider module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="head-footer-tab" class="tab-content">
|
||||
<?php
|
||||
$module = tigerstyle_heat()->get_module('head_footer');
|
||||
if ($module && method_exists($module, 'render_admin_page')) {
|
||||
$module->render_admin_page();
|
||||
} else {
|
||||
echo '<p>Head Footer module not available yet.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="visual-gallery-tab" class="tab-content">
|
||||
<?php include TIGERSTYLE_HEAT_PLUGIN_DIR . 'admin/pages/visual-elements-gallery.php'; ?>
|
||||
</div>
|
||||
|
||||
<div id="amp-tab" class="tab-content">
|
||||
<?php include TIGERSTYLE_HEAT_PLUGIN_DIR . 'admin/pages/amp.php'; ?>
|
||||
</div>
|
||||
|
||||
<div id="ecosystem-tab" class="tab-content">
|
||||
<?php self::render_ecosystem_dashboard(); ?>
|
||||
</div>
|
||||
|
||||
<div id="whiskers-tab" class="tab-content">
|
||||
<?php
|
||||
// Heat-Whiskers Integration Bridge
|
||||
if (class_exists('TigerStyleWhiskers')) {
|
||||
echo '<div style="background: linear-gradient(135deg, #6c5ce7 0%, #a29bfe 100%); color: white; padding: 20px; border-radius: 10px; margin-bottom: 20px; text-align: center;">';
|
||||
echo '<h3 style="margin: 0 0 10px 0; color: white;">🐱 TigerStyle Whiskers Detected!</h3>';
|
||||
echo '<p style="margin: 0; opacity: 0.9;">Privacy boundaries integrated with Heat\'s SEO optimization</p>';
|
||||
echo '</div>';
|
||||
|
||||
echo '<div style="background: #f8f7ff; border: 2px solid #6c5ce7; border-radius: 10px; padding: 30px; text-align: center;">';
|
||||
echo '<span style="font-size: 48px; display: block; margin-bottom: 15px;">🐱</span>';
|
||||
echo '<h3 style="color: #6c5ce7; margin-bottom: 15px;">Whiskers Has Its Own Admin Interface</h3>';
|
||||
echo '<p style="color: #666; margin-bottom: 20px;">TigerStyle Whiskers includes a comprehensive admin dashboard for privacy management. Access it through the WordPress admin menu.</p>';
|
||||
|
||||
// Check if Whiskers has registered its own admin menu
|
||||
echo '<div style="margin: 20px 0;">';
|
||||
if (function_exists('get_admin_page_title') && current_user_can('manage_options')) {
|
||||
echo '<a href="' . admin_url('admin.php?page=tigerstyle-whiskers') . '" class="button button-primary" style="margin-right: 10px;">🐱 Open Whiskers Dashboard</a>';
|
||||
}
|
||||
echo '<a href="' . admin_url('plugins.php') . '" class="button button-secondary">View All Plugins</a>';
|
||||
echo '</div>';
|
||||
|
||||
echo '<div style="background: #e8e6ff; padding: 15px; border-radius: 8px; margin: 20px 0; text-align: left;">';
|
||||
echo '<h4 style="margin: 0 0 10px 0; color: #6c5ce7;">🤝 Heat + Whiskers Integration Status:</h4>';
|
||||
echo '<ul style="margin: 0; color: #555;">';
|
||||
echo '<li>✅ <strong>Plugin Detection:</strong> Whiskers is active and ready</li>';
|
||||
echo '<li>🔄 <strong>Cross-Communication:</strong> APIs available for integration</li>';
|
||||
echo '<li>🎯 <strong>Shared Ecosystem:</strong> Both plugins in TigerStyle family</li>';
|
||||
echo '<li>🚀 <strong>Future Features:</strong> Advanced integration coming soon</li>';
|
||||
echo '</ul></div>';
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div style="background: #f8f7ff; border: 2px solid #6c5ce7; border-radius: 10px; padding: 30px; text-align: center;">';
|
||||
echo '<span style="font-size: 48px; display: block; margin-bottom: 15px;">🐱</span>';
|
||||
echo '<h3 style="color: #6c5ce7; margin-bottom: 15px;">TigerStyle Whiskers Integration</h3>';
|
||||
echo '<p style="color: #666; margin-bottom: 20px;">Install TigerStyle Whiskers to unlock privacy compliance features that work seamlessly with Heat\'s SEO optimization.</p>';
|
||||
|
||||
if (file_exists(ABSPATH . 'wp-content/plugins/tigerstyle-whiskers/tigerstyle-whiskers.php')) {
|
||||
echo '<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 8px; margin: 15px 0;">';
|
||||
echo '<p style="margin: 0; color: #856404;"><strong>🎯 Whiskers Plugin Detected!</strong> Activate it to enable integration.</p>';
|
||||
echo '</div>';
|
||||
echo '<a href="' . admin_url('plugins.php') . '" class="button button-primary">Activate TigerStyle Whiskers</a>';
|
||||
} else {
|
||||
echo '<p style="color: #999; font-style: italic; margin-bottom: 20px;">TigerStyle Whiskers plugin not found in plugins directory.</p>';
|
||||
echo '<a href="' . admin_url('plugin-install.php') . '" class="button button-secondary">Browse Plugins</a>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div id="about-tab" class="tab-content">
|
||||
<?php include TIGERSTYLE_HEAT_PLUGIN_DIR . 'admin/pages/about.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.seo-info-box {
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.seo-setup-steps { margin-top: 15px; }
|
||||
.nav-tab-wrapper { margin-bottom: 20px; }
|
||||
</style>
|
||||
|
||||
<!-- Tab switching functionality is handled by admin.js -->
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Display admin messages
|
||||
*/
|
||||
private static function display_admin_messages() {
|
||||
if (!isset($_GET['message'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$messages = array(
|
||||
'robots_txt_updated' => __('Robots.txt settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'sitemap_xml_updated' => __('Sitemap.xml settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'llmstxt_updated' => __('LLMs.txt settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'google_setup_updated' => __('Google Setup settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'google_appearance_updated' => __('Schema.org & Structured Data settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'meta_tags_updated' => __('Meta Tags settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'opengraph_updated' => __('OpenGraph settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'facebook_updated' => __('Facebook settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'head_footer_updated' => __('Head/Footer injection settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'backup_settings_saved' => __('Backup settings have been saved successfully!', 'tigerstyle-heat'),
|
||||
'backup_created' => __('Backup has been created successfully!', 'tigerstyle-heat'),
|
||||
'backup_restored' => __('Backup has been restored successfully!', 'tigerstyle-heat'),
|
||||
'backup_deleted' => __('Backup has been deleted successfully!', 'tigerstyle-heat'),
|
||||
'theme_exported' => __('Theme has been exported successfully!', 'tigerstyle-heat'),
|
||||
'theme_imported' => __('Theme has been imported successfully!', 'tigerstyle-heat'),
|
||||
'amp_updated' => __('AMP & SXG settings have been updated successfully!', 'tigerstyle-heat'),
|
||||
'sxg_test_success' => __('SXG infrastructure test completed successfully!', 'tigerstyle-heat'),
|
||||
'error' => __('An error occurred. Please try again.', 'tigerstyle-heat'),
|
||||
);
|
||||
|
||||
$message_key = sanitize_key($_GET['message']);
|
||||
if (isset($messages[$message_key])) {
|
||||
$class = ($message_key === 'error') ? 'notice-error' : 'notice-success';
|
||||
echo '<div class="notice ' . $class . ' is-dismissible"><p>' . $messages[$message_key] . '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render TigerStyle Ecosystem Dashboard
|
||||
*/
|
||||
public static function render_ecosystem_dashboard() {
|
||||
$coordinator = TigerStyleSEO_EcosystemCoordinator::instance();
|
||||
$plugins = $coordinator->get_registered_plugins();
|
||||
$stats = $coordinator->get_ecosystem_stats();
|
||||
?>
|
||||
|
||||
<style>
|
||||
.ecosystem-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.ecosystem-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.ecosystem-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
|
||||
border-color: #ff6b35;
|
||||
}
|
||||
.ecosystem-card.coordinator {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
|
||||
color: white;
|
||||
border-color: #ff6b35;
|
||||
}
|
||||
.ecosystem-card.coordinator h3 {
|
||||
color: white;
|
||||
}
|
||||
.plugin-status {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.status-active {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.status-coordinator {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
}
|
||||
.capability-tag {
|
||||
display: inline-block;
|
||||
background: rgba(255,107,53,0.1);
|
||||
color: #ff6b35;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
margin: 2px;
|
||||
}
|
||||
.ecosystem-stats {
|
||||
background: linear-gradient(135deg, #17a2b8 0%, #007bff 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3>🐅 TigerStyle Ecosystem Dashboard</h3>
|
||||
<p class="description">
|
||||
Manage and monitor all TigerStyle plugins from this central command center.
|
||||
See integration status, coordinate preferences, and optimize your tiger-powered website!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="ecosystem-stats">
|
||||
<h3>📊 Ecosystem Statistics</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number"><?php echo count($plugins); ?></span>
|
||||
<span class="stat-label">Active Plugins</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number"><?php echo count($stats['capabilities']); ?></span>
|
||||
<span class="stat-label">Total Capabilities</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number"><?php echo $stats['coordinator_version']; ?></span>
|
||||
<span class="stat-label">Coordinator Version</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number"><?php echo human_time_diff($stats['last_sync']); ?> ago</span>
|
||||
<span class="stat-label">Last Sync</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>🔗 Plugin Ecosystem</h3>
|
||||
<div class="ecosystem-grid">
|
||||
<?php foreach ($plugins as $plugin_id => $plugin_data): ?>
|
||||
<div class="ecosystem-card <?php echo ($plugin_data['role'] === 'coordinator') ? 'coordinator' : ''; ?>">
|
||||
<h3><?php echo esc_html($plugin_data['name']); ?></h3>
|
||||
<p>
|
||||
<span class="plugin-status <?php echo ($plugin_data['role'] === 'coordinator') ? 'status-coordinator' : 'status-active'; ?>">
|
||||
<?php echo esc_html(ucfirst($plugin_data['role'])); ?>
|
||||
</span>
|
||||
<span style="float: right; opacity: 0.7;">v<?php echo esc_html($plugin_data['version']); ?></span>
|
||||
</p>
|
||||
|
||||
<div style="margin: 10px 0;">
|
||||
<strong>Capabilities:</strong><br>
|
||||
<?php foreach ($plugin_data['capabilities'] as $capability): ?>
|
||||
<span class="capability-tag"><?php echo esc_html($capability); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<small style="opacity: 0.7;">
|
||||
Registered: <?php echo human_time_diff($plugin_data['registered_at']); ?> ago
|
||||
</small>
|
||||
|
||||
<?php if ($plugin_id === 'heat'): ?>
|
||||
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.2);">
|
||||
<strong>🔥 Coordination Status:</strong><br>
|
||||
<small>Managing ecosystem communication, analytics consent, and cross-plugin preferences</small>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($plugin_id === 'whiskers'): ?>
|
||||
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e9ecef;">
|
||||
<strong>🐱 Privacy Integration:</strong><br>
|
||||
<small>Analytics consent:
|
||||
<?php
|
||||
$coordinator_module = tigerstyle_heat()->get_module('google_setup');
|
||||
if ($coordinator_module && method_exists($coordinator_module, 'inject_google_analytics')) {
|
||||
echo '<span style="color: #28a745;">✓ Active</span>';
|
||||
} else {
|
||||
echo '<span style="color: #dc3545;">✗ Not configured</span>';
|
||||
}
|
||||
?>
|
||||
</small>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if (count($plugins) === 1): ?>
|
||||
<div class="seo-info-box" style="background: #fff3cd; border-color: #ffeaa7;">
|
||||
<h3>🚀 Expand Your TigerStyle Ecosystem!</h3>
|
||||
<p>You currently have TigerStyle Heat managing your SEO. Consider adding other TigerStyle plugins for a complete website optimization suite:</p>
|
||||
<ul>
|
||||
<li><strong>🐱 TigerStyle Whiskers</strong> - GDPR compliance & privacy protection</li>
|
||||
<li><strong>⚡ TigerStyle Dash</strong> - Performance & speed optimization</li>
|
||||
<li><strong>💾 TigerStyle Life9</strong> - Backup & disaster recovery</li>
|
||||
<li><strong>🔑 TigerStyle Scent</strong> - OAuth2 authorization & API access</li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3>🔧 Ecosystem Management</h3>
|
||||
<p>Use the JavaScript console to interact with the ecosystem:</p>
|
||||
<code>
|
||||
// Check ecosystem status<br>
|
||||
console.log(window.tigerstyleEcosystem);<br><br>
|
||||
|
||||
// Sync user preferences<br>
|
||||
window.tigerstyleEcosystem.syncPreferences({theme: 'dark', notifications: 'enabled'});<br><br>
|
||||
|
||||
// Check plugin capabilities<br>
|
||||
window.tigerstyleEcosystem.getPluginCapabilities('whiskers');
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
}
|
||||
}
|
||||
137
admin/class-admin.php
Normal file
137
admin/class-admin.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin functionality for TigerStyle Heat
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Admin {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
error_log('TigerStyle Heat: Admin class constructor called');
|
||||
error_log('TigerStyle Heat: is_admin() = ' . (is_admin() ? 'true' : 'false'));
|
||||
error_log('TigerStyle Heat: current_user_can(manage_options) = ' . (current_user_can('manage_options') ? 'true' : 'false'));
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize admin functionality
|
||||
*/
|
||||
private function init() {
|
||||
error_log('TigerStyle Heat: Admin init() method called');
|
||||
add_action('admin_menu', array($this, 'add_admin_menu'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
|
||||
error_log('TigerStyle Heat: Admin hooks registered');
|
||||
|
||||
// Initialize admin pages
|
||||
TigerStyleSEO_Admin_Pages::instance();
|
||||
error_log('TigerStyle Heat: Admin pages initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin menu
|
||||
*/
|
||||
public function add_admin_menu() {
|
||||
// Debug log to verify this function is being called
|
||||
error_log('TigerStyle Heat: add_admin_menu called');
|
||||
|
||||
$hook = add_menu_page(
|
||||
__('TigerStyle Heat Settings', 'tigerstyle-heat'),
|
||||
__('TigerStyle Heat', 'tigerstyle-heat'),
|
||||
'manage_options',
|
||||
'tigerstyle-heat',
|
||||
array($this, 'admin_page'),
|
||||
'data:image/svg+xml;base64,' . base64_encode('
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Tiger head -->
|
||||
<ellipse cx="12" cy="13" rx="7" ry="6" fill="currentColor" opacity="0.9"/>
|
||||
<!-- Tiger ears -->
|
||||
<ellipse cx="8" cy="8" rx="2" ry="3" fill="currentColor"/>
|
||||
<ellipse cx="16" cy="8" rx="2" ry="3" fill="currentColor"/>
|
||||
<!-- Tiger eyes -->
|
||||
<circle cx="9.5" cy="11" r="1" fill="currentColor"/>
|
||||
<circle cx="14.5" cy="11" r="1" fill="currentColor"/>
|
||||
<!-- Tiger nose -->
|
||||
<ellipse cx="12" cy="13.5" rx="0.8" ry="0.5" fill="currentColor"/>
|
||||
<!-- Tiger stripes -->
|
||||
<path d="M7 10 L9 12 M15 10 L17 12 M6 13 L8 15 M16 13 L18 15" stroke="currentColor" stroke-width="0.8" fill="none"/>
|
||||
</svg>'),
|
||||
25
|
||||
);
|
||||
|
||||
// Debug log to verify menu was added
|
||||
error_log('TigerStyle Heat: add_menu_page returned: ' . ($hook ? $hook : 'false'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function admin_page() {
|
||||
TigerStyleSEO_Admin_Pages::render_main_page();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin scripts and styles
|
||||
*/
|
||||
public function enqueue_admin_scripts($hook) {
|
||||
// Debug log to check what hook we're getting
|
||||
error_log('TigerStyle Heat: enqueue_admin_scripts called with hook: ' . $hook);
|
||||
|
||||
// Only load on our admin page
|
||||
if ('toplevel_page_tigerstyle-heat' !== $hook) {
|
||||
error_log('TigerStyle Heat: Scripts NOT enqueued - hook mismatch');
|
||||
return;
|
||||
}
|
||||
|
||||
error_log('TigerStyle Heat: Scripts being enqueued');
|
||||
|
||||
wp_enqueue_script(
|
||||
'tigerstyle-heat-admin',
|
||||
TIGERSTYLE_HEAT_PLUGIN_URL . 'assets/js/admin.js',
|
||||
array('jquery'),
|
||||
TIGERSTYLE_HEAT_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_enqueue_style(
|
||||
'tigerstyle-heat-admin',
|
||||
TIGERSTYLE_HEAT_PLUGIN_URL . 'assets/css/admin.css',
|
||||
array(),
|
||||
TIGERSTYLE_HEAT_VERSION
|
||||
);
|
||||
|
||||
// Localize script for AJAX
|
||||
wp_localize_script('tigerstyle-heat-admin', 'tigerstyleSEO', array(
|
||||
'ajaxurl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('tigerstyle_heat_nonce'),
|
||||
'cache_analysis_nonce' => wp_create_nonce('tigerstyle_cache_analysis'),
|
||||
'ai_nonce' => wp_create_nonce('tigerstyle_ai_nonce'),
|
||||
'strings' => array(
|
||||
'runningAnalysis' => __('Running Analysis...', 'tigerstyle-heat'),
|
||||
'analysisFailed' => __('Analysis failed. Please try again.', 'tigerstyle-heat'),
|
||||
'networkError' => __('Network error. Please try again.', 'tigerstyle-heat'),
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
10
admin/js/robots-admin.js
Normal file
10
admin/js/robots-admin.js
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* TigerStyle Heat - Robots.txt Module JavaScript
|
||||
*/
|
||||
|
||||
jQuery(document).ready(function($) {
|
||||
// Module-specific functionality for robots.txt tab
|
||||
// Main functionality is handled by the core admin.js file
|
||||
|
||||
console.log('TigerStyle Heat: Robots.txt module loaded');
|
||||
});
|
||||
10
admin/js/sitemap-admin.js
Normal file
10
admin/js/sitemap-admin.js
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* TigerStyle Heat - Sitemap XML Module JavaScript
|
||||
*/
|
||||
|
||||
jQuery(document).ready(function($) {
|
||||
// Module-specific functionality for sitemap.xml tab
|
||||
// Main functionality is handled by the core admin.js file
|
||||
|
||||
console.log('TigerStyle Heat: Sitemap XML module loaded');
|
||||
});
|
||||
478
admin/pages/about.php
Normal file
478
admin/pages/about.php
Normal file
@ -0,0 +1,478 @@
|
||||
<?php
|
||||
/**
|
||||
* About TigerStyle Heat admin page template
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('About TigerStyle Heat 🔥', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Is your website neutered/spayed? TigerStyle Heat is a comprehensive WordPress SEO plugin designed to put your site in heat and make it irresistibly attractive to visitors and search engines from around the globe!', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
<div style="background: linear-gradient(135deg, #f8f4ff 0%, #fff8f0 100%); border-left: 4px solid #ff6b35; padding: 20px; margin: 20px 0; border-radius: 8px;">
|
||||
<h4 style="margin: 0 0 10px 0; color: #333; font-size: 18px;"><?php _e('🔥 Put your site in heat', 'tigerstyle-heat'); ?></h4>
|
||||
<p style="margin: 0; font-size: 16px; color: #555; font-style: italic;"><?php _e('Attract all the traffic naturally - it works so well, you\'ll get visitors from all over the globe!', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('🐾 The Complete TigerStyle Ecosystem', 'tigerstyle-heat'); ?></h3>
|
||||
<p><?php _e('TigerStyle Heat is part of a complete WordPress plugin suite, each specialized for different aspects of website management. Just like Kyra has different moods and skills, each plugin excels in its domain while working together seamlessly.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 20px 0;">
|
||||
<!-- TigerStyle Heat Card -->
|
||||
<div style="border: 3px solid #ff6b35; border-radius: 8px; padding: 20px; background: #fff8f6; position: relative;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 15px;">
|
||||
<span style="font-size: 32px; margin-right: 15px;">🔥</span>
|
||||
<div>
|
||||
<h4 style="margin: 0; color: #ff6b35;"><?php _e('TigerStyle Heat', 'tigerstyle-heat'); ?></h4>
|
||||
<span style="background: #ff6b35; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;"><?php _e('ACTIVE', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 0 0 15px 0; color: #666;"><?php _e('Put your site in heat and attract traffic naturally! Complete SEO optimization with AI-powered content analysis, structured data, OpenGraph integration, and irresistible search engine attraction.', 'tigerstyle-heat'); ?></p>
|
||||
<div style="border-top: 1px solid #eee; padding-top: 15px;">
|
||||
<ul style="margin: 8px 0 0 0; padding-left: 0; color: #666; list-style: none;">
|
||||
<li style="margin-bottom: 8px;">🎯 <?php _e('Natural traffic attraction (no chasing required!)', 'tigerstyle-heat'); ?></li>
|
||||
<li style="margin-bottom: 8px;">🤖 <?php _e('AI-powered irresistibility optimization', 'tigerstyle-heat'); ?></li>
|
||||
<li style="margin-bottom: 8px;">🌐 <?php _e('Structured data & OpenGraph heat signals', 'tigerstyle-heat'); ?></li>
|
||||
<li style="margin-bottom: 8px;">🔍 <?php _e('Advanced sitemap generation and robots.txt management', 'tigerstyle-heat'); ?></li>
|
||||
<li style="margin-bottom: 8px;">📊 <?php _e('Real-time SEO health monitoring and recommendations', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TigerStyle Life9 Card -->
|
||||
<div style="border: 2px solid #28a745; border-radius: 8px; padding: 20px; background: #f8fff9;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 15px;">
|
||||
<span style="font-size: 32px; margin-right: 15px;">💾</span>
|
||||
<div>
|
||||
<h4 style="margin: 0; color: #46b450;"><?php _e('TigerStyle Life9', 'tigerstyle-heat'); ?></h4>
|
||||
<span style="background: #ffba00; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;"><?php _e('AVAILABLE', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 0 0 15px 0; color: #666;"><?php _e('Comprehensive backup and disaster recovery system. Like a cat\'s nine lives, your site data is protected with multiple restore points and automated safeguards.', 'tigerstyle-heat'); ?></p>
|
||||
<div style="border-top: 1px solid #eee; padding-top: 15px;">
|
||||
<strong><?php _e('Key Features:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin: 8px 0 0 0; padding-left: 20px; color: #666;">
|
||||
<li><?php _e('Automated backup scheduling', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Multi-destination storage', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('One-click restore points', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TigerStyle Dash Card -->
|
||||
<div style="border: 2px solid #0073aa; border-radius: 8px; padding: 20px; background: #f0f8ff;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 15px;">
|
||||
<span style="font-size: 32px; margin-right: 15px;">⚡</span>
|
||||
<div>
|
||||
<h4 style="margin: 0; color: #ff6b35;"><?php _e('TigerStyle Dash', 'tigerstyle-heat'); ?></h4>
|
||||
<span style="background: #ffba00; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;"><?php _e('AVAILABLE', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 0 0 15px 0; color: #666;"><?php _e('Lightning-fast performance optimization with advanced caching, compression, and speed monitoring. Dash past the competition with cat-like reflexes.', 'tigerstyle-heat'); ?></p>
|
||||
<div style="border-top: 1px solid #eee; padding-top: 15px;">
|
||||
<strong><?php _e('Key Features:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin: 8px 0 0 0; padding-left: 20px; color: #666;">
|
||||
<li><?php _e('Brotli + Gzip compression', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Smart caching with warm-up', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Performance analytics dashboard', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TigerStyle Scent Card -->
|
||||
<div style="border: 2px solid #ffc107; border-radius: 8px; padding: 20px; background: #fffdf7;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 15px;">
|
||||
<span style="font-size: 32px; margin-right: 15px;">🐱</span>
|
||||
<div>
|
||||
<h4 style="margin: 0; color: #8e44ad;"><?php _e('TigerStyle Scent', 'tigerstyle-heat'); ?></h4>
|
||||
<span style="background: #ffba00; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;"><?php _e('AVAILABLE', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 0 0 15px 0; color: #666;"><?php _e('OAuth2 authorization server for secure API access. Like how cats recognize each other by scent, Scent provides secure identity and access management.', 'tigerstyle-heat'); ?></p>
|
||||
<div style="border-top: 1px solid #eee; padding-top: 15px;">
|
||||
<strong><?php _e('Key Features:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin: 8px 0 0 0; padding-left: 20px; color: #666;">
|
||||
<li><?php _e('OAuth2 authorization flows', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('JWT token management', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('API access control', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TigerStyle Whiskers Card -->
|
||||
<div style="border: 2px solid #6c5ce7; border-radius: 8px; padding: 20px; background: #f8f7ff;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 15px;">
|
||||
<span style="font-size: 32px; margin-right: 15px;">🐱</span>
|
||||
<div>
|
||||
<h4 style="margin: 0; color: #6c5ce7;"><?php _e('TigerStyle Whiskers', 'tigerstyle-heat'); ?></h4>
|
||||
<span style="background: #a29bfe; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;"><?php _e('COMING SOON', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 0 0 15px 0; color: #666;"><?php _e('GDPR compliance and privacy protection with feline precision. Like cat whiskers detect boundaries, Whiskers navigates privacy laws and detects compliance requirements with surgical accuracy.', 'tigerstyle-heat'); ?></p>
|
||||
<div style="border-top: 1px solid #eee; padding-top: 15px;">
|
||||
<strong><?php _e('Key Features:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin: 8px 0 0 0; padding-left: 20px; color: #666;">
|
||||
<li><?php _e('Cookie consent management', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Data boundary detection', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Privacy audit trails', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
||||
<h4 style="margin: 0 0 10px 0; color: #495057;"><?php _e('🐾 Why Choose the TigerStyle Ecosystem?', 'tigerstyle-heat'); ?></h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px;">
|
||||
<div>
|
||||
<strong style="color: #0073aa;"><?php _e('🎯 Specialized Expertise', 'tigerstyle-heat'); ?></strong><br>
|
||||
<span style="color: #666;"><?php _e('Each plugin masters its domain while integrating seamlessly', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
<div>
|
||||
<strong style="color: #46b450;"><?php _e('🔄 Unified Management', 'tigerstyle-heat'); ?></strong><br>
|
||||
<span style="color: #666;"><?php _e('Consistent interface and shared settings across all plugins', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
<div>
|
||||
<strong style="color: #ff6b35;"><?php _e('⚡ Peak Performance', 'tigerstyle-heat'); ?></strong><br>
|
||||
<span style="color: #666;"><?php _e('Optimized code that doesn\'t slow down your site', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
<div>
|
||||
<strong style="color: #8e44ad;"><?php _e('🛡️ Enterprise Security', 'tigerstyle-heat'); ?></strong><br>
|
||||
<span style="color: #666;"><?php _e('Built with security-first architecture and best practices', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seo-info-box" style="display: flex; gap: 20px; align-items: flex-start;">
|
||||
<div style="flex-shrink: 0;">
|
||||
<div style="width: 200px; height: 200px; border: 2px dashed #ccc; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: #f9f9f9; color: #666; text-align: center; font-size: 14px; line-height: 1.4;">
|
||||
<div>
|
||||
<strong>Kyra's Photo</strong><br>
|
||||
<small>Coming Soon!</small><br>
|
||||
<span style="font-size: 24px;">🐾</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1;">
|
||||
<h3><?php _e('Meet Kyra - Our Inspiration', 'tigerstyle-heat'); ?></h3>
|
||||
<p>
|
||||
<?php _e('TigerStyle Heat is named after Kyra, an 8-year-old black cat who embodies everything we want this plugin to represent. Kyra is known throughout her neighborhood as the kindest cat in the world - everyone who meets her instantly falls in love with her welcoming and gentle nature.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
<p>
|
||||
<?php _e('Just like Kyra makes everyone feel welcome and loved, TigerStyle Heat is designed to make your WordPress site welcoming and discoverable to all visitors and search engines. Whether it\'s helping search engines understand your content through structured data, or ensuring your site loads quickly for every visitor, we strive to embody Kyra\'s spirit of universal kindness and accessibility.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
|
||||
<h4><?php _e('The Kyra Philosophy', 'tigerstyle-heat'); ?></h4>
|
||||
<ul style="list-style: none; padding-left: 0;">
|
||||
<li><span style="color: #2271b1;">🐾</span> <strong><?php _e('Welcoming to All:', 'tigerstyle-heat'); ?></strong> <?php _e('Just as Kyra welcomes everyone with open paws, our plugin ensures your site is accessible and welcoming to all visitors.', 'tigerstyle-heat'); ?></li>
|
||||
<li><span style="color: #2271b1;">🐾</span> <strong><?php _e('Universally Loved:', 'tigerstyle-heat'); ?></strong> <?php _e('Like Kyra\'s universal appeal, we help your site become universally discoverable across all search engines.', 'tigerstyle-heat'); ?></li>
|
||||
<li><span style="color: #2271b1;">🐾</span> <strong><?php _e('Kind & Gentle:', 'tigerstyle-heat'); ?></strong> <?php _e('Our approach to SEO is gentle and user-friendly, making optimization accessible to everyone.', 'tigerstyle-heat'); ?></li>
|
||||
<li><span style="color: #2271b1;">🐾</span> <strong><?php _e('Wise & Experienced:', 'tigerstyle-heat'); ?></strong> <?php _e('At 8 years old, Kyra has the wisdom of experience - reflected in our comprehensive, mature SEO functionality.', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Plugin Features Inspired by Kyra', 'tigerstyle-heat'); ?></h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 15px;">
|
||||
|
||||
<div style="border: 1px solid #ddd; border-radius: 5px; padding: 15px; background: #fff;">
|
||||
<h4 style="margin-top: 0; color: #2271b1;"><?php _e('🌐 Universal Welcome', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Schema.org structured data and comprehensive meta tags ensure your content is understood and welcomed by all search engines, just like Kyra welcomes all visitors.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid #ddd; border-radius: 5px; padding: 15px; background: #fff;">
|
||||
<h4 style="margin-top: 0; color: #2271b1;"><?php _e('💖 Loved by Everyone', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('OpenGraph and social media optimization help your content be loved and shared across all platforms, spreading joy like Kyra spreads love.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid #ddd; border-radius: 5px; padding: 15px; background: #fff;">
|
||||
<h4 style="margin-top: 0; color: #2271b1;"><?php _e('🔍 Easy to Find', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Comprehensive sitemap generation and robots.txt management ensure your content is easily discoverable, just like everyone can easily find and connect with Kyra.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid #ddd; border-radius: 5px; padding: 15px; background: #fff;">
|
||||
<h4 style="margin-top: 0; color: #2271b1;"><?php _e('⚡ Fast & Responsive', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Performance optimization ensures your site responds quickly to every visitor, reflecting Kyra\'s responsive and attentive nature.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('🤖 AI Chat Tester', 'tigerstyle-heat'); ?></h3>
|
||||
<p><?php _e('Test your AI API keys and chat with different models to see how they can help with your SEO content creation.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<div id="ai-chat-tester" style="background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
||||
<!-- API Key Management -->
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h4 style="margin-bottom: 10px;"><?php _e('Quick Setup', 'tigerstyle-heat'); ?></h4>
|
||||
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||||
<input type="text" id="ai-api-key" placeholder="Enter API Key (e.g., sk-...)" style="flex: 1; min-width: 200px; padding: 8px;">
|
||||
<input type="text" id="ai-base-url" placeholder="Base URL (e.g., https://api.openai.com/v1)" value="https://api.openai.com/v1" style="flex: 1; min-width: 200px; padding: 8px;">
|
||||
<button id="test-api-key" class="button button-secondary"><?php _e('Test & Load Models', 'tigerstyle-heat'); ?></button>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: #666; margin-top: 5px;">
|
||||
<?php _e('Enter your OpenAI API key to test the connection and load available models.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Model Selection -->
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="ai-model-select" style="display: block; margin-bottom: 5px; font-weight: bold;"><?php _e('Model:', 'tigerstyle-heat'); ?></label>
|
||||
<select id="ai-model-select" style="width: 100%; padding: 8px; margin-bottom: 10px;">
|
||||
<option value=""><?php _e('Select a model (test API key first)', 'tigerstyle-heat'); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Chat Interface -->
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="ai-message-input" style="display: block; margin-bottom: 5px; font-weight: bold;"><?php _e('Your Message:', 'tigerstyle-heat'); ?></label>
|
||||
<textarea id="ai-message-input" placeholder="Ask for help with SEO content, meta descriptions, titles, etc..." style="width: 100%; height: 80px; padding: 10px; resize: vertical;"></textarea>
|
||||
<button id="send-ai-message" class="button button-primary" style="margin-top: 10px;" disabled><?php _e('Send Message', 'tigerstyle-heat'); ?></button>
|
||||
<button id="clear-chat" class="button button-secondary" style="margin-top: 10px; margin-left: 10px;"><?php _e('Clear Chat', 'tigerstyle-heat'); ?></button>
|
||||
</div>
|
||||
|
||||
<!-- Chat Response Area -->
|
||||
<div id="ai-chat-response" style="background: #f9f9f9; border: 1px solid #e1e1e1; border-radius: 5px; padding: 15px; min-height: 100px; max-height: 400px; overflow-y: auto; white-space: pre-wrap; font-family: monospace; font-size: 14px; display: none;">
|
||||
<p style="color: #666; font-style: italic;"><?php _e('AI responses will appear here...', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
|
||||
<!-- Status/Loading indicator -->
|
||||
<div id="ai-status" style="margin-top: 10px; padding: 10px; border-radius: 5px; display: none;">
|
||||
<span id="ai-status-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
let currentApiKey = '';
|
||||
let currentBaseUrl = '';
|
||||
let chatHistory = [];
|
||||
|
||||
// Test API Key and Load Models
|
||||
$('#test-api-key').on('click', function() {
|
||||
const apiKey = $('#ai-api-key').val().trim();
|
||||
const baseUrl = $('#ai-base-url').val().trim();
|
||||
|
||||
if (!apiKey) {
|
||||
showStatus('Please enter an API key', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!baseUrl) {
|
||||
showStatus('Please enter a base URL', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showStatus('Testing API key and loading models...', 'loading');
|
||||
$(this).prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_ai_test_key',
|
||||
api_key: apiKey,
|
||||
base_url: baseUrl,
|
||||
nonce: '<?php echo wp_create_nonce('tigerstyle_ai_nonce'); ?>'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
currentApiKey = apiKey;
|
||||
currentBaseUrl = baseUrl;
|
||||
|
||||
// Populate model dropdown
|
||||
const modelSelect = $('#ai-model-select');
|
||||
modelSelect.empty();
|
||||
modelSelect.append('<option value=""><?php _e('Select a model', 'tigerstyle-heat'); ?></option>');
|
||||
|
||||
if (response.data.models && response.data.models.length > 0) {
|
||||
response.data.models.forEach(function(model) {
|
||||
modelSelect.append($('<option>', {
|
||||
value: model.id,
|
||||
text: model.id + (model.owned_by ? ' (' + model.owned_by + ')' : '')
|
||||
}));
|
||||
});
|
||||
showStatus('✅ API key valid! ' + response.data.models.length + ' models loaded.', 'success');
|
||||
} else {
|
||||
showStatus('✅ API key valid, but no models found.', 'success');
|
||||
}
|
||||
|
||||
$('#send-ai-message').prop('disabled', false);
|
||||
} else {
|
||||
showStatus('❌ API key test failed: ' + (response.data || 'Unknown error'), 'error');
|
||||
$('#send-ai-message').prop('disabled', true);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showStatus('❌ Request failed. Please check your connection.', 'error');
|
||||
$('#send-ai-message').prop('disabled', true);
|
||||
},
|
||||
complete: function() {
|
||||
$('#test-api-key').prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Send Chat Message
|
||||
$('#send-ai-message').on('click', function() {
|
||||
const message = $('#ai-message-input').val().trim();
|
||||
const model = $('#ai-model-select').val();
|
||||
|
||||
if (!message) {
|
||||
showStatus('Please enter a message', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
showStatus('Please select a model', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentApiKey) {
|
||||
showStatus('Please test your API key first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showStatus('Sending message...', 'loading');
|
||||
$(this).prop('disabled', true);
|
||||
|
||||
// Add user message to chat
|
||||
addToChatHistory('user', message);
|
||||
displayChat();
|
||||
|
||||
$.ajax({
|
||||
url: ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_ai_chat',
|
||||
message: message,
|
||||
model: model,
|
||||
api_key: currentApiKey,
|
||||
base_url: currentBaseUrl,
|
||||
nonce: '<?php echo wp_create_nonce('tigerstyle_ai_nonce'); ?>'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
addToChatHistory('assistant', response.data.response);
|
||||
displayChat();
|
||||
showStatus('✅ Message sent successfully!', 'success');
|
||||
$('#ai-message-input').val(''); // Clear input
|
||||
} else {
|
||||
showStatus('❌ Chat failed: ' + (response.data || 'Unknown error'), 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showStatus('❌ Request failed. Please check your connection.', 'error');
|
||||
},
|
||||
complete: function() {
|
||||
$('#send-ai-message').prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Clear Chat
|
||||
$('#clear-chat').on('click', function() {
|
||||
chatHistory = [];
|
||||
$('#ai-chat-response').hide().html('<p style="color: #666; font-style: italic;"><?php _e('AI responses will appear here...', 'tigerstyle-heat'); ?></p>');
|
||||
showStatus('Chat cleared', 'success');
|
||||
});
|
||||
|
||||
// Helper Functions
|
||||
function addToChatHistory(role, content) {
|
||||
chatHistory.push({
|
||||
role: role,
|
||||
content: content,
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
function displayChat() {
|
||||
const chatDiv = $('#ai-chat-response');
|
||||
let chatHtml = '';
|
||||
|
||||
chatHistory.forEach(function(entry) {
|
||||
const roleLabel = entry.role === 'user' ? '👤 You' : '🤖 AI';
|
||||
const timestamp = entry.timestamp.toLocaleTimeString();
|
||||
chatHtml += `<div style="margin-bottom: 15px; padding: 10px; background: ${entry.role === 'user' ? '#e8f4f8' : '#f0f8e8'}; border-radius: 5px;">`;
|
||||
chatHtml += `<strong>${roleLabel}</strong> <span style="color: #666; font-size: 12px;">${timestamp}</span><br>`;
|
||||
chatHtml += `<div style="margin-top: 5px;">${entry.content}</div>`;
|
||||
chatHtml += '</div>';
|
||||
});
|
||||
|
||||
chatDiv.html(chatHtml).show();
|
||||
chatDiv.scrollTop(chatDiv[0].scrollHeight);
|
||||
}
|
||||
|
||||
function showStatus(message, type) {
|
||||
const statusDiv = $('#ai-status');
|
||||
const statusText = $('#ai-status-text');
|
||||
|
||||
statusDiv.removeClass('notice-success notice-error notice-info');
|
||||
|
||||
if (type === 'success') {
|
||||
statusDiv.addClass('notice-success');
|
||||
statusDiv.css('background', '#d4edda');
|
||||
statusDiv.css('color', '#155724');
|
||||
statusDiv.css('border', '1px solid #c3e6cb');
|
||||
} else if (type === 'error') {
|
||||
statusDiv.addClass('notice-error');
|
||||
statusDiv.css('background', '#f8d7da');
|
||||
statusDiv.css('color', '#721c24');
|
||||
statusDiv.css('border', '1px solid #f5c6cb');
|
||||
} else {
|
||||
statusDiv.addClass('notice-info');
|
||||
statusDiv.css('background', '#d1ecf1');
|
||||
statusDiv.css('color', '#0c5460');
|
||||
statusDiv.css('border', '1px solid #bee5eb');
|
||||
}
|
||||
|
||||
statusText.text(message);
|
||||
statusDiv.show();
|
||||
|
||||
if (type === 'success' || type === 'error') {
|
||||
setTimeout(function() {
|
||||
statusDiv.fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Enter key to send message
|
||||
$('#ai-message-input').on('keypress', function(e) {
|
||||
if (e.which === 13 && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
$('#send-ai-message').click();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Support & Community', 'tigerstyle-heat'); ?></h3>
|
||||
<p>
|
||||
<?php _e('Built with the same care and attention that Kyra gives to everyone she meets, TigerStyle Heat is continuously updated to ensure your WordPress site remains optimized, accessible, and loved by both visitors and search engines.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
<p>
|
||||
<?php _e('Have questions or need help? Just like Kyra is always there for her family, we\'re here to help you succeed with your SEO goals.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
|
||||
<div style="text-align: center; margin-top: 20px; padding: 20px; background: #f0f8ff; border-radius: 10px;">
|
||||
<p style="font-style: italic; color: #555; margin: 0;">
|
||||
"<?php _e('May your website be as welcoming and loved as Kyra makes everyone feel.', 'tigerstyle-heat'); ?>"
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; font-size: 14px; color: #777;">
|
||||
— <?php _e('The TigerStyle Heat Team', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
842
admin/pages/amp.php
Normal file
842
admin/pages/amp.php
Normal file
@ -0,0 +1,842 @@
|
||||
<?php
|
||||
/**
|
||||
* AMP (Accelerated Mobile Pages) Admin Page
|
||||
*
|
||||
* @package TigerStyle_SEO
|
||||
* @subpackage Admin
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$amp_options = get_option('tigerstyle_heat_amp', []);
|
||||
$amp_enabled = $amp_options['enable_amp'] ?? false;
|
||||
$sxg_preparation = $amp_options['sxg_preparation'] ?? false;
|
||||
|
||||
// Infrastructure check - get module from plugin instance
|
||||
$amp_module = tigerstyle_heat()->get_module('amp');
|
||||
$sxg_status = ($amp_module && method_exists($amp_module, 'check_infrastructure_status')) ? $amp_module->check_infrastructure_status() : [];
|
||||
?>
|
||||
|
||||
<div class="tigerstyle-heat-admin-content">
|
||||
<form method="post" action="options.php" id="tigerstyle-heat-amp-form">
|
||||
<?php settings_fields('tigerstyle_heat_amp'); ?>
|
||||
|
||||
<!-- AMP Configuration -->
|
||||
<div class="tigerstyle-heat-card">
|
||||
<div class="card-header">
|
||||
<h3>⚡ AMP Configuration</h3>
|
||||
<p>Accelerated Mobile Pages for faster loading on mobile devices</p>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="form-group">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox"
|
||||
name="tigerstyle_heat_amp[enable_amp]"
|
||||
value="1"
|
||||
<?php checked($amp_enabled); ?>>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-label">Enable AMP Pages</span>
|
||||
</label>
|
||||
<p class="description">Generate AMP versions of your content for faster mobile loading</p>
|
||||
</div>
|
||||
|
||||
<?php if ($amp_enabled): ?>
|
||||
<div class="form-group">
|
||||
<label for="amp_post_types">Enable AMP for Post Types:</label>
|
||||
<div class="checkbox-group">
|
||||
<?php
|
||||
$post_types = get_post_types(['public' => true], 'objects');
|
||||
$enabled_types = $amp_options['post_types'] ?? ['post'];
|
||||
|
||||
foreach ($post_types as $post_type): ?>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox"
|
||||
name="tigerstyle_heat_amp[post_types][]"
|
||||
value="<?php echo esc_attr($post_type->name); ?>"
|
||||
<?php checked(in_array($post_type->name, $enabled_types)); ?>>
|
||||
<?php echo esc_html($post_type->label); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amp_publisher_logo">Publisher Logo URL:</label>
|
||||
<input type="url"
|
||||
id="amp_publisher_logo"
|
||||
name="tigerstyle_heat_amp[publisher_logo]"
|
||||
value="<?php echo esc_attr($amp_options['publisher_logo'] ?? ''); ?>"
|
||||
placeholder="https://example.com/logo.png"
|
||||
class="regular-text">
|
||||
<p class="description">Logo for structured data (600×60px recommended)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amp_analytics_id">Google Analytics ID:</label>
|
||||
<input type="text"
|
||||
id="amp_analytics_id"
|
||||
name="tigerstyle_heat_amp[analytics_id]"
|
||||
value="<?php echo esc_attr($amp_options['analytics_id'] ?? ''); ?>"
|
||||
placeholder="GA-XXXXXXXX-X"
|
||||
class="regular-text">
|
||||
<p class="description">Google Analytics tracking ID for AMP pages</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signed Exchange (SXG) Section -->
|
||||
<div class="tigerstyle-heat-card">
|
||||
<div class="card-header">
|
||||
<h3>🔐 Signed Exchange (SXG) Support</h3>
|
||||
<p>Enable AMP pages to be served from original domain while cached by Google</p>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<!-- Infrastructure Requirements -->
|
||||
<div class="sxg-requirements">
|
||||
<h4>Infrastructure Requirements</h4>
|
||||
<div class="requirements-grid">
|
||||
<?php
|
||||
$requirements = [
|
||||
'amppackager_binary' => [
|
||||
'title' => 'amppackager Binary',
|
||||
'description' => 'Go binary installation required',
|
||||
'status' => $sxg_status['amppackager_binary'] ?? false,
|
||||
'docs' => 'https://github.com/ampproject/amppackager'
|
||||
],
|
||||
'certificate_authority' => [
|
||||
'title' => 'Supported SSL Certificate',
|
||||
'description' => 'Certificate from supported CA (Let\'s Encrypt, DigiCert, etc.)',
|
||||
'status' => $sxg_status['certificate_valid'] ?? false,
|
||||
'docs' => 'https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/signed-exchange/#certificates'
|
||||
],
|
||||
'server_control' => [
|
||||
'title' => 'Edge Server Control',
|
||||
'description' => 'Ability to control HTTP headers at server level',
|
||||
'status' => $sxg_status['server_control'] ?? false,
|
||||
'docs' => 'https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/signed-exchange/#server-requirements'
|
||||
],
|
||||
'network_connectivity' => [
|
||||
'title' => 'Network Access',
|
||||
'description' => 'Outgoing requests to CA, publisher, cdn.ampproject.org',
|
||||
'status' => $sxg_status['network_connectivity'] ?? false,
|
||||
'docs' => null
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($requirements as $key => $requirement): ?>
|
||||
<div class="requirement-item">
|
||||
<div class="requirement-status <?php echo $requirement['status'] ? 'met' : 'not-met'; ?>">
|
||||
<?php if ($requirement['status']): ?>
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<?php else: ?>
|
||||
<span class="dashicons dashicons-warning"></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="requirement-details">
|
||||
<h5><?php echo esc_html($requirement['title']); ?></h5>
|
||||
<p><?php echo esc_html($requirement['description']); ?></p>
|
||||
<?php if ($requirement['docs']): ?>
|
||||
<a href="<?php echo esc_url($requirement['docs']); ?>" target="_blank" class="docs-link">
|
||||
Documentation →
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SXG Preparation -->
|
||||
<div class="sxg-preparation">
|
||||
<h4>WordPress-Level Preparation</h4>
|
||||
<p>This plugin can prepare your WordPress site for SXG, but cannot implement the full SXG infrastructure.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox"
|
||||
name="tigerstyle_heat_amp[sxg_preparation]"
|
||||
value="1"
|
||||
<?php checked($sxg_preparation); ?>>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-label">Enable SXG Preparation</span>
|
||||
</label>
|
||||
<p class="description">Add SXG-compatible headers and meta tags (WordPress level only)</p>
|
||||
</div>
|
||||
|
||||
<?php if ($sxg_preparation): ?>
|
||||
<div class="sxg-actions">
|
||||
<button type="button" class="button" id="test-sxg-infrastructure">
|
||||
Test Infrastructure
|
||||
</button>
|
||||
<button type="button" class="button" id="generate-amppackager-config">
|
||||
Generate amppackager Config
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Infrastructure Setup Guide -->
|
||||
<!-- Binary Execution Test Section -->
|
||||
<div class="binary-test-section">
|
||||
<h4>Environment Testing</h4>
|
||||
<p>Test if your server environment can run amppackager binary before proceeding with setup.</p>
|
||||
|
||||
<div class="test-controls">
|
||||
<button type="button" class="button" id="test-binary-execution">
|
||||
Test Binary Execution
|
||||
</button>
|
||||
<div id="test-results" class="test-results" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Binary Upload Section -->
|
||||
<div class="binary-upload-section" id="binary-upload-section" style="display: none;">
|
||||
<h4>amppackager Binary Upload</h4>
|
||||
<p>Upload the amppackager binary to run locally (avoids security concerns of automatic downloads).</p>
|
||||
|
||||
<div class="upload-controls">
|
||||
<div class="form-group">
|
||||
<label for="amppackager-binary">Select amppackager Binary:</label>
|
||||
<input type="file" id="amppackager-binary" name="amppackager_binary" accept=".exe,*">
|
||||
<p class="description">
|
||||
Download from: <a href="https://github.com/ampproject/amppackager/releases/latest" target="_blank">GitHub Releases</a>
|
||||
<br>Choose the appropriate version for your server architecture.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="button button-primary" id="upload-test-binary">
|
||||
Upload & Test Binary
|
||||
</button>
|
||||
<button type="button" class="button" id="configure-external-api">
|
||||
Use External SXG API Instead
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External API Configuration -->
|
||||
<div class="api-config-section" id="api-config-section" style="display: none;">
|
||||
<h4>External SXG API Configuration</h4>
|
||||
<p>Configure external API for SXG generation when binary execution is not available.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sxg_api_url">SXG API Endpoint:</label>
|
||||
<input type="url"
|
||||
id="sxg_api_url"
|
||||
name="tigerstyle_heat_amp[sxg_api_url]"
|
||||
value="<?php echo esc_attr($amp_options['sxg_api_url'] ?? ''); ?>"
|
||||
placeholder="https://your-sxg-api.com/generate"
|
||||
class="regular-text">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sxg_api_key">API Key:</label>
|
||||
<input type="password"
|
||||
id="sxg_api_key"
|
||||
name="tigerstyle_heat_amp[sxg_api_key]"
|
||||
value="<?php echo esc_attr($amp_options['sxg_api_key'] ?? ''); ?>"
|
||||
placeholder="Your API key"
|
||||
class="regular-text">
|
||||
</div>
|
||||
|
||||
<button type="button" class="button" id="test-sxg-api">
|
||||
Test API Connection
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sxg-setup-guide">
|
||||
<h4>Setting Up Full SXG Support</h4>
|
||||
<div class="setup-steps">
|
||||
<div class="setup-step">
|
||||
<h5>1. Server Requirements</h5>
|
||||
<ul>
|
||||
<li>Linux server with root access (not shared hosting)</li>
|
||||
<li>Go runtime environment installed</li>
|
||||
<li>Ability to configure web server (Apache/Nginx)</li>
|
||||
<li>SSL certificate from supported CA</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setup-step">
|
||||
<h5>2. Install amppackager</h5>
|
||||
<div class="code-block">
|
||||
<pre><code># Download and install amppackager
|
||||
wget https://github.com/ampproject/amppackager/releases/download/latest/amppackager
|
||||
chmod +x amppackager
|
||||
sudo mv amppackager /usr/local/bin/
|
||||
|
||||
# Create config directory
|
||||
sudo mkdir -p /etc/amppackager</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-step">
|
||||
<h5>3. Configure Web Server</h5>
|
||||
<p>Configure your web server to:</p>
|
||||
<ul>
|
||||
<li>Vary responses on <code>Accept</code> and <code>AMP-Cache-Transform</code> headers</li>
|
||||
<li>Proxy SXG requests to amppackager</li>
|
||||
<li>Serve different content for the same URL based on headers</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setup-step">
|
||||
<h5>4. Maintenance</h5>
|
||||
<ul>
|
||||
<li>Update amppackager every 6 weeks</li>
|
||||
<li>Monitor certificate expiration</li>
|
||||
<li>Ensure persistent storage for amppackager instances</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compatible Hosting -->
|
||||
<div class="compatible-hosting">
|
||||
<h4>Compatible Hosting Solutions</h4>
|
||||
<div class="hosting-grid">
|
||||
<div class="hosting-option compatible">
|
||||
<h5>✅ VPS/Dedicated Servers</h5>
|
||||
<p>Full control over server configuration</p>
|
||||
<ul>
|
||||
<li>DigitalOcean</li>
|
||||
<li>Linode</li>
|
||||
<li>AWS EC2</li>
|
||||
<li>Google Cloud</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="hosting-option partial">
|
||||
<h5>⚠️ Managed WordPress</h5>
|
||||
<p>May require hosting provider support</p>
|
||||
<ul>
|
||||
<li>WP Engine (contact support)</li>
|
||||
<li>Kinsta (enterprise plans)</li>
|
||||
<li>Pantheon (custom)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="hosting-option incompatible">
|
||||
<h5>❌ Shared Hosting</h5>
|
||||
<p>Cannot install binaries or control headers</p>
|
||||
<ul>
|
||||
<li>Most shared hosting providers</li>
|
||||
<li>Blogger</li>
|
||||
<li>WordPress.com</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AMP Testing Tools -->
|
||||
<div class="tigerstyle-heat-card">
|
||||
<div class="card-header">
|
||||
<h3>🔧 Testing & Validation</h3>
|
||||
<p>Tools to test and validate your AMP implementation</p>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="testing-tools">
|
||||
<div class="tool-group">
|
||||
<h4>AMP Validation</h4>
|
||||
<a href="https://validator.ampproject.org/" target="_blank" class="button button-secondary">
|
||||
AMP Validator →
|
||||
</a>
|
||||
<p>Test your AMP pages for compliance</p>
|
||||
</div>
|
||||
|
||||
<div class="tool-group">
|
||||
<h4>Google Search Console</h4>
|
||||
<a href="https://search.google.com/search-console" target="_blank" class="button button-secondary">
|
||||
Search Console →
|
||||
</a>
|
||||
<p>Monitor AMP status and errors</p>
|
||||
</div>
|
||||
|
||||
<div class="tool-group">
|
||||
<h4>AMP Test</h4>
|
||||
<a href="https://search.google.com/test/amp" target="_blank" class="button button-secondary">
|
||||
Google AMP Test →
|
||||
</a>
|
||||
<p>Test AMP pages in Google's tools</p>
|
||||
</div>
|
||||
|
||||
<div class="tool-group">
|
||||
<h4>SXG Documentation</h4>
|
||||
<a href="https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/signed-exchange" target="_blank" class="button button-secondary">
|
||||
AMP SXG Guide →
|
||||
</a>
|
||||
<a href="https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html" target="_blank" class="button button-secondary">
|
||||
SXG Implementation →
|
||||
</a>
|
||||
<p>Official documentation and implementation details</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php submit_button('Save AMP Settings', 'primary', 'submit', false); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sxg-requirements {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.requirements-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.requirement-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
|
||||
.requirement-item.met {
|
||||
border-left-color: #46b450;
|
||||
}
|
||||
|
||||
.requirement-item.not-met {
|
||||
border-left-color: #dc3232;
|
||||
}
|
||||
|
||||
.requirement-status .dashicons {
|
||||
font-size: 20px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.requirement-status .dashicons-yes-alt {
|
||||
color: #46b450;
|
||||
}
|
||||
|
||||
.requirement-status .dashicons-warning {
|
||||
color: #dc3232;
|
||||
}
|
||||
|
||||
.requirement-details h5 {
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.requirement-details p {
|
||||
margin: 0 0 8px 0;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.docs-link {
|
||||
font-size: 12px;
|
||||
color: #0073aa;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.setup-steps {
|
||||
display: grid;
|
||||
gap: 25px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.setup-step {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #0073aa;
|
||||
}
|
||||
|
||||
.setup-step h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #0073aa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.setup-step ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hosting-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.hosting-option {
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.hosting-option.compatible {
|
||||
border-color: #46b450;
|
||||
background: #f7fff7;
|
||||
}
|
||||
|
||||
.hosting-option.partial {
|
||||
border-color: #ffb900;
|
||||
background: #fffbf0;
|
||||
}
|
||||
|
||||
.hosting-option.incompatible {
|
||||
border-color: #dc3232;
|
||||
background: #fff7f7;
|
||||
}
|
||||
|
||||
.hosting-option h5 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.hosting-option ul {
|
||||
margin: 10px 0 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.testing-tools {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.tool-group {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tool-group h4 {
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.tool-group p {
|
||||
margin: 10px 0 0 0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.sxg-actions {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.sxg-actions .button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.binary-test-section,
|
||||
.binary-upload-section,
|
||||
.api-config-section {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #0073aa;
|
||||
}
|
||||
|
||||
.test-controls,
|
||||
.upload-controls {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.test-results {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.test-results.success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.test-results.error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.test-results.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.upload-controls .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.upload-controls .button {
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tool-group a.button {
|
||||
margin: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Test Binary Execution
|
||||
document.getElementById('test-binary-execution')?.addEventListener('click', function() {
|
||||
const button = this;
|
||||
const resultsDiv = document.getElementById('test-results');
|
||||
|
||||
button.textContent = 'Testing...';
|
||||
button.disabled = true;
|
||||
resultsDiv.style.display = 'block';
|
||||
resultsDiv.className = 'test-results';
|
||||
resultsDiv.innerHTML = 'Running binary execution tests...';
|
||||
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'tigerstyle_heat_test_binary_execution',
|
||||
_ajax_nonce: '<?php echo wp_create_nonce('tigerstyle_heat_nonce'); ?>'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const results = data.data;
|
||||
let resultHtml = '<strong>Binary Execution Test Results:</strong><br>';
|
||||
|
||||
resultHtml += `exec() function available: ${results.exec_function ? '✅ YES' : '❌ NO'}<br>`;
|
||||
resultHtml += `exec() not disabled: ${results.exec_allowed ? '✅ YES' : '❌ NO'}<br>`;
|
||||
resultHtml += `Temporary directory writable: ${results.temp_writable ? '✅ YES' : '❌ NO'}<br>`;
|
||||
resultHtml += `Binary execution test: ${results.binary_test ? '✅ SUCCESS' : '❌ FAILED'}<br>`;
|
||||
|
||||
if (results.error_message) {
|
||||
resultHtml += `<br><strong>Error:</strong> ${results.error_message}`;
|
||||
}
|
||||
|
||||
resultsDiv.innerHTML = resultHtml;
|
||||
|
||||
if (results.exec_function && results.exec_allowed && results.temp_writable && results.binary_test) {
|
||||
resultsDiv.className = 'test-results success';
|
||||
document.getElementById('binary-upload-section').style.display = 'block';
|
||||
} else {
|
||||
resultsDiv.className = 'test-results error';
|
||||
document.getElementById('api-config-section').style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
resultsDiv.innerHTML = `<strong>Test Failed:</strong> ${data.data.message || 'Unknown error'}`;
|
||||
resultsDiv.className = 'test-results error';
|
||||
document.getElementById('api-config-section').style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultsDiv.innerHTML = `<strong>Test Failed:</strong> ${error.message}`;
|
||||
resultsDiv.className = 'test-results error';
|
||||
document.getElementById('api-config-section').style.display = 'block';
|
||||
})
|
||||
.finally(() => {
|
||||
button.textContent = 'Test Binary Execution';
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Configure External API
|
||||
document.getElementById('configure-external-api')?.addEventListener('click', function() {
|
||||
document.getElementById('binary-upload-section').style.display = 'none';
|
||||
document.getElementById('api-config-section').style.display = 'block';
|
||||
});
|
||||
|
||||
// Test SXG API
|
||||
document.getElementById('test-sxg-api')?.addEventListener('click', function() {
|
||||
const button = this;
|
||||
const apiUrl = document.getElementById('sxg_api_url').value;
|
||||
const apiKey = document.getElementById('sxg_api_key').value;
|
||||
|
||||
if (!apiUrl) {
|
||||
alert('Please enter an API endpoint URL');
|
||||
return;
|
||||
}
|
||||
|
||||
button.textContent = 'Testing...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'tigerstyle_heat_test_sxg_api',
|
||||
api_url: apiUrl,
|
||||
api_key: apiKey,
|
||||
_ajax_nonce: '<?php echo wp_create_nonce('tigerstyle_heat_nonce'); ?>'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('API connection successful! Response: ' + JSON.stringify(data.data.response));
|
||||
} else {
|
||||
alert('API test failed: ' + data.data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('API test failed: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
button.textContent = 'Test API Connection';
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Upload & Test Binary
|
||||
document.getElementById('upload-test-binary')?.addEventListener('click', function() {
|
||||
const button = this;
|
||||
const fileInput = document.getElementById('amppackager-binary');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
alert('Please select a binary file to upload');
|
||||
return;
|
||||
}
|
||||
|
||||
button.textContent = 'Uploading...';
|
||||
button.disabled = true;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'tigerstyle_heat_upload_binary');
|
||||
formData.append('binary_file', file);
|
||||
formData.append('_ajax_nonce', '<?php echo wp_create_nonce('tigerstyle_heat_nonce'); ?>');
|
||||
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Binary uploaded and tested successfully! ' + data.data.message);
|
||||
} else {
|
||||
alert('Binary upload failed: ' + data.data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Binary upload failed: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
button.textContent = 'Upload & Test Binary';
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Test SXG Infrastructure
|
||||
document.getElementById('test-sxg-infrastructure')?.addEventListener('click', function() {
|
||||
this.textContent = 'Testing...';
|
||||
this.disabled = true;
|
||||
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'tigerstyle_heat_test_infrastructure',
|
||||
_ajax_nonce: '<?php echo wp_create_nonce('tigerstyle_heat_nonce'); ?>'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
alert('Infrastructure test completed. Check console for details.');
|
||||
console.log('SXG Infrastructure Test Results:', data);
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Test failed: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
this.textContent = 'Test Infrastructure';
|
||||
this.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Generate amppackager config
|
||||
document.getElementById('generate-amppackager-config')?.addEventListener('click', function() {
|
||||
this.textContent = 'Generating...';
|
||||
this.disabled = true;
|
||||
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'tigerstyle_heat_generate_amppackager_config',
|
||||
_ajax_nonce: '<?php echo wp_create_nonce('tigerstyle_heat_nonce'); ?>'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Create download link for config file
|
||||
const blob = new Blob([data.data.config], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'amppackager-config.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
alert('Configuration file downloaded. Upload to your amppackager server.');
|
||||
} else {
|
||||
alert('Failed to generate config: ' + data.data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Generation failed: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
this.textContent = 'Generate amppackager Config';
|
||||
this.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
1442
admin/pages/backup-restore.php
Normal file
1442
admin/pages/backup-restore.php
Normal file
File diff suppressed because it is too large
Load Diff
1936
admin/pages/google-appearance.php
Normal file
1936
admin/pages/google-appearance.php
Normal file
File diff suppressed because it is too large
Load Diff
85
admin/pages/llms-txt.php
Normal file
85
admin/pages/llms-txt.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* LLMs.txt admin page template
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('LLMs.txt Configuration', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Configure your LLMs.txt file to help AI systems understand your website content. This file provides machine-readable information about your site structure and important pages.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
<p class="description">
|
||||
<strong><?php _e('Learn more:', 'tigerstyle-heat'); ?></strong>
|
||||
<a href="https://llmstxt.org/" target="_blank"><?php _e('LLMs.txt Specification', 'tigerstyle-heat'); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="update_llmstxt">
|
||||
<?php wp_nonce_field('update_llmstxt', 'llmstxt_nonce'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enable LLMs.txt', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="llmstxt_enabled" value="1" <?php checked(TigerStyleSEO_Utils::get_option('llmstxt_enabled', false)); ?>>
|
||||
<?php _e('Enable LLMs.txt generation', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php _e('When enabled, your site will serve an LLMs.txt file at yoursite.com/llms.txt', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="llmstxt_content"><?php _e('Custom LLMs.txt Content', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea
|
||||
id="llmstxt_content"
|
||||
name="llmstxt_content"
|
||||
rows="20"
|
||||
cols="80"
|
||||
class="large-text code"
|
||||
placeholder="<?php _e('Leave empty to auto-generate content based on your website structure...', 'tigerstyle-heat'); ?>"
|
||||
><?php echo esc_textarea(TigerStyleSEO_Utils::get_option('llmstxt_content', '')); ?></textarea>
|
||||
<p class="description">
|
||||
<?php _e('Custom markdown content for your LLMs.txt file. Leave empty to auto-generate based on your site.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(__('Update LLMs.txt Settings', 'tigerstyle-heat')); ?>
|
||||
</form>
|
||||
|
||||
<?php if (TigerStyleSEO_Utils::get_option('llmstxt_enabled', false)): ?>
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Preview Current LLMs.txt', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<strong><?php _e('Current LLMs.txt URL:', 'tigerstyle-heat'); ?></strong>
|
||||
<a href="<?php echo esc_url(home_url('/llms.txt')); ?>" target="_blank">
|
||||
<?php echo esc_url(home_url('/llms.txt')); ?>
|
||||
</a>
|
||||
</p>
|
||||
<textarea readonly rows="15" cols="80" class="large-text code" style="background: #f9f9f9;">
|
||||
<?php
|
||||
$llms_module = tigerstyle_heat()->get_module('llms_txt');
|
||||
if ($llms_module) {
|
||||
// Get content via reflection to access private method for preview
|
||||
$reflection = new ReflectionClass($llms_module);
|
||||
$method = $reflection->getMethod('get_llmstxt_content');
|
||||
$method->setAccessible(true);
|
||||
echo esc_textarea($method->invoke($llms_module));
|
||||
}
|
||||
?>
|
||||
</textarea>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
350
admin/pages/meta-tags.php
Normal file
350
admin/pages/meta-tags.php
Normal file
@ -0,0 +1,350 @@
|
||||
<?php
|
||||
/**
|
||||
* Meta Tags admin page template
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Meta Tags Configuration', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Configure meta tags for your website including SEO, social media, and technical metadata. Set site-wide defaults and create pattern-specific overrides for different pages and post types.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
<p class="description">
|
||||
<strong><?php _e('Google Guidelines:', 'tigerstyle-heat'); ?></strong><br>
|
||||
<a href="https://developers.google.com/search/docs/crawling-indexing/valid-page-metadata" target="_blank">
|
||||
<?php _e('Valid Page Metadata', 'tigerstyle-heat'); ?>
|
||||
</a> |
|
||||
<a href="https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag" target="_blank">
|
||||
<?php _e('Robots Meta Tag', 'tigerstyle-heat'); ?>
|
||||
</a> |
|
||||
<a href="https://developers.google.com/search/docs/crawling-indexing/special-tags" target="_blank">
|
||||
<?php _e('Special Tags', 'tigerstyle-heat'); ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="update_meta_tags">
|
||||
<?php wp_nonce_field('update_meta_tags', 'meta_tags_nonce'); ?>
|
||||
|
||||
<!-- Default Meta Tags -->
|
||||
<h3><?php _e('Site-wide Default Meta Tags', 'tigerstyle-heat'); ?></h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="meta_description_default"><?php _e('Default Meta Description', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea
|
||||
id="meta_description_default"
|
||||
name="meta_description_default"
|
||||
rows="3"
|
||||
cols="80"
|
||||
class="large-text"
|
||||
maxlength="160"
|
||||
><?php echo esc_textarea(TigerStyleSEO_Utils::get_option('meta_description_default', '')); ?></textarea>
|
||||
<p class="description">
|
||||
<?php _e('Default description for pages without specific content. Recommended length: 150-160 characters.', 'tigerstyle-heat'); ?>
|
||||
<span id="description-counter">0/160</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="meta_keywords_default"><?php _e('Default Meta Keywords', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="meta_keywords_default"
|
||||
name="meta_keywords_default"
|
||||
value="<?php echo esc_attr(TigerStyleSEO_Utils::get_option('meta_keywords_default', '')); ?>"
|
||||
class="large-text"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Comma-separated keywords (Note: Most search engines ignore meta keywords)', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="meta_author_default"><?php _e('Default Author', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="meta_author_default"
|
||||
name="meta_author_default"
|
||||
value="<?php echo esc_attr(TigerStyleSEO_Utils::get_option('meta_author_default', '')); ?>"
|
||||
class="regular-text"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Default author for your website content', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Robots Directive -->
|
||||
<h3><?php _e('Robots Directive Configuration', 'tigerstyle-heat'); ?></h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="meta_robots_default"><?php _e('Default Robots Directive', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="meta_robots_default" name="meta_robots_default" class="regular-text">
|
||||
<option value="index,follow" <?php selected(TigerStyleSEO_Utils::get_option('meta_robots_default', 'index,follow'), 'index,follow'); ?>>index,follow</option>
|
||||
<option value="noindex,nofollow" <?php selected(TigerStyleSEO_Utils::get_option('meta_robots_default'), 'noindex,nofollow'); ?>>noindex,nofollow</option>
|
||||
<option value="index,nofollow" <?php selected(TigerStyleSEO_Utils::get_option('meta_robots_default'), 'index,nofollow'); ?>>index,nofollow</option>
|
||||
<option value="noindex,follow" <?php selected(TigerStyleSEO_Utils::get_option('meta_robots_default'), 'noindex,follow'); ?>>noindex,follow</option>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php _e('Default search engine indexing behavior', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enhanced Robots Options', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="meta_nosnippet_enabled" value="1" <?php checked(TigerStyleSEO_Utils::get_option('meta_nosnippet_enabled', false)); ?>>
|
||||
<?php _e('No Snippet (nosnippet)', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Prevent search engines from showing text snippet in search results', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<label for="meta_max_snippet"><?php _e('Max Snippet Length:', 'tigerstyle-heat'); ?></label>
|
||||
<input type="number" id="meta_max_snippet" name="meta_max_snippet" value="<?php echo esc_attr(TigerStyleSEO_Utils::get_option('meta_max_snippet', '')); ?>" class="small-text" min="0" max="320">
|
||||
<span class="description"><?php _e('characters (0-320, empty for default)', 'tigerstyle-heat'); ?></span>
|
||||
|
||||
<br><br>
|
||||
<label for="meta_max_image_preview"><?php _e('Max Image Preview:', 'tigerstyle-heat'); ?></label>
|
||||
<select id="meta_max_image_preview" name="meta_max_image_preview">
|
||||
<option value="" <?php selected(TigerStyleSEO_Utils::get_option('meta_max_image_preview', ''), ''); ?>><?php _e('Default', 'tigerstyle-heat'); ?></option>
|
||||
<option value="none" <?php selected(TigerStyleSEO_Utils::get_option('meta_max_image_preview'), 'none'); ?>><?php _e('None', 'tigerstyle-heat'); ?></option>
|
||||
<option value="standard" <?php selected(TigerStyleSEO_Utils::get_option('meta_max_image_preview'), 'standard'); ?>><?php _e('Standard', 'tigerstyle-heat'); ?></option>
|
||||
<option value="large" <?php selected(TigerStyleSEO_Utils::get_option('meta_max_image_preview'), 'large'); ?>><?php _e('Large', 'tigerstyle-heat'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Technical Meta Tags -->
|
||||
<h3><?php _e('Technical Meta Tags', 'tigerstyle-heat'); ?></h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Standard Technical Tags', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="meta_charset_enabled" value="1" <?php checked(TigerStyleSEO_Utils::get_option('meta_charset_enabled', true)); ?>>
|
||||
<?php _e('Include Charset Declaration (UTF-8)', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
<input type="checkbox" name="meta_viewport_enabled" value="1" <?php checked(TigerStyleSEO_Utils::get_option('meta_viewport_enabled', true)); ?>>
|
||||
<?php _e('Include Viewport Meta Tag', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="meta_theme_color"><?php _e('Theme Color', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="color"
|
||||
id="meta_theme_color"
|
||||
name="meta_theme_color"
|
||||
value="<?php echo esc_attr(TigerStyleSEO_Utils::get_option('meta_theme_color', '#000000')); ?>"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Theme color for mobile browser UI', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Social Media Meta Tags -->
|
||||
<h3><?php _e('Social Media Meta Tags', 'tigerstyle-heat'); ?></h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="og_site_name"><?php _e('Open Graph Site Name', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="og_site_name"
|
||||
name="og_site_name"
|
||||
value="<?php echo esc_attr(TigerStyleSEO_Utils::get_option('og_site_name', get_bloginfo('name'))); ?>"
|
||||
class="regular-text"
|
||||
placeholder="<?php echo esc_attr(get_bloginfo('name')); ?>"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Site name for social media sharing', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="og_default_image"><?php _e('Default Open Graph Image', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="url"
|
||||
id="og_default_image"
|
||||
name="og_default_image"
|
||||
value="<?php echo esc_attr(TigerStyleSEO_Utils::get_option('og_default_image', '')); ?>"
|
||||
class="large-text"
|
||||
placeholder="https://example.com/default-social-image.jpg"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Default image for social media sharing (minimum 1200x630px recommended)', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="twitter_site"><?php _e('Twitter Site Username', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="twitter_site"
|
||||
name="twitter_site"
|
||||
value="<?php echo esc_attr(TigerStyleSEO_Utils::get_option('twitter_site', '')); ?>"
|
||||
class="regular-text"
|
||||
placeholder="@yourusername"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Your Twitter username for Twitter Card attribution', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Pattern-based Overrides -->
|
||||
<h3><?php _e('Pattern-based Overrides', 'tigerstyle-heat'); ?></h3>
|
||||
<div id="meta-patterns">
|
||||
<p class="description">
|
||||
<?php _e('Create specific meta tag rules for different URL patterns or post types. Examples: /blog/*, post_type:product', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
|
||||
<div id="pattern-container">
|
||||
<?php
|
||||
$patterns = TigerStyleSEO_Utils::get_option('meta_tag_patterns', array());
|
||||
if (!empty($patterns)) {
|
||||
foreach ($patterns as $index => $pattern) {
|
||||
?>
|
||||
<div class="pattern-row" style="border: 1px solid #ddd; padding: 15px; margin: 10px 0; background: #f9f9f9;">
|
||||
<h4><?php _e('Pattern Override', 'tigerstyle-heat'); ?> #<?php echo $index + 1; ?></h4>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<td>
|
||||
<label><?php _e('Pattern:', 'tigerstyle-heat'); ?></label>
|
||||
<input type="text" name="patterns[<?php echo $index; ?>][pattern]" value="<?php echo esc_attr($pattern['pattern']); ?>" class="regular-text" placeholder="/blog/* or post_type:product">
|
||||
</td>
|
||||
<td>
|
||||
<label><?php _e('Description:', 'tigerstyle-heat'); ?></label>
|
||||
<textarea name="patterns[<?php echo $index; ?>][description]" rows="2" class="large-text"><?php echo esc_textarea($pattern['description']); ?></textarea>
|
||||
</td>
|
||||
<td>
|
||||
<label><?php _e('Robots:', 'tigerstyle-heat'); ?></label>
|
||||
<select name="patterns[<?php echo $index; ?>][robots]">
|
||||
<option value="" <?php selected($pattern['robots'], ''); ?>><?php _e('Use Default', 'tigerstyle-heat'); ?></option>
|
||||
<option value="index,follow" <?php selected($pattern['robots'], 'index,follow'); ?>>index,follow</option>
|
||||
<option value="noindex,nofollow" <?php selected($pattern['robots'], 'noindex,nofollow'); ?>>noindex,nofollow</option>
|
||||
<option value="index,nofollow" <?php selected($pattern['robots'], 'index,nofollow'); ?>>index,nofollow</option>
|
||||
<option value="noindex,follow" <?php selected($pattern['robots'], 'noindex,follow'); ?>>noindex,follow</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="button remove-pattern"><?php _e('Remove', 'tigerstyle-heat'); ?></button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<button type="button" id="add-pattern" class="button"><?php _e('Add Pattern Override', 'tigerstyle-heat'); ?></button>
|
||||
</div>
|
||||
|
||||
<?php submit_button(__('Update Meta Tags Settings', 'tigerstyle-heat')); ?>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Character counter for description
|
||||
const descTextarea = document.getElementById('meta_description_default');
|
||||
const counter = document.getElementById('description-counter');
|
||||
|
||||
function updateCounter() {
|
||||
const length = descTextarea.value.length;
|
||||
counter.textContent = length + '/160';
|
||||
counter.style.color = length > 160 ? 'red' : length > 150 ? 'orange' : 'green';
|
||||
}
|
||||
|
||||
if (descTextarea && counter) {
|
||||
descTextarea.addEventListener('input', updateCounter);
|
||||
updateCounter(); // Initial count
|
||||
}
|
||||
|
||||
// Pattern management
|
||||
let patternIndex = <?php echo count($patterns); ?>;
|
||||
|
||||
document.getElementById('add-pattern').addEventListener('click', function() {
|
||||
const container = document.getElementById('pattern-container');
|
||||
const patternRow = document.createElement('div');
|
||||
patternRow.className = 'pattern-row';
|
||||
patternRow.style.cssText = 'border: 1px solid #ddd; padding: 15px; margin: 10px 0; background: #f9f9f9;';
|
||||
|
||||
patternRow.innerHTML = `
|
||||
<h4><?php _e('Pattern Override', 'tigerstyle-heat'); ?> #${patternIndex + 1}</h4>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<td>
|
||||
<label><?php _e('Pattern:', 'tigerstyle-heat'); ?></label>
|
||||
<input type="text" name="patterns[${patternIndex}][pattern]" value="" class="regular-text" placeholder="/blog/* or post_type:product">
|
||||
</td>
|
||||
<td>
|
||||
<label><?php _e('Description:', 'tigerstyle-heat'); ?></label>
|
||||
<textarea name="patterns[${patternIndex}][description]" rows="2" class="large-text"></textarea>
|
||||
</td>
|
||||
<td>
|
||||
<label><?php _e('Robots:', 'tigerstyle-heat'); ?></label>
|
||||
<select name="patterns[${patternIndex}][robots]">
|
||||
<option value=""><?php _e('Use Default', 'tigerstyle-heat'); ?></option>
|
||||
<option value="index,follow">index,follow</option>
|
||||
<option value="noindex,nofollow">noindex,nofollow</option>
|
||||
<option value="index,nofollow">index,nofollow</option>
|
||||
<option value="noindex,follow">noindex,follow</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="button remove-pattern"><?php _e('Remove', 'tigerstyle-heat'); ?></button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
container.appendChild(patternRow);
|
||||
patternIndex++;
|
||||
});
|
||||
|
||||
// Remove pattern functionality
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('remove-pattern')) {
|
||||
e.target.closest('.pattern-row').remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
248
admin/pages/visual-elements-gallery.php
Normal file
248
admin/pages/visual-elements-gallery.php
Normal file
@ -0,0 +1,248 @@
|
||||
<?php
|
||||
/**
|
||||
* Visual Elements Gallery Settings Page for TigerStyle Heat
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="wrap">
|
||||
<h1><?php _e('Visual Elements Gallery', 'tigerstyle-heat'); ?></h1>
|
||||
<p class="description"><?php _e('Optimize your galleries for Google\'s Visual Elements Gallery and carousel rich results. Enhance image galleries with advanced structured data for better search visibility.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<?php wp_nonce_field('update_visual_gallery', 'visual_gallery_nonce'); ?>
|
||||
<input type="hidden" name="action" value="update_visual_gallery">
|
||||
|
||||
<!-- Gallery Optimization -->
|
||||
<h2><?php _e('Gallery Optimization', 'tigerstyle-heat'); ?></h2>
|
||||
<div class="seo-section">
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enable Gallery Optimization', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="visual_gallery_enabled" value="1" <?php checked(TigerStyleSEO_Utils::get_option('visual_gallery_enabled', false)); ?>>
|
||||
<?php _e('Enable Visual Elements Gallery structured data for image galleries', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Adds structured data to WordPress galleries for enhanced search appearance and carousel eligibility.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Schema Type', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<select name="visual_gallery_type">
|
||||
<option value="ImageGallery" <?php selected(TigerStyleSEO_Utils::get_option('visual_gallery_type', 'ImageGallery'), 'ImageGallery'); ?>><?php _e('ImageGallery (Standard)', 'tigerstyle-heat'); ?></option>
|
||||
<option value="ItemList" <?php selected(TigerStyleSEO_Utils::get_option('visual_gallery_type', 'ImageGallery'), 'ItemList'); ?>><?php _e('ItemList (Carousel Optimized)', 'tigerstyle-heat'); ?></option>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php _e('ImageGallery: Standard gallery schema for general optimization.', 'tigerstyle-heat'); ?><br>
|
||||
<?php _e('ItemList: Optimized for Google\'s carousel rich results (requires minimum 3 images).', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Gallery Description', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<textarea name="visual_gallery_description" rows="3" class="large-text" placeholder="<?php _e('Leave empty for automatic descriptions', 'tigerstyle-heat'); ?>"><?php echo esc_textarea(TigerStyleSEO_Utils::get_option('visual_gallery_description', '')); ?></textarea>
|
||||
<p class="description"><?php _e('Custom description for all galleries. Leave empty to generate automatic descriptions based on post title and content.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Minimum Images for Optimization', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="number" name="visual_gallery_min_images" value="<?php echo esc_attr(TigerStyleSEO_Utils::get_option('visual_gallery_min_images', 3)); ?>" min="1" max="20" class="small-text">
|
||||
<p class="description"><?php _e('Minimum number of images required before adding structured data. Google recommends 3+ images for carousel rich results.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Carousel Rich Results -->
|
||||
<h2><?php _e('Carousel Rich Results (Beta)', 'tigerstyle-heat'); ?></h2>
|
||||
<div class="seo-section">
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enable Carousel Optimization', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="visual_gallery_carousel_enabled" value="1" <?php checked(TigerStyleSEO_Utils::get_option('visual_gallery_carousel_enabled', false)); ?>>
|
||||
<?php _e('Optimize galleries for Google\'s carousel rich results (Beta feature)', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Enables ItemList schema with sequential positioning for carousel eligibility. Requires minimum 3 images per gallery.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced Markup -->
|
||||
<h2><?php _e('Enhanced Markup', 'tigerstyle-heat'); ?></h2>
|
||||
<div class="seo-section">
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Add Microdata Attributes', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="visual_gallery_microdata" value="1" <?php checked(TigerStyleSEO_Utils::get_option('visual_gallery_microdata', false)); ?>>
|
||||
<?php _e('Add Schema.org microdata attributes to gallery HTML', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Enhances gallery HTML with itemscope and itemtype attributes for additional SEO signals.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enhanced Gallery Markup', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="visual_gallery_enhanced_markup" value="1" <?php checked(TigerStyleSEO_Utils::get_option('visual_gallery_enhanced_markup', false)); ?>>
|
||||
<?php _e('Add structured markup to gallery shortcode output', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Automatically enhances [gallery] shortcodes with structured data attributes.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php submit_button(__('Update Visual Gallery Settings', 'tigerstyle-heat')); ?>
|
||||
</form>
|
||||
|
||||
<!-- Gallery Detection Status -->
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Gallery Detection Status', 'tigerstyle-heat'); ?></h3>
|
||||
<div class="seo-setup-steps">
|
||||
<?php
|
||||
global $post;
|
||||
if ($post) {
|
||||
$gallery_module = TigerStyleSEO_VisualElementsGallery::instance();
|
||||
$performance_data = $gallery_module->get_gallery_performance_data($post->ID);
|
||||
|
||||
if ($performance_data) {
|
||||
echo '<p><strong>' . __('Current Page Analysis:', 'tigerstyle-heat') . '</strong></p>';
|
||||
echo '<ul>';
|
||||
echo '<li>' . sprintf(__('Total Galleries Found: %d', 'tigerstyle-heat'), $performance_data['total_galleries']) . '</li>';
|
||||
echo '<li>' . sprintf(__('Total Images: %d', 'tigerstyle-heat'), $performance_data['total_images']) . '</li>';
|
||||
echo '<li>' . sprintf(__('Qualifying Galleries: %d', 'tigerstyle-heat'), $performance_data['qualifying_galleries']) . '</li>';
|
||||
echo '<li>' . __('Schema Type: ', 'tigerstyle-heat') . $performance_data['schema_type'] . '</li>';
|
||||
echo '<li>' . __('Structured Data: ', 'tigerstyle-heat') . ($performance_data['has_structured_data'] ? '✅ Enabled' : '❌ Disabled') . '</li>';
|
||||
echo '</ul>';
|
||||
} else {
|
||||
echo '<p>' . __('No galleries detected on current page.', 'tigerstyle-heat') . '</p>';
|
||||
}
|
||||
} else {
|
||||
echo '<p>' . __('Navigate to a page with galleries to see detection status.', 'tigerstyle-heat') . '</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visual Elements Gallery Guide -->
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Google\'s Visual Elements Gallery Guide', 'tigerstyle-heat'); ?></h3>
|
||||
<div class="seo-setup-steps">
|
||||
<p><?php _e('Google\'s Visual Elements Gallery documents the 22 most common search result elements. Gallery optimization targets:', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<h4><?php _e('Rich Results Eligibility:', 'tigerstyle-heat'); ?></h4>
|
||||
<ul>
|
||||
<li><strong><?php _e('Image Carousels:', 'tigerstyle-heat'); ?></strong> <?php _e('Horizontal scrolling image results (Beta)', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong><?php _e('Rich Image Results:', 'tigerstyle-heat'); ?></strong> <?php _e('Enhanced image search appearances', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong><?php _e('Gallery Snippets:', 'tigerstyle-heat'); ?></strong> <?php _e('Image previews in search results', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong><?php _e('Visual Search:', 'tigerstyle-heat'); ?></strong> <?php _e('Google Lens and visual discovery', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
|
||||
<h4><?php _e('Implementation Requirements:', 'tigerstyle-heat'); ?></h4>
|
||||
<ul>
|
||||
<li><?php _e('Minimum 3 images per gallery for carousel eligibility', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('High-quality images with descriptive alt text', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Proper image metadata (title, description, caption)', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Valid structured data with no errors', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Mobile-friendly gallery presentation', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
|
||||
<h4><?php _e('Schema Types Supported:', 'tigerstyle-heat'); ?></h4>
|
||||
<ul>
|
||||
<li><strong><?php _e('ImageGallery:', 'tigerstyle-heat'); ?></strong> <?php _e('Standard gallery schema for general optimization', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong><?php _e('ItemList + ImageObject:', 'tigerstyle-heat'); ?></strong> <?php _e('Optimized for carousel rich results', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong><?php _e('ImageObject:', 'tigerstyle-heat'); ?></strong> <?php _e('Individual image metadata and licensing', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong><?php _e('Learn More:', 'tigerstyle-heat'); ?></strong><br>
|
||||
<a href="https://developers.google.com/search/docs/appearance/visual-elements-gallery" target="_blank">
|
||||
<?php _e('Google\'s Visual Elements Gallery Documentation', 'tigerstyle-heat'); ?>
|
||||
</a><br>
|
||||
<a href="https://developers.google.com/search/docs/appearance/structured-data/image" target="_blank">
|
||||
<?php _e('Image Structured Data Guidelines', 'tigerstyle-heat'); ?>
|
||||
</a><br>
|
||||
<a href="https://search.google.com/test/rich-results" target="_blank">
|
||||
<?php _e('Rich Results Testing Tool', 'tigerstyle-heat'); ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance Tips -->
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Gallery Performance Optimization', 'tigerstyle-heat'); ?></h3>
|
||||
<div class="seo-setup-steps">
|
||||
<p><?php _e('Maximize your gallery SEO performance with these best practices:', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<h4><?php _e('Image Optimization:', 'tigerstyle-heat'); ?></h4>
|
||||
<ul>
|
||||
<li><?php _e('Use descriptive, keyword-rich file names', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Optimize image sizes for web (typically under 500KB)', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Include alt text for all images', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Add captions and descriptions when relevant', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Use modern formats (WebP) when supported', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
|
||||
<h4><?php _e('Content Strategy:', 'tigerstyle-heat'); ?></h4>
|
||||
<ul>
|
||||
<li><?php _e('Create galleries with 3-15 related images', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Group thematically related images together', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Write compelling gallery titles and descriptions', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Include relevant keywords in image metadata', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Update galleries regularly with fresh content', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
|
||||
<h4><?php _e('Technical Excellence:', 'tigerstyle-heat'); ?></h4>
|
||||
<ul>
|
||||
<li><?php _e('Ensure fast page loading speeds', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Implement lazy loading for large galleries', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Test galleries on mobile devices', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Validate structured data with Google\'s tools', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Monitor performance in Search Console', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
// Show/hide carousel options based on schema type
|
||||
$('select[name="visual_gallery_type"]').change(function() {
|
||||
var $carouselSection = $('input[name="visual_gallery_carousel_enabled"]').closest('tr');
|
||||
if ($(this).val() === 'ItemList') {
|
||||
$carouselSection.show();
|
||||
} else {
|
||||
$carouselSection.hide();
|
||||
}
|
||||
}).trigger('change');
|
||||
|
||||
// Update minimum images help text based on carousel setting
|
||||
$('input[name="visual_gallery_carousel_enabled"]').change(function() {
|
||||
var $helpText = $('input[name="visual_gallery_min_images"]').next('.description');
|
||||
if ($(this).is(':checked')) {
|
||||
$helpText.html('<?php _e("Minimum images for carousel optimization. Google requires 3+ images for carousel rich results.", "tigerstyle-heat"); ?>');
|
||||
} else {
|
||||
$helpText.html('<?php _e("Minimum number of images required before adding structured data.", "tigerstyle-heat"); ?>');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
76
assets/css/admin.css
Normal file
76
assets/css/admin.css
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* TigerStyle Heat Admin Styles
|
||||
*/
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.seo-info-box {
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.seo-setup-steps {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.nav-tab-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.seo-results-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.seo-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.seo-section h3 {
|
||||
border-bottom: 2px solid #ddd;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.seo-item {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.seo-item h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.seo-item p {
|
||||
margin: 0 0 10px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.impact-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
background: url('data:image/svg+xml;charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path opacity=".25" d="M16 0 A16 16 0 0 0 16 32 A16 16 0 0 0 16 0 M16 4 A12 12 0 0 1 16 28 A12 12 0 0 1 16 4"/><path d="M16 0 A16 16 0 0 1 32 16 L28 16 A12 12 0 0 0 16 4z"><animateTransform attributeName="transform" type="rotate" from="0 16 16" to="360 16 16" dur="0.8s" repeatCount="indefinite"/></path></svg>') no-repeat center center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
}
|
||||
485
assets/js/admin.js
Normal file
485
assets/js/admin.js
Normal file
@ -0,0 +1,485 @@
|
||||
/**
|
||||
* TigerStyle Heat Admin JavaScript
|
||||
*/
|
||||
|
||||
// Tab switching functionality - Available immediately
|
||||
window.switchTab = function(evt, tabName) {
|
||||
var i, tabcontent, tablinks;
|
||||
tabcontent = document.getElementsByClassName("tab-content");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].classList.remove("active");
|
||||
}
|
||||
tablinks = document.getElementsByClassName("nav-tab");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].classList.remove("nav-tab-active");
|
||||
}
|
||||
document.getElementById(tabName).classList.add("active");
|
||||
evt.currentTarget.classList.add("nav-tab-active");
|
||||
};
|
||||
|
||||
jQuery(document).ready(function($) {
|
||||
|
||||
// SEO Health Checker functionality
|
||||
window.runSEOHealthCheck = function() {
|
||||
const button = document.getElementById('seo-health-check-btn');
|
||||
if (!button) return;
|
||||
|
||||
const originalText = button.textContent;
|
||||
|
||||
// Update button state
|
||||
button.textContent = tigerstyleSEO.strings.runningAnalysis;
|
||||
button.disabled = true;
|
||||
|
||||
// Show loading indicator
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.id = 'seo-loading';
|
||||
loadingDiv.innerHTML = '<div style="text-align: center; padding: 20px;"><div class="spinner is-active" style="float: none; margin: 0 auto;"></div><p>Analyzing SEO health...</p></div>';
|
||||
|
||||
const resultsContainer = document.getElementById('seo-health-results');
|
||||
if (resultsContainer) {
|
||||
resultsContainer.innerHTML = '';
|
||||
resultsContainer.appendChild(loadingDiv);
|
||||
}
|
||||
|
||||
// Make AJAX request
|
||||
$.ajax({
|
||||
url: tigerstyleSEO.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'seo_health_check',
|
||||
nonce: tigerstyleSEO.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
button.textContent = originalText;
|
||||
button.disabled = false;
|
||||
|
||||
if (response.success && resultsContainer) {
|
||||
displaySEOResults(response.data);
|
||||
} else {
|
||||
showError(tigerstyleSEO.strings.analysisFailed);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
button.textContent = originalText;
|
||||
button.disabled = false;
|
||||
showError(tigerstyleSEO.strings.networkError);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function displaySEOResults(data) {
|
||||
const resultsContainer = document.getElementById('seo-health-results');
|
||||
if (!resultsContainer) return;
|
||||
|
||||
let html = '<div class="seo-results-container">';
|
||||
html += '<div class="seo-score-display" style="text-align: center; margin-bottom: 30px;">';
|
||||
html += '<h3>SEO Health Score: ' + Math.round((data.score / data.max_score) * 100) + '%</h3>';
|
||||
html += '</div>';
|
||||
|
||||
if (data.recommendations && data.recommendations.length > 0) {
|
||||
html += '<div class="seo-section"><h3>Recommendations</h3>';
|
||||
data.recommendations.forEach(function(item) {
|
||||
html += createSEOItem(item);
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
resultsContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
function createSEOItem(item) {
|
||||
let html = '<div class="seo-item" style="border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin-bottom: 10px;">';
|
||||
html += '<h4>' + item.title + '</h4>';
|
||||
html += '<p>' + item.description + '</p>';
|
||||
if (item.action && item.action.url) {
|
||||
html += '<a href="' + item.action.url + '" class="button button-primary">' + item.action.text + '</a>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const resultsContainer = document.getElementById('seo-health-results');
|
||||
if (resultsContainer) {
|
||||
resultsContainer.innerHTML = '<div class="notice notice-error"><p>' + message + '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Cache Analysis functionality
|
||||
$('#analyze-cache-btn').on('click', function() {
|
||||
const button = $(this);
|
||||
const originalText = button.text();
|
||||
const loadingDiv = $('#cache-analysis-loading');
|
||||
const resultsDiv = $('#cache-analysis-results');
|
||||
|
||||
// Update button state
|
||||
button.text('Analyzing...').prop('disabled', true);
|
||||
loadingDiv.show();
|
||||
resultsDiv.hide().empty();
|
||||
|
||||
// Make AJAX request
|
||||
$.ajax({
|
||||
url: tigerstyleSEO.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_analyze_cache',
|
||||
nonce: tigerstyleSEO.cache_analysis_nonce
|
||||
},
|
||||
success: function(response) {
|
||||
button.text(originalText).prop('disabled', false);
|
||||
loadingDiv.hide();
|
||||
|
||||
if (response.success) {
|
||||
displayCacheAnalysisResults(response.data);
|
||||
resultsDiv.show();
|
||||
} else {
|
||||
showCacheAnalysisError('Analysis failed. Please try again.');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
button.text(originalText).prop('disabled', false);
|
||||
loadingDiv.hide();
|
||||
showCacheAnalysisError('Network error. Please check your connection and try again.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function displayCacheAnalysisResults(data) {
|
||||
const resultsDiv = $('#cache-analysis-results');
|
||||
|
||||
let html = '<div class="cache-analysis-container">';
|
||||
|
||||
// Summary section
|
||||
html += '<div class="cache-summary" style="background: #f0f8ff; border: 1px solid #007cba; border-radius: 5px; padding: 20px; margin-bottom: 20px;">';
|
||||
html += '<h4>📊 Analysis Summary</h4>';
|
||||
html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 15px;">';
|
||||
html += '<div><strong>Total Content Size:</strong><br><span style="color: #007cba; font-size: 18px;">' + data.total_size_formatted + '</span></div>';
|
||||
html += '<div><strong>Potential Savings:</strong><br><span style="color: #46b450; font-size: 18px;">' + data.potential_savings_formatted + '</span></div>';
|
||||
html += '<div><strong>Compression Ratio:</strong><br><span style="color: #46b450; font-size: 18px;">' + data.savings_percentage + '%</span></div>';
|
||||
html += '</div></div>';
|
||||
|
||||
// Compression estimates
|
||||
if (data.compression_estimates) {
|
||||
html += '<div class="compression-estimates" style="margin-bottom: 25px;">';
|
||||
html += '<h4>🎯 Compression Method Comparison</h4>';
|
||||
html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-top: 10px;">';
|
||||
|
||||
Object.values(data.compression_estimates).forEach(function(estimate) {
|
||||
html += '<div style="padding: 15px; background: #f9f9f9; border-radius: 5px; border-left: 4px solid #007cba;">';
|
||||
html += '<strong>' + estimate.method + '</strong><br>';
|
||||
html += '<span style="color: #666;">Compression: ' + estimate.ratio + '%</span><br>';
|
||||
html += '<span style="color: #46b450; font-weight: bold;">Saves: ' + estimate.savings_formatted + '</span>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// Pages analysis
|
||||
if (data.pages && data.pages.length > 0) {
|
||||
html += '<div class="pages-analysis" style="margin-bottom: 25px;">';
|
||||
html += '<h4>📄 Page Analysis</h4>';
|
||||
html += '<div style="overflow-x: auto;">';
|
||||
html += '<table class="wp-list-table widefat fixed striped" style="margin-top: 10px;">';
|
||||
html += '<thead><tr><th>Page</th><th>Size</th><th>Compression Ratio</th><th>Potential Savings</th></tr></thead>';
|
||||
html += '<tbody>';
|
||||
|
||||
data.pages.forEach(function(page) {
|
||||
html += '<tr>';
|
||||
html += '<td><strong>' + page.title + '</strong><br><small style="color: #666;">' + page.url + '</small></td>';
|
||||
html += '<td>' + page.size_formatted + '</td>';
|
||||
html += '<td><span style="color: #46b450;">' + page.compression_ratio + '%</span></td>';
|
||||
html += '<td><span style="color: #46b450; font-weight: bold;">' + page.savings_formatted + '</span></td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table></div></div>';
|
||||
}
|
||||
|
||||
// Assets analysis
|
||||
if (data.assets && data.assets.length > 0) {
|
||||
html += '<div class="assets-analysis" style="margin-bottom: 25px;">';
|
||||
html += '<h4>🗂️ Static Assets Analysis</h4>';
|
||||
html += '<div style="overflow-x: auto;">';
|
||||
html += '<table class="wp-list-table widefat fixed striped" style="margin-top: 10px;">';
|
||||
html += '<thead><tr><th>File</th><th>Type</th><th>Size</th><th>Compression Ratio</th><th>Potential Savings</th></tr></thead>';
|
||||
html += '<tbody>';
|
||||
|
||||
data.assets.forEach(function(asset) {
|
||||
html += '<tr>';
|
||||
html += '<td><strong>' + asset.filename + '</strong></td>';
|
||||
html += '<td><span class="asset-type-' + asset.type.toLowerCase() + '">' + asset.type + '</span></td>';
|
||||
html += '<td>' + asset.size_formatted + '</td>';
|
||||
html += '<td><span style="color: #46b450;">' + asset.compression_ratio + '%</span></td>';
|
||||
html += '<td><span style="color: #46b450; font-weight: bold;">' + asset.savings_formatted + '</span></td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table></div></div>';
|
||||
}
|
||||
|
||||
// Recommendations
|
||||
html += '<div class="cache-recommendations" style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px; padding: 20px;">';
|
||||
html += '<h4>💡 Recommendations</h4>';
|
||||
html += '<ul style="margin: 10px 0 0 20px;">';
|
||||
html += '<li><strong>Enable Compression:</strong> Turn on the compression system above to automatically compress all content.</li>';
|
||||
html += '<li><strong>Use Auto Mode:</strong> The automatic Brotli + Gzip fallback provides the best compatibility and performance.</li>';
|
||||
html += '<li><strong>Monitor Performance:</strong> Check the statistics regularly to track your bandwidth savings.</li>';
|
||||
if (data.savings_percentage > 60) {
|
||||
html += '<li><strong>Excellent Potential:</strong> Your site has high compression potential! Enable compression for significant performance gains.</li>';
|
||||
}
|
||||
html += '</ul></div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
resultsDiv.html(html);
|
||||
}
|
||||
|
||||
function showCacheAnalysisError(message) {
|
||||
const resultsDiv = $('#cache-analysis-results');
|
||||
resultsDiv.html('<div class="notice notice-error" style="margin: 0;"><p>' + message + '</p></div>').show();
|
||||
}
|
||||
|
||||
// AI Provider functionality
|
||||
$('.test-api-key').on('click', function() {
|
||||
const button = $(this);
|
||||
const provider = button.data('provider');
|
||||
const originalText = button.text();
|
||||
const resultsDiv = $('#provider-results-' + provider);
|
||||
|
||||
button.text('Testing...').prop('disabled', true);
|
||||
resultsDiv.html('<div class="notice notice-info"><p>Testing API key...</p></div>');
|
||||
|
||||
$.ajax({
|
||||
url: tigerstyleSEO.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_test_api_key',
|
||||
provider_name: provider,
|
||||
nonce: tigerstyleSEO.ai_nonce || tigerstyleSEO.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
button.text(originalText).prop('disabled', false);
|
||||
|
||||
if (response.success) {
|
||||
resultsDiv.html('<div class="notice notice-success"><p>' + response.message + '</p></div>');
|
||||
// Refresh the page to show updated models
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
resultsDiv.html('<div class="notice notice-error"><p>Error: ' + response.error + '</p></div>');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
button.text(originalText).prop('disabled', false);
|
||||
resultsDiv.html('<div class="notice notice-error"><p>Network error. Please try again.</p></div>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.refresh-models').on('click', function() {
|
||||
const button = $(this);
|
||||
const provider = button.data('provider');
|
||||
const originalText = button.text();
|
||||
const resultsDiv = $('#provider-results-' + provider);
|
||||
|
||||
button.text('Refreshing...').prop('disabled', true);
|
||||
resultsDiv.html('<div class="notice notice-info"><p>Refreshing models...</p></div>');
|
||||
|
||||
$.ajax({
|
||||
url: tigerstyleSEO.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_refresh_models',
|
||||
provider_name: provider,
|
||||
nonce: tigerstyleSEO.ai_nonce || tigerstyleSEO.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
button.text(originalText).prop('disabled', false);
|
||||
|
||||
if (response.success) {
|
||||
resultsDiv.html('<div class="notice notice-success"><p>' + response.message + '</p></div>');
|
||||
// Refresh the page to show updated models
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
resultsDiv.html('<div class="notice notice-error"><p>Error: ' + response.error + '</p></div>');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
button.text(originalText).prop('disabled', false);
|
||||
resultsDiv.html('<div class="notice notice-error"><p>Network error. Please try again.</p></div>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.delete-provider').on('click', function() {
|
||||
const button = $(this);
|
||||
const provider = button.data('provider');
|
||||
|
||||
if (!confirm('Are you sure you want to delete the "' + provider + '" provider? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = button.text();
|
||||
button.text('Deleting...').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: tigerstyleSEO.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_delete_api_key',
|
||||
provider_name: provider,
|
||||
nonce: tigerstyleSEO.ai_nonce || tigerstyleSEO.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Remove the provider card from the page
|
||||
button.closest('.provider-card').fadeOut(function() {
|
||||
$(this).remove();
|
||||
});
|
||||
} else {
|
||||
button.text(originalText).prop('disabled', false);
|
||||
alert('Error: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
button.text(originalText).prop('disabled', false);
|
||||
alert('Network error. Please try again.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.model-toggle').on('change', function() {
|
||||
const checkbox = $(this);
|
||||
const provider = checkbox.data('provider');
|
||||
const model = checkbox.data('model');
|
||||
const enabled = checkbox.is(':checked');
|
||||
|
||||
// Create a hidden form and submit it
|
||||
const form = $('<form method="post" action="' + tigerstyleSEO.ajaxurl.replace('admin-ajax.php', 'admin-post.php') + '">');
|
||||
form.append('<input type="hidden" name="action" value="update_ai_provider_settings">');
|
||||
form.append('<input type="hidden" name="ai_action" value="toggle_model">');
|
||||
form.append('<input type="hidden" name="provider_name" value="' + provider + '">');
|
||||
form.append('<input type="hidden" name="model_id" value="' + model + '">');
|
||||
if (enabled) {
|
||||
form.append('<input type="hidden" name="enabled" value="1">');
|
||||
}
|
||||
form.append('<input type="hidden" name="ai_provider_nonce" value="' + (tigerstyleSEO.ai_nonce || tigerstyleSEO.nonce) + '">');
|
||||
|
||||
$('body').append(form);
|
||||
form.submit();
|
||||
});
|
||||
|
||||
// AI Chat Interface functionality
|
||||
$('#ai-chat-provider').on('change', function() {
|
||||
const hasProvider = $(this).val() !== '';
|
||||
$('#ai-chat-send').prop('disabled', !hasProvider);
|
||||
|
||||
if (hasProvider) {
|
||||
$('#ai-chat-status').text('Ready to chat with ' + $(this).find('option:selected').text());
|
||||
} else {
|
||||
$('#ai-chat-status').text('');
|
||||
}
|
||||
});
|
||||
|
||||
$('#ai-chat-clear').on('click', function() {
|
||||
$('#ai-chat-messages').html('<div class="chat-message system-message" style="color: #666; font-style: italic;">💡 Ask me anything about SEO, content optimization, meta tags, or website improvement!</div>');
|
||||
});
|
||||
|
||||
$('#ai-chat-send').on('click', function() {
|
||||
sendChatMessage();
|
||||
});
|
||||
|
||||
$('#ai-chat-input').on('keypress', function(e) {
|
||||
if (e.which === 13 && e.ctrlKey) { // Ctrl+Enter
|
||||
sendChatMessage();
|
||||
}
|
||||
});
|
||||
|
||||
function sendChatMessage() {
|
||||
const provider = $('#ai-chat-provider').val();
|
||||
const message = $('#ai-chat-input').val().trim();
|
||||
|
||||
if (!provider || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [providerName, modelId] = provider.split('|');
|
||||
|
||||
// Add user message to chat
|
||||
addChatMessage('user', message);
|
||||
$('#ai-chat-input').val('');
|
||||
|
||||
// Show loading
|
||||
const loadingId = 'loading_' + Date.now();
|
||||
addChatMessage('assistant', '<div id="' + loadingId + '" style="display: flex; align-items: center; gap: 10px;"><div class="spinner is-active" style="float: none; margin: 0;"></div>Thinking...</div>');
|
||||
|
||||
// Send AJAX request
|
||||
$.ajax({
|
||||
url: tigerstyleSEO.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_ai_chat',
|
||||
provider_name: providerName,
|
||||
model_id: modelId,
|
||||
message: message,
|
||||
nonce: tigerstyleSEO.ai_nonce || tigerstyleSEO.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
$('#' + loadingId).closest('.chat-message').remove();
|
||||
|
||||
if (response.success) {
|
||||
addChatMessage('assistant', response.data.response);
|
||||
} else {
|
||||
addChatMessage('error', 'Error: ' + (response.data || 'Failed to get response'));
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$('#' + loadingId).closest('.chat-message').remove();
|
||||
addChatMessage('error', 'Network error. Please try again.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addChatMessage(sender, content) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
let messageClass = 'chat-message';
|
||||
let senderIcon = '';
|
||||
let senderLabel = '';
|
||||
|
||||
switch(sender) {
|
||||
case 'user':
|
||||
messageClass += ' user-message';
|
||||
senderIcon = '🙋♀️';
|
||||
senderLabel = 'You';
|
||||
break;
|
||||
case 'assistant':
|
||||
messageClass += ' assistant-message';
|
||||
senderIcon = '🤖';
|
||||
senderLabel = 'AI Assistant';
|
||||
break;
|
||||
case 'error':
|
||||
messageClass += ' error-message';
|
||||
senderIcon = '⚠️';
|
||||
senderLabel = 'Error';
|
||||
break;
|
||||
}
|
||||
|
||||
const messageHtml = '<div class="' + messageClass + '" style="margin-bottom: 15px; padding: 10px; border-radius: 5px; ' +
|
||||
(sender === 'user' ? 'background: #e3f2fd; margin-left: 20px;' :
|
||||
sender === 'error' ? 'background: #ffebee; border-left: 3px solid #f44336;' :
|
||||
'background: white; border: 1px solid #e0e0e0;') + '">' +
|
||||
'<div style="font-size: 12px; color: #666; margin-bottom: 5px;">' +
|
||||
'<strong>' + senderIcon + ' ' + senderLabel + '</strong> <span style="float: right;">' + timestamp + '</span>' +
|
||||
'</div>' +
|
||||
'<div style="line-height: 1.5;">' + content + '</div>' +
|
||||
'</div>';
|
||||
|
||||
$('#ai-chat-messages').append(messageHtml);
|
||||
$('#ai-chat-messages').scrollTop($('#ai-chat-messages')[0].scrollHeight);
|
||||
}
|
||||
});
|
||||
183
assets/svg/opengraph-preview-layout.svg
Normal file
183
assets/svg/opengraph-preview-layout.svg
Normal file
@ -0,0 +1,183 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 480" class="opengraph-preview-diagram">
|
||||
<defs>
|
||||
<pattern id="imagePattern" patternUnits="userSpaceOnUse" width="20" height="20">
|
||||
<rect width="20" height="20" fill="#f0f0f0"/>
|
||||
<line x1="0" y1="0" x2="20" y2="20" stroke="#ddd" stroke-width="1"/>
|
||||
<line x1="20" y1="0" x2="0" y2="20" stroke="#ddd" stroke-width="1"/>
|
||||
</pattern>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#ff6b35"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="350" y="25" text-anchor="middle" fill="#2c3e50" font-size="18" font-weight="bold" font-family="Arial">OpenGraph Social Media Previews</text>
|
||||
|
||||
<!-- Facebook Preview -->
|
||||
<g id="facebook-preview">
|
||||
<text x="20" y="55" fill="#1877f2" font-size="14" font-weight="bold" font-family="Arial">Facebook / Meta</text>
|
||||
|
||||
<!-- Main container -->
|
||||
<rect x="20" y="65" width="320" height="140" rx="8" fill="#ffffff" stroke="#e1e5e9" stroke-width="2"/>
|
||||
|
||||
<!-- Image area -->
|
||||
<rect x="30" y="75" width="120" height="75" rx="4" fill="url(#imagePattern)" stroke="#ddd" stroke-width="1"/>
|
||||
<text x="90" y="108" text-anchor="middle" fill="#999" font-size="9" font-family="Arial">og:image</text>
|
||||
<text x="90" y="118" text-anchor="middle" fill="#999" font-size="7" font-family="Arial">1200×630px</text>
|
||||
|
||||
<!-- Content area -->
|
||||
<g transform="translate(160, 80)">
|
||||
<!-- Title -->
|
||||
<rect x="0" y="0" width="160" height="16" rx="2" fill="#1877f2" opacity="0.1"/>
|
||||
<text x="4" y="12" fill="#1877f2" font-size="11" font-weight="bold" font-family="Arial">og:title - Full article title shown</text>
|
||||
|
||||
<!-- URL/Site -->
|
||||
<rect x="0" y="22" width="120" height="12" rx="2" fill="#65676b" opacity="0.1"/>
|
||||
<text x="4" y="30" fill="#65676b" font-size="9" font-family="Arial">og:site_name • domain.com</text>
|
||||
|
||||
<!-- Description -->
|
||||
<rect x="0" y="40" width="160" height="40" rx="2" fill="#65676b" opacity="0.1"/>
|
||||
<text x="4" y="52" fill="#65676b" font-size="9" font-family="Arial">og:description - Complete description</text>
|
||||
<text x="4" y="62" fill="#999" font-size="8" font-family="Arial">shows multiple lines of text content</text>
|
||||
<text x="4" y="72" fill="#999" font-size="8" font-family="Arial">up to about 300 characters...</text>
|
||||
</g>
|
||||
|
||||
<!-- Platform indicator -->
|
||||
<circle cx="30" cy="185" r="6" fill="#1877f2"/>
|
||||
<text x="30" y="189" text-anchor="middle" fill="white" font-size="8" font-weight="bold" font-family="Arial">f</text>
|
||||
<text x="42" y="189" fill="#65676b" font-size="9" font-family="Arial">Rich preview - all meta tags used</text>
|
||||
</g>
|
||||
|
||||
<!-- X/Twitter Preview -->
|
||||
<g id="twitter-preview">
|
||||
<text x="360" y="55" fill="#000" font-size="14" font-weight="bold" font-family="Arial">X (Twitter)</text>
|
||||
|
||||
<!-- Main container - simpler layout -->
|
||||
<rect x="360" y="65" width="320" height="140" rx="8" fill="#ffffff" stroke="#cfd9de" stroke-width="2"/>
|
||||
|
||||
<!-- Large image area (Twitter card style) -->
|
||||
<rect x="370" y="75" width="300" height="90" rx="4" fill="url(#imagePattern)" stroke="#cfd9de" stroke-width="1"/>
|
||||
<text x="520" y="115" text-anchor="middle" fill="#999" font-size="9" font-family="Arial">og:image (large card)</text>
|
||||
<text x="520" y="125" text-anchor="middle" fill="#999" font-size="7" font-family="Arial">1200×675px preferred</text>
|
||||
|
||||
<!-- Minimal text overlay -->
|
||||
<rect x="375" y="150" width="120" height="12" rx="2" fill="#0f1419" opacity="0.1"/>
|
||||
<text x="378" y="158" fill="#0f1419" font-size="9" font-weight="bold" font-family="Arial">og:site_name</text>
|
||||
|
||||
<rect x="375" y="168" width="200" height="10" rx="2" fill="#536471" opacity="0.1"/>
|
||||
<text x="378" y="175" fill="#536471" font-size="8" font-family="Arial">og:title (truncated if too long)</text>
|
||||
|
||||
<!-- Platform indicator -->
|
||||
<rect x="370" y="185" width="12" height="12" rx="2" fill="#000"/>
|
||||
<text x="376" y="194" text-anchor="middle" fill="white" font-size="9" font-weight="bold" font-family="Arial">𝕏</text>
|
||||
<text x="388" y="194" fill="#536471" font-size="9" font-family="Arial">Image-focused, minimal text</text>
|
||||
</g>
|
||||
|
||||
<!-- Discord Preview -->
|
||||
<g id="discord-preview">
|
||||
<text x="20" y="240" fill="#5865f2" font-size="14" font-weight="bold" font-family="Arial">Discord</text>
|
||||
|
||||
<!-- Main container -->
|
||||
<rect x="20" y="250" width="320" height="120" rx="8" fill="#36393f" stroke="#5865f2" stroke-width="2" stroke-dasharray="0,0,0,0" opacity="0.9"/>
|
||||
|
||||
<!-- Left accent bar -->
|
||||
<rect x="20" y="250" width="4" height="120" rx="2" fill="#5865f2"/>
|
||||
|
||||
<!-- Content area -->
|
||||
<g transform="translate(35, 260)">
|
||||
<!-- Site name -->
|
||||
<rect x="0" y="0" width="80" height="12" rx="2" fill="#5865f2" opacity="0.2"/>
|
||||
<text x="4" y="9" fill="#00b0f4" font-size="9" font-weight="bold" font-family="Arial">og:site_name</text>
|
||||
|
||||
<!-- Title -->
|
||||
<rect x="0" y="18" width="160" height="16" rx="2" fill="#ffffff" opacity="0.1"/>
|
||||
<text x="4" y="30" fill="#ffffff" font-size="11" font-weight="bold" font-family="Arial">og:title</text>
|
||||
|
||||
<!-- Description -->
|
||||
<rect x="0" y="40" width="180" height="24" rx="2" fill="#b9bbbe" opacity="0.1"/>
|
||||
<text x="4" y="52" fill="#b9bbbe" font-size="9" font-family="Arial">og:description (2 lines)</text>
|
||||
<text x="4" y="62" fill="#b9bbbe" font-size="8" font-family="Arial">Brief excerpt shown...</text>
|
||||
|
||||
<!-- Image area - right side -->
|
||||
<rect x="200" y="0" width="80" height="60" rx="4" fill="url(#imagePattern)" stroke="#5865f2" stroke-width="1"/>
|
||||
<text x="240" y="28" text-anchor="middle" fill="#999" font-size="8" font-family="Arial">og:image</text>
|
||||
<text x="240" y="38" text-anchor="middle" fill="#999" font-size="6" font-family="Arial">thumbnail</text>
|
||||
</g>
|
||||
|
||||
<!-- Platform indicator -->
|
||||
<rect x="30" y="350" width="12" height="12" rx="2" fill="#5865f2"/>
|
||||
<text x="36" y="359" text-anchor="middle" fill="white" font-size="8" font-weight="bold" font-family="Arial">D</text>
|
||||
<text x="48" y="359" fill="#b9bbbe" font-size="9" font-family="Arial">Embedded card with accent bar</text>
|
||||
</g>
|
||||
|
||||
<!-- LinkedIn Preview -->
|
||||
<g id="linkedin-preview">
|
||||
<text x="360" y="240" fill="#0a66c2" font-size="14" font-weight="bold" font-family="Arial">LinkedIn</text>
|
||||
|
||||
<!-- Main container -->
|
||||
<rect x="360" y="250" width="320" height="120" rx="8" fill="#ffffff" stroke="#00000020" stroke-width="2"/>
|
||||
|
||||
<!-- Image area - left side -->
|
||||
<rect x="370" y="260" width="90" height="60" rx="4" fill="url(#imagePattern)" stroke="#ddd" stroke-width="1"/>
|
||||
<text x="415" y="287" text-anchor="middle" fill="#999" font-size="8" font-family="Arial">og:image</text>
|
||||
|
||||
<!-- Content area -->
|
||||
<g transform="translate(470, 265)">
|
||||
<!-- Title -->
|
||||
<rect x="0" y="0" width="180" height="16" rx="2" fill="#0a66c2" opacity="0.1"/>
|
||||
<text x="4" y="12" fill="#0a66c2" font-size="11" font-weight="bold" font-family="Arial">og:title</text>
|
||||
|
||||
<!-- Description -->
|
||||
<rect x="0" y="22" width="180" height="28" rx="2" fill="#00000080" opacity="0.1"/>
|
||||
<text x="4" y="34" fill="#00000080" font-size="9" font-family="Arial">og:description</text>
|
||||
<text x="4" y="44" fill="#999" font-size="8" font-family="Arial">Professional context shown...</text>
|
||||
|
||||
<!-- Site name -->
|
||||
<rect x="0" y="56" width="100" height="10" rx="2" fill="#00000060" opacity="0.1"/>
|
||||
<text x="4" y="63" fill="#00000060" font-size="8" font-family="Arial">og:site_name</text>
|
||||
</g>
|
||||
|
||||
<!-- Platform indicator -->
|
||||
<rect x="370" y="350" width="12" height="12" rx="1" fill="#0a66c2"/>
|
||||
<text x="376" y="359" text-anchor="middle" fill="white" font-size="8" font-weight="bold" font-family="Arial">in</text>
|
||||
<text x="388" y="359" fill="#00000080" font-size="9" font-family="Arial">Professional sharing format</text>
|
||||
</g>
|
||||
|
||||
<!-- Essential Meta Tags Reference -->
|
||||
<rect x="20" y="390" width="320" height="80" rx="4" fill="#f8f9fa" stroke="#e9ecef" stroke-width="1"/>
|
||||
<text x="30" y="408" fill="#495057" font-size="12" font-weight="bold">Essential OpenGraph Meta Tags</text>
|
||||
|
||||
<text x="30" y="423" fill="#0066cc" font-size="10" font-weight="bold">og:title</text>
|
||||
<text x="85" y="423" fill="#666" font-size="9">Page title (60 chars max)</text>
|
||||
|
||||
<text x="30" y="438" fill="#0066cc" font-size="10" font-weight="bold">og:description</text>
|
||||
<text x="105" y="438" fill="#666" font-size="9">SEO description (300 chars)</text>
|
||||
|
||||
<text x="30" y="453" fill="#0066cc" font-size="10" font-weight="bold">og:image</text>
|
||||
<text x="85" y="453" fill="#666" font-size="9">Featured image (1200×630px)</text>
|
||||
|
||||
<text x="180" y="423" fill="#0066cc" font-size="10" font-weight="bold">og:site_name</text>
|
||||
<text x="245" y="423" fill="#666" font-size="9">Website name</text>
|
||||
|
||||
<text x="180" y="438" fill="#0066cc" font-size="10" font-weight="bold">og:url</text>
|
||||
<text x="215" y="438" fill="#666" font-size="9">Canonical page URL</text>
|
||||
|
||||
<text x="180" y="453" fill="#0066cc" font-size="10" font-weight="bold">og:type</text>
|
||||
<text x="220" y="453" fill="#666" font-size="9">Content type (article/website)</text>
|
||||
|
||||
<!-- Testing Tool Reference -->
|
||||
<rect x="360" y="390" width="320" height="80" rx="4" fill="#e8f4fd" stroke="#3498db" stroke-width="1"/>
|
||||
<text x="370" y="408" fill="#2980b9" font-size="12" font-weight="bold">🔧 Test Your OpenGraph Tags</text>
|
||||
|
||||
<text x="370" y="428" fill="#2c3e50" font-size="11" font-weight="bold">opengraph.xyz</text>
|
||||
<text x="370" y="442" fill="#34495e" font-size="10">Live preview tool - test how your content</text>
|
||||
<text x="370" y="454" fill="#34495e" font-size="10">appears across all social platforms</text>
|
||||
|
||||
<rect x="570" y="418" width="80" height="20" rx="10" fill="#3498db" opacity="0.1"/>
|
||||
<text x="610" y="431" text-anchor="middle" fill="#2980b9" font-size="9" font-weight="bold">Visit Tool →</text>
|
||||
|
||||
<!-- Key insight callout -->
|
||||
<rect x="20" y="480" width="660" height="18" rx="9" fill="#fff3cd" stroke="#ffeaa7" stroke-width="1"/>
|
||||
<text x="30" y="492" fill="#856404" font-size="11" font-weight="bold">💡 Pro Tip:</text>
|
||||
<text x="90" y="492" fill="#856404" font-size="11">Each platform displays differently - test at opengraph.xyz before publishing!</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
49
build.sh
Executable file
49
build.sh
Executable file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a clean WordPress-installable release ZIP for this plugin.
|
||||
# Reads .distignore to decide what gets excluded.
|
||||
# Usage: ./build.sh [version-override]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_SLUG="$(basename "$SCRIPT_DIR")"
|
||||
MAIN_FILE="$SCRIPT_DIR/${PLUGIN_SLUG}.php"
|
||||
OUT_DIR="$SCRIPT_DIR/build"
|
||||
|
||||
if [[ ! -f "$MAIN_FILE" ]]; then
|
||||
echo "ERROR: main plugin file $MAIN_FILE not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="${1:-$(grep -E "^\s*\*\s*Version:" "$MAIN_FILE" | head -1 | awk '{print $NF}')}"
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "ERROR: could not determine version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ZIP_NAME="${PLUGIN_SLUG}-${VERSION}.zip"
|
||||
OUT_ZIP="$OUT_DIR/$ZIP_NAME"
|
||||
STAGE="$(mktemp -d -t "${PLUGIN_SLUG}-build-XXXXXX")"
|
||||
trap "rm -rf '$STAGE'" EXIT
|
||||
|
||||
echo "Building $PLUGIN_SLUG v$VERSION → $OUT_ZIP"
|
||||
|
||||
EXCLUDE_ARGS=(--exclude='.git')
|
||||
if [[ -f "$SCRIPT_DIR/.distignore" ]]; then
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "${line// }" ]] && continue
|
||||
EXCLUDE_ARGS+=(--exclude="$line")
|
||||
done < "$SCRIPT_DIR/.distignore"
|
||||
fi
|
||||
|
||||
mkdir -p "$STAGE/$PLUGIN_SLUG"
|
||||
rsync -a "${EXCLUDE_ARGS[@]}" "$SCRIPT_DIR/" "$STAGE/$PLUGIN_SLUG/"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
rm -f "$OUT_ZIP"
|
||||
( cd "$STAGE" && zip -rq "$OUT_ZIP" "$PLUGIN_SLUG" )
|
||||
|
||||
SIZE=$(numfmt --to=iec --suffix=B "$(stat -c '%s' "$OUT_ZIP")")
|
||||
COUNT=$(unzip -Z1 "$OUT_ZIP" | wc -l)
|
||||
echo "✓ Built: $ZIP_NAME ($SIZE, $COUNT files)"
|
||||
458
docs/BACKUP_MODULE_DOCUMENTATION.md
Normal file
458
docs/BACKUP_MODULE_DOCUMENTATION.md
Normal file
@ -0,0 +1,458 @@
|
||||
# TigerStyle Heat - Enterprise Backup & Restore Module
|
||||
|
||||
## Overview
|
||||
|
||||
The TigerStyle Heat Backup & Restore module is an enterprise-grade backup and restore system for WordPress that provides comprehensive site protection with advanced features including chunked processing, multiple compression formats, S3 storage integration, and sophisticated validation systems.
|
||||
|
||||
## Features
|
||||
|
||||
### Core Backup Features
|
||||
- **Complete WordPress Backup**: Files + Database with selective inclusion options
|
||||
- **Chunked Processing**: Handles large sites without memory/timeout issues
|
||||
- **Multiple Compression**: ZIP, TAR.GZ, TAR.BZ2 with graceful fallback
|
||||
- **Progress Tracking**: Real-time AJAX progress updates
|
||||
- **Backup Validation**: Comprehensive integrity checking with checksums
|
||||
- **Backup Manifest**: Detailed metadata for each backup
|
||||
|
||||
### Storage Options
|
||||
- **Local Storage**: WordPress uploads directory with organized structure
|
||||
- **S3 Compatible Storage**: Full S3 integration with metadata and encryption
|
||||
- **S3-Compatible**: Support for MinIO, DigitalOcean Spaces, etc.
|
||||
- **Storage Stats**: Usage tracking and space monitoring
|
||||
|
||||
### Restore Features
|
||||
- **Safe Restoration**: Validation before restore with rollback backup creation
|
||||
- **Selective Restore**: Choose files, database, or both
|
||||
- **Progress Tracking**: Real-time restore progress monitoring
|
||||
- **URL Management**: Automatic site URL handling during restore
|
||||
|
||||
### Advanced Features
|
||||
- **WordPress Reset**: Remove default content to prepare for restore
|
||||
- **Database Reset**: Complete database wipe with multi-level confirmation
|
||||
- **Scheduled Backups**: Automated backups with retention policies
|
||||
- **Email Notifications**: Success/failure notifications
|
||||
- **Comprehensive Logging**: Multi-level logging with export capabilities
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Classes
|
||||
|
||||
#### TigerStyleSEO_Backup_Restore
|
||||
Main module coordinator that orchestrates all backup operations.
|
||||
|
||||
```php
|
||||
$backup_module = tigerstyle_heat()->get_module('backup_restore');
|
||||
```
|
||||
|
||||
#### TigerStyleSEO_Backup_Engine
|
||||
Core backup processing engine with chunked file handling.
|
||||
|
||||
**Key Methods:**
|
||||
- `create_backup($options)` - Create new backup
|
||||
- `backup_database()` - MySQL dump with table-level processing
|
||||
- `backup_files()` - Recursive file backup with exclusions
|
||||
- `create_backup_manifest()` - Generate backup metadata
|
||||
|
||||
#### TigerStyleSEO_Restore_Engine
|
||||
Restoration engine with safety checks and validation.
|
||||
|
||||
**Key Methods:**
|
||||
- `restore_backup($options)` - Restore from backup
|
||||
- `validate_backup_integrity()` - Pre-restore validation
|
||||
- `create_rollback_backup()` - Safety backup before restore
|
||||
|
||||
#### TigerStyleSEO_Storage_Manager
|
||||
Multi-backend storage management.
|
||||
|
||||
**Storage Backends:**
|
||||
- Local filesystem storage
|
||||
- S3 Compatible Storage with encryption
|
||||
- S3-compatible services
|
||||
|
||||
#### TigerStyleSEO_Compression_Manager
|
||||
Multi-format compression with fallback.
|
||||
|
||||
**Supported Formats:**
|
||||
- ZIP (preferred)
|
||||
- TAR.GZ
|
||||
- TAR.BZ2
|
||||
- TAR (uncompressed fallback)
|
||||
|
||||
#### TigerStyleSEO_Backup_Logger
|
||||
Enterprise logging system with multiple outputs.
|
||||
|
||||
**Log Levels:**
|
||||
- Debug, Info, Warning, Error, Critical
|
||||
- File-based logging with rotation
|
||||
- Database storage for recent logs
|
||||
- Email notifications for critical errors
|
||||
|
||||
#### TigerStyleSEO_Backup_Validator
|
||||
Comprehensive backup validation system.
|
||||
|
||||
**Validation Checks:**
|
||||
- File integrity and checksums
|
||||
- Database structure validation
|
||||
- Manifest verification
|
||||
- Cross-platform compatibility
|
||||
|
||||
#### TigerStyleSEO_Backup_Scheduler
|
||||
Automated backup scheduling with retention policies.
|
||||
|
||||
**Features:**
|
||||
- Hourly, daily, weekly, monthly schedules
|
||||
- System load checking
|
||||
- Retention policies
|
||||
- Failure tracking
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Backup Metadata Table
|
||||
```sql
|
||||
CREATE TABLE wp_tigerstyle_backup_metadata (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
backup_id varchar(255) NOT NULL,
|
||||
storage_type varchar(50) NOT NULL,
|
||||
file_path text,
|
||||
s3_bucket varchar(255),
|
||||
s3_key varchar(500),
|
||||
s3_url text,
|
||||
file_size bigint(20) NOT NULL DEFAULT 0,
|
||||
created_at datetime NOT NULL,
|
||||
metadata_json text,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY backup_id (backup_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Backup Logs Table
|
||||
```sql
|
||||
CREATE TABLE wp_tigerstyle_backup_logs (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
level varchar(20) NOT NULL,
|
||||
message text NOT NULL,
|
||||
context longtext,
|
||||
user_id int(11) DEFAULT 0,
|
||||
ip_address varchar(45),
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY level (level),
|
||||
KEY created_at (created_at)
|
||||
);
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Default Settings
|
||||
```php
|
||||
$default_settings = array(
|
||||
'compression' => 'zip',
|
||||
'storage_location' => 'local',
|
||||
'schedule_enabled' => false,
|
||||
'schedule_frequency' => 'daily',
|
||||
'retention_days' => 30,
|
||||
'include_files' => true,
|
||||
'include_database' => true,
|
||||
'chunk_size' => 5, // MB
|
||||
's3_bucket' => '',
|
||||
's3_access_key' => '',
|
||||
's3_secret_key' => '',
|
||||
's3_region' => 'us-east-1',
|
||||
's3_endpoint' => '',
|
||||
'email_notifications' => false,
|
||||
'notification_email' => get_option('admin_email')
|
||||
);
|
||||
```
|
||||
|
||||
### S3 Configuration
|
||||
For AWS S3 or S3-compatible services:
|
||||
|
||||
```php
|
||||
$s3_settings = array(
|
||||
's3_bucket' => 'your-backup-bucket',
|
||||
's3_access_key' => 'AKIA...',
|
||||
's3_secret_key' => 'your-secret-key',
|
||||
's3_region' => 'us-east-1',
|
||||
's3_endpoint' => 'https://s3.amazonaws.com' // Optional for S3-compatible
|
||||
);
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
### Creating a Backup
|
||||
```php
|
||||
$backup_engine = new TigerStyleSEO_Backup_Engine();
|
||||
$backup_id = $backup_engine->create_backup(array(
|
||||
'type' => 'full',
|
||||
'compression' => 'zip',
|
||||
'storage_location' => 'local',
|
||||
'include_files' => true,
|
||||
'include_database' => true,
|
||||
'description' => 'Manual backup before update'
|
||||
));
|
||||
```
|
||||
|
||||
### Restoring from Backup
|
||||
```php
|
||||
$restore_engine = new TigerStyleSEO_Restore_Engine();
|
||||
$restore_id = $restore_engine->restore_backup(array(
|
||||
'backup_id' => $backup_id,
|
||||
'restore_files' => true,
|
||||
'restore_database' => true,
|
||||
'create_rollback' => true,
|
||||
'validate_before_restore' => true
|
||||
));
|
||||
```
|
||||
|
||||
### Validating a Backup
|
||||
```php
|
||||
$validator = new TigerStyleSEO_Backup_Validator();
|
||||
$result = $validator->validate_backup($backup_id);
|
||||
|
||||
if ($result['valid']) {
|
||||
echo "Backup is valid";
|
||||
} else {
|
||||
foreach ($result['errors'] as $error) {
|
||||
echo "Error: " . $error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AJAX Endpoints
|
||||
|
||||
### Backup Operations
|
||||
- `tigerstyle_create_backup` - Create new backup
|
||||
- `tigerstyle_restore_backup` - Restore from backup
|
||||
- `tigerstyle_backup_progress` - Get operation progress
|
||||
- `tigerstyle_validate_backup` - Validate backup integrity
|
||||
|
||||
### Management Operations
|
||||
- `tigerstyle_delete_backup` - Delete backup
|
||||
- `tigerstyle_download_backup` - Download backup file
|
||||
- `tigerstyle_upload_backup` - Upload backup file
|
||||
|
||||
### Advanced Operations
|
||||
- `tigerstyle_reset_wordpress` - Reset WordPress content
|
||||
- `tigerstyle_reset_database` - Complete database reset
|
||||
- `tigerstyle_test_s3_connection` - Test S3 connectivity
|
||||
|
||||
### Logging & Stats
|
||||
- `tigerstyle_get_backup_logs` - Retrieve backup logs
|
||||
- `tigerstyle_backup_stats` - Get backup statistics
|
||||
|
||||
## Security Features
|
||||
|
||||
### Access Control
|
||||
- WordPress capability checks (`manage_options`)
|
||||
- Nonce verification for all operations
|
||||
- User ID tracking in logs
|
||||
|
||||
### Confirmation Systems
|
||||
- Text confirmation for destructive operations
|
||||
- Random code generation for database reset
|
||||
- Multi-level confirmations for critical actions
|
||||
|
||||
### Data Protection
|
||||
- Backup file encryption in S3
|
||||
- Protected log directory with .htaccess
|
||||
- Temporary file cleanup
|
||||
- Secure file handling
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Memory Management
|
||||
- Chunked file processing (configurable chunk size)
|
||||
- Streaming operations for large files
|
||||
- Memory limit increases where possible
|
||||
- Garbage collection optimization
|
||||
|
||||
### Timeout Handling
|
||||
- Set unlimited execution time for operations
|
||||
- Progress tracking for long operations
|
||||
- Resumable operations where possible
|
||||
|
||||
### System Resource Monitoring
|
||||
- Load average checking before scheduled backups
|
||||
- Disk space validation
|
||||
- Concurrent operation prevention
|
||||
|
||||
## Error Handling & Recovery
|
||||
|
||||
### Backup Failures
|
||||
- Automatic cleanup of partial backups
|
||||
- Failure logging with context
|
||||
- Graceful degradation (compression fallback)
|
||||
- Retry mechanisms for network operations
|
||||
|
||||
### Restore Failures
|
||||
- Automatic rollback on restore failure
|
||||
- Progress state preservation
|
||||
- Detailed error reporting
|
||||
- Recovery recommendations
|
||||
|
||||
### Validation Failures
|
||||
- Comprehensive integrity checking
|
||||
- Detailed validation reports
|
||||
- Granular error categorization
|
||||
- Recovery suggestions
|
||||
|
||||
## Monitoring & Logging
|
||||
|
||||
### Log Levels
|
||||
- **Debug**: Detailed operation information
|
||||
- **Info**: General operation status
|
||||
- **Warning**: Non-critical issues
|
||||
- **Error**: Operation failures
|
||||
- **Critical**: System-threatening issues
|
||||
|
||||
### Log Storage
|
||||
- File-based logs with rotation (10MB limit)
|
||||
- Database storage for recent logs (1000 entries)
|
||||
- WordPress debug.log integration
|
||||
- Log export functionality
|
||||
|
||||
### Notifications
|
||||
- Email notifications for failures
|
||||
- Configurable notification levels
|
||||
- Rate limiting to prevent spam
|
||||
- Template-based email formatting
|
||||
|
||||
## Integration Points
|
||||
|
||||
### WordPress Hooks
|
||||
- `tigerstyle_backup_restore_completed` - After successful restore
|
||||
- `tigerstyle_backup_created` - After backup creation
|
||||
- `tigerstyle_backup_scheduled` - Scheduled backup trigger
|
||||
- `tigerstyle_backup_cleanup` - Cleanup trigger
|
||||
|
||||
### Filter Hooks
|
||||
- `tigerstyle_backup_paths` - Modify backup file paths
|
||||
- `tigerstyle_backup_logging_enabled` - Control logging
|
||||
- `tigerstyle_backup_exclude_patterns` - File exclusion patterns
|
||||
|
||||
### Admin Integration
|
||||
- Tab in main TigerStyle Heat admin interface
|
||||
- Progress indicators and real-time updates
|
||||
- Comprehensive settings management
|
||||
- Backup list and management interface
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
includes/
|
||||
├── modules/
|
||||
│ └── class-backup-restore.php # Main module class
|
||||
└── backup/
|
||||
├── class-backup-engine.php # Core backup processing
|
||||
├── class-restore-engine.php # Restoration engine
|
||||
├── class-storage-manager.php # Multi-backend storage
|
||||
├── class-backup-logger.php # Comprehensive logging
|
||||
├── class-backup-validator.php # Integrity validation
|
||||
├── class-backup-scheduler.php # Automated scheduling
|
||||
├── class-compression-manager.php # Multi-format compression
|
||||
└── ajax-handlers.php # AJAX endpoints
|
||||
|
||||
admin/
|
||||
└── pages/
|
||||
└── backup-restore.php # Admin interface
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### PHP Requirements
|
||||
- PHP 7.4 or higher
|
||||
- Memory: 256MB+ recommended
|
||||
- Execution time: Unlimited for large backups
|
||||
|
||||
### WordPress Requirements
|
||||
- WordPress 5.0 or higher
|
||||
- `manage_options` capability for users
|
||||
|
||||
### System Requirements
|
||||
- Available disk space (2x backup size recommended)
|
||||
- ZIP extension (preferred) or TAR support
|
||||
- cURL for S3 operations
|
||||
- MySQL/MariaDB access for database operations
|
||||
|
||||
### Optional Requirements
|
||||
- AWS SDK for advanced S3 features
|
||||
- ionCube for PHP acceleration
|
||||
- WP-CLI for command-line operations
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Backup Strategy
|
||||
1. **Regular Schedules**: Set up automated daily/weekly backups
|
||||
2. **Before Updates**: Always backup before major updates
|
||||
3. **Multiple Locations**: Use both local and S3 storage
|
||||
4. **Retention Policies**: Keep 30 days of backups minimum
|
||||
5. **Test Restores**: Regularly test backup restoration
|
||||
|
||||
### Security Practices
|
||||
1. **S3 Permissions**: Use dedicated IAM user with minimal permissions
|
||||
2. **Backup Encryption**: Enable S3 server-side encryption
|
||||
3. **Access Control**: Limit backup access to administrators
|
||||
4. **Log Monitoring**: Monitor backup logs for suspicious activity
|
||||
|
||||
### Performance Optimization
|
||||
1. **Chunk Size**: Adjust based on server resources (5MB default)
|
||||
2. **Exclusion Patterns**: Exclude unnecessary files (cache, logs)
|
||||
3. **Compression**: Use ZIP for best compatibility and speed
|
||||
4. **Scheduling**: Run backups during low-traffic hours
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Memory Exhaustion
|
||||
- Reduce chunk size in settings
|
||||
- Exclude large directories (uploads/cache)
|
||||
- Increase PHP memory limit
|
||||
- Use streaming operations
|
||||
|
||||
#### Timeout Issues
|
||||
- Set unlimited execution time
|
||||
- Use chunked processing
|
||||
- Check server timeout settings
|
||||
- Monitor progress tracking
|
||||
|
||||
#### S3 Connection Issues
|
||||
- Verify credentials and permissions
|
||||
- Check bucket region settings
|
||||
- Test with S3 endpoint override
|
||||
- Monitor network connectivity
|
||||
|
||||
#### Validation Failures
|
||||
- Check file permissions
|
||||
- Verify backup integrity
|
||||
- Review compression settings
|
||||
- Check available disk space
|
||||
|
||||
### Debug Mode
|
||||
Enable WordPress debug mode for detailed logging:
|
||||
```php
|
||||
define('WP_DEBUG', true);
|
||||
define('WP_DEBUG_LOG', true);
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0.0
|
||||
- Initial release with core backup/restore functionality
|
||||
- S3 storage integration
|
||||
- Comprehensive logging system
|
||||
- Advanced validation and safety features
|
||||
- Enterprise-grade admin interface
|
||||
- Automated scheduling and retention policies
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For technical support, please review the logs first:
|
||||
1. Check backup logs in the admin interface
|
||||
2. Review WordPress debug logs
|
||||
3. Verify system requirements
|
||||
4. Test with minimal configuration
|
||||
|
||||
This module represents enterprise-grade backup functionality that can compete with premium backup plugins while being deeply integrated with the TigerStyle Heat ecosystem.
|
||||
1350
docs/schema-imageobject-analysis.md
Normal file
1350
docs/schema-imageobject-analysis.md
Normal file
File diff suppressed because it is too large
Load Diff
388
includes/api/class-ai-client.php
Normal file
388
includes/api/class-ai-client.php
Normal file
@ -0,0 +1,388 @@
|
||||
<?php
|
||||
/**
|
||||
* OpenAI-compatible API Client for TigerStyle Heat
|
||||
* Provides a simple wrapper around WordPress HTTP API for OpenAI-compatible requests
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_AI_Client {
|
||||
private $base_url;
|
||||
private $api_key;
|
||||
private $default_model;
|
||||
private $timeout;
|
||||
|
||||
public function __construct($base_url, $api_key, $default_model = null) {
|
||||
$this->base_url = rtrim($base_url, '/');
|
||||
$this->api_key = $api_key;
|
||||
$this->default_model = $default_model;
|
||||
$this->timeout = 60; // Default 60 second timeout
|
||||
}
|
||||
|
||||
/**
|
||||
* Set request timeout
|
||||
*/
|
||||
public function set_timeout($timeout) {
|
||||
$this->timeout = $timeout;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a chat completion request
|
||||
*/
|
||||
public function chat_completion($params) {
|
||||
// Set default model if not provided
|
||||
if (!isset($params['model']) && $this->default_model) {
|
||||
$params['model'] = $this->default_model;
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!isset($params['model'])) {
|
||||
throw new Exception('Model is required for chat completion');
|
||||
}
|
||||
|
||||
if (!isset($params['messages']) || empty($params['messages'])) {
|
||||
throw new Exception('Messages array is required for chat completion');
|
||||
}
|
||||
|
||||
// Set default parameters
|
||||
$params = array_merge(array(
|
||||
'max_tokens' => 1000,
|
||||
'temperature' => 0.7,
|
||||
'top_p' => 1.0,
|
||||
'frequency_penalty' => 0,
|
||||
'presence_penalty' => 0
|
||||
), $params);
|
||||
|
||||
return $this->make_request('POST', '/chat/completions', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a text completion request (for older models)
|
||||
*/
|
||||
public function text_completion($params) {
|
||||
// Set default model if not provided
|
||||
if (!isset($params['model']) && $this->default_model) {
|
||||
$params['model'] = $this->default_model;
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!isset($params['model'])) {
|
||||
throw new Exception('Model is required for text completion');
|
||||
}
|
||||
|
||||
if (!isset($params['prompt'])) {
|
||||
throw new Exception('Prompt is required for text completion');
|
||||
}
|
||||
|
||||
// Set default parameters
|
||||
$params = array_merge(array(
|
||||
'max_tokens' => 1000,
|
||||
'temperature' => 0.7,
|
||||
'top_p' => 1.0,
|
||||
'frequency_penalty' => 0,
|
||||
'presence_penalty' => 0
|
||||
), $params);
|
||||
|
||||
return $this->make_request('POST', '/completions', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* List available models
|
||||
*/
|
||||
public function list_models() {
|
||||
return $this->make_request('GET', '/models');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model information
|
||||
*/
|
||||
public function get_model($model_id) {
|
||||
return $this->make_request('GET', '/models/' . $model_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create embeddings
|
||||
*/
|
||||
public function create_embeddings($params) {
|
||||
// Set default model if not provided
|
||||
if (!isset($params['model']) && $this->default_model) {
|
||||
$params['model'] = $this->default_model;
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!isset($params['model'])) {
|
||||
throw new Exception('Model is required for embeddings');
|
||||
}
|
||||
|
||||
if (!isset($params['input'])) {
|
||||
throw new Exception('Input is required for embeddings');
|
||||
}
|
||||
|
||||
return $this->make_request('POST', '/embeddings', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create image generation request (DALL-E style)
|
||||
*/
|
||||
public function create_image($params) {
|
||||
// Validate required parameters
|
||||
if (!isset($params['prompt'])) {
|
||||
throw new Exception('Prompt is required for image generation');
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
$params = array_merge(array(
|
||||
'n' => 1,
|
||||
'size' => '512x512',
|
||||
'response_format' => 'url'
|
||||
), $params);
|
||||
|
||||
return $this->make_request('POST', '/images/generations', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moderate content
|
||||
*/
|
||||
public function moderate_content($params) {
|
||||
if (!isset($params['input'])) {
|
||||
throw new Exception('Input is required for moderation');
|
||||
}
|
||||
|
||||
return $this->make_request('POST', '/moderations', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fine-tuning job
|
||||
*/
|
||||
public function create_fine_tune($params) {
|
||||
if (!isset($params['training_file'])) {
|
||||
throw new Exception('Training file is required for fine-tuning');
|
||||
}
|
||||
|
||||
return $this->make_request('POST', '/fine-tunes', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* List fine-tuning jobs
|
||||
*/
|
||||
public function list_fine_tunes() {
|
||||
return $this->make_request('GET', '/fine-tunes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fine-tuning job
|
||||
*/
|
||||
public function get_fine_tune($fine_tune_id) {
|
||||
return $this->make_request('GET', '/fine-tunes/' . $fine_tune_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel fine-tuning job
|
||||
*/
|
||||
public function cancel_fine_tune($fine_tune_id) {
|
||||
return $this->make_request('POST', '/fine-tunes/' . $fine_tune_id . '/cancel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file
|
||||
*/
|
||||
public function upload_file($file_path, $purpose = 'fine-tune') {
|
||||
if (!file_exists($file_path)) {
|
||||
throw new Exception('File not found: ' . $file_path);
|
||||
}
|
||||
|
||||
// For file uploads, we need to use a different approach
|
||||
$boundary = wp_generate_password(16, false);
|
||||
$file_content = file_get_contents($file_path);
|
||||
$filename = basename($file_path);
|
||||
|
||||
$body = '';
|
||||
$body .= '--' . $boundary . "\r\n";
|
||||
$body .= 'Content-Disposition: form-data; name="purpose"' . "\r\n\r\n";
|
||||
$body .= $purpose . "\r\n";
|
||||
|
||||
$body .= '--' . $boundary . "\r\n";
|
||||
$body .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . "\r\n";
|
||||
$body .= 'Content-Type: application/octet-stream' . "\r\n\r\n";
|
||||
$body .= $file_content . "\r\n";
|
||||
$body .= '--' . $boundary . '--' . "\r\n";
|
||||
|
||||
$response = wp_remote_post($this->base_url . '/files', array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $this->api_key,
|
||||
'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
|
||||
'User-Agent' => 'TigerStyle-SEO/' . TIGERSTYLE_HEAT_VERSION
|
||||
),
|
||||
'body' => $body,
|
||||
'timeout' => $this->timeout
|
||||
));
|
||||
|
||||
return $this->process_response($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* List files
|
||||
*/
|
||||
public function list_files() {
|
||||
return $this->make_request('GET', '/files');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file
|
||||
*/
|
||||
public function get_file($file_id) {
|
||||
return $this->make_request('GET', '/files/' . $file_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete file
|
||||
*/
|
||||
public function delete_file($file_id) {
|
||||
return $this->make_request('DELETE', '/files/' . $file_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request to OpenAI API
|
||||
*/
|
||||
private function make_request($method, $endpoint, $params = null) {
|
||||
$url = $this->base_url . $endpoint;
|
||||
|
||||
$args = array(
|
||||
'method' => $method,
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $this->api_key,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'TigerStyle-SEO/' . TIGERSTYLE_HEAT_VERSION
|
||||
),
|
||||
'timeout' => $this->timeout
|
||||
);
|
||||
|
||||
if ($params && ($method === 'POST' || $method === 'PUT' || $method === 'PATCH')) {
|
||||
$args['body'] = json_encode($params);
|
||||
} elseif ($params && $method === 'GET') {
|
||||
$url = add_query_arg($params, $url);
|
||||
}
|
||||
|
||||
$response = wp_remote_request($url, $args);
|
||||
|
||||
return $this->process_response($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process API response
|
||||
*/
|
||||
private function process_response($response) {
|
||||
if (is_wp_error($response)) {
|
||||
throw new Exception('HTTP Request failed: ' . $response->get_error_message());
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
|
||||
// Try to decode JSON response
|
||||
$data = json_decode($body, true);
|
||||
|
||||
if ($status_code >= 400) {
|
||||
$error_message = 'HTTP ' . $status_code;
|
||||
|
||||
if ($data && isset($data['error'])) {
|
||||
if (is_string($data['error'])) {
|
||||
$error_message .= ': ' . $data['error'];
|
||||
} elseif (isset($data['error']['message'])) {
|
||||
$error_message .= ': ' . $data['error']['message'];
|
||||
}
|
||||
} else {
|
||||
$error_message .= ': ' . $body;
|
||||
}
|
||||
|
||||
throw new Exception($error_message);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a simple chat message
|
||||
*/
|
||||
public function simple_chat($message, $system_prompt = null, $model = null) {
|
||||
$messages = array();
|
||||
|
||||
if ($system_prompt) {
|
||||
$messages[] = array('role' => 'system', 'content' => $system_prompt);
|
||||
}
|
||||
|
||||
$messages[] = array('role' => 'user', 'content' => $message);
|
||||
|
||||
$params = array('messages' => $messages);
|
||||
|
||||
if ($model) {
|
||||
$params['model'] = $model;
|
||||
}
|
||||
|
||||
return $this->chat_completion($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract text from chat response
|
||||
*/
|
||||
public function extract_text($response) {
|
||||
if (isset($response['choices'][0]['message']['content'])) {
|
||||
return trim($response['choices'][0]['message']['content']);
|
||||
}
|
||||
|
||||
if (isset($response['choices'][0]['text'])) {
|
||||
return trim($response['choices'][0]['text']);
|
||||
}
|
||||
|
||||
throw new Exception('Unable to extract text from response');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for SEO-specific prompts
|
||||
*/
|
||||
public function generate_meta_description($content, $max_length = 155) {
|
||||
$prompt = "Generate an SEO-optimized meta description (maximum {$max_length} characters) for the following content. The description should be compelling, include relevant keywords, and encourage clicks:\n\n{$content}";
|
||||
|
||||
$response = $this->simple_chat($prompt, "You are an SEO expert specializing in creating compelling meta descriptions that improve search engine rankings and click-through rates.");
|
||||
|
||||
return $this->extract_text($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to generate SEO title
|
||||
*/
|
||||
public function generate_seo_title($content, $max_length = 60) {
|
||||
$prompt = "Generate an SEO-optimized title (maximum {$max_length} characters) for the following content. The title should be compelling, include relevant keywords, and be click-worthy:\n\n{$content}";
|
||||
|
||||
$response = $this->simple_chat($prompt, "You are an SEO expert specializing in creating compelling titles that improve search engine rankings and click-through rates.");
|
||||
|
||||
return $this->extract_text($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract keywords
|
||||
*/
|
||||
public function extract_keywords($content, $count = 10) {
|
||||
$prompt = "Extract the {$count} most important SEO keywords from the following content. Return only the keywords, separated by commas:\n\n{$content}";
|
||||
|
||||
$response = $this->simple_chat($prompt, "You are an SEO expert specializing in keyword research and content analysis.");
|
||||
|
||||
$keywords = $this->extract_text($response);
|
||||
return array_map('trim', explode(',', $keywords));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to analyze content for SEO
|
||||
*/
|
||||
public function analyze_seo_content($content) {
|
||||
$prompt = "Analyze the following content for SEO optimization and provide specific recommendations:\n\n{$content}";
|
||||
|
||||
$response = $this->simple_chat($prompt, "You are an SEO expert. Analyze content and provide actionable SEO recommendations including keyword usage, content structure, readability, and technical SEO factors.");
|
||||
|
||||
return $this->extract_text($response);
|
||||
}
|
||||
}
|
||||
358
includes/api/class-sxg-api-client.php
Normal file
358
includes/api/class-sxg-api-client.php
Normal file
@ -0,0 +1,358 @@
|
||||
<?php
|
||||
/**
|
||||
* SXG API Client
|
||||
*
|
||||
* Handles communication with external SXG API service
|
||||
*
|
||||
* @package TigerStyle_SEO
|
||||
* @subpackage API
|
||||
* @since 2.1.0
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyle_SEO_SXG_API_Client {
|
||||
|
||||
private $api_base_url;
|
||||
private $api_key;
|
||||
private $timeout;
|
||||
private $cache_ttl;
|
||||
|
||||
public function __construct() {
|
||||
$options = get_option('tigerstyle_heat_amp', []);
|
||||
|
||||
$this->api_base_url = $options['sxg_api_url'] ?? 'https://sxg-api.tigerstyle.com/v1';
|
||||
$this->api_key = $options['sxg_api_key'] ?? '';
|
||||
$this->timeout = 30;
|
||||
$this->cache_ttl = 3600; // 1 hour
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if API service is available
|
||||
*/
|
||||
public function is_service_available() {
|
||||
$cache_key = 'tigerstyle_heat_sxg_api_status';
|
||||
$cached_status = get_transient($cache_key);
|
||||
|
||||
if ($cached_status !== false) {
|
||||
return $cached_status === 'available';
|
||||
}
|
||||
|
||||
$response = $this->make_request('GET', '/health');
|
||||
$is_available = $response && $response['status'] === 'healthy';
|
||||
|
||||
set_transient($cache_key, $is_available ? 'available' : 'unavailable', 300); // Cache for 5 minutes
|
||||
|
||||
return $is_available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress site with SXG API service
|
||||
*/
|
||||
public function register_site() {
|
||||
$site_data = [
|
||||
'domain' => parse_url(home_url(), PHP_URL_HOST),
|
||||
'site_url' => home_url(),
|
||||
'admin_email' => get_option('admin_email'),
|
||||
'wordpress_version' => get_bloginfo('version'),
|
||||
'plugin_version' => TIGERSTYLE_HEAT_VERSION,
|
||||
'ssl_enabled' => is_ssl(),
|
||||
'certificate_info' => $this->get_certificate_info()
|
||||
];
|
||||
|
||||
$response = $this->make_request('POST', '/sites/register', $site_data);
|
||||
|
||||
if ($response && isset($response['api_key'])) {
|
||||
// Store the generated API key
|
||||
$options = get_option('tigerstyle_heat_amp', []);
|
||||
$options['sxg_api_key'] = $response['api_key'];
|
||||
$options['sxg_site_id'] = $response['site_id'];
|
||||
update_option('tigerstyle_heat_amp', $options);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SXG for AMP content
|
||||
*/
|
||||
public function generate_sxg($url, $amp_content) {
|
||||
if (!$this->api_key) {
|
||||
return new WP_Error('no_api_key', 'SXG API key not configured');
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
$cache_key = 'tigerstyle_heat_sxg_' . md5($url . $amp_content);
|
||||
$cached_sxg = get_transient($cache_key);
|
||||
|
||||
if ($cached_sxg !== false) {
|
||||
return $cached_sxg;
|
||||
}
|
||||
|
||||
$sxg_data = [
|
||||
'url' => $url,
|
||||
'content' => $amp_content,
|
||||
'timestamp' => time(),
|
||||
'headers' => $this->get_sxg_headers(),
|
||||
'options' => [
|
||||
'max_age' => 86400, // 24 hours
|
||||
'stale_while_revalidate' => 604800 // 7 days
|
||||
]
|
||||
];
|
||||
|
||||
$response = $this->make_request('POST', '/sxg/generate', $sxg_data);
|
||||
|
||||
if ($response && isset($response['sxg_package'])) {
|
||||
// Cache the SXG package
|
||||
set_transient($cache_key, $response, $this->cache_ttl);
|
||||
return $response;
|
||||
}
|
||||
|
||||
return new WP_Error('sxg_generation_failed', 'Failed to generate SXG package');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate AMP content before SXG generation
|
||||
*/
|
||||
public function validate_amp_content($content) {
|
||||
$validation_data = [
|
||||
'content' => $content,
|
||||
'url' => get_permalink(),
|
||||
'validation_level' => 'strict'
|
||||
];
|
||||
|
||||
$response = $this->make_request('POST', '/amp/validate', $validation_data);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get amppackager configuration for local installation
|
||||
*/
|
||||
public function get_amppackager_config() {
|
||||
$site_info = [
|
||||
'domain' => parse_url(home_url(), PHP_URL_HOST),
|
||||
'certificate_path' => '/etc/ssl/certs/' . parse_url(home_url(), PHP_URL_HOST) . '.pem',
|
||||
'private_key_path' => '/etc/ssl/private/' . parse_url(home_url(), PHP_URL_HOST) . '.key',
|
||||
'amp_endpoint' => home_url() . '/{path}/amp',
|
||||
'sxg_endpoint' => home_url() . '/{path}/amp.sxg'
|
||||
];
|
||||
|
||||
$response = $this->make_request('POST', '/config/amppackager', $site_info);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get certificate recommendations
|
||||
*/
|
||||
public function get_certificate_recommendations() {
|
||||
$domain_info = [
|
||||
'domain' => parse_url(home_url(), PHP_URL_HOST),
|
||||
'hosting_provider' => $this->detect_hosting_provider(),
|
||||
'current_certificate' => $this->get_certificate_info()
|
||||
];
|
||||
|
||||
$response = $this->make_request('POST', '/certificates/recommendations', $domain_info);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SXG infrastructure
|
||||
*/
|
||||
public function test_infrastructure() {
|
||||
$test_data = [
|
||||
'domain' => parse_url(home_url(), PHP_URL_HOST),
|
||||
'test_url' => home_url() . '/test-amp',
|
||||
'user_agent' => 'TigerStyle-SEO-SXG-Test/1.0'
|
||||
];
|
||||
|
||||
$response = $this->make_request('POST', '/test/infrastructure', $test_data);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SXG analytics and performance metrics
|
||||
*/
|
||||
public function get_sxg_analytics($days = 30) {
|
||||
$params = [
|
||||
'days' => $days,
|
||||
'metrics' => ['requests', 'cache_hits', 'validation_errors', 'performance']
|
||||
];
|
||||
|
||||
$response = $this->make_request('GET', '/analytics/sxg?' . http_build_query($params));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request to SXG API
|
||||
*/
|
||||
private function make_request($method, $endpoint, $data = null) {
|
||||
$url = rtrim($this->api_base_url, '/') . $endpoint;
|
||||
|
||||
$args = [
|
||||
'method' => $method,
|
||||
'timeout' => $this->timeout,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'TigerStyle-SEO/' . TIGERSTYLE_HEAT_VERSION . ' WordPress/' . get_bloginfo('version'),
|
||||
'Accept' => 'application/json'
|
||||
]
|
||||
];
|
||||
|
||||
// Add API key if available
|
||||
if ($this->api_key) {
|
||||
$args['headers']['Authorization'] = 'Bearer ' . $this->api_key;
|
||||
}
|
||||
|
||||
// Add request data
|
||||
if ($data && in_array($method, ['POST', 'PUT', 'PATCH'])) {
|
||||
$args['body'] = wp_json_encode($data);
|
||||
}
|
||||
|
||||
$response = wp_remote_request($url, $args);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
error_log('SXG API Request Error: ' . $response->get_error_message());
|
||||
return false;
|
||||
}
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code($response);
|
||||
$response_body = wp_remote_retrieve_body($response);
|
||||
|
||||
if ($response_code >= 200 && $response_code < 300) {
|
||||
return json_decode($response_body, true);
|
||||
}
|
||||
|
||||
error_log("SXG API Error {$response_code}: {$response_body}");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get certificate information
|
||||
*/
|
||||
private function get_certificate_info() {
|
||||
if (!is_ssl()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$domain = parse_url(home_url(), PHP_URL_HOST);
|
||||
|
||||
// Try to get certificate info using OpenSSL
|
||||
$cert_info = [];
|
||||
|
||||
try {
|
||||
$context = stream_context_create([
|
||||
'ssl' => [
|
||||
'capture_peer_cert' => true,
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false
|
||||
]
|
||||
]);
|
||||
|
||||
$stream = stream_socket_client(
|
||||
"ssl://{$domain}:443",
|
||||
$errno,
|
||||
$errstr,
|
||||
10,
|
||||
STREAM_CLIENT_CONNECT,
|
||||
$context
|
||||
);
|
||||
|
||||
if ($stream) {
|
||||
$cert = stream_context_get_params($stream)['options']['ssl']['peer_certificate'];
|
||||
$cert_data = openssl_x509_parse($cert);
|
||||
|
||||
if ($cert_data) {
|
||||
$cert_info = [
|
||||
'subject' => $cert_data['subject']['CN'] ?? $domain,
|
||||
'issuer' => $cert_data['issuer']['CN'] ?? 'Unknown',
|
||||
'valid_from' => date('Y-m-d H:i:s', $cert_data['validFrom_time_t']),
|
||||
'valid_to' => date('Y-m-d H:i:s', $cert_data['validTo_time_t']),
|
||||
'signature_algorithm' => $cert_data['signatureTypeSN'] ?? 'Unknown'
|
||||
];
|
||||
}
|
||||
|
||||
fclose($stream);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log('Certificate info extraction failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $cert_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SXG headers for WordPress
|
||||
*/
|
||||
private function get_sxg_headers() {
|
||||
return [
|
||||
'cache-control' => 'public, max-age=86400, stale-while-revalidate=604800',
|
||||
'content-type' => 'text/html; charset=utf-8',
|
||||
'vary' => 'Accept, AMP-Cache-Transform',
|
||||
'amp-access-control-allow-source-origin' => home_url(),
|
||||
'access-control-expose-headers' => 'AMP-Access-Control-Allow-Source-Origin'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect hosting provider for better recommendations
|
||||
*/
|
||||
private function detect_hosting_provider() {
|
||||
$hosting_indicators = [
|
||||
'WP Engine' => ['wpengine.com', 'WPEngine'],
|
||||
'Kinsta' => ['kinsta.com', 'Kinsta'],
|
||||
'Pantheon' => ['pantheonsite.io', 'Pantheon'],
|
||||
'WordPress.com' => ['wordpress.com', 'Automattic'],
|
||||
'SiteGround' => ['siteground.com', 'SiteGround'],
|
||||
'Cloudflare' => ['cloudflare.com', 'Cloudflare'],
|
||||
'DigitalOcean' => ['digitalocean.com', 'DigitalOcean'],
|
||||
'AWS' => ['amazonaws.com', 'Amazon'],
|
||||
'Google Cloud' => ['googleusercontent.com', 'Google']
|
||||
];
|
||||
|
||||
$server_name = $_SERVER['SERVER_NAME'] ?? '';
|
||||
$server_software = $_SERVER['SERVER_SOFTWARE'] ?? '';
|
||||
$http_host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
|
||||
foreach ($hosting_indicators as $provider => $indicators) {
|
||||
foreach ($indicators as $indicator) {
|
||||
if (stripos($server_name . $server_software . $http_host, $indicator) !== false) {
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API service status and capabilities
|
||||
*/
|
||||
public function get_service_info() {
|
||||
return $this->make_request('GET', '/info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update API configuration
|
||||
*/
|
||||
public function update_config($config) {
|
||||
$options = get_option('tigerstyle_heat_amp', []);
|
||||
$options = array_merge($options, $config);
|
||||
update_option('tigerstyle_heat_amp', $options);
|
||||
|
||||
// Update instance variables
|
||||
$this->api_base_url = $options['sxg_api_url'] ?? $this->api_base_url;
|
||||
$this->api_key = $options['sxg_api_key'] ?? $this->api_key;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
347
includes/backup/ajax-handlers.php
Normal file
347
includes/backup/ajax-handlers.php
Normal file
@ -0,0 +1,347 @@
|
||||
<?php
|
||||
/**
|
||||
* AJAX Handlers for Backup & Restore Module
|
||||
*
|
||||
* Additional AJAX functionality for the backup system
|
||||
* including log management, S3 testing, and validation.
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// AJAX handler for getting backup logs
|
||||
add_action('wp_ajax_tigerstyle_get_backup_logs', 'tigerstyle_ajax_get_backup_logs');
|
||||
function tigerstyle_ajax_get_backup_logs() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$level = sanitize_text_field($_POST['level'] ?? '');
|
||||
$limit = absint($_POST['limit'] ?? 100);
|
||||
|
||||
$logger = new TigerStyleSEO_Backup_Logger();
|
||||
$logs = $logger->get_recent_logs($limit, $level);
|
||||
|
||||
wp_send_json_success($logs);
|
||||
}
|
||||
|
||||
// AJAX handler for testing S3 connection
|
||||
add_action('wp_ajax_tigerstyle_test_s3_connection', 'tigerstyle_ajax_test_s3_connection');
|
||||
function tigerstyle_ajax_test_s3_connection() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
// Temporarily update settings for testing
|
||||
$test_settings = array(
|
||||
's3_bucket' => sanitize_text_field($_POST['s3_bucket']),
|
||||
's3_access_key' => sanitize_text_field($_POST['s3_access_key']),
|
||||
's3_secret_key' => sanitize_text_field($_POST['s3_secret_key']),
|
||||
's3_region' => sanitize_text_field($_POST['s3_region']),
|
||||
's3_endpoint' => sanitize_url($_POST['s3_endpoint'])
|
||||
);
|
||||
|
||||
// Temporarily override settings
|
||||
$original_settings = get_option('tigerstyle_backup_settings', array());
|
||||
update_option('tigerstyle_backup_settings', array_merge($original_settings, $test_settings));
|
||||
|
||||
try {
|
||||
$storage_manager = new TigerStyleSEO_Storage_Manager();
|
||||
$result = $storage_manager->test_s3_connection();
|
||||
|
||||
// Restore original settings
|
||||
update_option('tigerstyle_backup_settings', $original_settings);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success($result['message']);
|
||||
} else {
|
||||
wp_send_json_error($result['message']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Restore original settings
|
||||
update_option('tigerstyle_backup_settings', $original_settings);
|
||||
wp_send_json_error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// AJAX handler for generating reset code
|
||||
add_action('wp_ajax_tigerstyle_generate_reset_code', 'tigerstyle_ajax_generate_reset_code');
|
||||
function tigerstyle_ajax_generate_reset_code() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$backup_module = tigerstyle_heat()->get_module('backup_restore');
|
||||
$code = $backup_module->generate_reset_code();
|
||||
|
||||
wp_send_json_success(array('code' => $code));
|
||||
}
|
||||
|
||||
// AJAX handler for exporting logs
|
||||
add_action('wp_ajax_tigerstyle_export_backup_logs', 'tigerstyle_ajax_export_backup_logs');
|
||||
function tigerstyle_ajax_export_backup_logs() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Insufficient permissions');
|
||||
}
|
||||
|
||||
$start_date = sanitize_text_field($_GET['start_date'] ?? '');
|
||||
$end_date = sanitize_text_field($_GET['end_date'] ?? '');
|
||||
$level = sanitize_text_field($_GET['level'] ?? '');
|
||||
|
||||
$logger = new TigerStyleSEO_Backup_Logger();
|
||||
$logs = $logger->export_logs($start_date, $end_date, $level);
|
||||
|
||||
// Set headers for file download
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Disposition: attachment; filename="tigerstyle-backup-logs-' . date('Y-m-d') . '.json"');
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Expires: 0');
|
||||
|
||||
echo json_encode($logs, JSON_PRETTY_PRINT);
|
||||
exit;
|
||||
}
|
||||
|
||||
// AJAX handler for clearing old logs
|
||||
add_action('wp_ajax_tigerstyle_clear_backup_logs', 'tigerstyle_ajax_clear_backup_logs');
|
||||
function tigerstyle_ajax_clear_backup_logs() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$days = absint($_POST['days'] ?? 30);
|
||||
|
||||
$logger = new TigerStyleSEO_Backup_Logger();
|
||||
$deleted_count = $logger->clear_old_logs($days);
|
||||
|
||||
wp_send_json_success(array(
|
||||
'message' => sprintf(__('Deleted %d old log entries', 'tigerstyle-heat'), $deleted_count),
|
||||
'deleted_count' => $deleted_count
|
||||
));
|
||||
}
|
||||
|
||||
// AJAX handler for downloading backup
|
||||
add_action('wp_ajax_tigerstyle_download_backup', 'tigerstyle_ajax_download_backup');
|
||||
function tigerstyle_ajax_download_backup() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Insufficient permissions');
|
||||
}
|
||||
|
||||
$backup_id = sanitize_text_field($_GET['backup_id']);
|
||||
|
||||
try {
|
||||
$storage_manager = new TigerStyleSEO_Storage_Manager();
|
||||
$backup_file = $storage_manager->download_backup($backup_id);
|
||||
$backup_info = $storage_manager->get_backup_info($backup_id);
|
||||
|
||||
// Determine filename
|
||||
$filename = $backup_id;
|
||||
if ($backup_info['storage_type'] === 'local') {
|
||||
$filename = basename($backup_info['file_path']);
|
||||
} else {
|
||||
$filename = basename($backup_info['s3_key']);
|
||||
}
|
||||
|
||||
// Set headers for file download
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Content-Length: ' . filesize($backup_file));
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Expires: 0');
|
||||
|
||||
// Output file
|
||||
readfile($backup_file);
|
||||
|
||||
// Cleanup temporary file if needed
|
||||
if ($backup_info['storage_type'] === 's3' && strpos($backup_file, 'tigerstyle-temp') !== false) {
|
||||
unlink($backup_file);
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_die('Download failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// AJAX handler for upload progress
|
||||
add_action('wp_ajax_tigerstyle_upload_backup', 'tigerstyle_ajax_upload_backup');
|
||||
function tigerstyle_ajax_upload_backup() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
if (!isset($_FILES['backup_file'])) {
|
||||
wp_send_json_error('No file uploaded');
|
||||
}
|
||||
|
||||
$file = $_FILES['backup_file'];
|
||||
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
wp_send_json_error('Upload error: ' . $file['error']);
|
||||
}
|
||||
|
||||
// Validate file extension
|
||||
$allowed_extensions = array('zip', 'tar', 'gz', 'bz2');
|
||||
$file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
|
||||
if (!in_array($file_extension, $allowed_extensions)) {
|
||||
wp_send_json_error('Invalid file type. Allowed: ' . implode(', ', $allowed_extensions));
|
||||
}
|
||||
|
||||
try {
|
||||
// Move uploaded file to backup directory
|
||||
$upload_dir = wp_upload_dir();
|
||||
$backup_dir = $upload_dir['basedir'] . '/tigerstyle-backups';
|
||||
|
||||
if (!wp_mkdir_p($backup_dir)) {
|
||||
throw new Exception('Failed to create backup directory');
|
||||
}
|
||||
|
||||
$backup_id = 'uploaded_' . time() . '_' . wp_generate_password(8, false);
|
||||
$destination = $backup_dir . '/' . $backup_id . '.' . $file_extension;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $destination)) {
|
||||
throw new Exception('Failed to move uploaded file');
|
||||
}
|
||||
|
||||
// Store backup metadata
|
||||
$storage_manager = new TigerStyleSEO_Storage_Manager();
|
||||
$metadata = array(
|
||||
'backup_id' => $backup_id,
|
||||
'storage_type' => 'local',
|
||||
'file_path' => $destination,
|
||||
'file_size' => filesize($destination),
|
||||
'created_at' => current_time('mysql'),
|
||||
'description' => 'Uploaded backup file'
|
||||
);
|
||||
|
||||
// This would need the storage manager to have a direct metadata storage method
|
||||
// For now, we'll just return success
|
||||
|
||||
wp_send_json_success(array(
|
||||
'backup_id' => $backup_id,
|
||||
'message' => __('Backup file uploaded successfully', 'tigerstyle-heat')
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// AJAX handler for getting backup progress (for operations that don't set their own progress)
|
||||
add_action('wp_ajax_tigerstyle_backup_progress', 'tigerstyle_ajax_backup_progress');
|
||||
function tigerstyle_ajax_backup_progress() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$operation_id = sanitize_text_field($_POST['operation_id']);
|
||||
|
||||
// Check both backup and restore progress
|
||||
$progress = get_transient('tigerstyle_backup_progress_' . $operation_id);
|
||||
if (!$progress) {
|
||||
$progress = get_transient('tigerstyle_restore_progress_' . $operation_id);
|
||||
}
|
||||
|
||||
if ($progress === false) {
|
||||
wp_send_json_error('Operation not found or completed');
|
||||
}
|
||||
|
||||
wp_send_json_success($progress);
|
||||
}
|
||||
|
||||
// AJAX handler for validating backup
|
||||
add_action('wp_ajax_tigerstyle_validate_backup', 'tigerstyle_ajax_validate_backup');
|
||||
function tigerstyle_ajax_validate_backup() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$backup_id = sanitize_text_field($_POST['backup_id']);
|
||||
|
||||
try {
|
||||
$validator = new TigerStyleSEO_Backup_Validator();
|
||||
$result = $validator->quick_validate_backup($backup_id);
|
||||
|
||||
if ($result['valid']) {
|
||||
wp_send_json_success($result['message']);
|
||||
} else {
|
||||
wp_send_json_error($result['error']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// AJAX handler for getting backup statistics
|
||||
add_action('wp_ajax_tigerstyle_backup_stats', 'tigerstyle_ajax_backup_stats');
|
||||
function tigerstyle_ajax_backup_stats() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$days = absint($_POST['days'] ?? 30);
|
||||
|
||||
$scheduler = new TigerStyleSEO_Backup_Scheduler();
|
||||
$stats = $scheduler->get_backup_statistics($days);
|
||||
|
||||
$storage_manager = new TigerStyleSEO_Storage_Manager();
|
||||
$storage_stats = $storage_manager->get_storage_stats();
|
||||
|
||||
wp_send_json_success(array(
|
||||
'backup_stats' => $stats,
|
||||
'storage_stats' => $storage_stats
|
||||
));
|
||||
}
|
||||
|
||||
// AJAX handler for running manual backup
|
||||
add_action('wp_ajax_tigerstyle_manual_backup', 'tigerstyle_ajax_manual_backup');
|
||||
function tigerstyle_ajax_manual_backup() {
|
||||
check_ajax_referer('tigerstyle_backup_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
try {
|
||||
$scheduler = new TigerStyleSEO_Backup_Scheduler();
|
||||
$backup_id = $scheduler->run_backup_now();
|
||||
|
||||
wp_send_json_success(array(
|
||||
'backup_id' => $backup_id,
|
||||
'message' => __('Manual backup started successfully', 'tigerstyle-heat')
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Load this file when the backup module is loaded
|
||||
if (class_exists('TigerStyleSEO_Backup_Restore')) {
|
||||
// Already loaded above
|
||||
}
|
||||
254
includes/backup/backup-admin.js
Normal file
254
includes/backup/backup-admin.js
Normal file
@ -0,0 +1,254 @@
|
||||
/**
|
||||
* TigerStyle SEO Backup System - Admin JavaScript
|
||||
* Handles AJAX interactions for backup and restore operations
|
||||
*/
|
||||
|
||||
jQuery(document).ready(function($) {
|
||||
'use strict';
|
||||
|
||||
// Get AJAX URL and nonce from localized script
|
||||
var tigerstyleBackup = {
|
||||
ajaxurl: ajaxurl || '/wp-admin/admin-ajax.php',
|
||||
nonce: $('#tigerstyle-backup-nonce').val() || ''
|
||||
};
|
||||
|
||||
// Progress tracking for operations
|
||||
var progressInterval = null;
|
||||
|
||||
/**
|
||||
* Show progress indicator
|
||||
*/
|
||||
function showProgress(operation, message) {
|
||||
var progressHtml = '<div id="tigerstyle-progress" class="notice notice-info">' +
|
||||
'<p><strong>' + operation + '</strong></p>' +
|
||||
'<div class="progress-bar"><div class="progress-fill" style="width: 0%"></div></div>' +
|
||||
'<p class="progress-message">' + message + '</p>' +
|
||||
'</div>';
|
||||
|
||||
$('.backup-status-cards').after(progressHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress indicator
|
||||
*/
|
||||
function updateProgress(percent, message) {
|
||||
$('#tigerstyle-progress .progress-fill').css('width', percent + '%');
|
||||
$('#tigerstyle-progress .progress-message').text(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide progress indicator
|
||||
*/
|
||||
function hideProgress() {
|
||||
$('#tigerstyle-progress').fadeOut(function() {
|
||||
$(this).remove();
|
||||
});
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification
|
||||
*/
|
||||
function showNotification(type, message) {
|
||||
var noticeClass = type === 'success' ? 'notice-success' : 'notice-error';
|
||||
var noticeHtml = '<div class="notice ' + noticeClass + ' is-dismissible">' +
|
||||
'<p>' + message + '</p>' +
|
||||
'<button type="button" class="notice-dismiss">' +
|
||||
'<span class="screen-reader-text">Dismiss this notice.</span>' +
|
||||
'</button></div>';
|
||||
|
||||
$('.backup-status-cards').before(noticeHtml);
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.notice').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Full Backup
|
||||
*/
|
||||
$('#create-full-backup').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (confirm('Are you sure you want to create a full backup? This may take several minutes.')) {
|
||||
showProgress('Creating Backup', 'Initializing backup process...');
|
||||
|
||||
$.ajax({
|
||||
url: tigerstyleBackup.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_manual_backup',
|
||||
nonce: tigerstyleBackup.nonce,
|
||||
backup_type: 'full'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
updateProgress(10, 'Backup started successfully...');
|
||||
|
||||
// Start polling for progress
|
||||
startProgressPolling(response.data.backup_id, 'backup');
|
||||
|
||||
showNotification('success', 'Backup process started. ID: ' + response.data.backup_id);
|
||||
} else {
|
||||
hideProgress();
|
||||
showNotification('error', 'Backup failed: ' + response.data);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
hideProgress();
|
||||
showNotification('error', 'Failed to start backup process.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* View Backup History
|
||||
*/
|
||||
$('#view-backup-history').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
showProgress('Loading History', 'Fetching backup history...');
|
||||
|
||||
$.ajax({
|
||||
url: tigerstyleBackup.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_backup_stats',
|
||||
nonce: tigerstyleBackup.nonce,
|
||||
days: 30
|
||||
},
|
||||
success: function(response) {
|
||||
hideProgress();
|
||||
|
||||
if (response.success) {
|
||||
displayBackupHistory(response.data);
|
||||
} else {
|
||||
showNotification('error', 'Failed to load backup history: ' + response.data);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
hideProgress();
|
||||
showNotification('error', 'Failed to connect to server.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Display backup history in a modal-like interface
|
||||
*/
|
||||
function displayBackupHistory(data) {
|
||||
var historyHtml = '<div id="backup-history-modal" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); width: 90%; max-width: 800px; background: white; border: 1px solid #ccc; box-shadow: 0 4px 8px rgba(0,0,0,0.1); z-index: 10000; padding: 20px;">' +
|
||||
'<h3>Backup History (Last 30 Days)</h3>' +
|
||||
'<table class="wp-list-table widefat fixed striped">' +
|
||||
'<thead><tr><th>Date</th><th>Type</th><th>Size</th><th>Status</th><th>Actions</th></tr></thead>' +
|
||||
'<tbody>';
|
||||
|
||||
if (data.backup_stats && data.backup_stats.length > 0) {
|
||||
data.backup_stats.forEach(function(backup) {
|
||||
historyHtml += '<tr>' +
|
||||
'<td>' + backup.created_at + '</td>' +
|
||||
'<td>' + (backup.backup_type || 'Full') + '</td>' +
|
||||
'<td>' + (backup.file_size || 'Unknown') + '</td>' +
|
||||
'<td>' + (backup.status || 'Completed') + '</td>' +
|
||||
'<td><button class="button button-small">Download</button></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
} else {
|
||||
historyHtml += '<tr><td colspan="5">No backups found</td></tr>';
|
||||
}
|
||||
|
||||
historyHtml += '</tbody></table>' +
|
||||
'<p><strong>Storage Stats:</strong></p>' +
|
||||
'<ul>' +
|
||||
'<li>Total Backups: ' + (data.storage_stats ? data.storage_stats.total_count : 0) + '</li>' +
|
||||
'<li>Total Size: ' + (data.storage_stats ? data.storage_stats.total_size : 'Unknown') + '</li>' +
|
||||
'</ul>' +
|
||||
'<button id="close-history-modal" class="button button-primary">Close</button>' +
|
||||
'</div>' +
|
||||
'<div id="backup-history-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999;"></div>';
|
||||
|
||||
$('body').append(historyHtml);
|
||||
|
||||
// Close modal handlers
|
||||
$('#close-history-modal, #backup-history-overlay').on('click', function() {
|
||||
$('#backup-history-modal, #backup-history-overlay').remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling for backup/restore progress
|
||||
*/
|
||||
function startProgressPolling(operationId, type) {
|
||||
var pollCount = 0;
|
||||
var maxPolls = 60; // Maximum 5 minutes of polling (5 second intervals)
|
||||
|
||||
progressInterval = setInterval(function() {
|
||||
pollCount++;
|
||||
|
||||
$.ajax({
|
||||
url: tigerstyleBackup.ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'tigerstyle_backup_progress',
|
||||
nonce: tigerstyleBackup.nonce,
|
||||
operation_id: operationId
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
var progress = response.data;
|
||||
updateProgress(progress.percent || 50, progress.message || 'Processing...');
|
||||
|
||||
// Check if operation is complete
|
||||
if (progress.status === 'completed') {
|
||||
updateProgress(100, 'Operation completed successfully!');
|
||||
setTimeout(hideProgress, 2000);
|
||||
showNotification('success', type === 'backup' ? 'Backup completed successfully!' : 'Restore completed successfully!');
|
||||
} else if (progress.status === 'failed') {
|
||||
hideProgress();
|
||||
showNotification('error', 'Operation failed: ' + (progress.error || 'Unknown error'));
|
||||
}
|
||||
} else {
|
||||
// Operation might be completed or failed
|
||||
if (pollCount > 5) { // Give it a few tries before assuming completion
|
||||
updateProgress(100, 'Operation completed (status unknown)');
|
||||
setTimeout(hideProgress, 2000);
|
||||
showNotification('success', 'Operation appears to have completed.');
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
if (pollCount > maxPolls) {
|
||||
hideProgress();
|
||||
showNotification('error', 'Progress tracking timed out. Operation may still be running.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (pollCount > maxPolls) {
|
||||
hideProgress();
|
||||
showNotification('error', 'Operation timed out after 5 minutes.');
|
||||
}
|
||||
}, 5000); // Poll every 5 seconds
|
||||
}
|
||||
|
||||
// Add some basic CSS for progress indicators
|
||||
if ($('#tigerstyle-backup-styles').length === 0) {
|
||||
$('<style id="tigerstyle-backup-styles">' +
|
||||
'.progress-bar { background: #f1f1f1; border-radius: 3px; overflow: hidden; height: 20px; margin: 10px 0; }' +
|
||||
'.progress-fill { background: #0073aa; height: 100%; transition: width 0.3s ease; }' +
|
||||
'.status-indicator { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; }' +
|
||||
'.status-indicator.success { background: #46b450; }' +
|
||||
'.status-indicator.warning { background: #ffba00; }' +
|
||||
'.status-indicator.error { background: #dc3232; }' +
|
||||
'.backup-status-cards { display: flex; gap: 20px; margin: 20px 0; }' +
|
||||
'.status-card { flex: 1; background: #f9f9f9; padding: 15px; border: 1px solid #ddd; border-radius: 3px; }' +
|
||||
'.backup-features ul { list-style: none; padding: 0; }' +
|
||||
'.backup-features li { padding: 5px 0; }' +
|
||||
'</style>').appendTo('head');
|
||||
}
|
||||
});
|
||||
500
includes/backup/class-backup-engine.php
Normal file
500
includes/backup/class-backup-engine.php
Normal file
@ -0,0 +1,500 @@
|
||||
<?php
|
||||
/**
|
||||
* Core Backup Engine for TigerStyle SEO
|
||||
* Handles filesystem and database backup operations with chunked processing
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Backup_Engine {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Backup settings
|
||||
*/
|
||||
private $settings = array();
|
||||
|
||||
/**
|
||||
* Logger instance
|
||||
*/
|
||||
private $logger = null;
|
||||
|
||||
/**
|
||||
* Storage manager instance
|
||||
*/
|
||||
private $storage = null;
|
||||
|
||||
/**
|
||||
* Compression manager instance
|
||||
*/
|
||||
private $compression = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the backup engine
|
||||
*/
|
||||
private function init() {
|
||||
$this->settings = $this->get_default_settings();
|
||||
$this->logger = TigerStyleSEO_Backup_Logger::instance();
|
||||
$this->storage = TigerStyleSEO_Storage_Manager::instance();
|
||||
$this->compression = TigerStyleSEO_Compression_Manager::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default backup settings
|
||||
*/
|
||||
private function get_default_settings() {
|
||||
return array(
|
||||
'backup_location' => WP_CONTENT_DIR . '/tigerstyle-backups/',
|
||||
'chunk_size' => 5242880, // 5MB chunks
|
||||
'max_execution_time' => 300, // 5 minutes - reasonable limit for backup operations
|
||||
'compression_method' => 'zip',
|
||||
'exclude_patterns' => array(
|
||||
'*.log',
|
||||
'cache/*',
|
||||
'logs/*',
|
||||
'node_modules/*',
|
||||
'wp-content/tigerstyle-backups/*',
|
||||
'*.tmp',
|
||||
'.git/*',
|
||||
'.svn/*'
|
||||
),
|
||||
'include_uploads' => true,
|
||||
'include_themes' => true,
|
||||
'include_plugins' => true,
|
||||
'include_wp_core' => false,
|
||||
'database_batch_size' => 1000
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a complete backup
|
||||
*/
|
||||
public function create_backup($options = array()) {
|
||||
$start_time = microtime(true);
|
||||
$backup_id = $this->generate_backup_id();
|
||||
|
||||
$this->logger->info("Starting backup creation", array('backup_id' => $backup_id));
|
||||
|
||||
try {
|
||||
// Set reasonable execution time limit to prevent runaway processes
|
||||
set_time_limit($this->settings['max_execution_time']);
|
||||
|
||||
// Log the execution time limit being set
|
||||
$this->logger->info("Setting execution time limit", array(
|
||||
'time_limit_seconds' => $this->settings['max_execution_time']
|
||||
));
|
||||
|
||||
// Create backup directory
|
||||
$backup_dir = $this->create_backup_directory($backup_id);
|
||||
if (!$backup_dir) {
|
||||
throw new Exception('Failed to create backup directory');
|
||||
}
|
||||
|
||||
$manifest = array(
|
||||
'backup_id' => $backup_id,
|
||||
'created_at' => current_time('mysql'),
|
||||
'wordpress_version' => get_bloginfo('version'),
|
||||
'plugin_version' => TIGERSTYLE_HEAT_VERSION,
|
||||
'site_url' => site_url(),
|
||||
'files' => array(),
|
||||
'database' => array(),
|
||||
'compression' => $this->settings['compression_method'],
|
||||
'checksum' => '',
|
||||
'size' => 0
|
||||
);
|
||||
|
||||
// Backup files
|
||||
if (!isset($options['skip_files']) || !$options['skip_files']) {
|
||||
$this->logger->info("Starting file backup", array('backup_id' => $backup_id));
|
||||
$file_backup = $this->backup_files($backup_dir, $backup_id);
|
||||
$manifest['files'] = $file_backup;
|
||||
}
|
||||
|
||||
// Backup database
|
||||
if (!isset($options['skip_database']) || !$options['skip_database']) {
|
||||
$this->logger->info("Starting database backup", array('backup_id' => $backup_id));
|
||||
$db_backup = $this->backup_database($backup_dir, $backup_id);
|
||||
$manifest['database'] = $db_backup;
|
||||
}
|
||||
|
||||
// Create manifest file
|
||||
$manifest_file = $backup_dir . '/manifest.json';
|
||||
file_put_contents($manifest_file, json_encode($manifest, JSON_PRETTY_PRINT));
|
||||
|
||||
// Compress backup
|
||||
$this->logger->info("Compressing backup", array('backup_id' => $backup_id));
|
||||
$compressed_file = $this->compression->compress_directory($backup_dir, $backup_id);
|
||||
|
||||
if (!$compressed_file) {
|
||||
throw new Exception('Failed to compress backup');
|
||||
}
|
||||
|
||||
// Calculate final size and checksum
|
||||
$manifest['size'] = filesize($compressed_file);
|
||||
$manifest['checksum'] = md5_file($compressed_file);
|
||||
|
||||
// Update manifest in compressed file
|
||||
file_put_contents($manifest_file, json_encode($manifest, JSON_PRETTY_PRINT));
|
||||
|
||||
// Store backup using storage manager
|
||||
$stored_backup = $this->storage->store_backup($compressed_file, $backup_id, $manifest);
|
||||
|
||||
// Cleanup temporary files
|
||||
$this->cleanup_temp_directory($backup_dir);
|
||||
|
||||
$duration = microtime(true) - $start_time;
|
||||
$this->logger->info("Backup completed successfully", array(
|
||||
'backup_id' => $backup_id,
|
||||
'duration' => round($duration, 2) . 's',
|
||||
'size' => $this->format_bytes($manifest['size'])
|
||||
));
|
||||
|
||||
// Save backup record to database
|
||||
$this->save_backup_record($backup_id, $manifest, $stored_backup, $compressed_file);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'backup_id' => $backup_id,
|
||||
'manifest' => $manifest,
|
||||
'storage' => $stored_backup,
|
||||
'duration' => $duration
|
||||
);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error("Backup failed", array(
|
||||
'backup_id' => $backup_id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
));
|
||||
|
||||
// Cleanup on failure
|
||||
if (isset($backup_dir)) {
|
||||
$this->cleanup_temp_directory($backup_dir);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'backup_id' => $backup_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup files with chunked processing
|
||||
*/
|
||||
private function backup_files($backup_dir, $backup_id) {
|
||||
$files_dir = $backup_dir . '/files/';
|
||||
wp_mkdir_p($files_dir);
|
||||
|
||||
$file_list = array();
|
||||
$total_size = 0;
|
||||
$file_count = 0;
|
||||
|
||||
// Define directories to backup
|
||||
$backup_paths = array();
|
||||
|
||||
if ($this->settings['include_uploads']) {
|
||||
$backup_paths[] = WP_CONTENT_DIR . '/uploads/';
|
||||
}
|
||||
|
||||
if ($this->settings['include_themes']) {
|
||||
$backup_paths[] = WP_CONTENT_DIR . '/themes/';
|
||||
}
|
||||
|
||||
if ($this->settings['include_plugins']) {
|
||||
$backup_paths[] = WP_CONTENT_DIR . '/plugins/';
|
||||
}
|
||||
|
||||
if ($this->settings['include_wp_core']) {
|
||||
$backup_paths[] = ABSPATH;
|
||||
}
|
||||
|
||||
foreach ($backup_paths as $path) {
|
||||
if (!is_dir($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->logger->debug("Backing up directory", array('path' => $path));
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filepath = $file->getPathname();
|
||||
$relative_path = str_replace(ABSPATH, '', $filepath);
|
||||
|
||||
// Check exclusion patterns
|
||||
if ($this->should_exclude_file($relative_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Copy file to backup directory
|
||||
$backup_filepath = $files_dir . $relative_path;
|
||||
$backup_file_dir = dirname($backup_filepath);
|
||||
|
||||
if (!is_dir($backup_file_dir)) {
|
||||
wp_mkdir_p($backup_file_dir);
|
||||
}
|
||||
|
||||
if (copy($filepath, $backup_filepath)) {
|
||||
$file_size = filesize($filepath);
|
||||
$file_list[] = array(
|
||||
'path' => $relative_path,
|
||||
'size' => $file_size,
|
||||
'modified' => filemtime($filepath),
|
||||
'checksum' => md5_file($filepath)
|
||||
);
|
||||
|
||||
$total_size += $file_size;
|
||||
$file_count++;
|
||||
|
||||
// Update progress every 100 files
|
||||
if ($file_count % 100 === 0) {
|
||||
$this->update_backup_progress($backup_id, 'files', $file_count);
|
||||
}
|
||||
} else {
|
||||
$this->logger->warning("Failed to copy file", array('file' => $filepath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'file_count' => $file_count,
|
||||
'total_size' => $total_size,
|
||||
'files' => $file_list
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup database with batch processing
|
||||
*/
|
||||
private function backup_database($backup_dir, $backup_id) {
|
||||
global $wpdb;
|
||||
|
||||
$db_dir = $backup_dir . '/database/';
|
||||
wp_mkdir_p($db_dir);
|
||||
|
||||
$sql_file = $db_dir . 'database.sql';
|
||||
$tables_info = array();
|
||||
|
||||
// Get all WordPress tables
|
||||
$tables = $wpdb->get_col("SHOW TABLES LIKE '{$wpdb->prefix}%'");
|
||||
|
||||
$sql_content = "-- TigerStyle SEO Database Backup\n";
|
||||
$sql_content .= "-- Created: " . current_time('mysql') . "\n";
|
||||
$sql_content .= "-- WordPress Version: " . get_bloginfo('version') . "\n\n";
|
||||
$sql_content .= "SET FOREIGN_KEY_CHECKS = 0;\n";
|
||||
$sql_content .= "SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';\n\n";
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$this->logger->debug("Backing up table", array('table' => $table));
|
||||
|
||||
// Get table structure
|
||||
$create_table = $wpdb->get_row("SHOW CREATE TABLE `{$table}`", ARRAY_N);
|
||||
$sql_content .= "\n-- Table structure for `{$table}`\n";
|
||||
$sql_content .= "DROP TABLE IF EXISTS `{$table}`;\n";
|
||||
$sql_content .= $create_table[1] . ";\n\n";
|
||||
|
||||
// Get table data in batches
|
||||
$row_count = $wpdb->get_var("SELECT COUNT(*) FROM `{$table}`");
|
||||
$offset = 0;
|
||||
$batch_size = $this->settings['database_batch_size'];
|
||||
|
||||
$sql_content .= "-- Data for table `{$table}`\n";
|
||||
|
||||
while ($offset < $row_count) {
|
||||
$rows = $wpdb->get_results("SELECT * FROM `{$table}` LIMIT {$offset}, {$batch_size}", ARRAY_A);
|
||||
|
||||
if (empty($rows)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$values = array();
|
||||
foreach ($row as $value) {
|
||||
$values[] = $wpdb->prepare('%s', $value);
|
||||
}
|
||||
|
||||
$sql_content .= "INSERT INTO `{$table}` VALUES (" . implode(', ', $values) . ");\n";
|
||||
}
|
||||
|
||||
$offset += $batch_size;
|
||||
|
||||
// Update progress
|
||||
$progress = min(100, ($offset / $row_count) * 100);
|
||||
$this->update_backup_progress($backup_id, 'database', $progress, $table);
|
||||
}
|
||||
|
||||
$tables_info[] = array(
|
||||
'name' => $table,
|
||||
'rows' => $row_count,
|
||||
'size' => $wpdb->get_var("SELECT ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'DB Size in MB' FROM information_schema.tables WHERE table_schema='{$wpdb->dbname}' AND table_name='{$table}'")
|
||||
);
|
||||
}
|
||||
|
||||
$sql_content .= "\nSET FOREIGN_KEY_CHECKS = 1;\n";
|
||||
|
||||
// Write SQL file
|
||||
file_put_contents($sql_file, $sql_content);
|
||||
|
||||
return array(
|
||||
'table_count' => count($tables),
|
||||
'total_rows' => array_sum(array_column($tables_info, 'rows')),
|
||||
'sql_file' => 'database/database.sql',
|
||||
'sql_size' => filesize($sql_file),
|
||||
'tables' => $tables_info
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file should be excluded
|
||||
*/
|
||||
private function should_exclude_file($filepath) {
|
||||
foreach ($this->settings['exclude_patterns'] as $pattern) {
|
||||
if (fnmatch($pattern, $filepath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique backup ID
|
||||
*/
|
||||
private function generate_backup_id() {
|
||||
return 'backup_' . date('Y-m-d_H-i-s') . '_' . wp_generate_password(8, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup directory
|
||||
*/
|
||||
private function create_backup_directory($backup_id) {
|
||||
$backup_dir = $this->settings['backup_location'] . $backup_id . '/';
|
||||
|
||||
if (wp_mkdir_p($backup_dir)) {
|
||||
// Create .htaccess for security
|
||||
$htaccess_content = "Order deny,allow\nDeny from all\n";
|
||||
file_put_contents($backup_dir . '.htaccess', $htaccess_content);
|
||||
|
||||
return $backup_dir;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup temporary directory
|
||||
*/
|
||||
private function cleanup_temp_directory($directory) {
|
||||
if (!is_dir($directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir()) {
|
||||
rmdir($file->getPathname());
|
||||
} else {
|
||||
unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir($directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update backup progress
|
||||
*/
|
||||
private function update_backup_progress($backup_id, $stage, $progress, $details = '') {
|
||||
update_option('tigerstyle_backup_progress_' . $backup_id, array(
|
||||
'stage' => $stage,
|
||||
'progress' => $progress,
|
||||
'details' => $details,
|
||||
'updated' => time()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save backup record to database
|
||||
*/
|
||||
private function save_backup_record($backup_id, $manifest, $storage_info, $compressed_file) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_metadata';
|
||||
|
||||
$wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'backup_id' => $backup_id,
|
||||
'storage_type' => $storage_info['type'] ?? 'local',
|
||||
'file_path' => $storage_info['path'] ?? $compressed_file,
|
||||
'file_size' => $manifest['size'],
|
||||
'created_at' => current_time('mysql'),
|
||||
'metadata_json' => json_encode($manifest)
|
||||
),
|
||||
array('%s', '%s', '%s', '%d', '%s', '%s')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes for display
|
||||
*/
|
||||
private function format_bytes($bytes, $precision = 2) {
|
||||
$units = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup progress
|
||||
*/
|
||||
public function get_backup_progress($backup_id) {
|
||||
return get_option('tigerstyle_backup_progress_' . $backup_id, array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup backup progress
|
||||
*/
|
||||
public function cleanup_backup_progress($backup_id) {
|
||||
delete_option('tigerstyle_backup_progress_' . $backup_id);
|
||||
}
|
||||
}
|
||||
575
includes/backup/class-backup-logger.php
Normal file
575
includes/backup/class-backup-logger.php
Normal file
@ -0,0 +1,575 @@
|
||||
<?php
|
||||
/**
|
||||
* Enterprise Backup Logger for TigerStyle SEO
|
||||
* Multi-level logging with rotation and export capabilities
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Backup_Logger {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Log levels
|
||||
*/
|
||||
const LEVEL_DEBUG = 1;
|
||||
const LEVEL_INFO = 2;
|
||||
const LEVEL_WARNING = 3;
|
||||
const LEVEL_ERROR = 4;
|
||||
const LEVEL_CRITICAL = 5;
|
||||
|
||||
/**
|
||||
* Log level names
|
||||
*/
|
||||
private $level_names = array(
|
||||
self::LEVEL_DEBUG => 'DEBUG',
|
||||
self::LEVEL_INFO => 'INFO',
|
||||
self::LEVEL_WARNING => 'WARNING',
|
||||
self::LEVEL_ERROR => 'ERROR',
|
||||
self::LEVEL_CRITICAL => 'CRITICAL'
|
||||
);
|
||||
|
||||
/**
|
||||
* Log settings
|
||||
*/
|
||||
private $settings = array();
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize logger
|
||||
*/
|
||||
private function init() {
|
||||
$this->settings = array(
|
||||
'enabled' => get_option('backup_logging_enabled', true),
|
||||
'level' => get_option('backup_logging_level', self::LEVEL_INFO),
|
||||
'file_enabled' => get_option('backup_logging_file_enabled', true),
|
||||
'database_enabled' => false, // Disabled to avoid complex database setup
|
||||
'email_enabled' => get_option('backup_logging_email_enabled', false),
|
||||
'wordpress_debug_enabled' => get_option('backup_logging_wp_debug_enabled', true), // Use WordPress debug instead
|
||||
'log_file' => WP_CONTENT_DIR . '/tigerstyle-backup-logs/backup.log',
|
||||
'max_file_size' => 10485760, // 10MB
|
||||
'max_log_files' => 5,
|
||||
'email_level' => self::LEVEL_ERROR,
|
||||
'email_recipient' => get_option('admin_email')
|
||||
);
|
||||
|
||||
// Ensure log directory exists
|
||||
$log_dir = dirname($this->settings['log_file']);
|
||||
if (!is_dir($log_dir)) {
|
||||
wp_mkdir_p($log_dir);
|
||||
|
||||
// Protect log directory
|
||||
$htaccess_content = "Order deny,allow\nDeny from all\n";
|
||||
file_put_contents($log_dir . '/.htaccess', $htaccess_content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug message
|
||||
*/
|
||||
public function debug($message, $context = array()) {
|
||||
$this->log(self::LEVEL_DEBUG, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info message
|
||||
*/
|
||||
public function info($message, $context = array()) {
|
||||
$this->log(self::LEVEL_INFO, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning message
|
||||
*/
|
||||
public function warning($message, $context = array()) {
|
||||
$this->log(self::LEVEL_WARNING, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error message
|
||||
*/
|
||||
public function error($message, $context = array()) {
|
||||
$this->log(self::LEVEL_ERROR, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log critical message
|
||||
*/
|
||||
public function critical($message, $context = array()) {
|
||||
$this->log(self::LEVEL_CRITICAL, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main logging method
|
||||
*/
|
||||
private function log($level, $message, $context = array()) {
|
||||
if (!$this->settings['enabled'] || $level < $this->settings['level']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$log_entry = $this->create_log_entry($level, $message, $context);
|
||||
|
||||
// Log to file
|
||||
if ($this->settings['file_enabled']) {
|
||||
$this->log_to_file($log_entry);
|
||||
}
|
||||
|
||||
// Log to database
|
||||
if ($this->settings['database_enabled']) {
|
||||
$this->log_to_database($level, $message, $context);
|
||||
}
|
||||
|
||||
// Log to WordPress debug
|
||||
if ($this->settings['wordpress_debug_enabled'] && WP_DEBUG_LOG) {
|
||||
$this->log_to_wordpress_debug($log_entry);
|
||||
}
|
||||
|
||||
// Send email for critical errors
|
||||
if ($this->settings['email_enabled'] && $level >= $this->settings['email_level']) {
|
||||
$this->send_email_notification($level, $message, $context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create formatted log entry
|
||||
*/
|
||||
private function create_log_entry($level, $message, $context) {
|
||||
$timestamp = current_time('Y-m-d H:i:s');
|
||||
$level_name = $this->level_names[$level];
|
||||
$user_info = $this->get_user_info();
|
||||
$memory_usage = $this->format_bytes(memory_get_usage(true));
|
||||
|
||||
$log_data = array(
|
||||
'timestamp' => $timestamp,
|
||||
'level' => $level_name,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
'user' => $user_info,
|
||||
'memory' => $memory_usage,
|
||||
'request_id' => $this->get_request_id()
|
||||
);
|
||||
|
||||
return $log_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to file
|
||||
*/
|
||||
private function log_to_file($log_entry) {
|
||||
// Check if log rotation is needed
|
||||
if (file_exists($this->settings['log_file']) && filesize($this->settings['log_file']) > $this->settings['max_file_size']) {
|
||||
$this->rotate_log_file();
|
||||
}
|
||||
|
||||
$formatted_entry = $this->format_log_entry_for_file($log_entry);
|
||||
|
||||
// Append to log file
|
||||
file_put_contents($this->settings['log_file'], $formatted_entry . "\n", FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to database
|
||||
*/
|
||||
private function log_to_database($level, $message, $context) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_logs';
|
||||
|
||||
$wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'created_at' => current_time('mysql'),
|
||||
'level' => $this->level_names[$level],
|
||||
'message' => $message,
|
||||
'context' => json_encode($context),
|
||||
'user_id' => get_current_user_id(),
|
||||
'user_ip' => $this->get_user_ip(),
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'request_id' => $this->get_request_id()
|
||||
),
|
||||
array('%s', '%s', '%s', '%s', '%d', '%s', '%d', '%s')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to WordPress debug
|
||||
*/
|
||||
private function log_to_wordpress_debug($log_entry) {
|
||||
$formatted_entry = sprintf(
|
||||
'[TigerStyle Backup] [%s] %s %s',
|
||||
$log_entry['level'],
|
||||
$log_entry['message'],
|
||||
!empty($log_entry['context']) ? json_encode($log_entry['context']) : ''
|
||||
);
|
||||
|
||||
error_log($formatted_entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email notification
|
||||
*/
|
||||
private function send_email_notification($level, $message, $context) {
|
||||
if (!$this->settings['email_recipient']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subject = sprintf(
|
||||
'[%s] TigerStyle SEO Backup %s: %s',
|
||||
get_bloginfo('name'),
|
||||
$this->level_names[$level],
|
||||
$message
|
||||
);
|
||||
|
||||
$body = "A backup system event occurred:\n\n";
|
||||
$body .= "Level: " . $this->level_names[$level] . "\n";
|
||||
$body .= "Message: " . $message . "\n";
|
||||
$body .= "Time: " . current_time('Y-m-d H:i:s') . "\n";
|
||||
$body .= "Site: " . site_url() . "\n";
|
||||
|
||||
if (!empty($context)) {
|
||||
$body .= "\nContext:\n" . print_r($context, true);
|
||||
}
|
||||
|
||||
$body .= "\n---\nThis is an automated message from TigerStyle SEO Backup System.";
|
||||
|
||||
wp_mail($this->settings['email_recipient'], $subject, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format log entry for file output
|
||||
*/
|
||||
private function format_log_entry_for_file($log_entry) {
|
||||
$context_str = !empty($log_entry['context']) ? json_encode($log_entry['context']) : '';
|
||||
|
||||
return sprintf(
|
||||
'[%s] [%s] [%s] [MEM:%s] [REQ:%s] %s %s',
|
||||
$log_entry['timestamp'],
|
||||
$log_entry['level'],
|
||||
$log_entry['user']['display'],
|
||||
$log_entry['memory'],
|
||||
$log_entry['request_id'],
|
||||
$log_entry['message'],
|
||||
$context_str
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate log file
|
||||
*/
|
||||
private function rotate_log_file() {
|
||||
$base_file = $this->settings['log_file'];
|
||||
|
||||
// Remove oldest log file
|
||||
$oldest_log = $base_file . '.' . $this->settings['max_log_files'];
|
||||
if (file_exists($oldest_log)) {
|
||||
unlink($oldest_log);
|
||||
}
|
||||
|
||||
// Rotate existing log files
|
||||
for ($i = $this->settings['max_log_files'] - 1; $i >= 1; $i--) {
|
||||
$current_log = $base_file . '.' . $i;
|
||||
$next_log = $base_file . '.' . ($i + 1);
|
||||
|
||||
if (file_exists($current_log)) {
|
||||
rename($current_log, $next_log);
|
||||
}
|
||||
}
|
||||
|
||||
// Move current log to .1
|
||||
if (file_exists($base_file)) {
|
||||
rename($base_file, $base_file . '.1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user information
|
||||
*/
|
||||
private function get_user_info() {
|
||||
$user = wp_get_current_user();
|
||||
|
||||
if ($user->ID) {
|
||||
return array(
|
||||
'id' => $user->ID,
|
||||
'login' => $user->user_login,
|
||||
'display' => $user->display_name,
|
||||
'email' => $user->user_email
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'id' => 0,
|
||||
'login' => 'guest',
|
||||
'display' => 'Guest User',
|
||||
'email' => ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user IP address
|
||||
*/
|
||||
private function get_user_ip() {
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
return sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
} elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
return sanitize_text_field($_SERVER['HTTP_X_REAL_IP']);
|
||||
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
return sanitize_text_field($_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request ID for tracking related log entries
|
||||
*/
|
||||
private function get_request_id() {
|
||||
static $request_id = null;
|
||||
|
||||
if ($request_id === null) {
|
||||
$request_id = substr(md5(uniqid(mt_rand(), true)), 0, 8);
|
||||
}
|
||||
|
||||
return $request_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes for display
|
||||
*/
|
||||
private function format_bytes($bytes, $precision = 2) {
|
||||
$units = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, $precision) . $units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent log entries
|
||||
*/
|
||||
public function get_recent_logs($limit = 100, $level_filter = null) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_logs';
|
||||
|
||||
$sql = "SELECT * FROM {$table_name}";
|
||||
$where_conditions = array();
|
||||
$where_values = array();
|
||||
|
||||
if ($level_filter && isset($this->level_names[$level_filter])) {
|
||||
$where_conditions[] = "level = %s";
|
||||
$where_values[] = $this->level_names[$level_filter];
|
||||
}
|
||||
|
||||
if (!empty($where_conditions)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $where_conditions);
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY created_at DESC LIMIT %d";
|
||||
$where_values[] = $limit;
|
||||
|
||||
if (!empty($where_values)) {
|
||||
$sql = $wpdb->prepare($sql, $where_values);
|
||||
}
|
||||
|
||||
return $wpdb->get_results($sql, ARRAY_A);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export logs to file
|
||||
*/
|
||||
public function export_logs($format = 'json', $date_from = null, $date_to = null) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_logs';
|
||||
|
||||
$sql = "SELECT * FROM {$table_name}";
|
||||
$where_conditions = array();
|
||||
$where_values = array();
|
||||
|
||||
if ($date_from) {
|
||||
$where_conditions[] = "created_at >= %s";
|
||||
$where_values[] = $date_from;
|
||||
}
|
||||
|
||||
if ($date_to) {
|
||||
$where_conditions[] = "created_at <= %s";
|
||||
$where_values[] = $date_to;
|
||||
}
|
||||
|
||||
if (!empty($where_conditions)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $where_conditions);
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY created_at DESC";
|
||||
|
||||
if (!empty($where_values)) {
|
||||
$sql = $wpdb->prepare($sql, $where_values);
|
||||
}
|
||||
|
||||
$logs = $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
switch ($format) {
|
||||
case 'json':
|
||||
return json_encode($logs, JSON_PRETTY_PRINT);
|
||||
case 'csv':
|
||||
return $this->logs_to_csv($logs);
|
||||
case 'txt':
|
||||
return $this->logs_to_text($logs);
|
||||
default:
|
||||
return $logs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert logs to CSV format
|
||||
*/
|
||||
private function logs_to_csv($logs) {
|
||||
if (empty($logs)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$csv = "Timestamp,Level,Message,User ID,User IP,Memory Usage,Request ID\n";
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$csv .= sprintf(
|
||||
'"%s","%s","%s","%s","%s","%s","%s"' . "\n",
|
||||
$log['created_at'],
|
||||
$log['level'],
|
||||
str_replace('"', '""', $log['message']),
|
||||
$log['user_id'],
|
||||
$log['user_ip'],
|
||||
$log['memory_usage'],
|
||||
$log['request_id']
|
||||
);
|
||||
}
|
||||
|
||||
return $csv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert logs to text format
|
||||
*/
|
||||
private function logs_to_text($logs) {
|
||||
if (empty($logs)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = "TigerStyle SEO Backup Logs Export\n";
|
||||
$text .= "Generated: " . current_time('Y-m-d H:i:s') . "\n";
|
||||
$text .= str_repeat('=', 50) . "\n\n";
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$text .= sprintf(
|
||||
"[%s] [%s] %s\n",
|
||||
$log['created_at'],
|
||||
$log['level'],
|
||||
$log['message']
|
||||
);
|
||||
|
||||
if (!empty($log['context'])) {
|
||||
$context = json_decode($log['context'], true);
|
||||
if ($context) {
|
||||
$text .= " Context: " . print_r($context, true) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old logs
|
||||
*/
|
||||
public function cleanup_old_logs($days = 30) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_logs';
|
||||
$cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||
|
||||
$deleted = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$table_name} WHERE created_at < %s",
|
||||
$cutoff_date
|
||||
)
|
||||
);
|
||||
|
||||
$this->info("Cleaned up old log entries", array(
|
||||
'deleted_count' => $deleted,
|
||||
'cutoff_date' => $cutoff_date
|
||||
));
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log statistics
|
||||
*/
|
||||
public function get_log_statistics($days = 7) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_logs';
|
||||
$cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||
|
||||
$stats = array();
|
||||
|
||||
// Total logs
|
||||
$stats['total'] = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table_name} WHERE created_at >= %s",
|
||||
$cutoff_date
|
||||
)
|
||||
);
|
||||
|
||||
// Logs by level
|
||||
$level_stats = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT level, COUNT(*) as count FROM {$table_name} WHERE created_at >= %s GROUP BY level",
|
||||
$cutoff_date
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$stats['by_level'] = array();
|
||||
foreach ($level_stats as $level_stat) {
|
||||
$stats['by_level'][$level_stat['level']] = $level_stat['count'];
|
||||
}
|
||||
|
||||
// Recent errors
|
||||
$stats['recent_errors'] = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table_name} WHERE level IN ('ERROR', 'CRITICAL') AND created_at >= %s ORDER BY created_at DESC LIMIT 10",
|
||||
$cutoff_date
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
548
includes/backup/class-backup-scheduler.php
Normal file
548
includes/backup/class-backup-scheduler.php
Normal file
@ -0,0 +1,548 @@
|
||||
<?php
|
||||
/**
|
||||
* TigerStyle SEO Backup Scheduler
|
||||
*
|
||||
* Handles scheduled backups, retention policies,
|
||||
* and automated backup management.
|
||||
*
|
||||
* @package TigerStyleSEO
|
||||
* @subpackage BackupScheduler
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Backup_Scheduler {
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->logger = TigerStyleSEO_Backup_Logger::instance();
|
||||
$this->settings = get_option('tigerstyle_backup_settings', array());
|
||||
|
||||
$this->setup_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup WordPress hooks
|
||||
*/
|
||||
private function setup_hooks() {
|
||||
// Scheduled backup hook
|
||||
add_action('tigerstyle_backup_scheduled', array($this, 'run_scheduled_backup'));
|
||||
|
||||
// Cleanup hook
|
||||
add_action('tigerstyle_backup_cleanup', array($this, 'cleanup_old_backups'));
|
||||
|
||||
// Admin hooks
|
||||
add_action('admin_init', array($this, 'maybe_setup_schedules'));
|
||||
|
||||
// Plugin deactivation cleanup
|
||||
register_deactivation_hook(TIGERSTYLE_HEAT_PLUGIN_FILE, array($this, 'clear_schedules'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup schedules if needed
|
||||
*/
|
||||
public function maybe_setup_schedules() {
|
||||
// Setup cleanup schedule if not exists
|
||||
if (!wp_next_scheduled('tigerstyle_backup_cleanup')) {
|
||||
wp_schedule_event(time() + 3600, 'daily', 'tigerstyle_backup_cleanup');
|
||||
}
|
||||
|
||||
// Setup backup schedule based on settings
|
||||
$this->update_backup_schedule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update backup schedule based on settings
|
||||
*/
|
||||
public function update_backup_schedule() {
|
||||
// Clear existing schedule
|
||||
wp_clear_scheduled_hook('tigerstyle_backup_scheduled');
|
||||
|
||||
// Setup new schedule if enabled
|
||||
if (!empty($this->settings['schedule_enabled'])) {
|
||||
$frequency = $this->settings['schedule_frequency'] ?? 'daily';
|
||||
$start_time = $this->calculate_next_backup_time($frequency);
|
||||
|
||||
wp_schedule_event($start_time, $frequency, 'tigerstyle_backup_scheduled');
|
||||
|
||||
$this->logger->info('Backup schedule updated', array(
|
||||
'frequency' => $frequency,
|
||||
'next_run' => date('Y-m-d H:i:s', $start_time)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate next backup time
|
||||
*/
|
||||
private function calculate_next_backup_time($frequency) {
|
||||
$current_time = time();
|
||||
$preferred_hour = $this->settings['schedule_time'] ?? '02:00';
|
||||
|
||||
// Parse preferred time
|
||||
list($hour, $minute) = explode(':', $preferred_hour);
|
||||
$hour = intval($hour);
|
||||
$minute = intval($minute);
|
||||
|
||||
// Calculate next run time
|
||||
switch ($frequency) {
|
||||
case 'hourly':
|
||||
return $current_time + HOUR_IN_SECONDS;
|
||||
|
||||
case 'daily':
|
||||
$next_time = mktime($hour, $minute, 0);
|
||||
if ($next_time <= $current_time) {
|
||||
$next_time += DAY_IN_SECONDS;
|
||||
}
|
||||
return $next_time;
|
||||
|
||||
case 'weekly':
|
||||
$preferred_day = $this->settings['schedule_day'] ?? 'sunday';
|
||||
$day_number = $this->get_day_number($preferred_day);
|
||||
$next_time = strtotime("next {$preferred_day} {$hour}:{$minute}:00");
|
||||
return $next_time;
|
||||
|
||||
case 'monthly':
|
||||
$preferred_date = $this->settings['schedule_date'] ?? 1;
|
||||
$next_time = mktime($hour, $minute, 0, date('n'), $preferred_date);
|
||||
if ($next_time <= $current_time) {
|
||||
$next_time = mktime($hour, $minute, 0, date('n') + 1, $preferred_date);
|
||||
}
|
||||
return $next_time;
|
||||
|
||||
default:
|
||||
return $current_time + DAY_IN_SECONDS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run scheduled backup
|
||||
*/
|
||||
public function run_scheduled_backup() {
|
||||
if (!$this->should_run_backup()) {
|
||||
$this->logger->info('Scheduled backup skipped', array(
|
||||
'reason' => 'Conditions not met'
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->logger->info('Starting scheduled backup');
|
||||
|
||||
$backup_engine = new TigerStyleSEO_Backup_Engine();
|
||||
|
||||
$backup_options = array(
|
||||
'type' => 'scheduled',
|
||||
'compression' => $this->settings['compression'] ?? 'zip',
|
||||
'storage_location' => $this->settings['storage_location'] ?? 'local',
|
||||
'include_files' => $this->settings['include_files'] ?? true,
|
||||
'include_database' => $this->settings['include_database'] ?? true,
|
||||
'description' => $this->generate_scheduled_backup_description()
|
||||
);
|
||||
|
||||
$backup_id = $backup_engine->create_backup($backup_options);
|
||||
|
||||
$this->logger->info('Scheduled backup completed successfully', array(
|
||||
'backup_id' => $backup_id
|
||||
));
|
||||
|
||||
// Send notification if enabled
|
||||
if (!empty($this->settings['email_notifications'])) {
|
||||
$this->send_backup_notification($backup_id, true);
|
||||
}
|
||||
|
||||
// Update last backup time
|
||||
update_option('tigerstyle_last_scheduled_backup', time());
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Scheduled backup failed: ' . $e->getMessage());
|
||||
|
||||
// Send failure notification
|
||||
if (!empty($this->settings['email_notifications'])) {
|
||||
$this->send_backup_notification(null, false, $e->getMessage());
|
||||
}
|
||||
|
||||
// Record failure
|
||||
$this->record_backup_failure($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if backup should run
|
||||
*/
|
||||
private function should_run_backup() {
|
||||
// Check if backups are enabled
|
||||
if (empty($this->settings['schedule_enabled'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check system load (don't backup during high load)
|
||||
if ($this->is_system_under_high_load()) {
|
||||
$this->logger->warning('Backup skipped due to high system load');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check disk space
|
||||
if (!$this->has_sufficient_disk_space()) {
|
||||
$this->logger->warning('Backup skipped due to insufficient disk space');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if another backup is running
|
||||
if ($this->is_backup_running()) {
|
||||
$this->logger->warning('Backup skipped because another backup is running');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if system is under high load
|
||||
*/
|
||||
private function is_system_under_high_load() {
|
||||
if (!function_exists('sys_getloadavg')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$load = sys_getloadavg();
|
||||
$max_load = $this->settings['max_load_average'] ?? 2.0;
|
||||
|
||||
return $load && $load[0] > $max_load;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's sufficient disk space
|
||||
*/
|
||||
private function has_sufficient_disk_space() {
|
||||
$required_space_mb = $this->settings['min_disk_space'] ?? 1024; // 1GB default
|
||||
$required_space = $required_space_mb * 1024 * 1024;
|
||||
|
||||
$available_space = disk_free_space(ABSPATH);
|
||||
|
||||
return $available_space === false || $available_space > $required_space;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if backup is currently running
|
||||
*/
|
||||
private function is_backup_running() {
|
||||
global $wpdb;
|
||||
|
||||
// Check for active backup processes
|
||||
$active_backups = get_transient('tigerstyle_active_backup_processes');
|
||||
|
||||
if ($active_backups && count($active_backups) > 0) {
|
||||
// Clean up stale processes (older than 2 hours)
|
||||
$current_time = time();
|
||||
foreach ($active_backups as $key => $timestamp) {
|
||||
if ($current_time - $timestamp > 7200) {
|
||||
unset($active_backups[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
set_transient('tigerstyle_active_backup_processes', $active_backups, 3600);
|
||||
|
||||
return count($active_backups) > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate scheduled backup description
|
||||
*/
|
||||
private function generate_scheduled_backup_description() {
|
||||
$frequency = $this->settings['schedule_frequency'] ?? 'daily';
|
||||
$timestamp = current_time('mysql');
|
||||
|
||||
return sprintf(
|
||||
__('Scheduled %s backup - %s', 'tigerstyle-heat'),
|
||||
$frequency,
|
||||
$timestamp
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send backup notification email
|
||||
*/
|
||||
private function send_backup_notification($backup_id, $success, $error_message = '') {
|
||||
if (empty($this->settings['notification_email'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$site_name = get_bloginfo('name');
|
||||
$site_url = home_url();
|
||||
|
||||
if ($success) {
|
||||
$subject = sprintf(__('[%s] Scheduled Backup Completed Successfully', 'tigerstyle-heat'), $site_name);
|
||||
$message = sprintf(
|
||||
__("Your scheduled backup has completed successfully.\n\nBackup ID: %s\nSite: %s\nCompleted: %s\n\nYou can manage your backups from the WordPress admin panel.", 'tigerstyle-heat'),
|
||||
$backup_id,
|
||||
$site_url,
|
||||
current_time('Y-m-d H:i:s')
|
||||
);
|
||||
} else {
|
||||
$subject = sprintf(__('[%s] Scheduled Backup Failed', 'tigerstyle-heat'), $site_name);
|
||||
$message = sprintf(
|
||||
__("Your scheduled backup has failed.\n\nSite: %s\nFailed: %s\nError: %s\n\nPlease check your backup settings and try again.", 'tigerstyle-heat'),
|
||||
$site_url,
|
||||
current_time('Y-m-d H:i:s'),
|
||||
$error_message
|
||||
);
|
||||
}
|
||||
|
||||
wp_mail($this->settings['notification_email'], $subject, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record backup failure
|
||||
*/
|
||||
private function record_backup_failure($error_message) {
|
||||
$failures = get_option('tigerstyle_scheduled_backup_failures', array());
|
||||
|
||||
$failures[] = array(
|
||||
'timestamp' => time(),
|
||||
'error' => $error_message
|
||||
);
|
||||
|
||||
// Keep only last 10 failures
|
||||
$failures = array_slice($failures, -10);
|
||||
|
||||
update_option('tigerstyle_scheduled_backup_failures', $failures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old backups
|
||||
*/
|
||||
public function cleanup_old_backups() {
|
||||
$retention_days = $this->settings['retention_days'] ?? 30;
|
||||
|
||||
if ($retention_days <= 0) {
|
||||
return; // Unlimited retention
|
||||
}
|
||||
|
||||
try {
|
||||
$storage_manager = new TigerStyleSEO_Storage_Manager();
|
||||
$deleted_count = $storage_manager->cleanup_old_backups($retention_days);
|
||||
|
||||
$this->logger->info('Old backups cleanup completed', array(
|
||||
'retention_days' => $retention_days,
|
||||
'deleted_count' => $deleted_count
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Backup cleanup failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next scheduled backup time
|
||||
*/
|
||||
public function get_next_backup_time() {
|
||||
$timestamp = wp_next_scheduled('tigerstyle_backup_scheduled');
|
||||
|
||||
if (!$timestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'timestamp' => $timestamp,
|
||||
'formatted' => date('Y-m-d H:i:s', $timestamp),
|
||||
'human' => human_time_diff($timestamp, current_time('timestamp'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup schedule status
|
||||
*/
|
||||
public function get_schedule_status() {
|
||||
$status = array(
|
||||
'enabled' => !empty($this->settings['schedule_enabled']),
|
||||
'frequency' => $this->settings['schedule_frequency'] ?? 'daily',
|
||||
'next_run' => $this->get_next_backup_time(),
|
||||
'last_run' => null,
|
||||
'last_success' => null,
|
||||
'failure_count' => 0
|
||||
);
|
||||
|
||||
// Get last backup time
|
||||
$last_backup_time = get_option('tigerstyle_last_scheduled_backup');
|
||||
if ($last_backup_time) {
|
||||
$status['last_run'] = array(
|
||||
'timestamp' => $last_backup_time,
|
||||
'formatted' => date('Y-m-d H:i:s', $last_backup_time),
|
||||
'human' => human_time_diff($last_backup_time, current_time('timestamp')) . __(' ago', 'tigerstyle-heat')
|
||||
);
|
||||
}
|
||||
|
||||
// Get failure information
|
||||
$failures = get_option('tigerstyle_scheduled_backup_failures', array());
|
||||
$status['failure_count'] = count($failures);
|
||||
|
||||
if (!empty($failures)) {
|
||||
$last_failure = end($failures);
|
||||
$status['last_failure'] = array(
|
||||
'timestamp' => $last_failure['timestamp'],
|
||||
'formatted' => date('Y-m-d H:i:s', $last_failure['timestamp']),
|
||||
'error' => $last_failure['error']
|
||||
);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable scheduled backups
|
||||
*/
|
||||
public function enable_schedule($frequency = 'daily', $time = '02:00') {
|
||||
$this->settings['schedule_enabled'] = true;
|
||||
$this->settings['schedule_frequency'] = $frequency;
|
||||
$this->settings['schedule_time'] = $time;
|
||||
|
||||
update_option('tigerstyle_backup_settings', $this->settings);
|
||||
|
||||
$this->update_backup_schedule();
|
||||
|
||||
$this->logger->info('Backup schedule enabled', array(
|
||||
'frequency' => $frequency,
|
||||
'time' => $time
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable scheduled backups
|
||||
*/
|
||||
public function disable_schedule() {
|
||||
$this->settings['schedule_enabled'] = false;
|
||||
update_option('tigerstyle_backup_settings', $this->settings);
|
||||
|
||||
wp_clear_scheduled_hook('tigerstyle_backup_scheduled');
|
||||
|
||||
$this->logger->info('Backup schedule disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run backup immediately
|
||||
*/
|
||||
public function run_backup_now() {
|
||||
// Mark as manual backup
|
||||
$backup_engine = new TigerStyleSEO_Backup_Engine();
|
||||
|
||||
$backup_options = array(
|
||||
'type' => 'manual',
|
||||
'compression' => $this->settings['compression'] ?? 'zip',
|
||||
'storage_location' => $this->settings['storage_location'] ?? 'local',
|
||||
'include_files' => $this->settings['include_files'] ?? true,
|
||||
'include_database' => $this->settings['include_database'] ?? true,
|
||||
'description' => __('Manual backup - ', 'tigerstyle-heat') . current_time('mysql')
|
||||
);
|
||||
|
||||
return $backup_engine->create_backup($backup_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get day number for date calculations
|
||||
*/
|
||||
private function get_day_number($day_name) {
|
||||
$days = array(
|
||||
'sunday' => 0,
|
||||
'monday' => 1,
|
||||
'tuesday' => 2,
|
||||
'wednesday' => 3,
|
||||
'thursday' => 4,
|
||||
'friday' => 5,
|
||||
'saturday' => 6
|
||||
);
|
||||
|
||||
return $days[strtolower($day_name)] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all schedules (for plugin deactivation)
|
||||
*/
|
||||
public function clear_schedules() {
|
||||
wp_clear_scheduled_hook('tigerstyle_backup_scheduled');
|
||||
wp_clear_scheduled_hook('tigerstyle_backup_cleanup');
|
||||
|
||||
$this->logger->info('All backup schedules cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup statistics
|
||||
*/
|
||||
public function get_backup_statistics($days = 30) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_metadata';
|
||||
$since_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||
|
||||
$stats = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
COUNT(*) as total_backups,
|
||||
SUM(CASE WHEN backup_id LIKE 'backup_%scheduled%' THEN 1 ELSE 0 END) as scheduled_backups,
|
||||
SUM(CASE WHEN backup_id LIKE 'backup_%manual%' THEN 1 ELSE 0 END) as manual_backups,
|
||||
AVG(file_size) as average_size,
|
||||
SUM(file_size) as total_size
|
||||
FROM {$table_name}
|
||||
WHERE created_at >= %s",
|
||||
$since_date
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ($stats) {
|
||||
$stats['average_size_formatted'] = size_format($stats['average_size']);
|
||||
$stats['total_size_formatted'] = size_format($stats['total_size']);
|
||||
$stats['success_rate'] = $this->calculate_success_rate($days);
|
||||
}
|
||||
|
||||
return $stats ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate backup success rate
|
||||
*/
|
||||
private function calculate_success_rate($days) {
|
||||
$failures = get_option('tigerstyle_scheduled_backup_failures', array());
|
||||
$recent_failures = array_filter($failures, function($failure) use ($days) {
|
||||
return $failure['timestamp'] > strtotime("-{$days} days");
|
||||
});
|
||||
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_metadata';
|
||||
$since_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||
|
||||
$total_attempts = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table_name} WHERE created_at >= %s AND backup_id LIKE '%scheduled%'",
|
||||
$since_date
|
||||
)
|
||||
);
|
||||
|
||||
$total_attempts += count($recent_failures);
|
||||
|
||||
if ($total_attempts === 0) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
$success_rate = (($total_attempts - count($recent_failures)) / $total_attempts) * 100;
|
||||
return round($success_rate, 2);
|
||||
}
|
||||
}
|
||||
695
includes/backup/class-backup-validator.php
Normal file
695
includes/backup/class-backup-validator.php
Normal file
@ -0,0 +1,695 @@
|
||||
<?php
|
||||
/**
|
||||
* TigerStyle SEO Backup Validator
|
||||
*
|
||||
* Validates backup integrity, checksums, and structure
|
||||
* to ensure reliable restoration.
|
||||
*
|
||||
* @package TigerStyleSEO
|
||||
* @subpackage BackupValidator
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Backup_Validator {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->logger = TigerStyleSEO_Backup_Logger::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate backup by ID
|
||||
*/
|
||||
public function validate_backup($backup_id) {
|
||||
try {
|
||||
$storage_manager = new TigerStyleSEO_Storage_Manager();
|
||||
|
||||
// Check if backup exists
|
||||
if (!$storage_manager->backup_exists($backup_id)) {
|
||||
throw new Exception(__('Backup does not exist', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Download backup for validation
|
||||
$backup_file = $storage_manager->download_backup($backup_id);
|
||||
|
||||
// Extract backup temporarily
|
||||
$temp_dir = $this->extract_backup_for_validation($backup_file);
|
||||
|
||||
try {
|
||||
// Validate backup structure and integrity
|
||||
$result = $this->validate_backup_directory($temp_dir);
|
||||
|
||||
return $result;
|
||||
|
||||
} finally {
|
||||
// Cleanup temporary files
|
||||
$this->cleanup_temp_directory($temp_dir);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Backup validation failed: ' . $e->getMessage(), array(
|
||||
'backup_id' => $backup_id
|
||||
));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate backup directory structure and integrity
|
||||
*/
|
||||
public function validate_backup_directory($backup_dir, $manifest = null) {
|
||||
$validation_results = array(
|
||||
'valid' => true,
|
||||
'errors' => array(),
|
||||
'warnings' => array(),
|
||||
'checks' => array()
|
||||
);
|
||||
|
||||
try {
|
||||
// Load manifest if not provided
|
||||
if (!$manifest) {
|
||||
$manifest = $this->load_manifest($backup_dir);
|
||||
}
|
||||
|
||||
// Check required files
|
||||
$validation_results['checks']['manifest'] = $this->validate_manifest($backup_dir, $manifest);
|
||||
$validation_results['checks']['structure'] = $this->validate_backup_structure($backup_dir, $manifest);
|
||||
$validation_results['checks']['database'] = $this->validate_database_backup($backup_dir, $manifest);
|
||||
$validation_results['checks']['files'] = $this->validate_files_backup($backup_dir, $manifest);
|
||||
$validation_results['checks']['checksums'] = $this->validate_checksums($backup_dir, $manifest);
|
||||
|
||||
// Collect errors and warnings
|
||||
foreach ($validation_results['checks'] as $check_name => $check_result) {
|
||||
if (!$check_result['valid']) {
|
||||
$validation_results['valid'] = false;
|
||||
$validation_results['errors'] = array_merge(
|
||||
$validation_results['errors'],
|
||||
$check_result['errors']
|
||||
);
|
||||
}
|
||||
$validation_results['warnings'] = array_merge(
|
||||
$validation_results['warnings'],
|
||||
$check_result['warnings']
|
||||
);
|
||||
}
|
||||
|
||||
$this->logger->info('Backup validation completed', array(
|
||||
'backup_dir' => basename($backup_dir),
|
||||
'valid' => $validation_results['valid'],
|
||||
'error_count' => count($validation_results['errors']),
|
||||
'warning_count' => count($validation_results['warnings'])
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
$validation_results['valid'] = false;
|
||||
$validation_results['errors'][] = $e->getMessage();
|
||||
|
||||
$this->logger->error('Backup validation exception: ' . $e->getMessage(), array(
|
||||
'backup_dir' => $backup_dir
|
||||
));
|
||||
}
|
||||
|
||||
return $validation_results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and validate manifest file
|
||||
*/
|
||||
private function load_manifest($backup_dir) {
|
||||
$manifest_file = $backup_dir . '/manifest.json';
|
||||
|
||||
if (!file_exists($manifest_file)) {
|
||||
throw new Exception(__('Manifest file not found', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
$manifest_content = file_get_contents($manifest_file);
|
||||
$manifest = json_decode($manifest_content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception(__('Invalid manifest format: ', 'tigerstyle-heat') . json_last_error_msg());
|
||||
}
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate manifest structure and content
|
||||
*/
|
||||
private function validate_manifest($backup_dir, $manifest) {
|
||||
$result = array(
|
||||
'valid' => true,
|
||||
'errors' => array(),
|
||||
'warnings' => array()
|
||||
);
|
||||
|
||||
// Check required manifest fields
|
||||
$required_fields = array(
|
||||
'backup_id',
|
||||
'created_at',
|
||||
'wordpress_version',
|
||||
'site_url',
|
||||
'plugin_version',
|
||||
'options'
|
||||
);
|
||||
|
||||
foreach ($required_fields as $field) {
|
||||
if (!isset($manifest[$field])) {
|
||||
$result['errors'][] = sprintf(__('Missing required manifest field: %s', 'tigerstyle-heat'), $field);
|
||||
$result['valid'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate manifest data types
|
||||
if (isset($manifest['created_at']) && !$this->is_valid_datetime($manifest['created_at'])) {
|
||||
$result['errors'][] = __('Invalid created_at format in manifest', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
}
|
||||
|
||||
if (isset($manifest['site_url']) && !filter_var($manifest['site_url'], FILTER_VALIDATE_URL)) {
|
||||
$result['warnings'][] = __('Invalid site_url format in manifest', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
// Check WordPress version compatibility
|
||||
if (isset($manifest['wordpress_version'])) {
|
||||
$current_wp_version = get_bloginfo('version');
|
||||
if (version_compare($manifest['wordpress_version'], $current_wp_version, '>')) {
|
||||
$result['warnings'][] = sprintf(
|
||||
__('Backup was created with newer WordPress version (%s) than current (%s)', 'tigerstyle-heat'),
|
||||
$manifest['wordpress_version'],
|
||||
$current_wp_version
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate options structure
|
||||
if (isset($manifest['options']) && !is_array($manifest['options'])) {
|
||||
$result['errors'][] = __('Invalid options structure in manifest', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate backup directory structure
|
||||
*/
|
||||
private function validate_backup_structure($backup_dir, $manifest) {
|
||||
$result = array(
|
||||
'valid' => true,
|
||||
'errors' => array(),
|
||||
'warnings' => array()
|
||||
);
|
||||
|
||||
// Check for manifest file
|
||||
if (!file_exists($backup_dir . '/manifest.json')) {
|
||||
$result['errors'][] = __('Manifest file is missing', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
}
|
||||
|
||||
// Check for database backup if included
|
||||
if (isset($manifest['options']['include_database']) && $manifest['options']['include_database']) {
|
||||
if (!file_exists($backup_dir . '/database.sql')) {
|
||||
$result['errors'][] = __('Database backup file is missing', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for files backup if included
|
||||
if (isset($manifest['options']['include_files']) && $manifest['options']['include_files']) {
|
||||
if (!is_dir($backup_dir . '/files')) {
|
||||
$result['errors'][] = __('Files backup directory is missing', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate database backup
|
||||
*/
|
||||
private function validate_database_backup($backup_dir, $manifest) {
|
||||
$result = array(
|
||||
'valid' => true,
|
||||
'errors' => array(),
|
||||
'warnings' => array()
|
||||
);
|
||||
|
||||
// Skip if database backup not included
|
||||
if (!isset($manifest['options']['include_database']) || !$manifest['options']['include_database']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$sql_file = $backup_dir . '/database.sql';
|
||||
|
||||
if (!file_exists($sql_file)) {
|
||||
$result['errors'][] = __('Database backup file not found', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Check file size
|
||||
$file_size = filesize($sql_file);
|
||||
if ($file_size === 0) {
|
||||
$result['errors'][] = __('Database backup file is empty', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Validate SQL content structure
|
||||
$sql_validation = $this->validate_sql_file($sql_file);
|
||||
if (!$sql_validation['valid']) {
|
||||
$result['errors'] = array_merge($result['errors'], $sql_validation['errors']);
|
||||
$result['warnings'] = array_merge($result['warnings'], $sql_validation['warnings']);
|
||||
$result['valid'] = false;
|
||||
}
|
||||
|
||||
// Validate checksum if available
|
||||
if (isset($manifest['database_info']['checksum'])) {
|
||||
$actual_checksum = md5_file($sql_file);
|
||||
if ($actual_checksum !== $manifest['database_info']['checksum']) {
|
||||
$result['errors'][] = __('Database backup checksum mismatch', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check file size against manifest
|
||||
if (isset($manifest['database_info']['file_size'])) {
|
||||
if ($file_size !== $manifest['database_info']['file_size']) {
|
||||
$result['warnings'][] = __('Database backup file size differs from manifest', 'tigerstyle-heat');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SQL file structure
|
||||
*/
|
||||
private function validate_sql_file($sql_file) {
|
||||
$result = array(
|
||||
'valid' => true,
|
||||
'errors' => array(),
|
||||
'warnings' => array()
|
||||
);
|
||||
|
||||
$handle = fopen($sql_file, 'r');
|
||||
if (!$handle) {
|
||||
$result['errors'][] = __('Cannot read database backup file', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
return $result;
|
||||
}
|
||||
|
||||
$line_count = 0;
|
||||
$has_wp_tables = false;
|
||||
$has_create_statements = false;
|
||||
$has_insert_statements = false;
|
||||
|
||||
try {
|
||||
while (($line = fgets($handle)) !== false && $line_count < 1000) { // Check first 1000 lines
|
||||
$line = trim($line);
|
||||
$line_count++;
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (empty($line) || strpos($line, '--') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for WordPress table patterns
|
||||
if (preg_match('/CREATE TABLE.*wp_\w+/', $line) || preg_match('/INSERT INTO.*wp_\w+/', $line)) {
|
||||
$has_wp_tables = true;
|
||||
}
|
||||
|
||||
// Check for CREATE TABLE statements
|
||||
if (strpos($line, 'CREATE TABLE') !== false) {
|
||||
$has_create_statements = true;
|
||||
}
|
||||
|
||||
// Check for INSERT statements
|
||||
if (strpos($line, 'INSERT INTO') !== false) {
|
||||
$has_insert_statements = true;
|
||||
}
|
||||
|
||||
// Check for SQL syntax errors (basic validation)
|
||||
if (!$this->is_valid_sql_line($line)) {
|
||||
$result['warnings'][] = sprintf(__('Potential SQL syntax issue at line %d', 'tigerstyle-heat'), $line_count);
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
// Validate SQL content
|
||||
if (!$has_wp_tables) {
|
||||
$result['warnings'][] = __('No WordPress tables found in database backup', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
if (!$has_create_statements) {
|
||||
$result['errors'][] = __('No CREATE TABLE statements found in database backup', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
}
|
||||
|
||||
if (!$has_insert_statements) {
|
||||
$result['warnings'][] = __('No INSERT statements found in database backup', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
if ($line_count === 0) {
|
||||
$result['errors'][] = __('Database backup file appears to be empty', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate files backup
|
||||
*/
|
||||
private function validate_files_backup($backup_dir, $manifest) {
|
||||
$result = array(
|
||||
'valid' => true,
|
||||
'errors' => array(),
|
||||
'warnings' => array()
|
||||
);
|
||||
|
||||
// Skip if files backup not included
|
||||
if (!isset($manifest['options']['include_files']) || !$manifest['options']['include_files']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$files_dir = $backup_dir . '/files';
|
||||
|
||||
if (!is_dir($files_dir)) {
|
||||
$result['errors'][] = __('Files backup directory not found', 'tigerstyle-heat');
|
||||
$result['valid'] = false;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Check for essential WordPress files
|
||||
$essential_files = array(
|
||||
'wp-config.php',
|
||||
'wp-content'
|
||||
);
|
||||
|
||||
foreach ($essential_files as $file) {
|
||||
$file_path = $files_dir . '/' . $file;
|
||||
if (!file_exists($file_path)) {
|
||||
$result['warnings'][] = sprintf(__('Essential file/directory missing: %s', 'tigerstyle-heat'), $file);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate file manifest if available
|
||||
if (isset($manifest['files']) && is_array($manifest['files'])) {
|
||||
$file_validation = $this->validate_file_manifest($files_dir, $manifest['files']);
|
||||
$result['errors'] = array_merge($result['errors'], $file_validation['errors']);
|
||||
$result['warnings'] = array_merge($result['warnings'], $file_validation['warnings']);
|
||||
|
||||
if (!$file_validation['valid']) {
|
||||
$result['valid'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file manifest against actual files
|
||||
*/
|
||||
private function validate_file_manifest($files_dir, $file_manifest) {
|
||||
$result = array(
|
||||
'valid' => true,
|
||||
'errors' => array(),
|
||||
'warnings' => array()
|
||||
);
|
||||
|
||||
$checked_files = 0;
|
||||
$missing_files = 0;
|
||||
$checksum_mismatches = 0;
|
||||
|
||||
foreach ($file_manifest as $relative_path => $file_info) {
|
||||
$full_path = $files_dir . '/' . $relative_path;
|
||||
$checked_files++;
|
||||
|
||||
if (!file_exists($full_path)) {
|
||||
$missing_files++;
|
||||
if ($missing_files <= 10) { // Limit error messages
|
||||
$result['errors'][] = sprintf(__('Missing file: %s', 'tigerstyle-heat'), $relative_path);
|
||||
}
|
||||
$result['valid'] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (isset($file_info['size'])) {
|
||||
$actual_size = filesize($full_path);
|
||||
if ($actual_size !== $file_info['size']) {
|
||||
$result['warnings'][] = sprintf(__('File size mismatch: %s', 'tigerstyle-heat'), $relative_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checksum (sample validation for performance)
|
||||
if (isset($file_info['checksum']) && $checked_files % 10 === 0) { // Check every 10th file
|
||||
$actual_checksum = md5_file($full_path);
|
||||
if ($actual_checksum !== $file_info['checksum']) {
|
||||
$checksum_mismatches++;
|
||||
if ($checksum_mismatches <= 5) { // Limit error messages
|
||||
$result['warnings'][] = sprintf(__('Checksum mismatch: %s', 'tigerstyle-heat'), $relative_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($missing_files > 10) {
|
||||
$result['errors'][] = sprintf(__('... and %d more missing files', 'tigerstyle-heat'), $missing_files - 10);
|
||||
}
|
||||
|
||||
if ($checksum_mismatches > 5) {
|
||||
$result['warnings'][] = sprintf(__('... and %d more checksum mismatches', 'tigerstyle-heat'), $checksum_mismatches - 5);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate backup checksums
|
||||
*/
|
||||
private function validate_checksums($backup_dir, $manifest) {
|
||||
$result = array(
|
||||
'valid' => true,
|
||||
'errors' => array(),
|
||||
'warnings' => array()
|
||||
);
|
||||
|
||||
if (!isset($manifest['checksums']) || !is_array($manifest['checksums'])) {
|
||||
$result['warnings'][] = __('No checksums found in manifest', 'tigerstyle-heat');
|
||||
return $result;
|
||||
}
|
||||
|
||||
foreach ($manifest['checksums'] as $file => $expected_checksum) {
|
||||
$file_path = $backup_dir . '/' . $file;
|
||||
|
||||
if (!file_exists($file_path)) {
|
||||
continue; // File validation handled elsewhere
|
||||
}
|
||||
|
||||
$actual_checksum = md5_file($file_path);
|
||||
if ($actual_checksum !== $expected_checksum) {
|
||||
$result['errors'][] = sprintf(__('Checksum mismatch for file: %s', 'tigerstyle-heat'), $file);
|
||||
$result['valid'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract backup for validation
|
||||
*/
|
||||
private function extract_backup_for_validation($backup_file) {
|
||||
$upload_dir = wp_upload_dir();
|
||||
$temp_dir = $upload_dir['basedir'] . '/tigerstyle-validation/' . uniqid('validation_');
|
||||
|
||||
if (!wp_mkdir_p($temp_dir)) {
|
||||
throw new Exception(__('Failed to create validation directory', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
$compression_manager = new TigerStyleSEO_Compression_Manager();
|
||||
$compression_method = $this->detect_compression_method($backup_file);
|
||||
|
||||
$compression_manager->extract_archive($backup_file, $temp_dir, $compression_method);
|
||||
|
||||
return $temp_dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect compression method from filename
|
||||
*/
|
||||
private function detect_compression_method($filename) {
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
switch ($extension) {
|
||||
case 'zip':
|
||||
return 'zip';
|
||||
case 'gz':
|
||||
return 'tar.gz';
|
||||
case 'bz2':
|
||||
return 'tar.bz2';
|
||||
default:
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup temporary directory
|
||||
*/
|
||||
private function cleanup_temp_directory($temp_dir) {
|
||||
if (!is_dir($temp_dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($temp_dir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir()) {
|
||||
rmdir($file->getPathname());
|
||||
} else {
|
||||
unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir($temp_dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate datetime format
|
||||
*/
|
||||
private function is_valid_datetime($datetime) {
|
||||
$d = DateTime::createFromFormat('Y-m-d H:i:s', $datetime);
|
||||
return $d && $d->format('Y-m-d H:i:s') === $datetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic SQL line validation
|
||||
*/
|
||||
private function is_valid_sql_line($line) {
|
||||
// Skip empty lines and comments
|
||||
if (empty($line) || strpos($line, '--') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for common SQL syntax patterns
|
||||
$sql_keywords = array(
|
||||
'CREATE', 'DROP', 'INSERT', 'UPDATE', 'DELETE', 'SELECT',
|
||||
'ALTER', 'SET', 'START', 'COMMIT', 'ROLLBACK'
|
||||
);
|
||||
|
||||
$line_upper = strtoupper($line);
|
||||
|
||||
// Check if line starts with SQL keyword or is part of a multi-line statement
|
||||
foreach ($sql_keywords as $keyword) {
|
||||
if (strpos($line_upper, $keyword) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for VALUES clause (part of INSERT statements)
|
||||
if (strpos($line_upper, 'VALUES') !== false || strpos($line_upper, '(') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for properly quoted strings and escaped characters
|
||||
if (preg_match('/^[^\']*(?:\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'[^\']*)*$/', $line)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick backup validation (without full extraction)
|
||||
*/
|
||||
public function quick_validate_backup($backup_id) {
|
||||
try {
|
||||
$storage_manager = new TigerStyleSEO_Storage_Manager();
|
||||
|
||||
// Check if backup exists
|
||||
if (!$storage_manager->backup_exists($backup_id)) {
|
||||
return array('valid' => false, 'error' => __('Backup does not exist', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Get backup info
|
||||
$backup_info = $storage_manager->get_backup_info($backup_id);
|
||||
|
||||
// Basic checks
|
||||
if (empty($backup_info['file_size']) || $backup_info['file_size'] < 1000) {
|
||||
return array('valid' => false, 'error' => __('Backup file is too small', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// For local backups, check if file exists and is readable
|
||||
if ($backup_info['storage_type'] === 'local') {
|
||||
if (!file_exists($backup_info['file_path']) || !is_readable($backup_info['file_path'])) {
|
||||
return array('valid' => false, 'error' => __('Backup file is not accessible', 'tigerstyle-heat'));
|
||||
}
|
||||
}
|
||||
|
||||
return array('valid' => true, 'message' => __('Backup appears to be valid', 'tigerstyle-heat'));
|
||||
|
||||
} catch (Exception $e) {
|
||||
return array('valid' => false, 'error' => $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate backup before restore (comprehensive)
|
||||
*/
|
||||
public function validate_for_restore($backup_id) {
|
||||
// Perform quick validation first
|
||||
$quick_result = $this->quick_validate_backup($backup_id);
|
||||
if (!$quick_result['valid']) {
|
||||
return $quick_result;
|
||||
}
|
||||
|
||||
// Perform full validation
|
||||
$full_result = $this->validate_backup($backup_id);
|
||||
|
||||
// Add restore-specific validations
|
||||
if ($full_result['valid']) {
|
||||
// Check compatibility with current WordPress version
|
||||
// Check available disk space
|
||||
// Check database permissions
|
||||
// etc.
|
||||
}
|
||||
|
||||
return $full_result;
|
||||
}
|
||||
}
|
||||
643
includes/backup/class-compression-manager.php
Normal file
643
includes/backup/class-compression-manager.php
Normal file
@ -0,0 +1,643 @@
|
||||
<?php
|
||||
/**
|
||||
* Compression Manager for TigerStyle SEO Backup System
|
||||
* Supports multiple compression formats with graceful fallback
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Compression_Manager {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Supported compression methods in order of preference
|
||||
*/
|
||||
private $compression_methods = array();
|
||||
|
||||
/**
|
||||
* Logger instance
|
||||
*/
|
||||
private $logger = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize compression manager
|
||||
*/
|
||||
private function init() {
|
||||
$this->logger = TigerStyleSEO_Backup_Logger::instance();
|
||||
$this->detect_compression_methods();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect available compression methods
|
||||
*/
|
||||
private function detect_compression_methods() {
|
||||
$this->compression_methods = array();
|
||||
|
||||
// Check for ZIP support (most widely available)
|
||||
if (class_exists('ZipArchive')) {
|
||||
$this->compression_methods['zip'] = array(
|
||||
'name' => 'ZIP',
|
||||
'extension' => '.zip',
|
||||
'mime_type' => 'application/zip',
|
||||
'available' => true,
|
||||
'compression_ratio' => 0.7,
|
||||
'speed' => 'fast'
|
||||
);
|
||||
}
|
||||
|
||||
// Check for TAR.GZ support (better compression)
|
||||
if (extension_loaded('zlib') && class_exists('PharData')) {
|
||||
$this->compression_methods['tar.gz'] = array(
|
||||
'name' => 'TAR.GZ',
|
||||
'extension' => '.tar.gz',
|
||||
'mime_type' => 'application/gzip',
|
||||
'available' => true,
|
||||
'compression_ratio' => 0.6,
|
||||
'speed' => 'medium'
|
||||
);
|
||||
}
|
||||
|
||||
// Check for TAR.BZ2 support (best compression)
|
||||
if (extension_loaded('bz2') && class_exists('PharData')) {
|
||||
$this->compression_methods['tar.bz2'] = array(
|
||||
'name' => 'TAR.BZ2',
|
||||
'extension' => '.tar.bz2',
|
||||
'mime_type' => 'application/bzip2',
|
||||
'available' => true,
|
||||
'compression_ratio' => 0.5,
|
||||
'speed' => 'slow'
|
||||
);
|
||||
}
|
||||
|
||||
// Check for 7Z support (if available)
|
||||
if (extension_loaded('rar') || $this->command_exists('7z')) {
|
||||
$this->compression_methods['7z'] = array(
|
||||
'name' => '7ZIP',
|
||||
'extension' => '.7z',
|
||||
'mime_type' => 'application/x-7z-compressed',
|
||||
'available' => true,
|
||||
'compression_ratio' => 0.4,
|
||||
'speed' => 'very_slow'
|
||||
);
|
||||
}
|
||||
|
||||
$this->logger->info("Detected compression methods", array(
|
||||
'methods' => array_keys($this->compression_methods)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get best available compression method
|
||||
*/
|
||||
public function get_best_compression_method($priority = 'balanced') {
|
||||
if (empty($this->compression_methods)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($priority) {
|
||||
case 'speed':
|
||||
$preferred_order = array('zip', 'tar.gz', 'tar.bz2', '7z');
|
||||
break;
|
||||
case 'compression':
|
||||
$preferred_order = array('7z', 'tar.bz2', 'tar.gz', 'zip');
|
||||
break;
|
||||
case 'balanced':
|
||||
default:
|
||||
$preferred_order = array('tar.gz', 'zip', 'tar.bz2', '7z');
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($preferred_order as $method) {
|
||||
if (isset($this->compression_methods[$method]) && $this->compression_methods[$method]['available']) {
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
|
||||
// Return first available method as fallback
|
||||
return array_keys($this->compression_methods)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress directory
|
||||
*/
|
||||
public function compress_directory($directory, $backup_id, $method = null) {
|
||||
if (!$method) {
|
||||
$method = $this->get_best_compression_method();
|
||||
}
|
||||
|
||||
if (!$method || !isset($this->compression_methods[$method])) {
|
||||
$this->logger->error("Compression method not available", array('method' => $method));
|
||||
return false;
|
||||
}
|
||||
|
||||
$backup_location = get_option('backup_location', WP_CONTENT_DIR . '/tigerstyle-backups/');
|
||||
$compressed_file = $backup_location . $backup_id . $this->compression_methods[$method]['extension'];
|
||||
|
||||
// Ensure backup directory exists
|
||||
wp_mkdir_p(dirname($compressed_file));
|
||||
|
||||
$this->logger->info("Starting compression", array(
|
||||
'method' => $method,
|
||||
'directory' => $directory,
|
||||
'output' => $compressed_file
|
||||
));
|
||||
|
||||
$start_time = microtime(true);
|
||||
$success = false;
|
||||
|
||||
try {
|
||||
switch ($method) {
|
||||
case 'zip':
|
||||
$success = $this->create_zip_archive($directory, $compressed_file);
|
||||
break;
|
||||
case 'tar.gz':
|
||||
$success = $this->create_tar_gz_archive($directory, $compressed_file);
|
||||
break;
|
||||
case 'tar.bz2':
|
||||
$success = $this->create_tar_bz2_archive($directory, $compressed_file);
|
||||
break;
|
||||
case '7z':
|
||||
$success = $this->create_7z_archive($directory, $compressed_file);
|
||||
break;
|
||||
default:
|
||||
$this->logger->error("Unknown compression method", array('method' => $method));
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($success && file_exists($compressed_file)) {
|
||||
$duration = microtime(true) - $start_time;
|
||||
$original_size = $this->get_directory_size($directory);
|
||||
$compressed_size = filesize($compressed_file);
|
||||
$compression_ratio = $compressed_size / $original_size;
|
||||
|
||||
$this->logger->info("Compression completed", array(
|
||||
'method' => $method,
|
||||
'duration' => round($duration, 2) . 's',
|
||||
'original_size' => $this->format_bytes($original_size),
|
||||
'compressed_size' => $this->format_bytes($compressed_size),
|
||||
'compression_ratio' => round($compression_ratio * 100, 1) . '%'
|
||||
));
|
||||
|
||||
return $compressed_file;
|
||||
} else {
|
||||
throw new Exception("Compression failed - output file not created");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error("Compression failed", array(
|
||||
'method' => $method,
|
||||
'error' => $e->getMessage()
|
||||
));
|
||||
|
||||
// Try fallback method
|
||||
$fallback_methods = array_keys($this->compression_methods);
|
||||
$current_index = array_search($method, $fallback_methods);
|
||||
|
||||
if ($current_index !== false && isset($fallback_methods[$current_index + 1])) {
|
||||
$fallback_method = $fallback_methods[$current_index + 1];
|
||||
$this->logger->info("Trying fallback compression method", array('method' => $fallback_method));
|
||||
return $this->compress_directory($directory, $backup_id, $fallback_method);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ZIP archive
|
||||
*/
|
||||
private function create_zip_archive($directory, $output_file) {
|
||||
$zip = new ZipArchive();
|
||||
$result = $zip->open($output_file, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
||||
|
||||
if ($result !== TRUE) {
|
||||
throw new Exception("Cannot create ZIP archive: " . $result);
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
$file_path = $file->getPathname();
|
||||
$relative_path = substr($file_path, strlen($directory) + 1);
|
||||
|
||||
if ($file->isDir()) {
|
||||
$zip->addEmptyDir($relative_path);
|
||||
} else {
|
||||
$zip->addFile($file_path, $relative_path);
|
||||
}
|
||||
}
|
||||
|
||||
$result = $zip->close();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create TAR.GZ archive
|
||||
*/
|
||||
private function create_tar_gz_archive($directory, $output_file) {
|
||||
$tar_file = str_replace('.gz', '', $output_file);
|
||||
|
||||
// Create TAR first
|
||||
$phar = new PharData($tar_file);
|
||||
$phar->buildFromDirectory($directory);
|
||||
|
||||
// Compress to GZ
|
||||
$phar->compress(Phar::GZ);
|
||||
|
||||
// Remove uncompressed TAR file
|
||||
if (file_exists($tar_file)) {
|
||||
unlink($tar_file);
|
||||
}
|
||||
|
||||
return file_exists($output_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create TAR.BZ2 archive
|
||||
*/
|
||||
private function create_tar_bz2_archive($directory, $output_file) {
|
||||
$tar_file = str_replace('.bz2', '', $output_file);
|
||||
|
||||
// Create TAR first
|
||||
$phar = new PharData($tar_file);
|
||||
$phar->buildFromDirectory($directory);
|
||||
|
||||
// Compress to BZ2
|
||||
$phar->compress(Phar::BZ2);
|
||||
|
||||
// Remove uncompressed TAR file
|
||||
if (file_exists($tar_file)) {
|
||||
unlink($tar_file);
|
||||
}
|
||||
|
||||
return file_exists($output_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ZIP archive using PHP ZipArchive (replaces 7z for security)
|
||||
*/
|
||||
private function create_7z_archive($directory, $output_file) {
|
||||
if (!extension_loaded('zip')) {
|
||||
throw new Exception("ZIP extension not available");
|
||||
}
|
||||
|
||||
// Convert .7z extension to .zip for compatibility
|
||||
if (pathinfo($output_file, PATHINFO_EXTENSION) === '7z') {
|
||||
$output_file = substr($output_file, 0, -2) . 'zip';
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$result = $zip->open($output_file, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
||||
|
||||
if ($result !== TRUE) {
|
||||
throw new Exception("Cannot create ZIP archive: " . $this->getZipError($result));
|
||||
}
|
||||
|
||||
// Add all files from directory recursively
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
$file_path = $file->getPathname();
|
||||
$relative_path = substr($file_path, strlen($directory) + 1);
|
||||
|
||||
if ($file->isDir()) {
|
||||
$zip->addEmptyDir($relative_path);
|
||||
} elseif ($file->isFile()) {
|
||||
// Use maximum compression level (9)
|
||||
$zip->addFile($file_path, $relative_path);
|
||||
$zip->setCompressionName($relative_path, ZipArchive::CM_DEFLATE, 9);
|
||||
}
|
||||
}
|
||||
|
||||
$close_result = $zip->close();
|
||||
|
||||
if (!$close_result) {
|
||||
throw new Exception("Failed to close ZIP archive");
|
||||
}
|
||||
|
||||
return file_exists($output_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract compressed archive
|
||||
*/
|
||||
public function extract_archive($archive_file, $destination_directory) {
|
||||
if (!file_exists($archive_file)) {
|
||||
throw new Exception("Archive file not found: " . $archive_file);
|
||||
}
|
||||
|
||||
$method = $this->detect_compression_method($archive_file);
|
||||
|
||||
if (!$method) {
|
||||
throw new Exception("Cannot determine compression method for: " . $archive_file);
|
||||
}
|
||||
|
||||
// Ensure destination directory exists
|
||||
wp_mkdir_p($destination_directory);
|
||||
|
||||
$this->logger->info("Extracting archive", array(
|
||||
'method' => $method,
|
||||
'archive' => $archive_file,
|
||||
'destination' => $destination_directory
|
||||
));
|
||||
|
||||
$start_time = microtime(true);
|
||||
|
||||
try {
|
||||
switch ($method) {
|
||||
case 'zip':
|
||||
$success = $this->extract_zip_archive($archive_file, $destination_directory);
|
||||
break;
|
||||
case 'tar.gz':
|
||||
case 'tar.bz2':
|
||||
$success = $this->extract_tar_archive($archive_file, $destination_directory);
|
||||
break;
|
||||
case '7z':
|
||||
$success = $this->extract_7z_archive($archive_file, $destination_directory);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unsupported compression method: " . $method);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$duration = microtime(true) - $start_time;
|
||||
$this->logger->info("Extraction completed", array(
|
||||
'method' => $method,
|
||||
'duration' => round($duration, 2) . 's'
|
||||
));
|
||||
return true;
|
||||
} else {
|
||||
throw new Exception("Extraction failed");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error("Extraction failed", array(
|
||||
'method' => $method,
|
||||
'error' => $e->getMessage()
|
||||
));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ZIP archive
|
||||
*/
|
||||
private function extract_zip_archive($archive_file, $destination) {
|
||||
$zip = new ZipArchive();
|
||||
$result = $zip->open($archive_file);
|
||||
|
||||
if ($result !== TRUE) {
|
||||
throw new Exception("Cannot open ZIP archive: " . $result);
|
||||
}
|
||||
|
||||
$result = $zip->extractTo($destination);
|
||||
$zip->close();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract TAR archive (GZ or BZ2)
|
||||
*/
|
||||
private function extract_tar_archive($archive_file, $destination) {
|
||||
$phar = new PharData($archive_file);
|
||||
$phar->extractTo($destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract 7Z archive
|
||||
*/
|
||||
private function extract_7z_archive($archive_file, $destination) {
|
||||
if (!extension_loaded('zip')) {
|
||||
throw new Exception("ZIP extension not available");
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$result = $zip->open($archive_file);
|
||||
|
||||
if ($result !== TRUE) {
|
||||
throw new Exception("Cannot open ZIP archive: " . $this->getZipError($result));
|
||||
}
|
||||
|
||||
// Ensure destination directory exists
|
||||
if (!is_dir($destination)) {
|
||||
wp_mkdir_p($destination);
|
||||
}
|
||||
|
||||
// Extract with security validation
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$entry = $zip->getNameIndex($i);
|
||||
|
||||
// Security check: prevent directory traversal
|
||||
if (strpos($entry, '../') !== false || strpos($entry, '..\\') !== false) {
|
||||
$zip->close();
|
||||
throw new Exception("Archive contains unsafe path: " . $entry);
|
||||
}
|
||||
}
|
||||
|
||||
$extract_result = $zip->extractTo($destination);
|
||||
$zip->close();
|
||||
|
||||
if (!$extract_result) {
|
||||
throw new Exception("Failed to extract ZIP archive");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect compression method from file extension
|
||||
*/
|
||||
private function detect_compression_method($file_path) {
|
||||
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
|
||||
|
||||
// Handle compound extensions
|
||||
if ($extension === 'gz' && strtolower(pathinfo(pathinfo($file_path, PATHINFO_FILENAME), PATHINFO_EXTENSION)) === 'tar') {
|
||||
return 'tar.gz';
|
||||
}
|
||||
|
||||
if ($extension === 'bz2' && strtolower(pathinfo(pathinfo($file_path, PATHINFO_FILENAME), PATHINFO_EXTENSION)) === 'tar') {
|
||||
return 'tar.bz2';
|
||||
}
|
||||
|
||||
$extension_map = array(
|
||||
'zip' => 'zip',
|
||||
'7z' => '7z',
|
||||
'gz' => 'tar.gz',
|
||||
'bz2' => 'tar.bz2'
|
||||
);
|
||||
|
||||
return isset($extension_map[$extension]) ? $extension_map[$extension] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get directory size
|
||||
*/
|
||||
private function get_directory_size($directory) {
|
||||
$size = 0;
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$size += $file->getSize();
|
||||
}
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if PHP extension/feature exists (replaces shell command detection)
|
||||
*/
|
||||
private function command_exists($command) {
|
||||
switch ($command) {
|
||||
case '7z':
|
||||
// 7z now uses ZIP extension for security
|
||||
return extension_loaded('zip');
|
||||
case 'zip':
|
||||
return extension_loaded('zip');
|
||||
case 'tar':
|
||||
case 'gzip':
|
||||
return class_exists('PharData') && extension_loaded('zlib');
|
||||
case 'bzip2':
|
||||
return class_exists('PharData') && extension_loaded('bz2');
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable ZIP error message
|
||||
*/
|
||||
private function getZipError($code) {
|
||||
switch($code) {
|
||||
case ZipArchive::ER_OK: return 'No error';
|
||||
case ZipArchive::ER_MULTIDISK: return 'Multi-disk zip archives not supported';
|
||||
case ZipArchive::ER_RENAME: return 'Renaming temporary file failed';
|
||||
case ZipArchive::ER_CLOSE: return 'Closing zip archive failed';
|
||||
case ZipArchive::ER_SEEK: return 'Seek error';
|
||||
case ZipArchive::ER_READ: return 'Read error';
|
||||
case ZipArchive::ER_WRITE: return 'Write error';
|
||||
case ZipArchive::ER_CRC: return 'CRC error';
|
||||
case ZipArchive::ER_ZIPCLOSED: return 'Containing zip archive was closed';
|
||||
case ZipArchive::ER_NOENT: return 'No such file';
|
||||
case ZipArchive::ER_EXISTS: return 'File already exists';
|
||||
case ZipArchive::ER_OPEN: return 'Can not open file';
|
||||
case ZipArchive::ER_TMPOPEN: return 'Failure to create temporary file';
|
||||
case ZipArchive::ER_ZLIB: return 'Zlib error';
|
||||
case ZipArchive::ER_MEMORY: return 'Memory allocation failure';
|
||||
case ZipArchive::ER_CHANGED: return 'Entry has been changed';
|
||||
case ZipArchive::ER_COMPNOTSUPP: return 'Compression method not supported';
|
||||
case ZipArchive::ER_EOF: return 'Premature EOF';
|
||||
case ZipArchive::ER_INVAL: return 'Invalid argument';
|
||||
case ZipArchive::ER_NOZIP: return 'Not a zip archive';
|
||||
case ZipArchive::ER_INTERNAL: return 'Internal error';
|
||||
case ZipArchive::ER_INCONS: return 'Zip archive inconsistent';
|
||||
case ZipArchive::ER_REMOVE: return 'Can not remove file';
|
||||
case ZipArchive::ER_DELETED: return 'Entry has been deleted';
|
||||
default: return "Unknown error code: $code";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes for display
|
||||
*/
|
||||
private function format_bytes($bytes, $precision = 2) {
|
||||
$units = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available compression methods
|
||||
*/
|
||||
public function get_available_methods() {
|
||||
return $this->compression_methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate compressed file
|
||||
*/
|
||||
public function validate_archive($archive_file) {
|
||||
if (!file_exists($archive_file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$method = $this->detect_compression_method($archive_file);
|
||||
|
||||
if (!$method) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($method) {
|
||||
case 'zip':
|
||||
$zip = new ZipArchive();
|
||||
$result = $zip->open($archive_file, ZipArchive::CHECKCONS);
|
||||
$zip->close();
|
||||
return $result === TRUE;
|
||||
|
||||
case 'tar.gz':
|
||||
case 'tar.bz2':
|
||||
$phar = new PharData($archive_file);
|
||||
return $phar->valid();
|
||||
|
||||
case '7z':
|
||||
// 7z files are now treated as ZIP files for security
|
||||
$zip = new ZipArchive();
|
||||
$result = $zip->open($archive_file, ZipArchive::CHECKCONS);
|
||||
if ($result === TRUE) {
|
||||
$zip->close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logger->warning("Archive validation failed", array(
|
||||
'file' => $archive_file,
|
||||
'error' => $e->getMessage()
|
||||
));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
323
includes/backup/class-database-installer.php
Normal file
323
includes/backup/class-database-installer.php
Normal file
@ -0,0 +1,323 @@
|
||||
<?php
|
||||
/**
|
||||
* Database Installer for TigerStyle SEO Backup System
|
||||
* Handles database table creation and schema management
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Database_Installer {
|
||||
|
||||
/**
|
||||
* Database version
|
||||
*/
|
||||
const DB_VERSION = '1.0.0';
|
||||
|
||||
/**
|
||||
* Install database tables
|
||||
*/
|
||||
public static function install() {
|
||||
global $wpdb;
|
||||
|
||||
try {
|
||||
// Create backup logs table
|
||||
self::create_backup_logs_table();
|
||||
|
||||
// Create backup history table
|
||||
self::create_backup_history_table();
|
||||
|
||||
// Update database version
|
||||
update_option('tigerstyle_backup_db_version', self::DB_VERSION);
|
||||
|
||||
// Log installation
|
||||
if (class_exists('TigerStyleSEO_Backup_Logger')) {
|
||||
$logger = TigerStyleSEO_Backup_Logger::instance();
|
||||
$logger->info("Database tables installed successfully", array(
|
||||
'version' => self::DB_VERSION,
|
||||
'tables_created' => array('backup_logs', 'backup_history')
|
||||
));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Silently mark as installed to prevent blocking the site
|
||||
// Tables might already exist or have partial structure
|
||||
update_option('tigerstyle_backup_db_version', self::DB_VERSION);
|
||||
error_log('TigerStyle SEO: Database installation warning - ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup logs table
|
||||
*/
|
||||
private static function create_backup_logs_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_logs';
|
||||
|
||||
// Check if table already exists
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name) {
|
||||
return; // Table already exists, skip creation
|
||||
}
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE $table_name (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
level varchar(20) NOT NULL,
|
||||
message text NOT NULL,
|
||||
context longtext,
|
||||
user_id bigint(20) DEFAULT 0,
|
||||
user_ip varchar(45) DEFAULT 'unknown',
|
||||
memory_usage bigint(20) DEFAULT 0,
|
||||
request_id varchar(32) DEFAULT '',
|
||||
backup_id varchar(255),
|
||||
PRIMARY KEY (id),
|
||||
KEY backup_id (backup_id),
|
||||
KEY level (level),
|
||||
KEY created_at (created_at),
|
||||
KEY user_id (user_id),
|
||||
KEY request_id (request_id)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
|
||||
// Suppress WordPress database errors during dbDelta
|
||||
$wpdb->suppress_errors(true);
|
||||
dbDelta($sql);
|
||||
$wpdb->suppress_errors(false);
|
||||
|
||||
// Check if table exists (don't throw error if it doesn't)
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
|
||||
error_log("TigerStyle SEO: Backup logs table creation may have failed, but continuing...");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup history table
|
||||
*/
|
||||
private static function create_backup_history_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backups';
|
||||
|
||||
// Check if table already exists
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name) {
|
||||
return; // Table already exists, skip creation
|
||||
}
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE $table_name (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
backup_id varchar(255) NOT NULL UNIQUE,
|
||||
backup_type enum('full', 'files', 'database', 'custom') NOT NULL DEFAULT 'full',
|
||||
status enum('pending', 'running', 'completed', 'failed', 'cancelled') NOT NULL DEFAULT 'pending',
|
||||
file_path text,
|
||||
file_size bigint(20) DEFAULT 0,
|
||||
compression_method varchar(50),
|
||||
compression_ratio decimal(5,4),
|
||||
storage_location varchar(100) DEFAULT 'local',
|
||||
storage_path text,
|
||||
manifest longtext,
|
||||
error_message text,
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at datetime,
|
||||
created_by bigint(20),
|
||||
restore_point tinyint(1) DEFAULT 0,
|
||||
scheduled tinyint(1) DEFAULT 0,
|
||||
retention_days int(11) DEFAULT 30,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY backup_id (backup_id),
|
||||
KEY backup_type (backup_type),
|
||||
KEY status (status),
|
||||
KEY created_at (created_at),
|
||||
KEY created_by (created_by),
|
||||
KEY restore_point (restore_point),
|
||||
KEY scheduled (scheduled)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
|
||||
// Suppress WordPress database errors during dbDelta
|
||||
$wpdb->suppress_errors(true);
|
||||
dbDelta($sql);
|
||||
$wpdb->suppress_errors(false);
|
||||
|
||||
// Check if table exists (don't throw error if it doesn't)
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
|
||||
error_log("TigerStyle SEO: Backup history table creation may have failed, but continuing...");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database needs update
|
||||
*/
|
||||
public static function needs_update() {
|
||||
$current_version = get_option('tigerstyle_backup_db_version', '0.0.0');
|
||||
return version_compare($current_version, self::DB_VERSION, '<');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update database if needed
|
||||
*/
|
||||
public static function maybe_update() {
|
||||
if (self::needs_update()) {
|
||||
self::install();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall database tables
|
||||
*/
|
||||
public static function uninstall() {
|
||||
global $wpdb;
|
||||
|
||||
$tables = array(
|
||||
$wpdb->prefix . 'tigerstyle_backup_logs',
|
||||
$wpdb->prefix . 'tigerstyle_backups'
|
||||
);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$wpdb->query("DROP TABLE IF EXISTS $table");
|
||||
}
|
||||
|
||||
// Remove options
|
||||
delete_option('tigerstyle_backup_db_version');
|
||||
|
||||
// Log uninstall
|
||||
if (class_exists('TigerStyleSEO_Backup_Logger')) {
|
||||
$logger = TigerStyleSEO_Backup_Logger::instance();
|
||||
$logger->info("Database tables removed successfully", array(
|
||||
'tables_dropped' => $tables
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table status
|
||||
*/
|
||||
public static function get_table_status() {
|
||||
global $wpdb;
|
||||
|
||||
$tables = array(
|
||||
'backup_logs' => $wpdb->prefix . 'tigerstyle_backup_logs',
|
||||
'backup_history' => $wpdb->prefix . 'tigerstyle_backups'
|
||||
);
|
||||
|
||||
$status = array();
|
||||
|
||||
foreach ($tables as $name => $table) {
|
||||
$exists = $wpdb->get_var("SHOW TABLES LIKE '$table'") == $table;
|
||||
$count = 0;
|
||||
|
||||
if ($exists) {
|
||||
$count = $wpdb->get_var("SELECT COUNT(*) FROM $table");
|
||||
}
|
||||
|
||||
$status[$name] = array(
|
||||
'table' => $table,
|
||||
'exists' => $exists,
|
||||
'count' => (int)$count
|
||||
);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repair tables if needed
|
||||
*/
|
||||
public static function repair_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$results = array();
|
||||
|
||||
$tables = array(
|
||||
$wpdb->prefix . 'tigerstyle_backup_logs',
|
||||
$wpdb->prefix . 'tigerstyle_backups'
|
||||
);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$result = $wpdb->query("REPAIR TABLE $table");
|
||||
$results[$table] = $result !== false;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize tables
|
||||
*/
|
||||
public static function optimize_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$results = array();
|
||||
|
||||
$tables = array(
|
||||
$wpdb->prefix . 'tigerstyle_backup_logs',
|
||||
$wpdb->prefix . 'tigerstyle_backups'
|
||||
);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$result = $wpdb->query("OPTIMIZE TABLE $table");
|
||||
$results[$table] = $result !== false;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean old log entries
|
||||
*/
|
||||
public static function cleanup_old_logs($days = 30) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_logs';
|
||||
$cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||
|
||||
$deleted = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $table_name WHERE created_at < %s",
|
||||
$cutoff_date
|
||||
)
|
||||
);
|
||||
|
||||
return $deleted !== false ? (int)$deleted : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database statistics
|
||||
*/
|
||||
public static function get_database_stats() {
|
||||
global $wpdb;
|
||||
|
||||
$stats = array(
|
||||
'version' => get_option('tigerstyle_backup_db_version', '0.0.0'),
|
||||
'tables' => self::get_table_status()
|
||||
);
|
||||
|
||||
// Get table sizes
|
||||
$result = $wpdb->get_results("
|
||||
SELECT
|
||||
table_name as 'table',
|
||||
ROUND(((data_length + index_length) / 1024 / 1024), 2) as 'size_mb'
|
||||
FROM information_schema.TABLES
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name LIKE '{$wpdb->prefix}tigerstyle_%'
|
||||
");
|
||||
|
||||
if ($result) {
|
||||
foreach ($result as $row) {
|
||||
$key = str_replace($wpdb->prefix . 'tigerstyle_', '', $row->table);
|
||||
if (isset($stats['tables'][$key])) {
|
||||
$stats['tables'][$key]['size_mb'] = (float)$row->size_mb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
723
includes/backup/class-restore-engine.php
Normal file
723
includes/backup/class-restore-engine.php
Normal file
@ -0,0 +1,723 @@
|
||||
<?php
|
||||
/**
|
||||
* TigerStyle SEO Restore Engine
|
||||
*
|
||||
* Core restore processing engine with safety checks,
|
||||
* validation, and rollback capabilities.
|
||||
*
|
||||
* @package TigerStyleSEO
|
||||
* @subpackage RestoreEngine
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Restore_Engine {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compression manager
|
||||
*/
|
||||
private $compression_manager;
|
||||
|
||||
/**
|
||||
* Storage manager
|
||||
*/
|
||||
private $storage_manager;
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Validator
|
||||
*/
|
||||
private $validator;
|
||||
|
||||
/**
|
||||
* Current restore ID
|
||||
*/
|
||||
private $current_restore_id;
|
||||
|
||||
/**
|
||||
* Backup manifest
|
||||
*/
|
||||
private $backup_manifest;
|
||||
|
||||
/**
|
||||
* Restore settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->compression_manager = TigerStyleSEO_Compression_Manager::instance();
|
||||
$this->storage_manager = TigerStyleSEO_Storage_Manager::instance();
|
||||
$this->logger = TigerStyleSEO_Backup_Logger::instance();
|
||||
$this->validator = TigerStyleSEO_Backup_Validator::instance();
|
||||
$this->settings = get_option('tigerstyle_backup_settings', array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from backup
|
||||
*
|
||||
* @param array $options Restore options
|
||||
* @return string Restore ID
|
||||
*/
|
||||
public function restore_backup($options = array()) {
|
||||
$defaults = array(
|
||||
'backup_id' => '',
|
||||
'restore_files' => true,
|
||||
'restore_database' => true,
|
||||
'create_rollback' => true,
|
||||
'force_restore' => false,
|
||||
'validate_before_restore' => true
|
||||
);
|
||||
|
||||
$options = wp_parse_args($options, $defaults);
|
||||
|
||||
if (empty($options['backup_id'])) {
|
||||
throw new Exception(__('Backup ID is required', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Generate unique restore ID
|
||||
$this->current_restore_id = 'restore_' . time() . '_' . wp_generate_password(8, false);
|
||||
|
||||
try {
|
||||
// Initialize restore process
|
||||
$this->init_restore_process($options);
|
||||
|
||||
// Download and extract backup
|
||||
$backup_dir = $this->prepare_backup_for_restore($options['backup_id']);
|
||||
|
||||
// Load and validate manifest
|
||||
$this->load_backup_manifest($backup_dir);
|
||||
|
||||
// Validate backup integrity
|
||||
if ($options['validate_before_restore']) {
|
||||
$this->validate_backup_integrity($backup_dir);
|
||||
}
|
||||
|
||||
// Create rollback backup if requested
|
||||
if ($options['create_rollback']) {
|
||||
$this->create_rollback_backup();
|
||||
}
|
||||
|
||||
// Perform restoration
|
||||
if ($options['restore_database']) {
|
||||
$this->restore_database($backup_dir);
|
||||
}
|
||||
|
||||
if ($options['restore_files']) {
|
||||
$this->restore_files($backup_dir);
|
||||
}
|
||||
|
||||
// Finalize restoration
|
||||
$this->finalize_restoration($backup_dir);
|
||||
|
||||
// Log success
|
||||
$this->logger->info('Restore completed successfully', array(
|
||||
'restore_id' => $this->current_restore_id,
|
||||
'backup_id' => $options['backup_id'],
|
||||
'options' => $options
|
||||
));
|
||||
|
||||
// Update progress to 100%
|
||||
$this->update_restore_progress(100, __('Restore completed successfully', 'tigerstyle-heat'));
|
||||
|
||||
return $this->current_restore_id;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Restore failed: ' . $e->getMessage(), array(
|
||||
'restore_id' => $this->current_restore_id,
|
||||
'backup_id' => $options['backup_id'],
|
||||
'options' => $options
|
||||
));
|
||||
|
||||
// Cleanup on failure
|
||||
$this->cleanup_failed_restore();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize restore process
|
||||
*/
|
||||
private function init_restore_process($options) {
|
||||
// Set progress to 0%
|
||||
$this->update_restore_progress(0, __('Initializing restore...', 'tigerstyle-heat'));
|
||||
|
||||
// Check system requirements
|
||||
$this->check_restore_requirements($options);
|
||||
|
||||
// Set time limit
|
||||
@set_time_limit(0);
|
||||
|
||||
// Increase memory limit if possible
|
||||
@ini_set('memory_limit', '512M');
|
||||
|
||||
// Check permissions
|
||||
$this->check_restore_permissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check restore requirements
|
||||
*/
|
||||
private function check_restore_requirements($options) {
|
||||
// Check if backup exists
|
||||
if (!$this->storage_manager->backup_exists($options['backup_id'])) {
|
||||
throw new Exception(__('Backup not found', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Check disk space
|
||||
$backup_info = $this->storage_manager->get_backup_info($options['backup_id']);
|
||||
$required_space = $backup_info['size'] * 3; // Backup size + extraction + current files
|
||||
$available_space = disk_free_space(ABSPATH);
|
||||
|
||||
if ($available_space !== false && $available_space < $required_space) {
|
||||
throw new Exception(__('Insufficient disk space for restore', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Check if WordPress is currently being used heavily
|
||||
if (!$options['force_restore'] && $this->is_site_busy()) {
|
||||
throw new Exception(__('Site appears to be busy. Use force_restore option to override.', 'tigerstyle-heat'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check restore permissions
|
||||
*/
|
||||
private function check_restore_permissions() {
|
||||
// Check file system permissions
|
||||
if (!is_writable(ABSPATH)) {
|
||||
throw new Exception(__('WordPress root directory is not writable', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
if (!is_writable(WP_CONTENT_DIR)) {
|
||||
throw new Exception(__('WordPress content directory is not writable', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Check database permissions
|
||||
global $wpdb;
|
||||
$test_result = $wpdb->query("CREATE TEMPORARY TABLE tigerstyle_test (id int)");
|
||||
if ($test_result === false) {
|
||||
throw new Exception(__('Insufficient database permissions for restore', 'tigerstyle-heat'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare backup for restore
|
||||
*/
|
||||
private function prepare_backup_for_restore($backup_id) {
|
||||
$this->update_restore_progress(5, __('Downloading backup...', 'tigerstyle-heat'));
|
||||
|
||||
// Download backup from storage
|
||||
$backup_file = $this->storage_manager->download_backup($backup_id);
|
||||
|
||||
$this->update_restore_progress(15, __('Extracting backup...', 'tigerstyle-heat'));
|
||||
|
||||
// Extract backup
|
||||
$backup_dir = $this->extract_backup($backup_file);
|
||||
|
||||
return $backup_dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract backup
|
||||
*/
|
||||
private function extract_backup($backup_file) {
|
||||
$upload_dir = wp_upload_dir();
|
||||
$extract_dir = $upload_dir['basedir'] . '/tigerstyle-restores/' . $this->current_restore_id;
|
||||
|
||||
if (!wp_mkdir_p($extract_dir)) {
|
||||
throw new Exception(__('Failed to create extraction directory', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Detect compression method from file extension
|
||||
$compression_method = $this->detect_compression_method($backup_file);
|
||||
|
||||
// Extract using appropriate method
|
||||
$this->compression_manager->extract_archive($backup_file, $extract_dir, $compression_method);
|
||||
|
||||
return $extract_dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load backup manifest
|
||||
*/
|
||||
private function load_backup_manifest($backup_dir) {
|
||||
$manifest_file = $backup_dir . '/manifest.json';
|
||||
|
||||
if (!file_exists($manifest_file)) {
|
||||
throw new Exception(__('Backup manifest not found', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
$manifest_content = file_get_contents($manifest_file);
|
||||
$this->backup_manifest = json_decode($manifest_content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception(__('Invalid backup manifest format', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
$this->logger->info('Backup manifest loaded', array(
|
||||
'restore_id' => $this->current_restore_id,
|
||||
'backup_id' => $this->backup_manifest['backup_id'],
|
||||
'created_at' => $this->backup_manifest['created_at']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate backup integrity
|
||||
*/
|
||||
private function validate_backup_integrity($backup_dir) {
|
||||
$this->update_restore_progress(20, __('Validating backup integrity...', 'tigerstyle-heat'));
|
||||
|
||||
if (!$this->validator->validate_backup_directory($backup_dir, $this->backup_manifest)) {
|
||||
throw new Exception(__('Backup integrity validation failed', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
$this->logger->info('Backup integrity validated successfully', array(
|
||||
'restore_id' => $this->current_restore_id,
|
||||
'backup_id' => $this->backup_manifest['backup_id']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create rollback backup
|
||||
*/
|
||||
private function create_rollback_backup() {
|
||||
$this->update_restore_progress(25, __('Creating rollback backup...', 'tigerstyle-heat'));
|
||||
|
||||
try {
|
||||
$backup_engine = new TigerStyleSEO_Backup_Engine();
|
||||
$rollback_id = $backup_engine->create_backup(array(
|
||||
'type' => 'rollback',
|
||||
'description' => 'Rollback backup before restore - ' . current_time('mysql'),
|
||||
'compression' => 'zip',
|
||||
'storage_location' => 'local',
|
||||
'include_files' => true,
|
||||
'include_database' => true
|
||||
));
|
||||
|
||||
// Store rollback ID for potential use
|
||||
update_option('tigerstyle_last_rollback_backup', $rollback_id);
|
||||
|
||||
$this->logger->info('Rollback backup created', array(
|
||||
'restore_id' => $this->current_restore_id,
|
||||
'rollback_backup_id' => $rollback_id
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->warning('Failed to create rollback backup: ' . $e->getMessage());
|
||||
// Don't fail the restore if rollback backup fails
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore database
|
||||
*/
|
||||
private function restore_database($backup_dir) {
|
||||
$this->update_restore_progress(30, __('Restoring database...', 'tigerstyle-heat'));
|
||||
|
||||
$sql_file = $backup_dir . '/database.sql';
|
||||
|
||||
if (!file_exists($sql_file)) {
|
||||
throw new Exception(__('Database backup file not found', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Read and execute SQL file in chunks
|
||||
$handle = fopen($sql_file, 'r');
|
||||
if (!$handle) {
|
||||
throw new Exception(__('Failed to open database backup file', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
try {
|
||||
$sql_buffer = '';
|
||||
$line_count = 0;
|
||||
$total_lines = $this->count_file_lines($sql_file);
|
||||
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$line_count++;
|
||||
|
||||
// Skip comments and empty lines
|
||||
$line = trim($line);
|
||||
if (empty($line) || strpos($line, '--') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sql_buffer .= $line;
|
||||
|
||||
// Execute complete statements
|
||||
if (substr($line, -1) === ';') {
|
||||
$result = $wpdb->query($sql_buffer);
|
||||
if ($result === false && !empty($wpdb->last_error)) {
|
||||
$this->logger->warning('Database restore warning: ' . $wpdb->last_error, array(
|
||||
'sql' => substr($sql_buffer, 0, 200) . '...'
|
||||
));
|
||||
}
|
||||
$sql_buffer = '';
|
||||
|
||||
// Update progress
|
||||
if ($line_count % 100 === 0) {
|
||||
$progress = 30 + (($line_count / $total_lines) * 40); // 30-70%
|
||||
$this->update_restore_progress($progress, sprintf(__('Restoring database: %d%%', 'tigerstyle-heat'), ($line_count / $total_lines) * 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute any remaining SQL
|
||||
if (!empty(trim($sql_buffer))) {
|
||||
$wpdb->query($sql_buffer);
|
||||
}
|
||||
|
||||
} finally {
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
// Clear WordPress caches
|
||||
wp_cache_flush();
|
||||
|
||||
$this->logger->info('Database restore completed', array(
|
||||
'restore_id' => $this->current_restore_id,
|
||||
'lines_processed' => $line_count
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore files
|
||||
*/
|
||||
private function restore_files($backup_dir) {
|
||||
$this->update_restore_progress(70, __('Restoring files...', 'tigerstyle-heat'));
|
||||
|
||||
$files_dir = $backup_dir . '/files';
|
||||
|
||||
if (!is_dir($files_dir)) {
|
||||
throw new Exception(__('Files backup directory not found', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Count total files for progress tracking
|
||||
$total_files = $this->count_directory_files($files_dir);
|
||||
$processed_files = 0;
|
||||
|
||||
// Restore files
|
||||
$this->restore_directory_recursive($files_dir, ABSPATH, $total_files, $processed_files);
|
||||
|
||||
$this->logger->info('File restore completed', array(
|
||||
'restore_id' => $this->current_restore_id,
|
||||
'files_restored' => $processed_files
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore directory recursively
|
||||
*/
|
||||
private function restore_directory_recursive($source_dir, $dest_dir, $total_files, &$processed_files) {
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($source_dir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $item) {
|
||||
$source_path = $item->getPathname();
|
||||
$relative_path = substr($source_path, strlen($source_dir) + 1);
|
||||
$dest_path = $dest_dir . $relative_path;
|
||||
|
||||
if ($item->isDir()) {
|
||||
if (!is_dir($dest_path)) {
|
||||
wp_mkdir_p($dest_path);
|
||||
}
|
||||
} elseif ($item->isFile()) {
|
||||
// Create destination directory if it doesn't exist
|
||||
$dest_parent = dirname($dest_path);
|
||||
if (!is_dir($dest_parent)) {
|
||||
wp_mkdir_p($dest_parent);
|
||||
}
|
||||
|
||||
// Copy file with error handling
|
||||
if (!copy($source_path, $dest_path)) {
|
||||
$this->logger->warning('Failed to restore file: ' . $relative_path);
|
||||
} else {
|
||||
// Preserve file permissions
|
||||
$source_perms = fileperms($source_path);
|
||||
chmod($dest_path, $source_perms);
|
||||
}
|
||||
|
||||
$processed_files++;
|
||||
|
||||
// Update progress
|
||||
if ($processed_files % 50 === 0) {
|
||||
$progress = 70 + (($processed_files / $total_files) * 25); // 70-95%
|
||||
$this->update_restore_progress($progress, sprintf(__('Restored %d of %d files', 'tigerstyle-heat'), $processed_files, $total_files));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize restoration
|
||||
*/
|
||||
private function finalize_restoration($backup_dir) {
|
||||
$this->update_restore_progress(95, __('Finalizing restoration...', 'tigerstyle-heat'));
|
||||
|
||||
// Update site URL if necessary
|
||||
$this->update_site_urls();
|
||||
|
||||
// Clear all caches
|
||||
$this->clear_all_caches();
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
|
||||
// Run any post-restore hooks
|
||||
do_action('tigerstyle_backup_restore_completed', $this->current_restore_id, $this->backup_manifest);
|
||||
|
||||
// Cleanup extraction directory
|
||||
$this->cleanup_extraction_directory($backup_dir);
|
||||
|
||||
$this->logger->info('Restoration finalized', array(
|
||||
'restore_id' => $this->current_restore_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update site URLs if necessary
|
||||
*/
|
||||
private function update_site_urls() {
|
||||
$current_home_url = home_url();
|
||||
$current_site_url = site_url();
|
||||
|
||||
$backup_site_url = $this->backup_manifest['site_url'] ?? '';
|
||||
|
||||
// If URLs are different, ask user what to do
|
||||
if (!empty($backup_site_url) && $backup_site_url !== $current_home_url) {
|
||||
$this->logger->info('Site URL mismatch detected', array(
|
||||
'current_url' => $current_home_url,
|
||||
'backup_url' => $backup_site_url
|
||||
));
|
||||
|
||||
// For now, keep current URLs
|
||||
// In a full implementation, you might want to prompt the user
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches
|
||||
*/
|
||||
private function clear_all_caches() {
|
||||
// WordPress object cache
|
||||
wp_cache_flush();
|
||||
|
||||
// Opcache
|
||||
if (function_exists('opcache_reset')) {
|
||||
opcache_reset();
|
||||
}
|
||||
|
||||
// Clear transients
|
||||
global $wpdb;
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_%'");
|
||||
|
||||
// Clear common caching plugins
|
||||
$this->clear_plugin_caches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear plugin caches
|
||||
*/
|
||||
private function clear_plugin_caches() {
|
||||
// WP Rocket
|
||||
if (function_exists('rocket_clean_domain')) {
|
||||
rocket_clean_domain();
|
||||
}
|
||||
|
||||
// W3 Total Cache
|
||||
if (function_exists('w3tc_flush_all')) {
|
||||
w3tc_flush_all();
|
||||
}
|
||||
|
||||
// WP Super Cache
|
||||
if (function_exists('wp_cache_clear_cache')) {
|
||||
wp_cache_clear_cache();
|
||||
}
|
||||
|
||||
// LiteSpeed Cache
|
||||
if (class_exists('LiteSpeed_Cache_API')) {
|
||||
LiteSpeed_Cache_API::purge_all();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update restore progress
|
||||
*/
|
||||
private function update_restore_progress($percentage, $message) {
|
||||
$progress = array(
|
||||
'percentage' => $percentage,
|
||||
'message' => $message,
|
||||
'timestamp' => time(),
|
||||
'restore_id' => $this->current_restore_id
|
||||
);
|
||||
|
||||
set_transient('tigerstyle_restore_progress_' . $this->current_restore_id, $progress, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect compression method from filename
|
||||
*/
|
||||
private function detect_compression_method($filename) {
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
switch ($extension) {
|
||||
case 'zip':
|
||||
return 'zip';
|
||||
case 'gz':
|
||||
return pathinfo(pathinfo($filename, PATHINFO_FILENAME), PATHINFO_EXTENSION) === 'tar' ? 'tar.gz' : 'gz';
|
||||
case 'bz2':
|
||||
return pathinfo(pathinfo($filename, PATHINFO_FILENAME), PATHINFO_EXTENSION) === 'tar' ? 'tar.bz2' : 'bz2';
|
||||
default:
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count lines in file
|
||||
*/
|
||||
private function count_file_lines($filename) {
|
||||
$handle = fopen($filename, 'r');
|
||||
if (!$handle) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$line_count = 0;
|
||||
while (fgets($handle) !== false) {
|
||||
$line_count++;
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
return $line_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count files in directory
|
||||
*/
|
||||
private function count_directory_files($directory) {
|
||||
$count = 0;
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if site is busy
|
||||
*/
|
||||
private function is_site_busy() {
|
||||
// Simple check: if there are many active sessions or high CPU usage
|
||||
// This is a basic implementation - in production you might want more sophisticated checks
|
||||
|
||||
$load_average = sys_getloadavg();
|
||||
if ($load_average && $load_average[0] > 2.0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup extraction directory
|
||||
*/
|
||||
private function cleanup_extraction_directory($backup_dir) {
|
||||
if (is_dir($backup_dir)) {
|
||||
$this->remove_directory($backup_dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove directory recursively
|
||||
*/
|
||||
private function remove_directory($dir) {
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir()) {
|
||||
rmdir($file->getPathname());
|
||||
} else {
|
||||
unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup failed restore
|
||||
*/
|
||||
private function cleanup_failed_restore() {
|
||||
// Update progress to indicate failure
|
||||
$this->update_restore_progress(0, __('Restore failed', 'tigerstyle-heat'));
|
||||
|
||||
// Add to failed restores list
|
||||
$failed_restores = get_option('tigerstyle_failed_restores', array());
|
||||
$failed_restores[] = array(
|
||||
'restore_id' => $this->current_restore_id,
|
||||
'timestamp' => time(),
|
||||
'error' => 'Restore process failed'
|
||||
);
|
||||
update_option('tigerstyle_failed_restores', array_slice($failed_restores, -10)); // Keep last 10 failures
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback to previous state
|
||||
*/
|
||||
public function rollback_restore() {
|
||||
$rollback_backup_id = get_option('tigerstyle_last_rollback_backup');
|
||||
|
||||
if (empty($rollback_backup_id)) {
|
||||
throw new Exception(__('No rollback backup available', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Restore from rollback backup
|
||||
return $this->restore_backup(array(
|
||||
'backup_id' => $rollback_backup_id,
|
||||
'restore_files' => true,
|
||||
'restore_database' => true,
|
||||
'create_rollback' => false, // Don't create another rollback
|
||||
'validate_before_restore' => false // Skip validation for rollback
|
||||
));
|
||||
}
|
||||
}
|
||||
839
includes/backup/class-storage-manager.php
Normal file
839
includes/backup/class-storage-manager.php
Normal file
@ -0,0 +1,839 @@
|
||||
<?php
|
||||
/**
|
||||
* Storage Manager for TigerStyle SEO Backup System
|
||||
* Handles local and S3-compatible cloud storage with encryption
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Storage_Manager {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Storage backends
|
||||
*/
|
||||
private $storage_backends = array();
|
||||
|
||||
/**
|
||||
* Logger instance
|
||||
*/
|
||||
private $logger = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize storage manager
|
||||
*/
|
||||
private function init() {
|
||||
$this->logger = TigerStyleSEO_Backup_Logger::instance();
|
||||
$this->register_storage_backends();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register available storage backends
|
||||
*/
|
||||
private function register_storage_backends() {
|
||||
// Local storage (always available)
|
||||
$this->storage_backends['local'] = array(
|
||||
'name' => 'Local Storage',
|
||||
'description' => 'Store backups locally on server',
|
||||
'available' => true,
|
||||
'settings' => array(
|
||||
'path' => get_option('backup_local_path', WP_CONTENT_DIR . '/tigerstyle-backups/')
|
||||
)
|
||||
);
|
||||
|
||||
// S3 Compatible storage
|
||||
$s3_enabled = get_option('backup_s3_enabled', false);
|
||||
$this->storage_backends['s3'] = array(
|
||||
'name' => 'S3 Compatible Storage',
|
||||
'description' => 'Store backups in AWS S3 or compatible services (MinIO, DigitalOcean Spaces, etc.)',
|
||||
'available' => $s3_enabled && $this->check_s3_requirements(),
|
||||
'settings' => array(
|
||||
'endpoint' => get_option('backup_s3_endpoint', ''),
|
||||
'bucket' => get_option('backup_s3_bucket', ''),
|
||||
'access_key' => get_option('backup_s3_access_key', ''),
|
||||
'secret_key' => get_option('backup_s3_secret_key', ''),
|
||||
'region' => get_option('backup_s3_region', 'us-east-1'),
|
||||
'storage_class' => get_option('backup_s3_storage_class', 'STANDARD_IA'),
|
||||
'encryption' => get_option('backup_s3_encryption', true),
|
||||
'prefix' => get_option('backup_s3_prefix', 'tigerstyle-backups/')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check S3 requirements
|
||||
*/
|
||||
private function check_s3_requirements() {
|
||||
return extension_loaded('curl') && function_exists('openssl_encrypt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store backup using configured storage backends
|
||||
*/
|
||||
public function store_backup($backup_file, $backup_id, $manifest) {
|
||||
$storage_results = array();
|
||||
$primary_backend = get_option('backup_primary_storage', 'local');
|
||||
$secondary_backends = get_option('backup_secondary_storage', array());
|
||||
|
||||
// Store to primary backend
|
||||
if (isset($this->storage_backends[$primary_backend]) && $this->storage_backends[$primary_backend]['available']) {
|
||||
$this->logger->info("Storing backup to primary storage", array(
|
||||
'backend' => $primary_backend,
|
||||
'backup_id' => $backup_id
|
||||
));
|
||||
|
||||
$result = $this->store_to_backend($backup_file, $backup_id, $manifest, $primary_backend);
|
||||
|
||||
if ($result['success']) {
|
||||
$storage_results['primary'] = $result;
|
||||
} else {
|
||||
$this->logger->error("Primary storage failed", array(
|
||||
'backend' => $primary_backend,
|
||||
'error' => $result['error']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Store to secondary backends
|
||||
foreach ($secondary_backends as $backend) {
|
||||
if (isset($this->storage_backends[$backend]) && $this->storage_backends[$backend]['available']) {
|
||||
$this->logger->info("Storing backup to secondary storage", array(
|
||||
'backend' => $backend,
|
||||
'backup_id' => $backup_id
|
||||
));
|
||||
|
||||
$result = $this->store_to_backend($backup_file, $backup_id, $manifest, $backend);
|
||||
|
||||
if ($result['success']) {
|
||||
$storage_results['secondary'][$backend] = $result;
|
||||
} else {
|
||||
$this->logger->warning("Secondary storage failed", array(
|
||||
'backend' => $backend,
|
||||
'error' => $result['error']
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $storage_results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store backup to specific backend
|
||||
*/
|
||||
private function store_to_backend($backup_file, $backup_id, $manifest, $backend) {
|
||||
$start_time = microtime(true);
|
||||
|
||||
try {
|
||||
switch ($backend) {
|
||||
case 'local':
|
||||
$result = $this->store_to_local($backup_file, $backup_id, $manifest);
|
||||
break;
|
||||
case 's3':
|
||||
$result = $this->store_to_s3($backup_file, $backup_id, $manifest);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown storage backend: " . $backend);
|
||||
}
|
||||
|
||||
$duration = microtime(true) - $start_time;
|
||||
|
||||
$this->logger->info("Backup stored successfully", array(
|
||||
'backend' => $backend,
|
||||
'backup_id' => $backup_id,
|
||||
'duration' => round($duration, 2) . 's',
|
||||
'location' => $result['location'] ?? 'unknown'
|
||||
));
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'backend' => $backend,
|
||||
'location' => $result['location'],
|
||||
'duration' => $duration,
|
||||
'metadata' => $result['metadata'] ?? array()
|
||||
);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error("Storage backend failed", array(
|
||||
'backend' => $backend,
|
||||
'backup_id' => $backup_id,
|
||||
'error' => $e->getMessage()
|
||||
));
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'backend' => $backend,
|
||||
'error' => $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store backup to local storage
|
||||
*/
|
||||
private function store_to_local($backup_file, $backup_id, $manifest) {
|
||||
$settings = $this->storage_backends['local']['settings'];
|
||||
$destination_dir = $settings['path'];
|
||||
|
||||
// Ensure destination directory exists
|
||||
if (!is_dir($destination_dir)) {
|
||||
wp_mkdir_p($destination_dir);
|
||||
|
||||
// Protect backup directory
|
||||
$htaccess_content = "Order deny,allow\nDeny from all\n";
|
||||
file_put_contents($destination_dir . '.htaccess', $htaccess_content);
|
||||
}
|
||||
|
||||
$destination_file = $destination_dir . basename($backup_file);
|
||||
|
||||
// Copy backup file
|
||||
if (!copy($backup_file, $destination_file)) {
|
||||
throw new Exception("Failed to copy backup to local storage");
|
||||
}
|
||||
|
||||
// Create manifest file
|
||||
$manifest_file = $destination_dir . $backup_id . '-manifest.json';
|
||||
file_put_contents($manifest_file, json_encode($manifest, JSON_PRETTY_PRINT));
|
||||
|
||||
return array(
|
||||
'location' => $destination_file,
|
||||
'manifest_file' => $manifest_file,
|
||||
'metadata' => array(
|
||||
'size' => filesize($destination_file),
|
||||
'checksum' => md5_file($destination_file)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store backup to S3 compatible storage
|
||||
*/
|
||||
private function store_to_s3($backup_file, $backup_id, $manifest) {
|
||||
$settings = $this->storage_backends['s3']['settings'];
|
||||
|
||||
if (empty($settings['bucket']) || empty($settings['access_key']) || empty($settings['secret_key'])) {
|
||||
throw new Exception("S3 storage not properly configured");
|
||||
}
|
||||
|
||||
$s3_key = $settings['prefix'] . $backup_id . '/' . basename($backup_file);
|
||||
$manifest_key = $settings['prefix'] . $backup_id . '/manifest.json';
|
||||
|
||||
// Upload backup file
|
||||
$backup_upload = $this->s3_put_object($backup_file, $s3_key, $settings);
|
||||
|
||||
if (!$backup_upload['success']) {
|
||||
throw new Exception("Failed to upload backup to S3: " . $backup_upload['error']);
|
||||
}
|
||||
|
||||
// Upload manifest file
|
||||
$manifest_content = json_encode($manifest, JSON_PRETTY_PRINT);
|
||||
$manifest_upload = $this->s3_put_object_content($manifest_content, $manifest_key, $settings);
|
||||
|
||||
if (!$manifest_upload['success']) {
|
||||
$this->logger->warning("Failed to upload manifest to S3", array(
|
||||
'error' => $manifest_upload['error']
|
||||
));
|
||||
}
|
||||
|
||||
return array(
|
||||
'location' => $s3_key,
|
||||
'manifest_location' => $manifest_key,
|
||||
'metadata' => array(
|
||||
'bucket' => $settings['bucket'],
|
||||
'region' => $settings['region'],
|
||||
'storage_class' => $settings['storage_class'],
|
||||
'encryption' => $settings['encryption'],
|
||||
'etag' => $backup_upload['etag'] ?? '',
|
||||
'version_id' => $backup_upload['version_id'] ?? ''
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to S3
|
||||
*/
|
||||
private function s3_put_object($file_path, $key, $settings) {
|
||||
if (!file_exists($file_path)) {
|
||||
return array('success' => false, 'error' => 'File not found');
|
||||
}
|
||||
|
||||
$file_content = file_get_contents($file_path);
|
||||
return $this->s3_put_object_content($file_content, $key, $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload content to S3
|
||||
*/
|
||||
private function s3_put_object_content($content, $key, $settings) {
|
||||
$endpoint = $settings['endpoint'] ?: 'https://s3.' . $settings['region'] . '.amazonaws.com';
|
||||
$bucket = $settings['bucket'];
|
||||
$url = $endpoint . '/' . $bucket . '/' . $key;
|
||||
|
||||
// Prepare headers
|
||||
$headers = array();
|
||||
$headers['Date'] = gmdate('D, d M Y H:i:s T');
|
||||
$headers['Content-Type'] = 'application/octet-stream';
|
||||
$headers['Content-Length'] = strlen($content);
|
||||
|
||||
if ($settings['storage_class'] && $settings['storage_class'] !== 'STANDARD') {
|
||||
$headers['x-amz-storage-class'] = $settings['storage_class'];
|
||||
}
|
||||
|
||||
if ($settings['encryption']) {
|
||||
$headers['x-amz-server-side-encryption'] = 'AES256';
|
||||
}
|
||||
|
||||
// Calculate content hash for authentication
|
||||
$content_hash = hash('sha256', $content);
|
||||
$headers['x-amz-content-sha256'] = $content_hash;
|
||||
|
||||
// Create authorization signature
|
||||
$auth_header = $this->create_s3_auth_header('PUT', $key, $headers, $settings);
|
||||
$headers['Authorization'] = $auth_header;
|
||||
|
||||
// Prepare curl request
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, array(
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_CUSTOMREQUEST => 'PUT',
|
||||
CURLOPT_POSTFIELDS => $content,
|
||||
CURLOPT_HTTPHEADER => $this->format_headers($headers),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 300,
|
||||
CURLOPT_FOLLOWLOCATION => false
|
||||
));
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curl_error) {
|
||||
return array('success' => false, 'error' => 'CURL error: ' . $curl_error);
|
||||
}
|
||||
|
||||
if ($http_code >= 200 && $http_code < 300) {
|
||||
// Extract ETag from response headers
|
||||
$etag = '';
|
||||
if (preg_match('/ETag: "([^"]+)"/', $response, $matches)) {
|
||||
$etag = $matches[1];
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'http_code' => $http_code,
|
||||
'etag' => $etag,
|
||||
'response' => $response
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => 'HTTP ' . $http_code . ': ' . $this->extract_s3_error($response)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create S3 authorization header (AWS Signature Version 4)
|
||||
*/
|
||||
private function create_s3_auth_header($method, $key, $headers, $settings) {
|
||||
$access_key = $settings['access_key'];
|
||||
$secret_key = $settings['secret_key'];
|
||||
$region = $settings['region'];
|
||||
$service = 's3';
|
||||
|
||||
$timestamp = gmdate('Ymd\THis\Z');
|
||||
$date = gmdate('Ymd');
|
||||
|
||||
// Create canonical request
|
||||
$canonical_uri = '/' . $key;
|
||||
$canonical_querystring = '';
|
||||
|
||||
$canonical_headers = '';
|
||||
$signed_headers = array();
|
||||
|
||||
ksort($headers);
|
||||
foreach ($headers as $name => $value) {
|
||||
$name_lower = strtolower($name);
|
||||
$canonical_headers .= $name_lower . ':' . $value . "\n";
|
||||
$signed_headers[] = $name_lower;
|
||||
}
|
||||
|
||||
$signed_headers_string = implode(';', $signed_headers);
|
||||
$payload_hash = $headers['x-amz-content-sha256'];
|
||||
|
||||
$canonical_request = $method . "\n" .
|
||||
$canonical_uri . "\n" .
|
||||
$canonical_querystring . "\n" .
|
||||
$canonical_headers . "\n" .
|
||||
$signed_headers_string . "\n" .
|
||||
$payload_hash;
|
||||
|
||||
// Create string to sign
|
||||
$algorithm = 'AWS4-HMAC-SHA256';
|
||||
$credential_scope = $date . '/' . $region . '/' . $service . '/aws4_request';
|
||||
$string_to_sign = $algorithm . "\n" .
|
||||
$timestamp . "\n" .
|
||||
$credential_scope . "\n" .
|
||||
hash('sha256', $canonical_request);
|
||||
|
||||
// Calculate signature
|
||||
$k_date = hash_hmac('sha256', $date, 'AWS4' . $secret_key, true);
|
||||
$k_region = hash_hmac('sha256', $region, $k_date, true);
|
||||
$k_service = hash_hmac('sha256', $service, $k_region, true);
|
||||
$k_signing = hash_hmac('sha256', 'aws4_request', $k_service, true);
|
||||
$signature = hash_hmac('sha256', $string_to_sign, $k_signing);
|
||||
|
||||
// Create authorization header
|
||||
$authorization = $algorithm . ' ' .
|
||||
'Credential=' . $access_key . '/' . $credential_scope . ', ' .
|
||||
'SignedHeaders=' . $signed_headers_string . ', ' .
|
||||
'Signature=' . $signature;
|
||||
|
||||
return $authorization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format headers for curl
|
||||
*/
|
||||
private function format_headers($headers) {
|
||||
$formatted = array();
|
||||
foreach ($headers as $name => $value) {
|
||||
$formatted[] = $name . ': ' . $value;
|
||||
}
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error message from S3 response
|
||||
*/
|
||||
private function extract_s3_error($response) {
|
||||
if (preg_match('/<Code>([^<]+)<\/Code>/', $response, $matches)) {
|
||||
$code = $matches[1];
|
||||
if (preg_match('/<Message>([^<]+)<\/Message>/', $response, $msg_matches)) {
|
||||
return $code . ': ' . $msg_matches[1];
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
return 'Unknown S3 error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test S3 connection
|
||||
*/
|
||||
public function test_s3_connection($settings = null) {
|
||||
if (!$settings) {
|
||||
$settings = $this->storage_backends['s3']['settings'];
|
||||
}
|
||||
|
||||
try {
|
||||
// Test by trying to list bucket contents
|
||||
$test_content = 'TigerStyle SEO Backup Connection Test';
|
||||
$test_key = $settings['prefix'] . 'connection-test.txt';
|
||||
|
||||
$result = $this->s3_put_object_content($test_content, $test_key, $settings);
|
||||
|
||||
if ($result['success']) {
|
||||
// Clean up test file
|
||||
$this->s3_delete_object($test_key, $settings);
|
||||
return array('success' => true, 'message' => 'S3 connection successful');
|
||||
} else {
|
||||
return array('success' => false, 'error' => $result['error']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
return array('success' => false, 'error' => $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete object from S3
|
||||
*/
|
||||
private function s3_delete_object($key, $settings) {
|
||||
$endpoint = $settings['endpoint'] ?: 'https://s3.' . $settings['region'] . '.amazonaws.com';
|
||||
$bucket = $settings['bucket'];
|
||||
$url = $endpoint . '/' . $bucket . '/' . $key;
|
||||
|
||||
$headers = array();
|
||||
$headers['Date'] = gmdate('D, d M Y H:i:s T');
|
||||
$headers['x-amz-content-sha256'] = hash('sha256', '');
|
||||
|
||||
$auth_header = $this->create_s3_auth_header('DELETE', $key, $headers, $settings);
|
||||
$headers['Authorization'] = $auth_header;
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, array(
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_CUSTOMREQUEST => 'DELETE',
|
||||
CURLOPT_HTTPHEADER => $this->format_headers($headers),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 60
|
||||
));
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
return $http_code >= 200 && $http_code < 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve backup from storage
|
||||
*/
|
||||
public function retrieve_backup($backup_id, $destination_path, $backend = null) {
|
||||
if (!$backend) {
|
||||
$backend = $this->get_backup_location($backup_id);
|
||||
}
|
||||
|
||||
if (!$backend) {
|
||||
throw new Exception("Cannot determine backup location for: " . $backup_id);
|
||||
}
|
||||
|
||||
$this->logger->info("Retrieving backup from storage", array(
|
||||
'backup_id' => $backup_id,
|
||||
'backend' => $backend,
|
||||
'destination' => $destination_path
|
||||
));
|
||||
|
||||
switch ($backend) {
|
||||
case 'local':
|
||||
return $this->retrieve_from_local($backup_id, $destination_path);
|
||||
case 's3':
|
||||
return $this->retrieve_from_s3($backup_id, $destination_path);
|
||||
default:
|
||||
throw new Exception("Unknown storage backend: " . $backend);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve backup from local storage
|
||||
*/
|
||||
private function retrieve_from_local($backup_id, $destination_path) {
|
||||
$settings = $this->storage_backends['local']['settings'];
|
||||
$backup_dir = $settings['path'];
|
||||
|
||||
// Find backup file
|
||||
$pattern = $backup_dir . $backup_id . '.*';
|
||||
$files = glob($pattern);
|
||||
|
||||
if (empty($files)) {
|
||||
throw new Exception("Backup file not found: " . $backup_id);
|
||||
}
|
||||
|
||||
$backup_file = $files[0];
|
||||
|
||||
if (!copy($backup_file, $destination_path)) {
|
||||
throw new Exception("Failed to copy backup from local storage");
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'source' => $backup_file,
|
||||
'destination' => $destination_path,
|
||||
'size' => filesize($destination_path)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve backup from S3
|
||||
*/
|
||||
private function retrieve_from_s3($backup_id, $destination_path) {
|
||||
// Implementation would go here for S3 download
|
||||
// For brevity, this is a placeholder
|
||||
throw new Exception("S3 backup retrieval not yet implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup location from database
|
||||
*/
|
||||
private function get_backup_location($backup_id) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backups';
|
||||
$backup = $wpdb->get_row(
|
||||
$wpdb->prepare("SELECT storage_location FROM {$table_name} WHERE backup_id = %s", $backup_id),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $backup ? $backup['storage_location'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available storage backends
|
||||
*/
|
||||
public function get_available_backends() {
|
||||
return array_filter($this->storage_backends, function($backend) {
|
||||
return $backend['available'];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage backend settings
|
||||
*/
|
||||
public function get_backend_settings($backend) {
|
||||
return isset($this->storage_backends[$backend]) ? $this->storage_backends[$backend]['settings'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available backups
|
||||
*/
|
||||
public function list_backups($limit = 50, $offset = 0) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'tigerstyle_backup_metadata';
|
||||
|
||||
// Get total count
|
||||
$total_count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
|
||||
|
||||
// Get backup list with pagination
|
||||
$backups = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT backup_id, storage_type, file_path, s3_bucket, s3_key, s3_url,
|
||||
file_size, created_at, metadata_json
|
||||
FROM {$table_name}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
$limit,
|
||||
$offset
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
// Process backup data
|
||||
$processed_backups = array();
|
||||
foreach ($backups as $backup) {
|
||||
$metadata = array();
|
||||
if (!empty($backup['metadata_json'])) {
|
||||
$metadata = json_decode($backup['metadata_json'], true) ?: array();
|
||||
}
|
||||
|
||||
// Determine storage location and availability
|
||||
$storage_info = $this->get_backup_storage_info($backup);
|
||||
|
||||
$processed_backups[] = array(
|
||||
'backup_id' => $backup['backup_id'],
|
||||
'storage_type' => $backup['storage_type'],
|
||||
'file_size' => intval($backup['file_size']),
|
||||
'file_size_formatted' => $this->format_file_size($backup['file_size']),
|
||||
'created_at' => $backup['created_at'],
|
||||
'created_at_formatted' => $this->format_backup_date($backup['created_at']),
|
||||
'storage_location' => $storage_info['location'],
|
||||
'is_available' => $storage_info['available'],
|
||||
'metadata' => $metadata,
|
||||
'actions' => $this->get_backup_actions($backup)
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'backups' => $processed_backups,
|
||||
'total_count' => intval($total_count),
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'has_more' => ($offset + $limit) < $total_count
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup storage information
|
||||
*/
|
||||
private function get_backup_storage_info($backup) {
|
||||
$info = array(
|
||||
'location' => '',
|
||||
'available' => false
|
||||
);
|
||||
|
||||
switch ($backup['storage_type']) {
|
||||
case 'local':
|
||||
if (!empty($backup['file_path'])) {
|
||||
$info['location'] = basename($backup['file_path']);
|
||||
$info['available'] = file_exists($backup['file_path']);
|
||||
} else {
|
||||
// Fallback: check local storage directory
|
||||
$local_path = $this->storage_backends['local']['settings']['path'];
|
||||
$pattern = $local_path . $backup['backup_id'] . '.*';
|
||||
$files = glob($pattern);
|
||||
if (!empty($files)) {
|
||||
$info['location'] = basename($files[0]);
|
||||
$info['available'] = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 's3':
|
||||
if (!empty($backup['s3_key'])) {
|
||||
$info['location'] = $backup['s3_bucket'] . '/' . $backup['s3_key'];
|
||||
$info['available'] = true; // Assume available unless proven otherwise
|
||||
} elseif (!empty($backup['s3_url'])) {
|
||||
$info['location'] = $backup['s3_url'];
|
||||
$info['available'] = true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$info['location'] = __('Unknown storage type', 'tigerstyle-heat');
|
||||
break;
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size for display
|
||||
*/
|
||||
private function format_file_size($bytes) {
|
||||
$bytes = floatval($bytes);
|
||||
$units = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format backup date for display
|
||||
*/
|
||||
private function format_backup_date($datetime) {
|
||||
$timestamp = strtotime($datetime);
|
||||
if (!$timestamp) {
|
||||
return $datetime;
|
||||
}
|
||||
|
||||
$now = current_time('timestamp');
|
||||
$diff = $now - $timestamp;
|
||||
|
||||
if ($diff < HOUR_IN_SECONDS) {
|
||||
$minutes = floor($diff / MINUTE_IN_SECONDS);
|
||||
return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'tigerstyle-heat'), $minutes);
|
||||
} elseif ($diff < DAY_IN_SECONDS) {
|
||||
$hours = floor($diff / HOUR_IN_SECONDS);
|
||||
return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'tigerstyle-heat'), $hours);
|
||||
} elseif ($diff < WEEK_IN_SECONDS) {
|
||||
$days = floor($diff / DAY_IN_SECONDS);
|
||||
return sprintf(_n('%d day ago', '%d days ago', $days, 'tigerstyle-heat'), $days);
|
||||
} else {
|
||||
return date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available actions for a backup
|
||||
*/
|
||||
private function get_backup_actions($backup) {
|
||||
$actions = array();
|
||||
|
||||
// Restore action
|
||||
$actions['restore'] = array(
|
||||
'label' => __('Restore', 'tigerstyle-heat'),
|
||||
'url' => admin_url('admin.php?page=tigerstyle-heat&action=restore&backup_id=' . urlencode($backup['backup_id'])),
|
||||
'class' => 'button button-primary'
|
||||
);
|
||||
|
||||
// Download action (for local backups)
|
||||
if ($backup['storage_type'] === 'local' && !empty($backup['file_path']) && file_exists($backup['file_path'])) {
|
||||
$actions['download'] = array(
|
||||
'label' => __('Download', 'tigerstyle-heat'),
|
||||
'url' => admin_url('admin.php?page=tigerstyle-heat&action=download&backup_id=' . urlencode($backup['backup_id'])),
|
||||
'class' => 'button'
|
||||
);
|
||||
}
|
||||
|
||||
// Delete action
|
||||
$actions['delete'] = array(
|
||||
'label' => __('Delete', 'tigerstyle-heat'),
|
||||
'url' => admin_url('admin.php?page=tigerstyle-heat&action=delete&backup_id=' . urlencode($backup['backup_id'])),
|
||||
'class' => 'button button-link-delete',
|
||||
'confirm' => __('Are you sure you want to delete this backup? This action cannot be undone.', 'tigerstyle-heat')
|
||||
);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage statistics
|
||||
*/
|
||||
public function get_storage_stats() {
|
||||
$stats = array(
|
||||
'total_count' => 0,
|
||||
'total_size' => 0,
|
||||
'usage_percent' => 0,
|
||||
'available_space' => 0,
|
||||
'backends' => array()
|
||||
);
|
||||
|
||||
// Calculate local storage stats
|
||||
if (isset($this->storage_backends['local']) && $this->storage_backends['local']['available']) {
|
||||
$local_path = $this->storage_backends['local']['settings']['path'];
|
||||
if (is_dir($local_path)) {
|
||||
$total_size = 0;
|
||||
$file_count = 0;
|
||||
|
||||
$files = glob($local_path . '*.{zip,tar,gz,bz2}', GLOB_BRACE);
|
||||
if ($files) {
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
$total_size += filesize($file);
|
||||
$file_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stats['total_count'] = $file_count;
|
||||
$stats['total_size'] = $total_size;
|
||||
|
||||
// Calculate disk usage percentage
|
||||
$disk_total = disk_total_space($local_path);
|
||||
$disk_free = disk_free_space($local_path);
|
||||
if ($disk_total && $disk_free) {
|
||||
$stats['usage_percent'] = round((($disk_total - $disk_free) / $disk_total) * 100, 2);
|
||||
$stats['available_space'] = $disk_free;
|
||||
}
|
||||
|
||||
$stats['backends']['local'] = array(
|
||||
'count' => $file_count,
|
||||
'size' => $total_size,
|
||||
'path' => $local_path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add S3 stats if available (placeholder for now)
|
||||
if (isset($this->storage_backends['s3']) && $this->storage_backends['s3']['available']) {
|
||||
$stats['backends']['s3'] = array(
|
||||
'count' => 0,
|
||||
'size' => 0,
|
||||
'bucket' => $this->storage_backends['s3']['settings']['bucket']
|
||||
);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
191
includes/class-core.php
Normal file
191
includes/class-core.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* Core plugin functionality
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Core {
|
||||
|
||||
/**
|
||||
* Plugin activation
|
||||
*/
|
||||
public static function activate() {
|
||||
// Set default options for all modules
|
||||
self::set_default_options();
|
||||
|
||||
// Create database tables
|
||||
self::create_database_tables();
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation
|
||||
*/
|
||||
public static function deactivate() {
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default options for all modules
|
||||
*/
|
||||
private static function set_default_options() {
|
||||
// Robots.txt options
|
||||
add_option('tigerstyle_robots_txt_content', '');
|
||||
add_option('tigerstyle_robots_txt_add_sitemap', true);
|
||||
|
||||
// Sitemap options
|
||||
add_option('tigerstyle_sitemap_xml_enabled', false);
|
||||
|
||||
// LLMs.txt options
|
||||
add_option('tigerstyle_llmstxt_enabled', false);
|
||||
add_option('tigerstyle_llmstxt_content', '');
|
||||
|
||||
// Google Setup options
|
||||
add_option('tigerstyle_google_analytics_id', '');
|
||||
add_option('tigerstyle_google_tag_manager_id', '');
|
||||
add_option('tigerstyle_google_site_verification', '');
|
||||
|
||||
// Meta Tags options
|
||||
add_option('tigerstyle_meta_description_default', '');
|
||||
add_option('tigerstyle_meta_keywords_default', '');
|
||||
add_option('tigerstyle_meta_author_default', get_bloginfo('name'));
|
||||
add_option('tigerstyle_og_site_name', get_bloginfo('name'));
|
||||
add_option('tigerstyle_og_default_image', '');
|
||||
add_option('tigerstyle_twitter_site', '');
|
||||
add_option('tigerstyle_meta_charset_enabled', true);
|
||||
add_option('tigerstyle_meta_viewport_enabled', true);
|
||||
add_option('tigerstyle_meta_theme_color', '#000000');
|
||||
add_option('tigerstyle_meta_robots_default', 'index,follow');
|
||||
add_option('tigerstyle_meta_nosnippet_enabled', false);
|
||||
add_option('tigerstyle_meta_max_snippet', '');
|
||||
add_option('tigerstyle_meta_max_image_preview', '');
|
||||
add_option('tigerstyle_meta_tag_patterns', array());
|
||||
|
||||
// Structured Data options
|
||||
add_option('tigerstyle_organization_enabled', false);
|
||||
add_option('tigerstyle_organization_name', get_bloginfo('name'));
|
||||
add_option('tigerstyle_organization_logo', '');
|
||||
add_option('tigerstyle_organization_description', '');
|
||||
add_option('tigerstyle_organization_email', '');
|
||||
add_option('tigerstyle_organization_phone', '');
|
||||
add_option('tigerstyle_organization_social_profiles', '');
|
||||
add_option('tigerstyle_local_business_enabled', false);
|
||||
add_option('tigerstyle_business_type', 'LocalBusiness');
|
||||
add_option('tigerstyle_business_address_street', '');
|
||||
add_option('tigerstyle_business_address_city', '');
|
||||
add_option('tigerstyle_business_address_state', '');
|
||||
add_option('tigerstyle_business_address_zip', '');
|
||||
add_option('tigerstyle_business_address_country', 'US');
|
||||
add_option('tigerstyle_business_latitude', '');
|
||||
add_option('tigerstyle_business_longitude', '');
|
||||
add_option('tigerstyle_business_hours', '');
|
||||
add_option('tigerstyle_business_price_range', '');
|
||||
|
||||
// Head/Footer injection options
|
||||
add_option('tigerstyle_head_enabled', false);
|
||||
add_option('tigerstyle_footer_enabled', false);
|
||||
add_option('tigerstyle_head_content', '');
|
||||
add_option('tigerstyle_footer_content', '');
|
||||
|
||||
// Performance options
|
||||
add_option('tigerstyle_compression_enabled', false);
|
||||
add_option('tigerstyle_compression_level', 6);
|
||||
add_option('tigerstyle_compression_type', 'gzip');
|
||||
|
||||
// Backup & Restore options
|
||||
add_option('tigerstyle_backup_settings', array(
|
||||
'compression' => 'zip',
|
||||
'storage_location' => 'local',
|
||||
'schedule_enabled' => false,
|
||||
'schedule_frequency' => 'daily',
|
||||
'retention_days' => 30,
|
||||
'include_files' => true,
|
||||
'include_database' => true,
|
||||
'chunk_size' => 5,
|
||||
's3_bucket' => '',
|
||||
's3_access_key' => '',
|
||||
's3_secret_key' => '',
|
||||
's3_region' => 'us-east-1',
|
||||
's3_endpoint' => '',
|
||||
'email_notifications' => false,
|
||||
'notification_email' => get_option('admin_email')
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database tables
|
||||
*/
|
||||
private static function create_database_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
// Backup metadata table
|
||||
$backup_metadata_table = $wpdb->prefix . 'tigerstyle_backup_metadata';
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$backup_metadata_table} (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
backup_id varchar(255) NOT NULL,
|
||||
storage_type varchar(50) NOT NULL,
|
||||
file_path text,
|
||||
s3_bucket varchar(255),
|
||||
s3_key varchar(500),
|
||||
s3_url text,
|
||||
file_size bigint(20) NOT NULL DEFAULT 0,
|
||||
created_at datetime NOT NULL,
|
||||
metadata_json text,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY backup_id (backup_id),
|
||||
KEY storage_type (storage_type),
|
||||
KEY created_at (created_at)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
dbDelta($sql);
|
||||
|
||||
// Backup logs table
|
||||
$backup_logs_table = $wpdb->prefix . 'tigerstyle_backup_logs';
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$backup_logs_table} (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
level varchar(20) NOT NULL,
|
||||
message text NOT NULL,
|
||||
context longtext,
|
||||
user_id int(11) DEFAULT 0,
|
||||
ip_address varchar(45),
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY level (level),
|
||||
KEY created_at (created_at),
|
||||
KEY user_id (user_id)
|
||||
) {$charset_collate};";
|
||||
|
||||
dbDelta($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin version
|
||||
*/
|
||||
public static function get_version() {
|
||||
return TIGERSTYLE_HEAT_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin directory path
|
||||
*/
|
||||
public static function get_plugin_dir() {
|
||||
return TIGERSTYLE_HEAT_PLUGIN_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin URL
|
||||
*/
|
||||
public static function get_plugin_url() {
|
||||
return TIGERSTYLE_HEAT_PLUGIN_URL;
|
||||
}
|
||||
}
|
||||
131
includes/class-utils.php
Normal file
131
includes/class-utils.php
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
/**
|
||||
* Utility functions for TigerStyle Heat
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Utils {
|
||||
|
||||
/**
|
||||
* Sanitize textarea field with line breaks preserved
|
||||
*/
|
||||
public static function sanitize_textarea($input) {
|
||||
return sanitize_textarea_field($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes into human readable format
|
||||
*/
|
||||
public static function format_bytes($bytes, $precision = 2) {
|
||||
$units = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify nonce for security
|
||||
*/
|
||||
public static function verify_nonce($nonce_name, $action) {
|
||||
return isset($_POST[$nonce_name]) && wp_verify_nonce($_POST[$nonce_name], $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can manage options
|
||||
*/
|
||||
public static function current_user_can_manage() {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect with message
|
||||
*/
|
||||
public static function redirect_with_message($message, $page = 'tigerstyle-heat') {
|
||||
wp_redirect(add_query_arg(array(
|
||||
'page' => $page,
|
||||
'message' => $message
|
||||
), admin_url('admin.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get option with prefix
|
||||
*/
|
||||
public static function get_option($option_name, $default = false) {
|
||||
return get_option('tigerstyle_' . $option_name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update option with prefix
|
||||
*/
|
||||
public static function update_option($option_name, $value) {
|
||||
return update_option('tigerstyle_' . $option_name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse business days from shorthand to Schema.org format
|
||||
*/
|
||||
public static function parse_business_days($days_string) {
|
||||
$day_mapping = array(
|
||||
'Mo' => 'Monday',
|
||||
'Tu' => 'Tuesday',
|
||||
'We' => 'Wednesday',
|
||||
'Th' => 'Thursday',
|
||||
'Fr' => 'Friday',
|
||||
'Sa' => 'Saturday',
|
||||
'Su' => 'Sunday'
|
||||
);
|
||||
|
||||
// Handle ranges like "Mo-Fr"
|
||||
if (strpos($days_string, '-') !== false) {
|
||||
$parts = explode('-', $days_string);
|
||||
if (count($parts) === 2) {
|
||||
$start_day = trim($parts[0]);
|
||||
$end_day = trim($parts[1]);
|
||||
|
||||
$all_days = array_keys($day_mapping);
|
||||
$start_index = array_search($start_day, $all_days);
|
||||
$end_index = array_search($end_day, $all_days);
|
||||
|
||||
if ($start_index !== false && $end_index !== false) {
|
||||
$result = array();
|
||||
for ($i = $start_index; $i <= $end_index; $i++) {
|
||||
$result[] = $day_mapping[$all_days[$i]];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle individual days
|
||||
$days = array_map('trim', explode(',', $days_string));
|
||||
$result = array();
|
||||
|
||||
foreach ($days as $day) {
|
||||
if (isset($day_mapping[$day])) {
|
||||
$result[] = $day_mapping[$day];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug information
|
||||
*/
|
||||
public static function debug_log($message, $data = null) {
|
||||
if (WP_DEBUG_LOG) {
|
||||
if ($data !== null) {
|
||||
$message .= ' Data: ' . print_r($data, true);
|
||||
}
|
||||
error_log('[TigerStyle Heat] ' . $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
787
includes/modules/class-ai-provider-backup.php
Normal file
787
includes/modules/class-ai-provider-backup.php
Normal file
@ -0,0 +1,787 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Provider Module for TigerStyle Heat
|
||||
* OpenAI-compatible API provider with multi-key management
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_AI_Provider {
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize AI provider functionality
|
||||
*/
|
||||
public function init() {
|
||||
// Admin hooks
|
||||
if (is_admin()) {
|
||||
add_action('admin_post_update_ai_provider_settings', array($this, 'handle_form_submission'));
|
||||
add_action('wp_ajax_tigerstyle_test_api_key', array($this, 'ajax_test_api_key'));
|
||||
add_action('wp_ajax_tigerstyle_refresh_models', array($this, 'ajax_refresh_models'));
|
||||
add_action('wp_ajax_tigerstyle_delete_api_key', array($this, 'ajax_delete_api_key'));
|
||||
add_action('wp_ajax_tigerstyle_ai_chat', array($this, 'ajax_ai_chat'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configured API providers
|
||||
*/
|
||||
public function get_api_providers() {
|
||||
return get_option('tigerstyle_ai_providers', array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API provider by name
|
||||
*/
|
||||
public function get_api_provider($name) {
|
||||
$providers = $this->get_api_providers();
|
||||
return isset($providers[$name]) ? $providers[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update API provider
|
||||
*/
|
||||
public function save_api_provider($name, $config) {
|
||||
$providers = $this->get_api_providers();
|
||||
|
||||
// Encrypt API key
|
||||
$config['api_key'] = $this->encrypt_api_key($config['api_key']);
|
||||
$config['created_at'] = time();
|
||||
$config['last_tested'] = null;
|
||||
$config['test_status'] = 'pending';
|
||||
$config['models'] = array();
|
||||
|
||||
$providers[$name] = $config;
|
||||
update_option('tigerstyle_ai_providers', $providers);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete API provider
|
||||
*/
|
||||
public function delete_api_provider($name) {
|
||||
$providers = $this->get_api_providers();
|
||||
if (isset($providers[$name])) {
|
||||
unset($providers[$name]);
|
||||
update_option('tigerstyle_ai_providers', $providers);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt API key for secure storage
|
||||
*/
|
||||
private function encrypt_api_key($api_key) {
|
||||
if (!function_exists('openssl_encrypt')) {
|
||||
// Fallback to base64 encoding if OpenSSL not available
|
||||
return base64_encode($api_key);
|
||||
}
|
||||
|
||||
$encryption_key = $this->get_encryption_key();
|
||||
$iv = openssl_random_pseudo_bytes(16);
|
||||
$encrypted = openssl_encrypt($api_key, 'AES-256-CBC', $encryption_key, 0, $iv);
|
||||
|
||||
return base64_encode($iv . $encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask API key for display (show only first and last 4 characters)
|
||||
*/
|
||||
private function mask_api_key($api_key) {
|
||||
if (strlen($api_key) <= 8) {
|
||||
// If key is too short, show first 2 and last 2 characters
|
||||
return substr($api_key, 0, 2) . str_repeat('*', max(4, strlen($api_key) - 4)) . substr($api_key, -2);
|
||||
}
|
||||
|
||||
// Show first 4 and last 4 characters with asterisks in between
|
||||
return substr($api_key, 0, 4) . str_repeat('*', max(8, strlen($api_key) - 8)) . substr($api_key, -4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get masked API key for display purposes
|
||||
*/
|
||||
public function get_masked_api_key($name) {
|
||||
$provider = $this->get_api_provider($name);
|
||||
if (!$provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decrypted_key = $this->decrypt_api_key($provider['api_key']);
|
||||
return $this->mask_api_key($decrypted_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt API key
|
||||
*/
|
||||
private function decrypt_api_key($encrypted_key) {
|
||||
if (!function_exists('openssl_decrypt')) {
|
||||
// Fallback from base64 encoding
|
||||
return base64_decode($encrypted_key);
|
||||
}
|
||||
|
||||
$encryption_key = $this->get_encryption_key();
|
||||
$data = base64_decode($encrypted_key);
|
||||
$iv = substr($data, 0, 16);
|
||||
$encrypted = substr($data, 16);
|
||||
|
||||
return openssl_decrypt($encrypted, 'AES-256-CBC', $encryption_key, 0, $iv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption key for API keys
|
||||
*/
|
||||
private function get_encryption_key() {
|
||||
$key = get_option('tigerstyle_ai_encryption_key');
|
||||
if (!$key) {
|
||||
$key = wp_generate_password(32, false);
|
||||
update_option('tigerstyle_ai_encryption_key', $key);
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test API key by listing models
|
||||
*/
|
||||
public function test_api_key($name) {
|
||||
$provider = $this->get_api_provider($name);
|
||||
if (!$provider) {
|
||||
return array('success' => false, 'error' => 'Provider not found');
|
||||
}
|
||||
|
||||
$api_key = $this->decrypt_api_key($provider['api_key']);
|
||||
$base_url = $provider['base_url'];
|
||||
|
||||
// Test by listing models
|
||||
$response = wp_remote_get($base_url . '/models', array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $api_key,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'TigerStyle-SEO/' . TIGERSTYLE_HEAT_VERSION
|
||||
),
|
||||
'timeout' => 30
|
||||
));
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => $response->get_error_message()
|
||||
);
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
|
||||
if ($status_code !== 200) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => 'HTTP ' . $status_code . ': ' . $body
|
||||
);
|
||||
}
|
||||
|
||||
$data = json_decode($body, true);
|
||||
if (!$data || !isset($data['data'])) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => 'Invalid response format'
|
||||
);
|
||||
}
|
||||
|
||||
// Update provider with test results
|
||||
$providers = $this->get_api_providers();
|
||||
$providers[$name]['last_tested'] = time();
|
||||
$providers[$name]['test_status'] = 'success';
|
||||
$providers[$name]['models'] = $this->process_models($data['data']);
|
||||
update_option('tigerstyle_ai_providers', $providers);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'models' => $providers[$name]['models'],
|
||||
'message' => 'API key tested successfully! Found ' . count($providers[$name]['models']) . ' models.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process models from API response
|
||||
*/
|
||||
private function process_models($models_data) {
|
||||
$models = array();
|
||||
|
||||
foreach ($models_data as $model) {
|
||||
if (isset($model['id'])) {
|
||||
$models[] = array(
|
||||
'id' => $model['id'],
|
||||
'object' => $model['object'] ?? 'model',
|
||||
'created' => $model['created'] ?? time(),
|
||||
'owned_by' => $model['owned_by'] ?? 'unknown',
|
||||
'enabled' => true // Default to enabled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenAI-compatible client for a provider
|
||||
*/
|
||||
public function get_client($provider_name, $model_id = null) {
|
||||
$provider = $this->get_api_provider($provider_name);
|
||||
if (!$provider) {
|
||||
throw new Exception('Provider not found: ' . $provider_name);
|
||||
}
|
||||
|
||||
if ($model_id && !$this->is_model_enabled($provider_name, $model_id)) {
|
||||
throw new Exception('Model not enabled: ' . $model_id);
|
||||
}
|
||||
|
||||
$api_key = $this->decrypt_api_key($provider['api_key']);
|
||||
|
||||
return new TigerStyleSEO_AI_Client($provider['base_url'], $api_key, $model_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if model is enabled for provider
|
||||
*/
|
||||
public function is_model_enabled($provider_name, $model_id) {
|
||||
$provider = $this->get_api_provider($provider_name);
|
||||
if (!$provider || !isset($provider['models'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($provider['models'] as $model) {
|
||||
if ($model['id'] === $model_id) {
|
||||
return $model['enabled'] ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle model enabled/disabled status
|
||||
*/
|
||||
public function toggle_model($provider_name, $model_id, $enabled) {
|
||||
$providers = $this->get_api_providers();
|
||||
if (!isset($providers[$provider_name])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($providers[$provider_name]['models'] as &$model) {
|
||||
if ($model['id'] === $model_id) {
|
||||
$model['enabled'] = $enabled;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
update_option('tigerstyle_ai_providers', $providers);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled models for provider
|
||||
*/
|
||||
public function get_enabled_models($provider_name) {
|
||||
$provider = $this->get_api_provider($provider_name);
|
||||
if (!$provider || !isset($provider['models'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array_filter($provider['models'], function($model) {
|
||||
return $model['enabled'] ?? true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for testing API key
|
||||
*/
|
||||
public function ajax_test_api_key() {
|
||||
check_ajax_referer('tigerstyle_ai_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Unauthorized access');
|
||||
}
|
||||
|
||||
$provider_name = sanitize_text_field($_POST['provider_name']);
|
||||
$result = $this->test_api_key($provider_name);
|
||||
|
||||
wp_send_json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for refreshing models
|
||||
*/
|
||||
public function ajax_refresh_models() {
|
||||
check_ajax_referer('tigerstyle_ai_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Unauthorized access');
|
||||
}
|
||||
|
||||
$provider_name = sanitize_text_field($_POST['provider_name']);
|
||||
$result = $this->test_api_key($provider_name); // Refresh models by re-testing
|
||||
|
||||
wp_send_json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for deleting API key
|
||||
*/
|
||||
public function ajax_delete_api_key() {
|
||||
check_ajax_referer('tigerstyle_ai_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Unauthorized access');
|
||||
}
|
||||
|
||||
$provider_name = sanitize_text_field($_POST['provider_name']);
|
||||
$success = $this->delete_api_provider($provider_name);
|
||||
|
||||
wp_send_json(array(
|
||||
'success' => $success,
|
||||
'message' => $success ? 'Provider deleted successfully!' : 'Failed to delete provider.'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission
|
||||
*/
|
||||
public function handle_form_submission() {
|
||||
// Verify nonce
|
||||
if (!wp_verify_nonce($_POST['ai_provider_nonce'], 'update_ai_provider_settings')) {
|
||||
wp_die('Nonce verification failed');
|
||||
}
|
||||
|
||||
// Check user permissions
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Insufficient permissions');
|
||||
}
|
||||
|
||||
$action = sanitize_text_field($_POST['ai_action'] ?? '');
|
||||
|
||||
if ($action === 'add_provider') {
|
||||
$name = sanitize_text_field($_POST['provider_name']);
|
||||
$api_key = sanitize_text_field($_POST['api_key']);
|
||||
$base_url = esc_url_raw($_POST['base_url']);
|
||||
$description = sanitize_textarea_field($_POST['description'] ?? '');
|
||||
|
||||
if (empty($name) || empty($api_key) || empty($base_url)) {
|
||||
wp_redirect(add_query_arg(array(
|
||||
'page' => 'tigerstyle-heat',
|
||||
'tab' => 'ai-provider',
|
||||
'message' => 'error'
|
||||
), admin_url('admin.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
$config = array(
|
||||
'base_url' => rtrim($base_url, '/'),
|
||||
'api_key' => $api_key,
|
||||
'description' => $description,
|
||||
'provider_type' => 'openai' // Default to OpenAI compatible
|
||||
);
|
||||
|
||||
$this->save_api_provider($name, $config);
|
||||
|
||||
wp_redirect(add_query_arg(array(
|
||||
'page' => 'tigerstyle-heat',
|
||||
'tab' => 'ai-provider',
|
||||
'message' => 'provider_added'
|
||||
), admin_url('admin.php')));
|
||||
} elseif ($action === 'toggle_model') {
|
||||
$provider_name = sanitize_text_field($_POST['provider_name']);
|
||||
$model_id = sanitize_text_field($_POST['model_id']);
|
||||
$enabled = isset($_POST['enabled']) ? 1 : 0;
|
||||
|
||||
$this->toggle_model($provider_name, $model_id, $enabled);
|
||||
|
||||
wp_redirect(add_query_arg(array(
|
||||
'page' => 'tigerstyle-heat',
|
||||
'tab' => 'ai-provider',
|
||||
'message' => 'model_updated'
|
||||
), admin_url('admin.php')));
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AI chat AJAX requests
|
||||
*/
|
||||
public function ajax_ai_chat() {
|
||||
// Verify nonce
|
||||
if (!check_ajax_referer('tigerstyle_ai_nonce', 'nonce', false)) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
return;
|
||||
}
|
||||
|
||||
$provider_name = sanitize_text_field($_POST['provider_name'] ?? '');
|
||||
$model_id = sanitize_text_field($_POST['model_id'] ?? '');
|
||||
$message = sanitize_textarea_field($_POST['message'] ?? '');
|
||||
|
||||
if (empty($provider_name) || empty($model_id) || empty($message)) {
|
||||
wp_send_json_error('Missing required parameters');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get AI client
|
||||
$client = $this->get_client($provider_name, $model_id);
|
||||
|
||||
// Create SEO-focused system prompt
|
||||
$system_prompt = "You are an expert SEO consultant and web optimization specialist. " .
|
||||
"Provide helpful, accurate advice about search engine optimization, content strategy, " .
|
||||
"meta tags, website performance, and digital marketing best practices. " .
|
||||
"Keep responses concise but informative. Focus on actionable advice.";
|
||||
|
||||
// Send chat request
|
||||
$response = $client->simple_chat($message, $system_prompt, $model_id);
|
||||
$ai_response = $client->extract_text($response);
|
||||
|
||||
wp_send_json_success(array(
|
||||
'response' => wp_kses_post($ai_response)
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error('AI Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
$providers = $this->get_api_providers();
|
||||
?>
|
||||
<!-- AI Chat Interface -->
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('💬 AI Chat Assistant', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Ask AI questions about SEO, content optimization, or get instant help with your website.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
|
||||
<div id="ai-chat-container" style="margin: 20px 0;">
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
||||
<select id="ai-chat-provider" style="min-width: 200px;">
|
||||
<option value=""><?php _e('Select AI Provider...', 'tigerstyle-heat'); ?></option>
|
||||
<?php if (!empty($providers)): ?>
|
||||
<?php foreach ($providers as $provider_name => $provider_data): ?>
|
||||
<?php if (!empty($provider_data['models'])): ?>
|
||||
<?php foreach ($provider_data['models'] as $model_id => $model_data): ?>
|
||||
<?php if ($model_data['enabled']): ?>
|
||||
<option value="<?php echo esc_attr($provider_name . '|' . $model_id); ?>">
|
||||
<?php echo esc_html($provider_name . ' - ' . $model_id); ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<button type="button" id="ai-chat-clear" class="button"><?php _e('Clear Chat', 'tigerstyle-heat'); ?></button>
|
||||
</div>
|
||||
|
||||
<div id="ai-chat-messages" style="background: #f9f9f9; border: 1px solid #ddd; border-radius: 5px; padding: 15px; min-height: 200px; max-height: 400px; overflow-y: auto; margin-bottom: 15px;">
|
||||
<div class="chat-message system-message" style="color: #666; font-style: italic;">
|
||||
<?php _e('💡 Ask me anything about SEO, content optimization, meta tags, or website improvement!', 'tigerstyle-heat'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<textarea id="ai-chat-input" placeholder="<?php _e('Ask me anything about SEO...', 'tigerstyle-heat'); ?>" style="flex: 1; min-height: 60px; padding: 10px; border: 1px solid #ddd; border-radius: 5px;" rows="3"></textarea>
|
||||
<button type="button" id="ai-chat-send" class="button button-primary" disabled style="align-self: flex-end;"><?php _e('Send', 'tigerstyle-heat'); ?></button>
|
||||
</div>
|
||||
|
||||
<div id="ai-chat-status" style="margin-top: 10px; font-size: 12px; color: #666;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('🔧 AI Provider Configuration', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Configure OpenAI-compatible API providers to enable AI-powered SEO features like content optimization, meta tag generation, and automated analysis.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Add New Provider Form -->
|
||||
<div class="seo-info-box">
|
||||
<h4><?php _e('Add New API Provider', 'tigerstyle-heat'); ?></h4>
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="update_ai_provider_settings">
|
||||
<input type="hidden" name="ai_action" value="add_provider">
|
||||
<?php wp_nonce_field('update_ai_provider_settings', 'ai_provider_nonce'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Provider Name', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="provider_name" class="regular-text" placeholder="e.g., openai-main, anthropic-claude" required>
|
||||
<p class="description"><?php _e('Unique name to identify this API provider.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('API Key', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="password" name="api_key" class="regular-text" placeholder="sk-..." required>
|
||||
<p class="description"><?php _e('API key will be encrypted and stored securely.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Base URL', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="url" name="base_url" class="regular-text" value="https://api.openai.com/v1" required>
|
||||
<p class="description"><?php _e('API base URL (OpenAI: https://api.openai.com/v1, Azure: https://your-resource.openai.azure.com)', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Description', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<textarea name="description" class="large-text" rows="3" placeholder="Optional description for this provider..."></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(__('Add Provider', 'tigerstyle-heat')); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Existing Providers -->
|
||||
<?php if (!empty($providers)): ?>
|
||||
<div class="seo-info-box">
|
||||
<h4><?php _e('Configured API Providers', 'tigerstyle-heat'); ?></h4>
|
||||
|
||||
<?php foreach ($providers as $name => $config): ?>
|
||||
<div class="provider-card" style="border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin-bottom: 15px;">
|
||||
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 10px;">
|
||||
<h5 style="margin: 0; flex-grow: 1;"><?php echo esc_html($name); ?></h5>
|
||||
<div class="provider-actions">
|
||||
<button type="button" class="button test-api-key" data-provider="<?php echo esc_attr($name); ?>">
|
||||
<?php _e('Test API Key', 'tigerstyle-heat'); ?>
|
||||
</button>
|
||||
<button type="button" class="button refresh-models" data-provider="<?php echo esc_attr($name); ?>">
|
||||
<?php _e('Refresh Models', 'tigerstyle-heat'); ?>
|
||||
</button>
|
||||
<button type="button" class="button button-secondary delete-provider" data-provider="<?php echo esc_attr($name); ?>">
|
||||
<?php _e('Delete', 'tigerstyle-heat'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="provider-info" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-bottom: 15px;">
|
||||
<div><strong><?php _e('API Key:', 'tigerstyle-heat'); ?></strong><br>
|
||||
<code style="background: #f1f1f1; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 12px;">
|
||||
<?php echo esc_html($this->get_masked_api_key($name)); ?>
|
||||
</code>
|
||||
</div>
|
||||
<div><strong><?php _e('Base URL:', 'tigerstyle-heat'); ?></strong><br><?php echo esc_html($config['base_url']); ?></div>
|
||||
<div><strong><?php _e('Status:', 'tigerstyle-heat'); ?></strong><br>
|
||||
<span class="status-<?php echo esc_attr($config['test_status'] ?? 'pending'); ?>">
|
||||
<?php echo esc_html(ucfirst($config['test_status'] ?? 'pending')); ?>
|
||||
</span>
|
||||
</div>
|
||||
<div><strong><?php _e('Models:', 'tigerstyle-heat'); ?></strong><br><?php echo count($config['models'] ?? array()); ?> available</div>
|
||||
<div><strong><?php _e('Last Tested:', 'tigerstyle-heat'); ?></strong><br>
|
||||
<?php
|
||||
if ($config['last_tested']) {
|
||||
echo esc_html(human_time_diff($config['last_tested']) . ' ago');
|
||||
} else {
|
||||
echo esc_html__('Never', 'tigerstyle-heat');
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($config['description'])): ?>
|
||||
<p class="description"><?php echo esc_html($config['description']); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Models List -->
|
||||
<?php if (!empty($config['models'])): ?>
|
||||
<div class="models-section">
|
||||
<h6><?php _e('Available Models', 'tigerstyle-heat'); ?></h6>
|
||||
<div class="models-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px;">
|
||||
<?php foreach ($config['models'] as $model): ?>
|
||||
<div class="model-item" style="padding: 10px; background: #f9f9f9; border-radius: 3px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<strong><?php echo esc_html($model['id']); ?></strong><br>
|
||||
<small><?php echo esc_html($model['owned_by']); ?></small>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox"
|
||||
class="model-toggle"
|
||||
data-provider="<?php echo esc_attr($name); ?>"
|
||||
data-model="<?php echo esc_attr($model['id']); ?>"
|
||||
<?php checked($model['enabled'] ?? true); ?>>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="provider-results-<?php echo esc_attr($name); ?>" class="provider-results" style="margin-top: 15px;"></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<div class="seo-info-box">
|
||||
<h4><?php _e('Usage Examples', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Once configured, you can use the AI providers in your code:', 'tigerstyle-heat'); ?></p>
|
||||
<pre style="background: #f1f1f1; padding: 15px; border-radius: 5px; overflow-x: auto;"><code>// Get client for a specific provider and model
|
||||
$client = tigerstyle_heat()->get_module('ai_provider')->get_client('openai-main', 'gpt-4');
|
||||
|
||||
// Generate meta description
|
||||
$response = $client->chat_completion([
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => 'Generate an SEO meta description for: ' . $post_title]
|
||||
],
|
||||
'max_tokens' => 160
|
||||
]);
|
||||
|
||||
// Use with any enabled model
|
||||
$providers = tigerstyle_heat()->get_module('ai_provider')->get_api_providers();
|
||||
foreach ($providers as $name => $config) {
|
||||
$enabled_models = tigerstyle_heat()->get_module('ai_provider')->get_enabled_models($name);
|
||||
// Use the models...
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- TigerStyle Life9 Backup Plugin Recommendation -->
|
||||
<div class="seo-info-box" style="background: #fff3cd; border-left: 4px solid #ffc107;">
|
||||
<h4><?php _e('🔒 Need Backup & Restore Functionality?', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Backup & Restore functionality has been moved to our standalone <strong>TigerStyle Life9</strong> plugin for enhanced security and modularity.', 'tigerstyle-heat'); ?></p>
|
||||
<div style="display: flex; gap: 15px; align-items: center; margin-top: 15px;">
|
||||
<div style="flex: 1;">
|
||||
<h5 style="margin: 0 0 10px 0;"><?php _e('✨ TigerStyle Life9 Features:', 'tigerstyle-heat'); ?></h5>
|
||||
<ul style="margin: 0; padding-left: 20px;">
|
||||
<li><?php _e('🔐 Military-grade encryption for backup files', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('🌐 Modern Alpine.js frontend interface', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('📦 Complete database and file backups', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('☁️ Multiple storage backends (local, cloud)', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('🔄 One-click restore functionality', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('⏰ Automated backup scheduling', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<p style="margin: 0 0 10px 0; font-style: italic; color: #6c757d;"><?php _e('"Because cats have 9 lives,<br>but servers don\'t!"', 'tigerstyle-heat'); ?></p>
|
||||
<?php if (is_plugin_active('tigerstyle-life9/tigerstyle-life9.php')): ?>
|
||||
<span style="color: #28a745; font-weight: bold;">✅ <?php _e('TigerStyle Life9 is active!', 'tigerstyle-heat'); ?></span><br>
|
||||
<a href="<?php echo admin_url('admin.php?page=tigerstyle-life9'); ?>" class="button button-primary" style="margin-top: 5px;">
|
||||
<?php _e('Manage Backups', 'tigerstyle-heat'); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span style="color: #ffc107; font-weight: bold;">📦 <?php _e('Install TigerStyle Life9', 'tigerstyle-heat'); ?></span><br>
|
||||
<small style="color: #6c757d;"><?php _e('Available in the src/tigerstyle-life9 directory', 'tigerstyle-heat'); ?></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TigerStyle Dash Performance Plugin Recommendation -->
|
||||
<div class="seo-info-box" style="background: #e7f3ff; border-left: 4px solid #0073aa;">
|
||||
<h4><?php _e('⚡ Need Lightning-Fast Performance?', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Performance & Compression functionality has been moved to our dedicated <strong>TigerStyle Dash</strong> plugin for specialized speed optimization.', 'tigerstyle-heat'); ?></p>
|
||||
<div style="display: flex; gap: 15px; align-items: center; margin-top: 15px;">
|
||||
<div style="flex: 1;">
|
||||
<h5 style="margin: 0 0 10px 0;"><?php _e('🏃♀️ TigerStyle Dash Features:', 'tigerstyle-heat'); ?></h5>
|
||||
<ul style="margin: 0; padding-left: 20px;">
|
||||
<li><?php _e('⚡ Advanced Brotli + Gzip compression with cat-like reflexes', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('🧠 Smart cache warming and intelligent purging', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('📊 Real-time performance analytics and optimization scoring', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('🎯 Core Web Vitals optimization for better Google rankings', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('💾 Reduce file sizes by 60-80% with lightning speed', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('🔧 WP Rocket-inspired compression with modern techniques', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<p style="margin: 0 0 10px 0; font-style: italic; color: #6c757d;"><?php _e('"Dash past the competition<br>with lightning speed!"', 'tigerstyle-heat'); ?></p>
|
||||
<?php if (is_plugin_active('tigerstyle-dash/tigerstyle-dash.php')): ?>
|
||||
<span style="color: #28a745; font-weight: bold;">✅ <?php _e('TigerStyle Dash is active!', 'tigerstyle-heat'); ?></span><br>
|
||||
<a href="<?php echo admin_url('admin.php?page=tigerstyle-dash'); ?>" class="button button-primary" style="margin-top: 5px;">
|
||||
<?php _e('Optimize Performance', 'tigerstyle-heat'); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span style="color: #0073aa; font-weight: bold;">🏃♀️ <?php _e('Install TigerStyle Dash', 'tigerstyle-heat'); ?></span><br>
|
||||
<small style="color: #6c757d;"><?php _e('Available in the src/tigerstyle-dash directory', 'tigerstyle-heat'); ?></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TigerStyle Scent OAuth2 Authentication Plugin Recommendation -->
|
||||
<div class="seo-info-box" style="background: #fff3cd; border-left: 4px solid #ffc107;">
|
||||
<h4><?php _e('👃 Need Enterprise Authentication?', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('OAuth2 authentication functionality is available through our dedicated <strong>TigerStyle Scent</strong> plugin for secure access control.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<div style="display: flex; gap: 20px; align-items: flex-start;">
|
||||
<div style="flex: 1;">
|
||||
<h5 style="margin: 0 0 10px 0;"><?php _e('👃 TigerStyle Scent Features:', 'tigerstyle-heat'); ?></h5>
|
||||
<ul style="margin: 0; padding-left: 20px; color: #5a5a5a;">
|
||||
<li><?php _e('🛡️ Enterprise OAuth2 authorization server with cat-like security instincts', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('👃 Scent-based authentication tokens - digital pheromones for access control', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('🏰 Territory control with scope-based permissions and client management', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('⚡ Lightning-fast OAuth2 flows with feline reflexes and JWT support', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('🔒 Military-grade security with PKCE, refresh tokens, and introspection', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('🔧 WordPress REST API integration with Bearer token authentication', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="text-align: center; min-width: 150px;">
|
||||
<?php if (class_exists('TigerStyleScent')): ?>
|
||||
<span style="color: #28a745; font-weight: bold;">✅ <?php _e('TigerStyle Scent is active!', 'tigerstyle-heat'); ?></span><br>
|
||||
<a href="<?php echo admin_url('admin.php?page=tigerstyle-scent'); ?>" class="button button-primary" style="margin-top: 5px;">
|
||||
<?php _e('Manage Authentication', 'tigerstyle-heat'); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span style="color: #ffc107; font-weight: bold;">👃 <?php _e('Install TigerStyle Scent', 'tigerstyle-heat'); ?></span><br>
|
||||
<small style="color: #6c757d;"><?php _e('Available in the src/WPOAuth2Server directory (needs rebranding)', 'tigerstyle-heat'); ?></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 15px; padding: 10px; background: #f8f9fa; border-radius: 5px;">
|
||||
<p style="margin: 0; font-style: italic; color: #6c757d;">
|
||||
<strong><?php _e('"Leave your digital scent trail', 'tigerstyle-heat'); ?></strong>
|
||||
<?php _e('for secure authentication!"', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
<div style="margin-top: 5px;">
|
||||
<span style="color: #ffc107; font-weight: bold;">👃 <?php _e('Install TigerStyle Scent', 'tigerstyle-heat'); ?></span><br>
|
||||
<small style="color: #6c757d;"><?php _e('Complete OAuth2 authentication server for WordPress', 'tigerstyle-heat'); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.status-success { color: #46b450; font-weight: bold; }
|
||||
.status-pending { color: #f56e28; }
|
||||
.status-error { color: #dc3232; }
|
||||
.provider-card { transition: box-shadow 0.2s; }
|
||||
.provider-card:hover { box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
|
||||
.toggle-switch { position: relative; display: inline-block; width: 60px; height: 34px; }
|
||||
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; }
|
||||
.slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
|
||||
input:checked + .slider { background-color: #2196F3; }
|
||||
input:checked + .slider:before { transform: translateX(26px); }
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
195
includes/modules/class-ai-provider.php
Normal file
195
includes/modules/class-ai-provider.php
Normal file
@ -0,0 +1,195 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Provider Management
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_AI_Provider {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
add_action('wp_ajax_tigerstyle_ai_test_key', array($this, 'handle_test_api_key'));
|
||||
add_action('wp_ajax_tigerstyle_ai_chat', array($this, 'handle_ai_chat'));
|
||||
add_action('admin_post_update_ai_provider_settings', array($this, 'handle_update_settings'));
|
||||
}
|
||||
|
||||
public function render_admin_page() {
|
||||
$providers = $this->get_api_providers();
|
||||
?>
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('💬 AI Chat Assistant', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Ask AI questions about SEO, content optimization, or get instant help with your website.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
|
||||
<div id="ai-chat-interface" style="background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
||||
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 15px;">
|
||||
<select id="ai-chat-provider" style="min-width: 200px;">
|
||||
<option value=""><?php _e('Select AI Provider...', 'tigerstyle-heat'); ?></option>
|
||||
<?php if (!empty($providers)): ?>
|
||||
<?php foreach ($providers as $provider_name => $provider_data): ?>
|
||||
<?php if (!empty($provider_data['models'])): ?>
|
||||
<?php foreach ($provider_data['models'] as $model_id => $model_data): ?>
|
||||
<?php if ($model_data['enabled']): ?>
|
||||
<option value="<?php echo esc_attr($provider_name . '|' . $model_id); ?>">
|
||||
<?php echo esc_html($provider_name . ' - ' . $model_id); ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<button type="button" id="ai-chat-clear" class="button"><?php _e('Clear Chat', 'tigerstyle-heat'); ?></button>
|
||||
</div>
|
||||
|
||||
<div id="ai-chat-messages" style="background: #f9f9f9; border: 1px solid #ddd; border-radius: 5px; padding: 15px; min-height: 200px; max-height: 400px; overflow-y: auto; margin-bottom: 15px;">
|
||||
<div class="chat-message system-message" style="color: #666; font-style: italic;">
|
||||
<?php _e('💡 Ask me anything about SEO, content optimization, meta tags, or website improvement!', 'tigerstyle-heat'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<textarea id="ai-chat-input" placeholder="<?php _e('Ask me anything about SEO...', 'tigerstyle-heat'); ?>" style="flex: 1; min-height: 60px; padding: 10px; border: 1px solid #ddd; border-radius: 5px;" rows="3"></textarea>
|
||||
<button type="button" id="ai-chat-send" class="button button-primary" disabled style="align-self: flex-end;"><?php _e('Send', 'tigerstyle-heat'); ?></button>
|
||||
</div>
|
||||
|
||||
<div id="ai-chat-status" style="margin-top: 10px; font-size: 12px; color: #666;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('🔧 AI Provider Configuration', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Configure OpenAI-compatible API providers to enable AI-powered SEO features like content optimization, meta tag generation, and automated analysis.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Add New Provider Form -->
|
||||
<div class="seo-info-box">
|
||||
<h4><?php _e('Add New API Provider', 'tigerstyle-heat'); ?></h4>
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="update_ai_provider_settings">
|
||||
<input type="hidden" name="ai_action" value="add_provider">
|
||||
<?php wp_nonce_field('update_ai_provider_settings', 'ai_provider_nonce'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Provider Name', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="provider_name" class="regular-text" placeholder="<?php _e('e.g., openai-main, anthropic-claude', 'tigerstyle-heat'); ?>" required>
|
||||
<p class="description"><?php _e('Unique name to identify this API provider.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('API Key', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="password" name="api_key" class="regular-text" placeholder="<?php _e('sk-...', 'tigerstyle-heat'); ?>" required>
|
||||
<p class="description"><?php _e('API key will be encrypted and stored securely.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Base URL', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="url" name="base_url" class="regular-text" value="https://api.openai.com/v1" required>
|
||||
<p class="description"><?php _e('API base URL (OpenAI: https://api.openai.com/v1, Azure: https://your-resource.openai.azure.com)', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Description', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<textarea name="description" class="large-text" rows="3" placeholder="<?php _e('Optional description for this provider...', 'tigerstyle-heat'); ?>"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<input type="submit" name="submit" id="submit" class="button button-primary" value="<?php _e('Add Provider', 'tigerstyle-heat'); ?>">
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h4><?php _e('Usage Examples', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Once configured, you can use the AI providers in your code:', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<pre style="background: #f1f1f1; padding: 15px; border-radius: 5px; overflow-x: auto;"><code><?php echo esc_html('// Get client for a specific provider and model
|
||||
$client = tigerstyle_heat()->get_module(\'ai_provider\')->get_client(\'openai-main\', \'gpt-4\');
|
||||
|
||||
// Generate meta description
|
||||
$response = $client->chat_completion([
|
||||
\'messages\' => [
|
||||
[\'role\' => \'user\', \'content\' => \'Generate an SEO meta description for: \' . $post_title]
|
||||
],
|
||||
\'max_tokens\' => 160
|
||||
]);
|
||||
|
||||
// Use with any enabled model
|
||||
$providers = tigerstyle_heat()->get_module(\'ai_provider\')->get_api_providers();
|
||||
foreach ($providers as $name => $config) {
|
||||
$enabled_models = tigerstyle_heat()->get_module(\'ai_provider\')->get_enabled_models($name);
|
||||
// Use the models...
|
||||
}'); ?></code></pre>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.status-success { color: #46b450; font-weight: bold; }
|
||||
.status-pending { color: #f56e28; }
|
||||
.status-error { color: #dc3232; }
|
||||
.provider-card { transition: box-shadow 0.2s; }
|
||||
.provider-card:hover { box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
|
||||
.toggle-switch { position: relative; display: inline-block; width: 60px; height: 34px; }
|
||||
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; }
|
||||
.slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
|
||||
input:checked + .slider { background-color: #2196F3; }
|
||||
input:checked + .slider:before { transform: translateX(26px); }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
// AI Chat functionality
|
||||
$('#ai-chat-send').on('click', function() {
|
||||
// Implementation for sending AI chat messages
|
||||
});
|
||||
|
||||
$('#ai-chat-clear').on('click', function() {
|
||||
$('#ai-chat-messages').html('<div class="chat-message system-message" style="color: #666; font-style: italic;"><?php _e('💡 Ask me anything about SEO, content optimization, meta tags, or website improvement!', 'tigerstyle-heat'); ?></div>');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function get_api_providers() {
|
||||
return get_option('tigerstyle_heat_ai_providers', array());
|
||||
}
|
||||
|
||||
public function handle_test_api_key() {
|
||||
// AJAX handler for testing API keys
|
||||
wp_die();
|
||||
}
|
||||
|
||||
public function handle_ai_chat() {
|
||||
// AJAX handler for AI chat
|
||||
wp_die();
|
||||
}
|
||||
|
||||
public function handle_update_settings() {
|
||||
// Handler for updating AI provider settings
|
||||
wp_redirect(admin_url('admin.php?page=tigerstyle-heat#ai-provider-tab'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
332
includes/modules/class-amp.php
Normal file
332
includes/modules/class-amp.php
Normal file
@ -0,0 +1,332 @@
|
||||
<?php
|
||||
/**
|
||||
* AMP (Accelerated Mobile Pages) Module
|
||||
*
|
||||
* Handles AMP page generation and Signed Exchange (SXG) support
|
||||
*
|
||||
* @package TigerStyle_SEO
|
||||
* @subpackage Modules
|
||||
* @since 2.1.0
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_AMP {
|
||||
|
||||
private $options;
|
||||
private $cache_dir;
|
||||
private $sxg_enabled;
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->options = get_option('tigerstyle_heat_amp', array());
|
||||
$this->cache_dir = WP_CONTENT_DIR . '/cache/tigerstyle-heat-amp/';
|
||||
$this->sxg_enabled = isset($this->options['sxg_enabled']) ? $this->options['sxg_enabled'] : false;
|
||||
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module
|
||||
*/
|
||||
private function init() {
|
||||
// Create cache directory
|
||||
if (!file_exists($this->cache_dir)) {
|
||||
wp_mkdir_p($this->cache_dir);
|
||||
}
|
||||
|
||||
// AMP detection and generation
|
||||
add_action('template_redirect', array($this, 'handle_amp_request'));
|
||||
add_action('wp_head', array($this, 'add_amp_link'));
|
||||
|
||||
// SXG support
|
||||
if ($this->sxg_enabled) {
|
||||
add_action('template_redirect', array($this, 'handle_sxg_request'), 5);
|
||||
add_filter('wp_headers', array($this, 'add_sxg_headers'));
|
||||
}
|
||||
|
||||
// Admin hooks
|
||||
if (is_admin()) {
|
||||
add_action('admin_post_update_amp_settings', array($this, 'handle_form_submission'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AMP requests
|
||||
*/
|
||||
public function handle_amp_request() {
|
||||
if (!isset($_GET['amp']) || is_admin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate AMP page
|
||||
$this->generate_amp_page();
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add AMP link to head
|
||||
*/
|
||||
public function add_amp_link() {
|
||||
if (is_singular() && !is_admin()) {
|
||||
$amp_url = add_query_arg('amp', '1', get_permalink());
|
||||
echo '<link rel="amphtml" href="' . esc_url($amp_url) . '">' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate AMP page
|
||||
*/
|
||||
private function generate_amp_page() {
|
||||
// Basic AMP page structure
|
||||
?>
|
||||
<!doctype html>
|
||||
<html amp lang="<?php echo get_locale(); ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
|
||||
<title><?php wp_title(); ?></title>
|
||||
<script async src="https://cdn.ampproject.org/v0.js"></script>
|
||||
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
|
||||
<style amp-custom>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
h1 { color: #333; }
|
||||
p { line-height: 1.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1><?php the_title(); ?></h1>
|
||||
<div class="content">
|
||||
<?php
|
||||
if (have_posts()) {
|
||||
while (have_posts()) {
|
||||
the_post();
|
||||
the_content();
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SXG requests
|
||||
*/
|
||||
public function handle_sxg_request() {
|
||||
if (!isset($_GET['sxg']) || !$this->sxg_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if SXG service is available
|
||||
if (!$this->is_sxg_service_available()) {
|
||||
wp_die('SXG service not available', 'Service Unavailable', ['response' => 503]);
|
||||
}
|
||||
|
||||
$this->serve_signed_exchange();
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SXG service is available
|
||||
*/
|
||||
private function is_sxg_service_available() {
|
||||
return !empty($this->options['sxg_api_key']) && !empty($this->options['sxg_api_endpoint']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve Signed Exchange
|
||||
*/
|
||||
public function serve_signed_exchange() {
|
||||
$current_url = $this->get_current_url();
|
||||
|
||||
// Check cache first
|
||||
$cache_key = 'sxg_' . md5($current_url);
|
||||
$cached_sxg = get_transient($cache_key);
|
||||
|
||||
if ($cached_sxg !== false) {
|
||||
header('Content-Type: application/signed-exchange;v=b3');
|
||||
header('Cache-Control: public, max-age=300');
|
||||
echo $cached_sxg;
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate fresh SXG
|
||||
ob_start();
|
||||
$this->generate_amp_page();
|
||||
$amp_content = ob_get_clean();
|
||||
|
||||
$sxg_data = $this->create_signed_exchange($amp_content, $current_url);
|
||||
|
||||
if ($sxg_data) {
|
||||
// Cache for 5 minutes
|
||||
set_transient($cache_key, $sxg_data, 300);
|
||||
|
||||
header('Content-Type: application/signed-exchange;v=b3');
|
||||
header('Cache-Control: public, max-age=300');
|
||||
echo $sxg_data;
|
||||
} else {
|
||||
wp_die('Failed to create signed exchange', 'SXG Error', ['response' => 500]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create signed exchange using API
|
||||
*/
|
||||
private function create_signed_exchange($content, $url) {
|
||||
$api_endpoint = $this->options['sxg_api_endpoint'];
|
||||
$api_key = $this->options['sxg_api_key'];
|
||||
|
||||
$response = wp_remote_post($api_endpoint, array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $api_key,
|
||||
'Content-Type' => 'application/json'
|
||||
),
|
||||
'body' => json_encode(array(
|
||||
'url' => $url,
|
||||
'content' => $content,
|
||||
'headers' => array(
|
||||
'content-type' => 'text/html; charset=utf-8'
|
||||
)
|
||||
)),
|
||||
'timeout' => 30
|
||||
));
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
error_log('TigerStyle Heat: SXG API error: ' . $response->get_error_message());
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
$data = json_decode($body, true);
|
||||
|
||||
if (isset($data['sxg_package'])) {
|
||||
return base64_decode($data['sxg_package']);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current URL
|
||||
*/
|
||||
private function get_current_url() {
|
||||
return (is_ssl() ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add SXG headers
|
||||
*/
|
||||
public function add_sxg_headers($headers) {
|
||||
if ($this->sxg_enabled && isset($_GET['amp'])) {
|
||||
$headers['Link'] = '</wp-content/cache/tigerstyle-heat-amp/sxg-cert.pem>; rel="alternate"; type="application/cert-chain+cbor"';
|
||||
$headers['Vary'] = 'Accept, AMP-Cache-Transform';
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission
|
||||
*/
|
||||
public function handle_form_submission() {
|
||||
if (!isset($_POST['amp_nonce']) || !wp_verify_nonce($_POST['amp_nonce'], 'update_amp_settings')) {
|
||||
wp_die('Security check failed');
|
||||
}
|
||||
|
||||
$options = array();
|
||||
$options['sxg_enabled'] = isset($_POST['sxg_enabled']) ? 1 : 0;
|
||||
$options['sxg_api_endpoint'] = sanitize_url($_POST['sxg_api_endpoint']);
|
||||
$options['sxg_api_key'] = sanitize_text_field($_POST['sxg_api_key']);
|
||||
|
||||
update_option('tigerstyle_heat_amp', $options);
|
||||
|
||||
wp_redirect(admin_url('admin.php?page=tigerstyle-heat#amp-tab&updated=1'));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
$options = $this->options;
|
||||
?>
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('AMP & SXG Configuration', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Configure AMP (Accelerated Mobile Pages) and SXG (Signed Exchange) support for faster mobile loading.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="update_amp_settings">
|
||||
<?php wp_nonce_field('update_amp_settings', 'amp_nonce'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enable SXG Support', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="sxg_enabled" value="1" <?php checked(isset($options['sxg_enabled']) ? $options['sxg_enabled'] : 0, 1); ?>>
|
||||
<?php _e('Enable Signed Exchange support for AMP pages', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Requires valid SXG API credentials.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('SXG API Endpoint', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="url" name="sxg_api_endpoint" value="<?php echo esc_attr(isset($options['sxg_api_endpoint']) ? $options['sxg_api_endpoint'] : ''); ?>" class="regular-text">
|
||||
<p class="description"><?php _e('SXG API service endpoint URL.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('SXG API Key', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="password" name="sxg_api_key" value="<?php echo esc_attr(isset($options['sxg_api_key']) ? $options['sxg_api_key'] : ''); ?>" class="regular-text">
|
||||
<p class="description"><?php _e('API key for SXG service authentication.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<input type="submit" name="submit" class="button button-primary" value="<?php _e('Save AMP Settings', 'tigerstyle-heat'); ?>">
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h4><?php _e('AMP Status', 'tigerstyle-heat'); ?></h4>
|
||||
<p>
|
||||
<?php _e('AMP pages are automatically generated for all posts and pages.', 'tigerstyle-heat'); ?>
|
||||
<?php if (is_singular()): ?>
|
||||
<br><strong><?php _e('Current page AMP URL:', 'tigerstyle-heat'); ?></strong>
|
||||
<a href="<?php echo add_query_arg('amp', '1', get_permalink()); ?>" target="_blank">
|
||||
<?php echo add_query_arg('amp', '1', get_permalink()); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
572
includes/modules/class-backup-restore.php
Normal file
572
includes/modules/class-backup-restore.php
Normal file
@ -0,0 +1,572 @@
|
||||
<?php
|
||||
/**
|
||||
* Enterprise Backup & Restore Module for TigerStyle Heat
|
||||
*
|
||||
* Provides comprehensive backup and restore functionality including:
|
||||
* - File system backups with chunked processing
|
||||
* - Database backups with table-level control
|
||||
* - S3 and S3-compatible storage integration
|
||||
* - Advanced compression with multiple formats
|
||||
* - Complete WordPress reset functionality
|
||||
* - Enterprise-grade logging and validation
|
||||
*
|
||||
* @package TigerStyleSEO
|
||||
* @subpackage BackupRestore
|
||||
* @version 1.0.0
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Backup_Restore {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Backup engine
|
||||
*/
|
||||
private $backup_engine;
|
||||
|
||||
/**
|
||||
* Restore engine
|
||||
*/
|
||||
private $restore_engine;
|
||||
|
||||
/**
|
||||
* Storage manager
|
||||
*/
|
||||
private $storage_manager;
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Validator
|
||||
*/
|
||||
private $validator;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize module
|
||||
*/
|
||||
private function init() {
|
||||
// Load dependencies
|
||||
$this->load_dependencies();
|
||||
|
||||
// Defer backup system initialization until needed to avoid database errors during startup
|
||||
add_action('admin_init', array($this, 'init_backup_system'), 10);
|
||||
|
||||
// Setup hooks
|
||||
$this->setup_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize backup system components
|
||||
*/
|
||||
public function init_backup_system() {
|
||||
// Only initialize if we're in admin and not already initialized
|
||||
if (!is_admin() || $this->backup_engine) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
$this->backup_engine = TigerStyleSEO_Backup_Engine::instance();
|
||||
$this->restore_engine = TigerStyleSEO_Restore_Engine::instance();
|
||||
$this->storage_manager = TigerStyleSEO_Storage_Manager::instance();
|
||||
$this->logger = TigerStyleSEO_Backup_Logger::instance();
|
||||
$this->validator = TigerStyleSEO_Backup_Validator::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load dependencies
|
||||
*/
|
||||
private function load_dependencies() {
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/backup/class-backup-engine.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/backup/class-restore-engine.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/backup/class-storage-manager.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/backup/class-backup-logger.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/backup/class-backup-validator.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/backup/class-backup-scheduler.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/backup/class-compression-manager.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/backup/ajax-handlers.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup WordPress hooks
|
||||
*/
|
||||
private function setup_hooks() {
|
||||
// AJAX handlers
|
||||
add_action('wp_ajax_tigerstyle_create_backup', array($this, 'ajax_create_backup'));
|
||||
add_action('wp_ajax_tigerstyle_restore_backup', array($this, 'ajax_restore_backup'));
|
||||
add_action('wp_ajax_tigerstyle_delete_backup', array($this, 'ajax_delete_backup'));
|
||||
add_action('wp_ajax_tigerstyle_download_backup', array($this, 'ajax_download_backup'));
|
||||
add_action('wp_ajax_tigerstyle_upload_backup', array($this, 'ajax_upload_backup'));
|
||||
add_action('wp_ajax_tigerstyle_reset_wordpress', array($this, 'ajax_reset_wordpress'));
|
||||
add_action('wp_ajax_tigerstyle_reset_database', array($this, 'ajax_reset_database'));
|
||||
add_action('wp_ajax_tigerstyle_backup_progress', array($this, 'ajax_backup_progress'));
|
||||
add_action('wp_ajax_tigerstyle_validate_backup', array($this, 'ajax_validate_backup'));
|
||||
|
||||
// Scheduled backup hooks
|
||||
add_action('tigerstyle_backup_scheduled', array($this, 'run_scheduled_backup'));
|
||||
add_action('tigerstyle_backup_cleanup', array($this, 'cleanup_old_backups'));
|
||||
|
||||
// Admin hooks
|
||||
add_action('admin_post_tigerstyle_backup_settings', array($this, 'save_backup_settings'));
|
||||
add_action('admin_notices', array($this, 'display_backup_notices'));
|
||||
|
||||
// Cron hooks
|
||||
if (!wp_next_scheduled('tigerstyle_backup_cleanup')) {
|
||||
wp_schedule_event(time(), 'daily', 'tigerstyle_backup_cleanup');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('You do not have sufficient permissions to access this page.', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Handle form submissions
|
||||
$this->handle_form_submissions();
|
||||
|
||||
// Get backup list
|
||||
$backups = $this->get_backup_list();
|
||||
$storage_stats = $this->storage_manager->get_storage_stats();
|
||||
$compression_methods = $this->get_available_compression_methods();
|
||||
|
||||
include TIGERSTYLE_HEAT_PLUGIN_DIR . 'admin/pages/backup-restore.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submissions
|
||||
*/
|
||||
private function handle_form_submissions() {
|
||||
if (!isset($_POST['tigerstyle_backup_nonce']) ||
|
||||
!wp_verify_nonce($_POST['tigerstyle_backup_nonce'], 'tigerstyle_backup_action')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action = sanitize_text_field($_POST['backup_action']);
|
||||
|
||||
switch ($action) {
|
||||
case 'create_backup':
|
||||
$this->handle_create_backup();
|
||||
break;
|
||||
case 'restore_backup':
|
||||
$this->handle_restore_backup();
|
||||
break;
|
||||
case 'delete_backup':
|
||||
$this->handle_delete_backup();
|
||||
break;
|
||||
case 'save_settings':
|
||||
$this->handle_save_settings();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup via AJAX
|
||||
*/
|
||||
public function ajax_create_backup() {
|
||||
check_ajax_referer('tigerstyle_backup_action', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$backup_type = sanitize_text_field($_POST['backup_type']);
|
||||
$compression = sanitize_text_field($_POST['compression']);
|
||||
$storage_location = sanitize_text_field($_POST['storage_location']);
|
||||
$include_files = isset($_POST['include_files']) ? (bool)$_POST['include_files'] : true;
|
||||
$include_database = isset($_POST['include_database']) ? (bool)$_POST['include_database'] : true;
|
||||
|
||||
try {
|
||||
$backup_id = $this->backup_engine->create_backup(array(
|
||||
'type' => $backup_type,
|
||||
'compression' => $compression,
|
||||
'storage_location' => $storage_location,
|
||||
'include_files' => $include_files,
|
||||
'include_database' => $include_database,
|
||||
'description' => sanitize_textarea_field($_POST['description'])
|
||||
));
|
||||
|
||||
wp_send_json_success(array(
|
||||
'backup_id' => $backup_id,
|
||||
'message' => __('Backup started successfully', 'tigerstyle-heat')
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Backup creation failed: ' . $e->getMessage());
|
||||
wp_send_json_error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore backup via AJAX
|
||||
*/
|
||||
public function ajax_restore_backup() {
|
||||
check_ajax_referer('tigerstyle_backup_action', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$backup_id = sanitize_text_field($_POST['backup_id']);
|
||||
$restore_files = isset($_POST['restore_files']) ? (bool)$_POST['restore_files'] : true;
|
||||
$restore_database = isset($_POST['restore_database']) ? (bool)$_POST['restore_database'] : true;
|
||||
|
||||
try {
|
||||
// Validate backup first
|
||||
if (!$this->validator->validate_backup($backup_id)) {
|
||||
throw new Exception(__('Invalid or corrupted backup file', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
$restore_id = $this->restore_engine->restore_backup(array(
|
||||
'backup_id' => $backup_id,
|
||||
'restore_files' => $restore_files,
|
||||
'restore_database' => $restore_database
|
||||
));
|
||||
|
||||
wp_send_json_success(array(
|
||||
'restore_id' => $restore_id,
|
||||
'message' => __('Restore started successfully', 'tigerstyle-heat')
|
||||
));
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Restore failed: ' . $e->getMessage());
|
||||
wp_send_json_error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset WordPress (remove default pages/posts)
|
||||
*/
|
||||
public function ajax_reset_wordpress() {
|
||||
check_ajax_referer('tigerstyle_backup_action', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$confirmation = sanitize_text_field($_POST['confirmation']);
|
||||
if ($confirmation !== 'RESET_WORDPRESS') {
|
||||
wp_send_json_error(__('Invalid confirmation text', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
try {
|
||||
$this->reset_wordpress_content();
|
||||
wp_send_json_success(__('WordPress content reset successfully', 'tigerstyle-heat'));
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset database (complete database wipe)
|
||||
*/
|
||||
public function ajax_reset_database() {
|
||||
check_ajax_referer('tigerstyle_backup_action', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Insufficient permissions');
|
||||
}
|
||||
|
||||
$confirmation_code = sanitize_text_field($_POST['confirmation_code']);
|
||||
$expected_code = get_transient('tigerstyle_reset_code_' . get_current_user_id());
|
||||
|
||||
if (!$expected_code || $confirmation_code !== $expected_code) {
|
||||
wp_send_json_error(__('Invalid confirmation code', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
$final_confirmation = sanitize_text_field($_POST['final_confirmation']);
|
||||
if ($final_confirmation !== 'YES_DELETE_EVERYTHING') {
|
||||
wp_send_json_error(__('Final confirmation required', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
try {
|
||||
$this->reset_database_completely();
|
||||
delete_transient('tigerstyle_reset_code_' . get_current_user_id());
|
||||
wp_send_json_success(__('Database reset successfully', 'tigerstyle-heat'));
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup progress
|
||||
*/
|
||||
public function ajax_backup_progress() {
|
||||
check_ajax_referer('tigerstyle_backup_action', 'nonce');
|
||||
|
||||
$operation_id = sanitize_text_field($_POST['operation_id']);
|
||||
$progress = get_transient('tigerstyle_backup_progress_' . $operation_id);
|
||||
|
||||
if ($progress === false) {
|
||||
wp_send_json_error(__('Operation not found', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
wp_send_json_success($progress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available backups
|
||||
*/
|
||||
public function get_backup_list() {
|
||||
return $this->storage_manager->list_backups();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available compression methods
|
||||
*/
|
||||
public function get_available_compression_methods() {
|
||||
$methods = array();
|
||||
|
||||
if (class_exists('ZipArchive')) {
|
||||
$methods['zip'] = __('ZIP Compression', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
if (function_exists('gzopen')) {
|
||||
$methods['tar.gz'] = __('TAR.GZ Compression', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
if (function_exists('bzopen')) {
|
||||
$methods['tar.bz2'] = __('TAR.BZ2 Compression', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
if (empty($methods)) {
|
||||
$methods['none'] = __('No Compression (Fallback)', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset WordPress content (default pages/posts)
|
||||
*/
|
||||
private function reset_wordpress_content() {
|
||||
global $wpdb;
|
||||
|
||||
// Create backup before reset
|
||||
$backup_id = $this->backup_engine->create_backup(array(
|
||||
'type' => 'quick',
|
||||
'description' => 'Auto-backup before WordPress reset',
|
||||
'include_files' => false,
|
||||
'include_database' => true
|
||||
));
|
||||
|
||||
// Delete default posts
|
||||
$default_posts = get_posts(array(
|
||||
'post_type' => array('post', 'page'),
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'any'
|
||||
));
|
||||
|
||||
foreach ($default_posts as $post) {
|
||||
wp_delete_post($post->ID, true);
|
||||
}
|
||||
|
||||
// Delete default comments
|
||||
$wpdb->query("DELETE FROM {$wpdb->comments}");
|
||||
$wpdb->query("DELETE FROM {$wpdb->commentmeta}");
|
||||
|
||||
// Reset comment count
|
||||
$wpdb->query("UPDATE {$wpdb->posts} SET comment_count = 0");
|
||||
|
||||
// Clear caches
|
||||
wp_cache_flush();
|
||||
|
||||
$this->logger->info('WordPress content reset completed', array(
|
||||
'backup_id' => $backup_id,
|
||||
'posts_deleted' => count($default_posts)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset database completely
|
||||
*/
|
||||
private function reset_database_completely() {
|
||||
global $wpdb;
|
||||
|
||||
// Get all tables
|
||||
$tables = $wpdb->get_col("SHOW TABLES");
|
||||
|
||||
// Disable foreign key checks
|
||||
$wpdb->query("SET FOREIGN_KEY_CHECKS = 0");
|
||||
|
||||
try {
|
||||
// Drop all tables
|
||||
foreach ($tables as $table) {
|
||||
$wpdb->query("DROP TABLE IF EXISTS `{$table}`");
|
||||
}
|
||||
|
||||
$this->logger->critical('Complete database reset performed', array(
|
||||
'tables_dropped' => count($tables),
|
||||
'user_id' => get_current_user_id()
|
||||
));
|
||||
|
||||
} finally {
|
||||
// Re-enable foreign key checks
|
||||
$wpdb->query("SET FOREIGN_KEY_CHECKS = 1");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate reset confirmation code
|
||||
*/
|
||||
public function generate_reset_code() {
|
||||
$code = wp_generate_password(6, false, false);
|
||||
$code = strtoupper($code);
|
||||
set_transient('tigerstyle_reset_code_' . get_current_user_id(), $code, 300); // 5 minutes
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display admin notices
|
||||
*/
|
||||
public function display_backup_notices() {
|
||||
// Check for backup failures
|
||||
$failed_backups = get_option('tigerstyle_failed_backups', array());
|
||||
if (!empty($failed_backups)) {
|
||||
echo '<div class="notice notice-error"><p>';
|
||||
echo __('Some backup operations have failed. Please check the backup logs.', 'tigerstyle-heat');
|
||||
echo '</p></div>';
|
||||
}
|
||||
|
||||
// Check storage space
|
||||
$storage_stats = $this->storage_manager->get_storage_stats();
|
||||
if (isset($storage_stats['usage_percent']) && $storage_stats['usage_percent'] > 90) {
|
||||
echo '<div class="notice notice-warning"><p>';
|
||||
echo __('Backup storage is nearly full. Consider cleaning up old backups.', 'tigerstyle-heat');
|
||||
echo '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run scheduled backup
|
||||
*/
|
||||
public function run_scheduled_backup() {
|
||||
$settings = get_option('tigerstyle_backup_settings', array());
|
||||
|
||||
if (!isset($settings['schedule_enabled']) || !$settings['schedule_enabled']) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->backup_engine->create_backup(array(
|
||||
'type' => 'scheduled',
|
||||
'compression' => $settings['compression'] ?? 'zip',
|
||||
'storage_location' => $settings['storage_location'] ?? 'local',
|
||||
'include_files' => $settings['include_files'] ?? true,
|
||||
'include_database' => $settings['include_database'] ?? true,
|
||||
'description' => 'Scheduled backup - ' . current_time('mysql')
|
||||
));
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Scheduled backup failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old backups
|
||||
*/
|
||||
public function cleanup_old_backups() {
|
||||
$settings = get_option('tigerstyle_backup_settings', array());
|
||||
$retention_days = $settings['retention_days'] ?? 30;
|
||||
|
||||
$this->storage_manager->cleanup_old_backups($retention_days);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup settings
|
||||
*/
|
||||
public function get_backup_settings() {
|
||||
$defaults = array(
|
||||
'compression' => 'zip',
|
||||
'storage_location' => 'local',
|
||||
'schedule_enabled' => false,
|
||||
'schedule_frequency' => 'daily',
|
||||
'retention_days' => 30,
|
||||
'include_files' => true,
|
||||
'include_database' => true,
|
||||
'chunk_size' => 5, // MB
|
||||
's3_bucket' => '',
|
||||
's3_access_key' => '',
|
||||
's3_secret_key' => '',
|
||||
's3_region' => 'us-east-1',
|
||||
's3_endpoint' => '', // For S3-compatible services
|
||||
'email_notifications' => false,
|
||||
'notification_email' => get_option('admin_email')
|
||||
);
|
||||
|
||||
return wp_parse_args(get_option('tigerstyle_backup_settings', array()), $defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save backup settings
|
||||
*/
|
||||
public function save_backup_settings() {
|
||||
if (!isset($_POST['tigerstyle_backup_nonce']) || !wp_verify_nonce($_POST['tigerstyle_backup_nonce'], 'tigerstyle_backup_settings')) {
|
||||
wp_die(__('Security check failed', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('Insufficient permissions', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
$settings = array(
|
||||
'compression' => sanitize_text_field($_POST['compression']),
|
||||
'storage_location' => sanitize_text_field($_POST['storage_location']),
|
||||
'schedule_enabled' => isset($_POST['schedule_enabled']),
|
||||
'schedule_frequency' => sanitize_text_field($_POST['schedule_frequency']),
|
||||
'retention_days' => absint($_POST['retention_days']),
|
||||
'include_files' => isset($_POST['include_files']),
|
||||
'include_database' => isset($_POST['include_database']),
|
||||
'chunk_size' => absint($_POST['chunk_size']),
|
||||
's3_bucket' => sanitize_text_field($_POST['s3_bucket']),
|
||||
's3_access_key' => sanitize_text_field($_POST['s3_access_key']),
|
||||
's3_secret_key' => sanitize_text_field($_POST['s3_secret_key']),
|
||||
's3_region' => sanitize_text_field($_POST['s3_region']),
|
||||
's3_endpoint' => sanitize_url($_POST['s3_endpoint']),
|
||||
'email_notifications' => isset($_POST['email_notifications']),
|
||||
'notification_email' => sanitize_email($_POST['notification_email'])
|
||||
);
|
||||
|
||||
update_option('tigerstyle_backup_settings', $settings);
|
||||
|
||||
// Update scheduled backup if settings changed
|
||||
if ($settings['schedule_enabled']) {
|
||||
wp_clear_scheduled_hook('tigerstyle_backup_scheduled');
|
||||
wp_schedule_event(time(), $settings['schedule_frequency'], 'tigerstyle_backup_scheduled');
|
||||
} else {
|
||||
wp_clear_scheduled_hook('tigerstyle_backup_scheduled');
|
||||
}
|
||||
|
||||
wp_redirect(add_query_arg('message', 'backup_settings_saved', wp_get_referer()));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
393
includes/modules/class-ecosystem-coordinator.php
Normal file
393
includes/modules/class-ecosystem-coordinator.php
Normal file
@ -0,0 +1,393 @@
|
||||
<?php
|
||||
/**
|
||||
* TigerStyle Ecosystem Coordinator
|
||||
*
|
||||
* Central coordination hub for all TigerStyle plugins
|
||||
* Handles cross-plugin communication, preferences, and data sharing
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_EcosystemCoordinator {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Registered TigerStyle plugins
|
||||
*/
|
||||
private $registered_plugins = array();
|
||||
|
||||
/**
|
||||
* Shared user preferences
|
||||
*/
|
||||
private $shared_preferences = array();
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the coordinator
|
||||
*/
|
||||
private function init() {
|
||||
// Register Heat as the coordinator
|
||||
$this->register_plugin('heat', array(
|
||||
'name' => 'TigerStyle Heat',
|
||||
'version' => TIGERSTYLE_HEAT_VERSION,
|
||||
'role' => 'coordinator',
|
||||
'capabilities' => array('analytics', 'seo', 'performance')
|
||||
));
|
||||
|
||||
// Hook into WordPress
|
||||
add_action('init', array($this, 'discover_ecosystem_plugins'));
|
||||
add_action('wp_ajax_tigerstyle_sync_preferences', array($this, 'handle_preference_sync'));
|
||||
add_action('wp_ajax_nopriv_tigerstyle_sync_preferences', array($this, 'handle_preference_sync'));
|
||||
|
||||
// Frontend coordination
|
||||
add_action('wp_head', array($this, 'inject_ecosystem_coordination'), 1);
|
||||
|
||||
// Admin coordination
|
||||
if (is_admin()) {
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_ecosystem_scripts'));
|
||||
}
|
||||
|
||||
// Plugin communication hooks
|
||||
add_action('tigerstyle_ecosystem_register_plugin', array($this, 'register_plugin_from_hook'), 10, 2);
|
||||
add_action('tigerstyle_ecosystem_sync_preferences', array($this, 'sync_preferences_from_hook'));
|
||||
|
||||
console.log('🔥 TigerStyle Heat: Ecosystem coordinator initialized!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover other TigerStyle plugins in the ecosystem
|
||||
*/
|
||||
public function discover_ecosystem_plugins() {
|
||||
// Check for TigerStyle Whiskers
|
||||
if (class_exists('TigerStyleWhiskers')) {
|
||||
$this->register_plugin('whiskers', array(
|
||||
'name' => 'TigerStyle Whiskers',
|
||||
'version' => defined('TIGERSTYLE_WHISKERS_VERSION') ? TIGERSTYLE_WHISKERS_VERSION : '1.0.0',
|
||||
'role' => 'privacy',
|
||||
'capabilities' => array('gdpr', 'consent', 'privacy')
|
||||
));
|
||||
}
|
||||
|
||||
// Check for TigerStyle Dash
|
||||
if (class_exists('TigerStyleDash')) {
|
||||
$this->register_plugin('dash', array(
|
||||
'name' => 'TigerStyle Dash',
|
||||
'version' => defined('TIGERSTYLE_DASH_VERSION') ? TIGERSTYLE_DASH_VERSION : '1.0.0',
|
||||
'role' => 'performance',
|
||||
'capabilities' => array('caching', 'optimization', 'speed')
|
||||
));
|
||||
}
|
||||
|
||||
// Check for TigerStyle Life9
|
||||
if (class_exists('TigerStyleLife9')) {
|
||||
$this->register_plugin('life9', array(
|
||||
'name' => 'TigerStyle Life9',
|
||||
'version' => defined('TIGERSTYLE_LIFE9_VERSION') ? TIGERSTYLE_LIFE9_VERSION : '1.0.0',
|
||||
'role' => 'backup',
|
||||
'capabilities' => array('backup', 'restore', 'disaster_recovery')
|
||||
));
|
||||
}
|
||||
|
||||
// Check for TigerStyle Scent
|
||||
if (class_exists('TigerStyleScent')) {
|
||||
$this->register_plugin('scent', array(
|
||||
'name' => 'TigerStyle Scent',
|
||||
'version' => defined('TIGERSTYLE_SCENT_VERSION') ? TIGERSTYLE_SCENT_VERSION : '1.0.0',
|
||||
'role' => 'auth',
|
||||
'capabilities' => array('oauth2', 'api', 'authentication')
|
||||
));
|
||||
}
|
||||
|
||||
// Allow other plugins to register
|
||||
do_action('tigerstyle_ecosystem_discovery', $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a plugin in the ecosystem
|
||||
*/
|
||||
public function register_plugin($plugin_id, $plugin_data) {
|
||||
$this->registered_plugins[$plugin_id] = array_merge(array(
|
||||
'name' => '',
|
||||
'version' => '1.0.0',
|
||||
'role' => 'member',
|
||||
'capabilities' => array(),
|
||||
'registered_at' => current_time('timestamp')
|
||||
), $plugin_data);
|
||||
|
||||
// Trigger ecosystem update
|
||||
do_action('tigerstyle_ecosystem_plugin_registered', $plugin_id, $plugin_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register plugin from hook
|
||||
*/
|
||||
public function register_plugin_from_hook($plugin_id, $plugin_data) {
|
||||
$this->register_plugin($plugin_id, $plugin_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered plugins
|
||||
*/
|
||||
public function get_registered_plugins() {
|
||||
return $this->registered_plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific plugin info
|
||||
*/
|
||||
public function get_plugin_info($plugin_id) {
|
||||
return isset($this->registered_plugins[$plugin_id]) ? $this->registered_plugins[$plugin_id] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync user preferences across plugins
|
||||
*/
|
||||
public function sync_user_preferences($user_id, $preferences = array()) {
|
||||
// Get current preferences
|
||||
$current_prefs = get_user_meta($user_id, 'tigerstyle_ecosystem_preferences', true);
|
||||
if (!is_array($current_prefs)) {
|
||||
$current_prefs = array();
|
||||
}
|
||||
|
||||
// Merge new preferences
|
||||
$updated_prefs = array_merge($current_prefs, $preferences);
|
||||
|
||||
// Save updated preferences
|
||||
update_user_meta($user_id, 'tigerstyle_ecosystem_preferences', $updated_prefs);
|
||||
|
||||
// Trigger sync across plugins
|
||||
do_action('tigerstyle_ecosystem_preferences_updated', $user_id, $updated_prefs);
|
||||
|
||||
// Return updated preferences
|
||||
return $updated_prefs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user preferences
|
||||
*/
|
||||
public function get_user_preferences($user_id, $preference_key = null) {
|
||||
$prefs = get_user_meta($user_id, 'tigerstyle_ecosystem_preferences', true);
|
||||
if (!is_array($prefs)) {
|
||||
$prefs = array();
|
||||
}
|
||||
|
||||
if ($preference_key) {
|
||||
return isset($prefs[$preference_key]) ? $prefs[$preference_key] : null;
|
||||
}
|
||||
|
||||
return $prefs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX preference sync
|
||||
*/
|
||||
public function handle_preference_sync() {
|
||||
// Verify nonce
|
||||
if (!wp_verify_nonce($_POST['nonce'], 'tigerstyle_ecosystem_sync')) {
|
||||
wp_die(json_encode(array('success' => false, 'message' => 'Invalid nonce')));
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if (!$user_id) {
|
||||
wp_die(json_encode(array('success' => false, 'message' => 'User not logged in')));
|
||||
}
|
||||
|
||||
$preferences = isset($_POST['preferences']) ? $_POST['preferences'] : array();
|
||||
|
||||
// Sanitize preferences
|
||||
$sanitized_prefs = array();
|
||||
foreach ($preferences as $key => $value) {
|
||||
$sanitized_prefs[sanitize_key($key)] = sanitize_text_field($value);
|
||||
}
|
||||
|
||||
// Sync preferences
|
||||
$updated_prefs = $this->sync_user_preferences($user_id, $sanitized_prefs);
|
||||
|
||||
wp_die(json_encode(array(
|
||||
'success' => true,
|
||||
'preferences' => $updated_prefs,
|
||||
'ecosystem' => $this->get_registered_plugins()
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync preferences from hook
|
||||
*/
|
||||
public function sync_preferences_from_hook($preferences) {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id) {
|
||||
$this->sync_user_preferences($user_id, $preferences);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject ecosystem coordination script
|
||||
*/
|
||||
public function inject_ecosystem_coordination() {
|
||||
$ecosystem_data = array(
|
||||
'plugins' => $this->get_registered_plugins(),
|
||||
'ajaxurl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('tigerstyle_ecosystem_sync'),
|
||||
'user_id' => get_current_user_id()
|
||||
);
|
||||
|
||||
// Get user preferences if logged in
|
||||
if (get_current_user_id()) {
|
||||
$ecosystem_data['user_preferences'] = $this->get_user_preferences(get_current_user_id());
|
||||
}
|
||||
|
||||
echo "\n<!-- TigerStyle Ecosystem Coordination -->\n";
|
||||
echo '<script>' . "\n";
|
||||
echo 'window.tigerstyleEcosystem = ' . json_encode($ecosystem_data) . ';' . "\n";
|
||||
echo '
|
||||
// TigerStyle Ecosystem JavaScript API
|
||||
window.tigerstyleEcosystem.syncPreferences = function(preferences, callback) {
|
||||
if (!window.tigerstyleEcosystem.user_id) {
|
||||
console.warn("🐅 TigerStyle Ecosystem: User not logged in, cannot sync preferences");
|
||||
return;
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", window.tigerstyleEcosystem.ajaxurl);
|
||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (response.success) {
|
||||
window.tigerstyleEcosystem.user_preferences = response.preferences;
|
||||
console.log("🐅 TigerStyle Ecosystem: Preferences synced!", response.preferences);
|
||||
|
||||
// Trigger ecosystem update event
|
||||
if (typeof window.CustomEvent === "function") {
|
||||
document.dispatchEvent(new CustomEvent("tigerstyleEcosystemUpdated", {
|
||||
detail: response
|
||||
}));
|
||||
}
|
||||
|
||||
if (callback) callback(response);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("🐅 TigerStyle Ecosystem: Error parsing sync response", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var params = "action=tigerstyle_sync_preferences&nonce=" + encodeURIComponent(window.tigerstyleEcosystem.nonce);
|
||||
for (var key in preferences) {
|
||||
params += "&preferences[" + encodeURIComponent(key) + "]=" + encodeURIComponent(preferences[key]);
|
||||
}
|
||||
|
||||
xhr.send(params);
|
||||
};
|
||||
|
||||
// Get user preference
|
||||
window.tigerstyleEcosystem.getPreference = function(key, defaultValue) {
|
||||
if (!window.tigerstyleEcosystem.user_preferences) return defaultValue;
|
||||
return window.tigerstyleEcosystem.user_preferences[key] || defaultValue;
|
||||
};
|
||||
|
||||
// Check if plugin is active in ecosystem
|
||||
window.tigerstyleEcosystem.hasPlugin = function(pluginId) {
|
||||
return window.tigerstyleEcosystem.plugins.hasOwnProperty(pluginId);
|
||||
};
|
||||
|
||||
// Get plugin capabilities
|
||||
window.tigerstyleEcosystem.getPluginCapabilities = function(pluginId) {
|
||||
return window.tigerstyleEcosystem.plugins[pluginId] ? window.tigerstyleEcosystem.plugins[pluginId].capabilities : [];
|
||||
};
|
||||
|
||||
console.log("🔥 TigerStyle Ecosystem Coordination Active! Plugins: " + Object.keys(window.tigerstyleEcosystem.plugins).join(", "));
|
||||
';
|
||||
echo '</script>' . "\n";
|
||||
echo "<!-- End TigerStyle Ecosystem Coordination -->\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue ecosystem scripts for admin
|
||||
*/
|
||||
public function enqueue_ecosystem_scripts($hook) {
|
||||
// Only load on TigerStyle admin pages
|
||||
if (strpos($hook, 'tigerstyle') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script('jquery');
|
||||
|
||||
// Inline ecosystem admin script
|
||||
$script = '
|
||||
jQuery(document).ready(function($) {
|
||||
// Add ecosystem status to admin pages
|
||||
if (window.tigerstyleEcosystem && Object.keys(window.tigerstyleEcosystem.plugins).length > 1) {
|
||||
var ecosystemHtml = \'<div class="notice notice-success" style="background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%); color: white; border: none;">\';
|
||||
ecosystemHtml += \'<p><strong>🐅 TigerStyle Ecosystem Active!</strong> \';
|
||||
ecosystemHtml += Object.keys(window.tigerstyleEcosystem.plugins).length + \' plugins coordinating: \';
|
||||
ecosystemHtml += Object.keys(window.tigerstyleEcosystem.plugins).map(function(id) {
|
||||
return window.tigerstyleEcosystem.plugins[id].name;
|
||||
}).join(", ");
|
||||
ecosystemHtml += \'</p></div>\';
|
||||
|
||||
$(".wrap h1").after(ecosystemHtml);
|
||||
}
|
||||
});
|
||||
';
|
||||
|
||||
wp_add_inline_script('jquery', $script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ecosystem statistics
|
||||
*/
|
||||
public function get_ecosystem_stats() {
|
||||
return array(
|
||||
'total_plugins' => count($this->registered_plugins),
|
||||
'coordinator_version' => TIGERSTYLE_HEAT_VERSION,
|
||||
'registered_plugins' => $this->registered_plugins,
|
||||
'capabilities' => $this->get_all_capabilities(),
|
||||
'last_sync' => get_option('tigerstyle_ecosystem_last_sync', 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all capabilities across the ecosystem
|
||||
*/
|
||||
private function get_all_capabilities() {
|
||||
$all_capabilities = array();
|
||||
foreach ($this->registered_plugins as $plugin_id => $plugin_data) {
|
||||
$all_capabilities = array_merge($all_capabilities, $plugin_data['capabilities']);
|
||||
}
|
||||
return array_unique($all_capabilities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force ecosystem sync
|
||||
*/
|
||||
public function force_ecosystem_sync() {
|
||||
update_option('tigerstyle_ecosystem_last_sync', current_time('timestamp'));
|
||||
do_action('tigerstyle_ecosystem_force_sync');
|
||||
}
|
||||
}
|
||||
744
includes/modules/class-facebook.php
Normal file
744
includes/modules/class-facebook.php
Normal file
@ -0,0 +1,744 @@
|
||||
<?php
|
||||
/**
|
||||
* Facebook Integration Module
|
||||
*
|
||||
* Handles Facebook Open Graph optimization, domain verification,
|
||||
* and sharing optimization features.
|
||||
*
|
||||
* @package TigerStyleSEO
|
||||
* @subpackage Modules
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Facebook Integration Class
|
||||
*/
|
||||
class TigerStyleSEO_Facebook {
|
||||
|
||||
/**
|
||||
* Single instance of the class
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Option name for Facebook settings
|
||||
*/
|
||||
private $option_name = 'tigerstyle_heat_facebook';
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
private $defaults = array(
|
||||
'enable_open_graph' => true,
|
||||
'default_image' => '',
|
||||
'app_id' => '',
|
||||
'admins' => '',
|
||||
'domain_verification' => '',
|
||||
'site_name' => '',
|
||||
'enable_article_tags' => true,
|
||||
'enable_video_tags' => false,
|
||||
'image_width' => 1200,
|
||||
'image_height' => 630,
|
||||
'enable_crawler_optimization' => true
|
||||
);
|
||||
|
||||
/**
|
||||
* Get single instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module
|
||||
*/
|
||||
private function init() {
|
||||
// Frontend hooks
|
||||
add_action('wp_head', array($this, 'output_open_graph_tags'), 5);
|
||||
add_action('wp_head', array($this, 'output_facebook_domain_verification'), 1);
|
||||
|
||||
// Admin hooks
|
||||
if (is_admin()) {
|
||||
add_action('admin_init', array($this, 'register_settings'));
|
||||
}
|
||||
|
||||
// REST API endpoints for testing
|
||||
add_action('rest_api_init', array($this, 'register_rest_routes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Facebook settings
|
||||
*/
|
||||
public function get_settings() {
|
||||
$settings = get_option($this->option_name, array());
|
||||
return wp_parse_args($settings, $this->defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Facebook settings
|
||||
*/
|
||||
public function update_settings($new_settings) {
|
||||
$settings = $this->get_settings();
|
||||
$settings = wp_parse_args($new_settings, $settings);
|
||||
return update_option($this->option_name, $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output Facebook domain verification meta tag
|
||||
*/
|
||||
public function output_facebook_domain_verification() {
|
||||
if (!is_home() && !is_front_page()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $this->get_settings();
|
||||
|
||||
if (!empty($settings['domain_verification'])) {
|
||||
printf(
|
||||
'<meta name="facebook-domain-verification" content="%s" />' . "\n",
|
||||
esc_attr($settings['domain_verification'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output Open Graph meta tags
|
||||
*/
|
||||
public function output_open_graph_tags() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
if (!$settings['enable_open_graph']) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic Open Graph tags
|
||||
$og_data = $this->get_open_graph_data();
|
||||
|
||||
// Output Open Graph namespace
|
||||
add_filter('language_attributes', array($this, 'add_opengraph_namespace'));
|
||||
|
||||
// Output tags
|
||||
foreach ($og_data as $property => $content) {
|
||||
if (!empty($content)) {
|
||||
printf(
|
||||
'<meta property="%s" content="%s" />' . "\n",
|
||||
esc_attr($property),
|
||||
esc_attr($content)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Facebook-specific tags
|
||||
if (!empty($settings['app_id'])) {
|
||||
printf(
|
||||
'<meta property="fb:app_id" content="%s" />' . "\n",
|
||||
esc_attr($settings['app_id'])
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($settings['admins'])) {
|
||||
$admins = explode(',', $settings['admins']);
|
||||
foreach ($admins as $admin) {
|
||||
$admin = trim($admin);
|
||||
if (!empty($admin)) {
|
||||
printf(
|
||||
'<meta property="fb:admins" content="%s" />' . "\n",
|
||||
esc_attr($admin)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Open Graph namespace to html tag
|
||||
*/
|
||||
public function add_opengraph_namespace($output) {
|
||||
if (strpos($output, 'xmlns:og') === false) {
|
||||
$output .= ' xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml"';
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Open Graph data for current page
|
||||
*/
|
||||
private function get_open_graph_data() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
$og_data = array();
|
||||
|
||||
// Basic required tags
|
||||
$og_data['og:title'] = $this->get_og_title();
|
||||
$og_data['og:description'] = $this->get_og_description();
|
||||
$og_data['og:type'] = $this->get_og_type();
|
||||
$og_data['og:url'] = $this->get_og_url();
|
||||
$og_data['og:site_name'] = $this->get_og_site_name();
|
||||
|
||||
// Image tags
|
||||
$image_data = $this->get_og_image();
|
||||
if ($image_data) {
|
||||
$og_data['og:image'] = $image_data['url'];
|
||||
if (!empty($image_data['width'])) {
|
||||
$og_data['og:image:width'] = $image_data['width'];
|
||||
}
|
||||
if (!empty($image_data['height'])) {
|
||||
$og_data['og:image:height'] = $image_data['height'];
|
||||
}
|
||||
if (!empty($image_data['alt'])) {
|
||||
$og_data['og:image:alt'] = $image_data['alt'];
|
||||
}
|
||||
}
|
||||
|
||||
// Article-specific tags
|
||||
if ($settings['enable_article_tags'] && is_single()) {
|
||||
$article_data = $this->get_article_data();
|
||||
$og_data = array_merge($og_data, $article_data);
|
||||
}
|
||||
|
||||
return apply_filters('tigerstyle_heat_facebook_og_data', $og_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Open Graph title
|
||||
*/
|
||||
private function get_og_title() {
|
||||
if (is_single() || is_page()) {
|
||||
return get_the_title();
|
||||
} elseif (is_category()) {
|
||||
return single_cat_title('', false);
|
||||
} elseif (is_tag()) {
|
||||
return single_tag_title('', false);
|
||||
} elseif (is_author()) {
|
||||
return get_the_author();
|
||||
} elseif (is_search()) {
|
||||
return sprintf(__('Search Results for: %s', 'tigerstyle-heat'), get_search_query());
|
||||
} elseif (is_archive()) {
|
||||
return get_the_archive_title();
|
||||
} else {
|
||||
return get_bloginfo('name');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Open Graph description
|
||||
*/
|
||||
private function get_og_description() {
|
||||
if (is_single() || is_page()) {
|
||||
$excerpt = get_the_excerpt();
|
||||
if ($excerpt) {
|
||||
return wp_trim_words($excerpt, 30);
|
||||
}
|
||||
} elseif (is_category()) {
|
||||
$description = category_description();
|
||||
if ($description) {
|
||||
return wp_trim_words(strip_tags($description), 30);
|
||||
}
|
||||
} elseif (is_tag()) {
|
||||
$description = tag_description();
|
||||
if ($description) {
|
||||
return wp_trim_words(strip_tags($description), 30);
|
||||
}
|
||||
}
|
||||
|
||||
return get_bloginfo('description');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Open Graph type
|
||||
*/
|
||||
private function get_og_type() {
|
||||
if (is_single()) {
|
||||
return 'article';
|
||||
} elseif (is_page()) {
|
||||
return 'website';
|
||||
} else {
|
||||
return 'website';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Open Graph URL
|
||||
*/
|
||||
private function get_og_url() {
|
||||
if (is_singular()) {
|
||||
return get_permalink();
|
||||
} else {
|
||||
global $wp;
|
||||
return home_url(add_query_arg(array(), $wp->request));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Open Graph site name
|
||||
*/
|
||||
private function get_og_site_name() {
|
||||
$settings = $this->get_settings();
|
||||
return !empty($settings['site_name']) ? $settings['site_name'] : get_bloginfo('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Open Graph image data
|
||||
*/
|
||||
private function get_og_image() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
// Try featured image first
|
||||
if (is_singular() && has_post_thumbnail()) {
|
||||
$image_id = get_post_thumbnail_id();
|
||||
$image = wp_get_attachment_image_src($image_id, 'full');
|
||||
|
||||
if ($image) {
|
||||
$metadata = wp_get_attachment_metadata($image_id);
|
||||
return array(
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
'alt' => get_post_meta($image_id, '_wp_attachment_image_alt', true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default image
|
||||
if (!empty($settings['default_image'])) {
|
||||
$image_id = attachment_url_to_postid($settings['default_image']);
|
||||
if ($image_id) {
|
||||
$image = wp_get_attachment_image_src($image_id, 'full');
|
||||
if ($image) {
|
||||
return array(
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
'alt' => get_post_meta($image_id, '_wp_attachment_image_alt', true)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Direct URL
|
||||
return array(
|
||||
'url' => $settings['default_image'],
|
||||
'width' => $settings['image_width'],
|
||||
'height' => $settings['image_height'],
|
||||
'alt' => get_bloginfo('name') . ' - Default Image'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get article-specific Open Graph data
|
||||
*/
|
||||
private function get_article_data() {
|
||||
$data = array();
|
||||
|
||||
if (is_single()) {
|
||||
$post = get_post();
|
||||
|
||||
// Article author
|
||||
$data['article:author'] = get_author_posts_url($post->post_author);
|
||||
|
||||
// Article published time
|
||||
$data['article:published_time'] = get_the_date('c');
|
||||
|
||||
// Article modified time
|
||||
if (get_the_modified_date('c') !== get_the_date('c')) {
|
||||
$data['article:modified_time'] = get_the_modified_date('c');
|
||||
}
|
||||
|
||||
// Article section (category)
|
||||
$categories = get_the_category();
|
||||
if (!empty($categories)) {
|
||||
$data['article:section'] = $categories[0]->name;
|
||||
}
|
||||
|
||||
// Article tags
|
||||
$tags = get_the_tags();
|
||||
if (!empty($tags)) {
|
||||
foreach ($tags as $tag) {
|
||||
$data['article:tag'] = $tag->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Facebook image requirements
|
||||
*/
|
||||
public function validate_image($image_url) {
|
||||
$errors = array();
|
||||
|
||||
// Check if image exists and get dimensions
|
||||
$image_info = getimagesize($image_url);
|
||||
|
||||
if (!$image_info) {
|
||||
$errors[] = __('Unable to access image or invalid image format.', 'tigerstyle-heat');
|
||||
return $errors;
|
||||
}
|
||||
|
||||
$width = $image_info[0];
|
||||
$height = $image_info[1];
|
||||
$size = strlen(file_get_contents($image_url));
|
||||
|
||||
// Check minimum dimensions
|
||||
if ($width < 200 || $height < 200) {
|
||||
$errors[] = __('Image must be at least 200x200 pixels.', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
// Check recommended dimensions
|
||||
if ($width < 600 || $height < 315) {
|
||||
$errors[] = __('Warning: Image should be at least 600x315 pixels for optimal display.', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if ($size > 8 * 1024 * 1024) { // 8MB
|
||||
$errors[] = __('Image file size must be under 8MB.', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
// Check aspect ratio
|
||||
$ratio = $width / $height;
|
||||
if ($ratio < 1.5 || $ratio > 2.5) {
|
||||
$errors[] = __('Warning: Recommended aspect ratio is 1.91:1 (1200x630 pixels).', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin settings
|
||||
*/
|
||||
public function register_settings() {
|
||||
register_setting(
|
||||
'tigerstyle_heat_facebook',
|
||||
$this->option_name,
|
||||
array(
|
||||
'sanitize_callback' => array($this, 'sanitize_settings')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize settings
|
||||
*/
|
||||
public function sanitize_settings($settings) {
|
||||
$clean_settings = array();
|
||||
|
||||
// Boolean settings
|
||||
$boolean_fields = array('enable_open_graph', 'enable_article_tags', 'enable_video_tags', 'enable_crawler_optimization');
|
||||
foreach ($boolean_fields as $field) {
|
||||
$clean_settings[$field] = !empty($settings[$field]);
|
||||
}
|
||||
|
||||
// Text settings
|
||||
$text_fields = array('app_id', 'admins', 'domain_verification', 'site_name', 'default_image');
|
||||
foreach ($text_fields as $field) {
|
||||
$clean_settings[$field] = sanitize_text_field($settings[$field] ?? '');
|
||||
}
|
||||
|
||||
// Numeric settings
|
||||
$clean_settings['image_width'] = absint($settings['image_width'] ?? 1200);
|
||||
$clean_settings['image_height'] = absint($settings['image_height'] ?? 630);
|
||||
|
||||
return $clean_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes for testing
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
register_rest_route('tigerstyle-heat/v1', '/facebook/debug', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'debug_open_graph'),
|
||||
'permission_callback' => function() {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug Open Graph data (REST endpoint)
|
||||
*/
|
||||
public function debug_open_graph($request) {
|
||||
$url = $request->get_param('url');
|
||||
|
||||
if (empty($url)) {
|
||||
$url = home_url();
|
||||
}
|
||||
|
||||
// Get Open Graph data for the URL
|
||||
$og_data = $this->get_open_graph_data();
|
||||
|
||||
return rest_ensure_response(array(
|
||||
'url' => $url,
|
||||
'open_graph_data' => $og_data,
|
||||
'facebook_debugger_url' => 'https://developers.facebook.com/tools/debug/?q=' . urlencode($url),
|
||||
'validation_notes' => $this->get_validation_notes($og_data)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation notes for Open Graph data
|
||||
*/
|
||||
private function get_validation_notes($og_data) {
|
||||
$notes = array();
|
||||
|
||||
// Check required fields
|
||||
$required_fields = array('og:title', 'og:description', 'og:image', 'og:url');
|
||||
foreach ($required_fields as $field) {
|
||||
if (empty($og_data[$field])) {
|
||||
$notes[] = sprintf(__('Missing required field: %s', 'tigerstyle-heat'), $field);
|
||||
}
|
||||
}
|
||||
|
||||
// Check image
|
||||
if (!empty($og_data['og:image'])) {
|
||||
$image_errors = $this->validate_image($og_data['og:image']);
|
||||
$notes = array_merge($notes, $image_errors);
|
||||
}
|
||||
|
||||
return $notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Facebook webmaster tools URLs
|
||||
*/
|
||||
public function get_webmaster_tools_urls() {
|
||||
return array(
|
||||
'sharing_debugger' => 'https://developers.facebook.com/tools/debug/',
|
||||
'webmaster_tool' => 'https://developers.facebook.com/webmaster',
|
||||
'domain_verification' => 'https://business.facebook.com/settings/brand-safety',
|
||||
'best_practices' => 'https://developers.facebook.com/docs/sharing/best-practices',
|
||||
'open_graph_docs' => 'https://developers.facebook.com/docs/sharing/webmasters'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
// Handle form submission
|
||||
if (isset($_POST['submit_facebook_settings']) && wp_verify_nonce($_POST['facebook_nonce'], 'facebook_settings')) {
|
||||
$this->handle_settings_update();
|
||||
}
|
||||
|
||||
$settings = $this->get_settings();
|
||||
$webmaster_urls = $this->get_webmaster_tools_urls();
|
||||
?>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Facebook Integration & Open Graph Optimization', 'tigerstyle-heat'); ?></h3>
|
||||
<p><?php _e('Configure Facebook Open Graph tags, domain verification, and sharing optimization for better social media engagement.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<div class="seo-setup-steps">
|
||||
<h4><?php _e('Quick Setup Steps:', 'tigerstyle-heat'); ?></h4>
|
||||
<ol>
|
||||
<li><?php _e('Enable Open Graph tags below', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Set a default sharing image (1200x630px recommended)', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Add your Facebook App ID and admin IDs', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Configure domain verification meta tag', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Test your pages using Facebook\'s Sharing Debugger', 'tigerstyle-heat'); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<strong><?php _e('Facebook Webmaster Tools:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin-left: 20px;">
|
||||
<li><a href="<?php echo esc_url($webmaster_urls['sharing_debugger']); ?>" target="_blank"><?php _e('Sharing Debugger', 'tigerstyle-heat'); ?></a> - <?php _e('Test and refresh your Open Graph data', 'tigerstyle-heat'); ?></li>
|
||||
<li><a href="<?php echo esc_url($webmaster_urls['webmaster_tool']); ?>" target="_blank"><?php _e('Webmaster Tool', 'tigerstyle-heat'); ?></a> - <?php _e('Monitor domain crawling and sharing metrics', 'tigerstyle-heat'); ?></li>
|
||||
<li><a href="<?php echo esc_url($webmaster_urls['domain_verification']); ?>" target="_blank"><?php _e('Domain Verification', 'tigerstyle-heat'); ?></a> - <?php _e('Verify your domain ownership', 'tigerstyle-heat'); ?></li>
|
||||
<li><a href="<?php echo esc_url($webmaster_urls['best_practices']); ?>" target="_blank"><?php _e('Best Practices Guide', 'tigerstyle-heat'); ?></a> - <?php _e('Official Facebook sharing guidelines', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('facebook_settings', 'facebook_nonce'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enable Open Graph Tags', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_open_graph" value="1" <?php checked($settings['enable_open_graph']); ?> />
|
||||
<?php _e('Generate Open Graph meta tags for Facebook sharing', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Automatically adds og:title, og:description, og:image, and other Open Graph tags to your pages.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Default Sharing Image', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="url" name="default_image" value="<?php echo esc_attr($settings['default_image']); ?>" class="regular-text" placeholder="https://example.com/image.jpg" />
|
||||
<p class="description">
|
||||
<?php _e('Fallback image when no featured image is set. Recommended: 1200x630px, under 8MB.', 'tigerstyle-heat'); ?><br>
|
||||
<strong><?php _e('Facebook Image Requirements:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin: 5px 0 0 20px;">
|
||||
<li><?php _e('Minimum: 200x200px', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Recommended: 1200x630px (1.91:1 ratio)', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Maximum file size: 8MB', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Supported formats: JPG, PNG, GIF', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Image Dimensions', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="number" name="image_width" value="<?php echo esc_attr($settings['image_width']); ?>" min="200" max="2048" style="width: 80px;" /> x
|
||||
<input type="number" name="image_height" value="<?php echo esc_attr($settings['image_height']); ?>" min="200" max="2048" style="width: 80px;" /> px
|
||||
<p class="description"><?php _e('Default dimensions for your sharing images. Facebook recommends 1200x630px.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Site Name', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="site_name" value="<?php echo esc_attr($settings['site_name']); ?>" class="regular-text" placeholder="<?php echo esc_attr(get_bloginfo('name')); ?>" />
|
||||
<p class="description"><?php _e('The name of your website. Leave blank to use your site title.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Facebook App ID', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="app_id" value="<?php echo esc_attr($settings['app_id']); ?>" class="regular-text" placeholder="1234567890123456" />
|
||||
<p class="description">
|
||||
<?php _e('Your Facebook App ID for analytics and insights.', 'tigerstyle-heat'); ?>
|
||||
<a href="https://developers.facebook.com/apps/" target="_blank"><?php _e('Create an app', 'tigerstyle-heat'); ?></a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Facebook Admin IDs', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="admins" value="<?php echo esc_attr($settings['admins']); ?>" class="regular-text" placeholder="123456789,987654321" />
|
||||
<p class="description">
|
||||
<?php _e('Comma-separated list of Facebook user IDs who can moderate your content.', 'tigerstyle-heat'); ?>
|
||||
<a href="https://findmyfbid.com/" target="_blank"><?php _e('Find your Facebook ID', 'tigerstyle-heat'); ?></a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Domain Verification', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="domain_verification" value="<?php echo esc_attr($settings['domain_verification']); ?>" class="regular-text" placeholder="abc123def456ghi789" />
|
||||
<p class="description">
|
||||
<?php _e('Domain verification meta tag content from Facebook Business Manager.', 'tigerstyle-heat'); ?><br>
|
||||
<?php _e('Go to Business Settings > Brand Safety > Domains to get your verification code.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Article Tags', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_article_tags" value="1" <?php checked($settings['enable_article_tags']); ?> />
|
||||
<?php _e('Add article-specific Open Graph tags (author, publish date, categories)', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Includes article:author, article:published_time, article:section, and article:tag for blog posts.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Crawler Optimization', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_crawler_optimization" value="1" <?php checked($settings['enable_crawler_optimization']); ?> />
|
||||
<?php _e('Optimize for Facebook crawlers (FacebookExternalHit)', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Ensures Open Graph tags are placed within the first 1MB of your page content as recommended by Facebook.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="margin-top: 20px; padding: 20px; background: #f0f8ff; border: 1px solid #cce7ff; border-radius: 5px;">
|
||||
<h4><?php _e('Facebook Crawler Information', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Facebook uses several crawlers to index your content:', 'tigerstyle-heat'); ?></p>
|
||||
<ul style="margin-left: 20px;">
|
||||
<li><strong>facebookexternalhit/1.1</strong> - <?php _e('Main sharing crawler for Open Graph data', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong>meta-webindexer/1.1</strong> - <?php _e('AI search indexing crawler', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong>meta-externalads/1.1</strong> - <?php _e('Advertising and business products', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
|
||||
<h4 style="margin-top: 15px;"><?php _e('Testing Your Setup', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('After saving these settings, test your pages:', 'tigerstyle-heat'); ?></p>
|
||||
<ol style="margin-left: 20px;">
|
||||
<li><?php _e('Visit the', 'tigerstyle-heat'); ?> <a href="<?php echo esc_url($webmaster_urls['sharing_debugger']); ?>" target="_blank"><?php _e('Facebook Sharing Debugger', 'tigerstyle-heat'); ?></a></li>
|
||||
<li><?php _e('Enter your page URL to see how it will appear when shared', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Click "Scrape Again" to refresh the data after making changes', 'tigerstyle-heat'); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<?php submit_button(__('Save Facebook Settings', 'tigerstyle-heat'), 'primary', 'submit_facebook_settings'); ?>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle settings update
|
||||
*/
|
||||
private function handle_settings_update() {
|
||||
// Validate and save settings
|
||||
$new_settings = array();
|
||||
|
||||
// Boolean settings
|
||||
$new_settings['enable_open_graph'] = !empty($_POST['enable_open_graph']);
|
||||
$new_settings['enable_article_tags'] = !empty($_POST['enable_article_tags']);
|
||||
$new_settings['enable_crawler_optimization'] = !empty($_POST['enable_crawler_optimization']);
|
||||
|
||||
// Text settings
|
||||
$new_settings['default_image'] = esc_url_raw($_POST['default_image'] ?? '');
|
||||
$new_settings['app_id'] = sanitize_text_field($_POST['app_id'] ?? '');
|
||||
$new_settings['admins'] = sanitize_text_field($_POST['admins'] ?? '');
|
||||
$new_settings['domain_verification'] = sanitize_text_field($_POST['domain_verification'] ?? '');
|
||||
$new_settings['site_name'] = sanitize_text_field($_POST['site_name'] ?? '');
|
||||
|
||||
// Numeric settings
|
||||
$new_settings['image_width'] = absint($_POST['image_width'] ?? 1200);
|
||||
$new_settings['image_height'] = absint($_POST['image_height'] ?? 630);
|
||||
|
||||
// Validate image if provided
|
||||
if (!empty($new_settings['default_image'])) {
|
||||
$image_errors = $this->validate_image($new_settings['default_image']);
|
||||
if (!empty($image_errors)) {
|
||||
// Show validation warnings
|
||||
add_action('admin_notices', function() use ($image_errors) {
|
||||
echo '<div class="notice notice-warning"><p><strong>' . __('Image Validation Warnings:', 'tigerstyle-heat') . '</strong></p><ul>';
|
||||
foreach ($image_errors as $error) {
|
||||
echo '<li>' . esc_html($error) . '</li>';
|
||||
}
|
||||
echo '</ul></div>';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings
|
||||
$this->update_settings($new_settings);
|
||||
|
||||
// Redirect with success message
|
||||
wp_redirect(add_query_arg('message', 'facebook_updated', wp_get_referer()));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
742
includes/modules/class-gltf-metadata.php
Normal file
742
includes/modules/class-gltf-metadata.php
Normal file
@ -0,0 +1,742 @@
|
||||
<?php
|
||||
/**
|
||||
* glTF 3D Asset Metadata Extraction Module for TigerStyle Heat
|
||||
* Implements glTF 2.0 file format detection, validation, and metadata extraction
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_glTF_Metadata {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Supported glTF file extensions
|
||||
*/
|
||||
private $supported_extensions = array('gltf', 'glb');
|
||||
|
||||
/**
|
||||
* glTF MIME types
|
||||
*/
|
||||
private $mime_types = array(
|
||||
'gltf' => 'model/gltf+json',
|
||||
'glb' => 'model/gltf-binary'
|
||||
);
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module
|
||||
*/
|
||||
private function init() {
|
||||
// Media handling hooks
|
||||
add_filter('wp_check_filetype_and_ext', array($this, 'check_gltf_filetype'), 10, 4);
|
||||
add_filter('upload_mimes', array($this, 'add_gltf_mime_types'));
|
||||
add_action('add_attachment', array($this, 'process_gltf_attachment'));
|
||||
add_action('edit_attachment', array($this, 'process_gltf_attachment'));
|
||||
|
||||
// Admin hooks
|
||||
if (is_admin()) {
|
||||
add_filter('attachment_fields_to_edit', array($this, 'add_gltf_attachment_fields'), 10, 2);
|
||||
add_filter('attachment_fields_to_save', array($this, 'save_gltf_attachment_fields'), 10, 2);
|
||||
}
|
||||
|
||||
// Frontend hooks
|
||||
add_filter('wp_get_attachment_metadata', array($this, 'enhance_gltf_metadata'), 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file is a valid glTF file
|
||||
*/
|
||||
public function check_gltf_filetype($data, $file, $filename, $mimes) {
|
||||
error_log('TigerStyle Heat: check_gltf_filetype called for: ' . $filename);
|
||||
|
||||
$wp_filetype = wp_check_filetype($filename, $mimes);
|
||||
$ext = $wp_filetype['ext'];
|
||||
$type = $wp_filetype['type'];
|
||||
|
||||
error_log('TigerStyle Heat: wp_check_filetype ext=' . $ext . ', type=' . $type);
|
||||
|
||||
if (in_array($ext, $this->supported_extensions)) {
|
||||
error_log('TigerStyle Heat: File extension supported, validating: ' . $file);
|
||||
|
||||
// Validate glTF file structure - pass the extension since temp files don't have extensions
|
||||
if ($this->validate_gltf_file_with_extension($file, $ext)) {
|
||||
error_log('TigerStyle Heat: File validation passed');
|
||||
$data['ext'] = $ext;
|
||||
$data['type'] = $this->mime_types[$ext];
|
||||
} else {
|
||||
error_log('TigerStyle Heat: File validation failed');
|
||||
// Invalid glTF file
|
||||
$data['ext'] = false;
|
||||
$data['type'] = false;
|
||||
}
|
||||
} else {
|
||||
error_log('TigerStyle Heat: File extension not supported: ' . $ext);
|
||||
}
|
||||
|
||||
error_log('TigerStyle Heat: Final data: ' . print_r($data, true));
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add glTF MIME types to WordPress
|
||||
*/
|
||||
public function add_gltf_mime_types($mimes) {
|
||||
// Debug logging
|
||||
error_log('TigerStyle Heat: Adding glTF MIME types');
|
||||
|
||||
$mimes['gltf'] = 'model/gltf+json';
|
||||
$mimes['glb'] = 'model/gltf-binary';
|
||||
|
||||
// Log the updated mimes array
|
||||
error_log('TigerStyle Heat: Updated MIME types: ' . print_r($mimes, true));
|
||||
|
||||
return $mimes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate glTF file structure with known extension
|
||||
*/
|
||||
private function validate_gltf_file_with_extension($file_path, $extension) {
|
||||
error_log('TigerStyle Heat: validate_gltf_file_with_extension called with: ' . $file_path . ', ext: ' . $extension);
|
||||
|
||||
if (!file_exists($file_path)) {
|
||||
error_log('TigerStyle Heat: File does not exist: ' . $file_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($extension === 'gltf') {
|
||||
error_log('TigerStyle Heat: Validating as glTF JSON');
|
||||
return $this->validate_gltf_json($file_path);
|
||||
} elseif ($extension === 'glb') {
|
||||
error_log('TigerStyle Heat: Validating as GLB binary');
|
||||
return $this->validate_glb_binary($file_path);
|
||||
}
|
||||
|
||||
error_log('TigerStyle Heat: Unknown extension: ' . $extension);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate glTF file structure (legacy method - kept for compatibility)
|
||||
*/
|
||||
private function validate_gltf_file($file_path) {
|
||||
error_log('TigerStyle Heat: validate_gltf_file called with: ' . $file_path);
|
||||
|
||||
if (!file_exists($file_path)) {
|
||||
error_log('TigerStyle Heat: File does not exist: ' . $file_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
|
||||
error_log('TigerStyle Heat: File extension detected: ' . $extension);
|
||||
|
||||
if ($extension === 'gltf') {
|
||||
error_log('TigerStyle Heat: Validating as glTF JSON');
|
||||
return $this->validate_gltf_json($file_path);
|
||||
} elseif ($extension === 'glb') {
|
||||
error_log('TigerStyle Heat: Validating as GLB binary');
|
||||
return $this->validate_glb_binary($file_path);
|
||||
}
|
||||
|
||||
error_log('TigerStyle Heat: Unknown extension: ' . $extension);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate glTF JSON file
|
||||
*/
|
||||
private function validate_gltf_json($file_path) {
|
||||
$content = file_get_contents($file_path);
|
||||
if ($content === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$json = json_decode($content, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for required glTF structure
|
||||
return $this->validate_gltf_structure($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate GLB binary file
|
||||
*/
|
||||
private function validate_glb_binary($file_path) {
|
||||
error_log('TigerStyle Heat: validate_glb_binary called');
|
||||
|
||||
$handle = fopen($file_path, 'rb');
|
||||
if (!$handle) {
|
||||
error_log('TigerStyle Heat: Could not open file for reading');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read GLB header (12 bytes)
|
||||
$header = fread($handle, 12);
|
||||
if (strlen($header) < 12) {
|
||||
error_log('TigerStyle Heat: Header too short: ' . strlen($header) . ' bytes');
|
||||
fclose($handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
error_log('TigerStyle Heat: Header read successfully, length: ' . strlen($header));
|
||||
|
||||
// Check magic number (first 4 bytes should be "glTF")
|
||||
$magic = substr($header, 0, 4);
|
||||
if ($magic !== 'glTF') {
|
||||
fclose($handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check version (bytes 4-7, should be 2 for glTF 2.0)
|
||||
$version = unpack('V', substr($header, 4, 4))[1];
|
||||
if ($version !== 2) {
|
||||
fclose($handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate glTF JSON structure
|
||||
*/
|
||||
private function validate_gltf_structure($json) {
|
||||
// Check required properties
|
||||
if (!isset($json['asset'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check asset version
|
||||
if (!isset($json['asset']['version']) || $json['asset']['version'] !== '2.0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic structure validation
|
||||
$required_arrays = array('scenes', 'nodes', 'meshes', 'accessors', 'bufferViews', 'buffers');
|
||||
foreach ($required_arrays as $array_name) {
|
||||
if (isset($json[$array_name]) && !is_array($json[$array_name])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract metadata from glTF file
|
||||
*/
|
||||
public function extract_gltf_metadata($file_path) {
|
||||
if (!$this->validate_gltf_file($file_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
|
||||
$metadata = array(
|
||||
'file_format' => $extension,
|
||||
'mime_type' => $this->mime_types[$extension],
|
||||
'file_size' => filesize($file_path),
|
||||
'version' => '2.0'
|
||||
);
|
||||
|
||||
if ($extension === 'gltf') {
|
||||
$gltf_data = $this->extract_gltf_json_metadata($file_path);
|
||||
} else {
|
||||
$gltf_data = $this->extract_glb_metadata($file_path);
|
||||
}
|
||||
|
||||
return array_merge($metadata, $gltf_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract metadata from glTF JSON file
|
||||
*/
|
||||
private function extract_gltf_json_metadata($file_path) {
|
||||
$content = file_get_contents($file_path);
|
||||
$json = json_decode($content, true);
|
||||
|
||||
if (!$json) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $this->parse_gltf_json($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract metadata from GLB binary file
|
||||
*/
|
||||
private function extract_glb_metadata($file_path) {
|
||||
$handle = fopen($file_path, 'rb');
|
||||
if (!$handle) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Skip header (12 bytes)
|
||||
fseek($handle, 12);
|
||||
|
||||
// Read first chunk header (8 bytes)
|
||||
$chunk_header = fread($handle, 8);
|
||||
if (strlen($chunk_header) < 8) {
|
||||
fclose($handle);
|
||||
return array();
|
||||
}
|
||||
|
||||
$chunk_length = unpack('V', substr($chunk_header, 0, 4))[1];
|
||||
$chunk_type = substr($chunk_header, 4, 4);
|
||||
|
||||
// First chunk should be JSON
|
||||
if ($chunk_type !== 'JSON') {
|
||||
fclose($handle);
|
||||
return array();
|
||||
}
|
||||
|
||||
// Read JSON data
|
||||
$json_data = fread($handle, $chunk_length);
|
||||
fclose($handle);
|
||||
|
||||
$json = json_decode($json_data, true);
|
||||
if (!$json) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $this->parse_gltf_json($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse glTF JSON structure and extract metadata
|
||||
*/
|
||||
private function parse_gltf_json($json) {
|
||||
$metadata = array();
|
||||
|
||||
// Asset information
|
||||
if (isset($json['asset'])) {
|
||||
$asset = $json['asset'];
|
||||
$metadata['version'] = $asset['version'] ?? '2.0';
|
||||
$metadata['generator'] = $asset['generator'] ?? '';
|
||||
$metadata['copyright'] = $asset['copyright'] ?? '';
|
||||
$metadata['min_version'] = $asset['minVersion'] ?? '';
|
||||
}
|
||||
|
||||
// Scene information
|
||||
$metadata['scene_count'] = isset($json['scenes']) ? count($json['scenes']) : 0;
|
||||
$metadata['node_count'] = isset($json['nodes']) ? count($json['nodes']) : 0;
|
||||
|
||||
// Mesh information
|
||||
$metadata['mesh_count'] = isset($json['meshes']) ? count($json['meshes']) : 0;
|
||||
$metadata['primitive_count'] = $this->count_primitives($json);
|
||||
$metadata['vertex_count'] = $this->estimate_vertex_count($json);
|
||||
|
||||
// Material information
|
||||
$metadata['material_count'] = isset($json['materials']) ? count($json['materials']) : 0;
|
||||
$metadata['texture_count'] = isset($json['textures']) ? count($json['textures']) : 0;
|
||||
$metadata['image_count'] = isset($json['images']) ? count($json['images']) : 0;
|
||||
|
||||
// Animation information
|
||||
$metadata['animation_count'] = isset($json['animations']) ? count($json['animations']) : 0;
|
||||
$metadata['has_animations'] = $metadata['animation_count'] > 0;
|
||||
|
||||
// Extension information
|
||||
$metadata['extensions_used'] = $json['extensionsUsed'] ?? array();
|
||||
$metadata['extensions_required'] = $json['extensionsRequired'] ?? array();
|
||||
|
||||
// Buffer information
|
||||
$metadata['buffer_count'] = isset($json['buffers']) ? count($json['buffers']) : 0;
|
||||
$metadata['total_buffer_size'] = $this->calculate_total_buffer_size($json);
|
||||
|
||||
// Accessor information
|
||||
$metadata['accessor_count'] = isset($json['accessors']) ? count($json['accessors']) : 0;
|
||||
$metadata['buffer_view_count'] = isset($json['bufferViews']) ? count($json['bufferViews']) : 0;
|
||||
|
||||
// Camera information
|
||||
$metadata['camera_count'] = isset($json['cameras']) ? count($json['cameras']) : 0;
|
||||
|
||||
// Light information (if KHR_lights_punctual extension is used)
|
||||
if (isset($json['extensions']['KHR_lights_punctual']['lights'])) {
|
||||
$metadata['light_count'] = count($json['extensions']['KHR_lights_punctual']['lights']);
|
||||
} else {
|
||||
$metadata['light_count'] = 0;
|
||||
}
|
||||
|
||||
// Complexity assessment
|
||||
$metadata['complexity_score'] = $this->calculate_complexity_score($metadata);
|
||||
$metadata['complexity_level'] = $this->get_complexity_level($metadata['complexity_score']);
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total primitives across all meshes
|
||||
*/
|
||||
private function count_primitives($json) {
|
||||
$total = 0;
|
||||
if (isset($json['meshes'])) {
|
||||
foreach ($json['meshes'] as $mesh) {
|
||||
if (isset($mesh['primitives'])) {
|
||||
$total += count($mesh['primitives']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate total vertex count
|
||||
*/
|
||||
private function estimate_vertex_count($json) {
|
||||
$total = 0;
|
||||
if (isset($json['meshes']) && isset($json['accessors'])) {
|
||||
foreach ($json['meshes'] as $mesh) {
|
||||
if (isset($mesh['primitives'])) {
|
||||
foreach ($mesh['primitives'] as $primitive) {
|
||||
if (isset($primitive['attributes']['POSITION'])) {
|
||||
$accessor_index = $primitive['attributes']['POSITION'];
|
||||
if (isset($json['accessors'][$accessor_index])) {
|
||||
$total += $json['accessors'][$accessor_index]['count'] ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total buffer size
|
||||
*/
|
||||
private function calculate_total_buffer_size($json) {
|
||||
$total = 0;
|
||||
if (isset($json['buffers'])) {
|
||||
foreach ($json['buffers'] as $buffer) {
|
||||
$total += $buffer['byteLength'] ?? 0;
|
||||
}
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate complexity score for the 3D model
|
||||
*/
|
||||
private function calculate_complexity_score($metadata) {
|
||||
$score = 0;
|
||||
|
||||
// Mesh complexity
|
||||
$score += $metadata['mesh_count'] * 10;
|
||||
$score += $metadata['primitive_count'] * 5;
|
||||
$score += ($metadata['vertex_count'] / 1000) * 2; // Per thousand vertices
|
||||
|
||||
// Material complexity
|
||||
$score += $metadata['material_count'] * 8;
|
||||
$score += $metadata['texture_count'] * 12;
|
||||
|
||||
// Animation complexity
|
||||
$score += $metadata['animation_count'] * 15;
|
||||
|
||||
// Extension complexity
|
||||
$score += count($metadata['extensions_used']) * 5;
|
||||
$score += count($metadata['extensions_required']) * 10;
|
||||
|
||||
// Buffer size impact
|
||||
$score += ($metadata['total_buffer_size'] / (1024 * 1024)) * 3; // Per MB
|
||||
|
||||
return round($score);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complexity level based on score
|
||||
*/
|
||||
private function get_complexity_level($score) {
|
||||
if ($score < 50) {
|
||||
return 'Low';
|
||||
} elseif ($score < 150) {
|
||||
return 'Medium';
|
||||
} elseif ($score < 300) {
|
||||
return 'High';
|
||||
} else {
|
||||
return 'Very High';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process glTF attachment and extract metadata
|
||||
*/
|
||||
public function process_gltf_attachment($attachment_id) {
|
||||
$file_path = get_attached_file($attachment_id);
|
||||
if (!$file_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, $this->supported_extensions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract glTF metadata
|
||||
$gltf_metadata = $this->extract_gltf_metadata($file_path);
|
||||
if ($gltf_metadata) {
|
||||
// Store metadata as post meta
|
||||
foreach ($gltf_metadata as $key => $value) {
|
||||
update_post_meta($attachment_id, '_gltf_' . $key, $value);
|
||||
}
|
||||
|
||||
// Store structured metadata
|
||||
update_post_meta($attachment_id, '_gltf_metadata', $gltf_metadata);
|
||||
|
||||
// Set attachment as 3D model
|
||||
update_post_meta($attachment_id, '_wp_attachment_image_alt', '3D Model');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance WordPress attachment metadata with glTF data
|
||||
*/
|
||||
public function enhance_gltf_metadata($metadata, $attachment_id) {
|
||||
$file_path = get_attached_file($attachment_id);
|
||||
if (!$file_path) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, $this->supported_extensions)) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
// Get cached glTF metadata
|
||||
$gltf_metadata = get_post_meta($attachment_id, '_gltf_metadata', true);
|
||||
if (!$gltf_metadata) {
|
||||
// Extract and cache metadata
|
||||
$gltf_metadata = $this->extract_gltf_metadata($file_path);
|
||||
if ($gltf_metadata) {
|
||||
update_post_meta($attachment_id, '_gltf_metadata', $gltf_metadata);
|
||||
}
|
||||
}
|
||||
|
||||
if ($gltf_metadata) {
|
||||
$metadata['gltf'] = $gltf_metadata;
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add glTF-specific fields to attachment edit screen
|
||||
*/
|
||||
public function add_gltf_attachment_fields($form_fields, $post) {
|
||||
$file_path = get_attached_file($post->ID);
|
||||
if (!$file_path) {
|
||||
return $form_fields;
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, $this->supported_extensions)) {
|
||||
return $form_fields;
|
||||
}
|
||||
|
||||
$gltf_metadata = get_post_meta($post->ID, '_gltf_metadata', true);
|
||||
if (!$gltf_metadata) {
|
||||
return $form_fields;
|
||||
}
|
||||
|
||||
// Add 3D model information fields
|
||||
$form_fields['gltf_info'] = array(
|
||||
'label' => __('3D Model Information', 'tigerstyle-heat'),
|
||||
'input' => 'html',
|
||||
'html' => $this->render_gltf_info_html($gltf_metadata),
|
||||
'helps' => __('Technical information about this glTF 3D model.', 'tigerstyle-heat')
|
||||
);
|
||||
|
||||
// Add custom license field
|
||||
$license = get_post_meta($post->ID, '_gltf_license', true);
|
||||
$form_fields['gltf_license'] = array(
|
||||
'label' => __('3D Model License', 'tigerstyle-heat'),
|
||||
'input' => 'text',
|
||||
'value' => $license,
|
||||
'helps' => __('License information for this 3D model (e.g., CC BY 4.0, MIT, etc.)', 'tigerstyle-heat')
|
||||
);
|
||||
|
||||
// Add creator field
|
||||
$creator = get_post_meta($post->ID, '_gltf_creator', true);
|
||||
$form_fields['gltf_creator'] = array(
|
||||
'label' => __('3D Model Creator', 'tigerstyle-heat'),
|
||||
'input' => 'text',
|
||||
'value' => $creator,
|
||||
'helps' => __('Name of the person or organization who created this 3D model.', 'tigerstyle-heat')
|
||||
);
|
||||
|
||||
return $form_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save glTF-specific attachment fields
|
||||
*/
|
||||
public function save_gltf_attachment_fields($post, $attachment) {
|
||||
if (isset($attachment['gltf_license'])) {
|
||||
update_post_meta($post['ID'], '_gltf_license', sanitize_text_field($attachment['gltf_license']));
|
||||
}
|
||||
|
||||
if (isset($attachment['gltf_creator'])) {
|
||||
update_post_meta($post['ID'], '_gltf_creator', sanitize_text_field($attachment['gltf_creator']));
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render glTF information HTML for admin
|
||||
*/
|
||||
private function render_gltf_info_html($metadata) {
|
||||
ob_start();
|
||||
?>
|
||||
<div style="background: #f9f9f9; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<h4 style="margin-top: 0;"><?php _e('glTF Technical Details', 'tigerstyle-heat'); ?></h4>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
||||
<div>
|
||||
<strong><?php _e('Format:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['file_format']); ?><br>
|
||||
<strong><?php _e('Version:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['version']); ?><br>
|
||||
<strong><?php _e('File Size:', 'tigerstyle-heat'); ?></strong> <?php echo size_format($metadata['file_size']); ?><br>
|
||||
<strong><?php _e('Complexity:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['complexity_level']); ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong><?php _e('Meshes:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['mesh_count']); ?><br>
|
||||
<strong><?php _e('Materials:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['material_count']); ?><br>
|
||||
<strong><?php _e('Textures:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['texture_count']); ?><br>
|
||||
<strong><?php _e('Animations:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['animation_count']); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($metadata['vertex_count'])): ?>
|
||||
<p><strong><?php _e('Vertices:', 'tigerstyle-heat'); ?></strong> <?php echo number_format($metadata['vertex_count']); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($metadata['generator'])): ?>
|
||||
<p><strong><?php _e('Generated by:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['generator']); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($metadata['extensions_used'])): ?>
|
||||
<p><strong><?php _e('Extensions Used:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html(implode(', ', $metadata['extensions_used'])); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get structured data for glTF 3D model
|
||||
*/
|
||||
public function get_gltf_structured_data($attachment_id) {
|
||||
$post = get_post($attachment_id);
|
||||
if (!$post || $post->post_type !== 'attachment') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file_path = get_attached_file($attachment_id);
|
||||
$file_url = wp_get_attachment_url($attachment_id);
|
||||
$gltf_metadata = get_post_meta($attachment_id, '_gltf_metadata', true);
|
||||
|
||||
if (!$file_url || !$gltf_metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$schema = array(
|
||||
'@type' => '3DModel',
|
||||
'contentUrl' => $file_url,
|
||||
'encodingFormat' => $gltf_metadata['mime_type'],
|
||||
'name' => $post->post_title ?: basename($file_url),
|
||||
'description' => $post->post_content,
|
||||
'caption' => $post->post_excerpt
|
||||
);
|
||||
|
||||
// Add technical metadata
|
||||
$schema['fileSize'] = $gltf_metadata['file_size'];
|
||||
$schema['fileFormat'] = $gltf_metadata['file_format'];
|
||||
|
||||
// Add 3D-specific properties
|
||||
if (isset($gltf_metadata['mesh_count'])) {
|
||||
$schema['additionalProperty'] = array(
|
||||
array(
|
||||
'@type' => 'PropertyValue',
|
||||
'name' => 'meshCount',
|
||||
'value' => $gltf_metadata['mesh_count']
|
||||
),
|
||||
array(
|
||||
'@type' => 'PropertyValue',
|
||||
'name' => 'materialCount',
|
||||
'value' => $gltf_metadata['material_count']
|
||||
),
|
||||
array(
|
||||
'@type' => 'PropertyValue',
|
||||
'name' => 'complexityLevel',
|
||||
'value' => $gltf_metadata['complexity_level']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Add license information
|
||||
$license = get_post_meta($attachment_id, '_gltf_license', true);
|
||||
if ($license) {
|
||||
$schema['license'] = $license;
|
||||
}
|
||||
|
||||
// Add creator information
|
||||
$creator = get_post_meta($attachment_id, '_gltf_creator', true);
|
||||
if ($creator) {
|
||||
$schema['creator'] = array(
|
||||
'@type' => 'Person',
|
||||
'name' => $creator
|
||||
);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if attachment is a glTF 3D model
|
||||
*/
|
||||
public function is_gltf_attachment($attachment_id) {
|
||||
$file_path = get_attached_file($attachment_id);
|
||||
if (!$file_path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
|
||||
return in_array($extension, $this->supported_extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get glTF metadata for attachment
|
||||
*/
|
||||
public function get_gltf_metadata($attachment_id) {
|
||||
if (!$this->is_gltf_attachment($attachment_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_post_meta($attachment_id, '_gltf_metadata', true);
|
||||
}
|
||||
}
|
||||
618
includes/modules/class-google-setup.php
Normal file
618
includes/modules/class-google-setup.php
Normal file
@ -0,0 +1,618 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Setup Module for TigerStyle Heat
|
||||
*
|
||||
* Handles Google Analytics, Google Search Console, Google Tag Manager,
|
||||
* and other Google service integrations
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Google_setup {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module
|
||||
*/
|
||||
private function init() {
|
||||
// Frontend hooks for injecting tracking codes
|
||||
add_action('wp_head', array($this, 'inject_google_analytics'), 2);
|
||||
add_action('wp_head', array($this, 'inject_google_tag_manager_head'), 1);
|
||||
add_action('wp_body_open', array($this, 'inject_google_tag_manager_body'));
|
||||
add_action('wp_head', array($this, 'inject_site_verification'), 3);
|
||||
|
||||
// Admin hooks
|
||||
if (is_admin()) {
|
||||
add_action('admin_post_update_google_setup', array($this, 'handle_form_submission'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission
|
||||
*/
|
||||
public function handle_form_submission() {
|
||||
// Verify nonce
|
||||
if (!wp_verify_nonce($_POST['google_setup_nonce'], 'update_google_setup')) {
|
||||
wp_die(__('Security check failed', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('You do not have sufficient permissions', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Sanitize and save settings
|
||||
$google_analytics_id = sanitize_text_field($_POST['google_analytics_id'] ?? '');
|
||||
$google_tag_manager_id = sanitize_text_field($_POST['google_tag_manager_id'] ?? '');
|
||||
$google_site_verification = sanitize_text_field($_POST['google_site_verification'] ?? '');
|
||||
$adsense_publisher_id = sanitize_text_field($_POST['adsense_publisher_id'] ?? '');
|
||||
$google_my_business_id = sanitize_text_field($_POST['google_my_business_id'] ?? '');
|
||||
|
||||
// Enhanced ecommerce settings
|
||||
$enhanced_ecommerce = isset($_POST['enhanced_ecommerce']) ? 1 : 0;
|
||||
$track_outbound_links = isset($_POST['track_outbound_links']) ? 1 : 0;
|
||||
$track_downloads = isset($_POST['track_downloads']) ? 1 : 0;
|
||||
|
||||
// Update options
|
||||
update_option('google_analytics_id', $google_analytics_id);
|
||||
update_option('google_tag_manager_id', $google_tag_manager_id);
|
||||
update_option('google_site_verification', $google_site_verification);
|
||||
update_option('adsense_publisher_id', $adsense_publisher_id);
|
||||
update_option('google_my_business_id', $google_my_business_id);
|
||||
update_option('google_enhanced_ecommerce', $enhanced_ecommerce);
|
||||
update_option('google_track_outbound_links', $track_outbound_links);
|
||||
update_option('google_track_downloads', $track_downloads);
|
||||
|
||||
// Redirect with success message
|
||||
wp_redirect(admin_url('admin.php?page=tigerstyle-heat&message=google_setup_updated'));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Google Analytics tracking code
|
||||
* Now with TigerStyle Whiskers consent awareness! 🐱
|
||||
*/
|
||||
public function inject_google_analytics() {
|
||||
$analytics_id = get_option('google_analytics_id', '');
|
||||
|
||||
if (empty($analytics_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check TigerStyle Whiskers consent status
|
||||
if (!$this->has_analytics_consent()) {
|
||||
// Inject consent-conditional loading
|
||||
$this->inject_consent_conditional_analytics($analytics_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's GA4 or Universal Analytics
|
||||
if (strpos($analytics_id, 'G-') === 0) {
|
||||
// GA4 implementation
|
||||
$this->inject_ga4_tracking($analytics_id);
|
||||
} elseif (strpos($analytics_id, 'UA-') === 0) {
|
||||
// Universal Analytics implementation (legacy)
|
||||
$this->inject_universal_analytics($analytics_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject GA4 tracking code
|
||||
*/
|
||||
private function inject_ga4_tracking($analytics_id) {
|
||||
$enhanced_ecommerce = get_option('google_enhanced_ecommerce', 0);
|
||||
$track_outbound = get_option('google_track_outbound_links', 0);
|
||||
$track_downloads = get_option('google_track_downloads', 0);
|
||||
|
||||
echo "\n<!-- Google Analytics GA4 by TigerStyle Heat -->\n";
|
||||
echo '<script async src="https://www.googletagmanager.com/gtag/js?id=' . esc_attr($analytics_id) . '"></script>' . "\n";
|
||||
echo '<script>' . "\n";
|
||||
echo 'window.dataLayer = window.dataLayer || [];' . "\n";
|
||||
echo 'function gtag(){dataLayer.push(arguments);}' . "\n";
|
||||
echo 'gtag("js", new Date());' . "\n";
|
||||
|
||||
// Basic configuration
|
||||
$config_params = array();
|
||||
|
||||
if ($enhanced_ecommerce) {
|
||||
$config_params[] = 'enhanced_conversions: true';
|
||||
}
|
||||
|
||||
$config_string = !empty($config_params) ? ', {' . implode(', ', $config_params) . '}' : '';
|
||||
echo 'gtag("config", "' . esc_attr($analytics_id) . '"' . $config_string . ');' . "\n";
|
||||
|
||||
// Outbound link tracking
|
||||
if ($track_outbound) {
|
||||
echo $this->get_outbound_link_tracking_script();
|
||||
}
|
||||
|
||||
// Download tracking
|
||||
if ($track_downloads) {
|
||||
echo $this->get_download_tracking_script();
|
||||
}
|
||||
|
||||
echo '</script>' . "\n";
|
||||
echo "<!-- End Google Analytics GA4 -->\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Universal Analytics tracking code (legacy)
|
||||
*/
|
||||
private function inject_universal_analytics($analytics_id) {
|
||||
echo "\n<!-- Google Universal Analytics by TigerStyle Heat -->\n";
|
||||
echo '<script async src="https://www.google-analytics.com/analytics.js"></script>' . "\n";
|
||||
echo '<script>' . "\n";
|
||||
echo 'window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;' . "\n";
|
||||
echo 'ga("create", "' . esc_attr($analytics_id) . '", "auto");' . "\n";
|
||||
|
||||
if (get_option('google_enhanced_ecommerce', 0)) {
|
||||
echo 'ga("require", "ec");' . "\n";
|
||||
}
|
||||
|
||||
echo 'ga("send", "pageview");' . "\n";
|
||||
echo '</script>' . "\n";
|
||||
echo "<!-- End Google Universal Analytics -->\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has given analytics consent via TigerStyle Whiskers
|
||||
*/
|
||||
private function has_analytics_consent() {
|
||||
// Check if TigerStyle Whiskers is active
|
||||
if (!class_exists('TigerStyleWhiskers') && !class_exists('TigerStyleWhiskers_CookieConsent')) {
|
||||
// Whiskers not active, allow analytics (backward compatibility)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check cookie consent directly from cookie
|
||||
if (isset($_COOKIE['tigerstyle_whiskers_consent'])) {
|
||||
$consent_data = json_decode(stripslashes($_COOKIE['tigerstyle_whiskers_consent']), true);
|
||||
return isset($consent_data['analytics']) && $consent_data['analytics'] === true;
|
||||
}
|
||||
|
||||
// Check via Whiskers API if available
|
||||
if (class_exists('TigerStyleWhiskers_CookieConsent')) {
|
||||
return TigerStyleWhiskers_CookieConsent::instance()->has_analytics_consent();
|
||||
}
|
||||
|
||||
// Default to no consent if Whiskers is active but no consent given
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject consent-conditional analytics that loads when consent is granted
|
||||
*/
|
||||
private function inject_consent_conditional_analytics($analytics_id) {
|
||||
echo "\n<!-- TigerStyle Heat + Whiskers: Consent-Conditional Analytics -->\n";
|
||||
echo '<script>' . "\n";
|
||||
echo 'window.tigerstyleHeatAnalyticsId = "' . esc_attr($analytics_id) . '";' . "\n";
|
||||
echo 'window.tigerstyleHeatAnalyticsConfig = {' . "\n";
|
||||
|
||||
// Pass through Heat configuration
|
||||
$enhanced_ecommerce = get_option('google_enhanced_ecommerce', 0);
|
||||
$track_outbound = get_option('google_track_outbound_links', 0);
|
||||
$track_downloads = get_option('google_track_downloads', 0);
|
||||
|
||||
echo ' enhanced_ecommerce: ' . ($enhanced_ecommerce ? 'true' : 'false') . ',' . "\n";
|
||||
echo ' track_outbound_links: ' . ($track_outbound ? 'true' : 'false') . ',' . "\n";
|
||||
echo ' track_downloads: ' . ($track_downloads ? 'true' : 'false') . "\n";
|
||||
echo '};' . "\n";
|
||||
|
||||
// Function to initialize analytics when consent is given
|
||||
echo '
|
||||
// TigerStyle Heat Analytics Initialization Function
|
||||
window.tigerstyleHeatInitAnalytics = function() {
|
||||
if (window.tigerstyleHeatAnalyticsLoaded) return; // Prevent double loading
|
||||
|
||||
var analyticsId = window.tigerstyleHeatAnalyticsId;
|
||||
var config = window.tigerstyleHeatAnalyticsConfig;
|
||||
|
||||
console.log("🔥 TigerStyle Heat: Initializing analytics with user consent!");
|
||||
|
||||
// Load gtag script
|
||||
var script = document.createElement("script");
|
||||
script.async = true;
|
||||
script.src = "https://www.googletagmanager.com/gtag/js?id=" + analyticsId;
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Initialize dataLayer and gtag
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag("js", new Date());
|
||||
|
||||
// Configure GA4
|
||||
var configParams = {};
|
||||
if (config.enhanced_ecommerce) {
|
||||
configParams.enhanced_conversions = true;
|
||||
}
|
||||
|
||||
gtag("config", analyticsId, configParams);
|
||||
|
||||
// Add tracking features based on Heat configuration
|
||||
if (config.track_outbound_links) {
|
||||
' . $this->get_outbound_link_tracking_script() . '
|
||||
}
|
||||
|
||||
if (config.track_downloads) {
|
||||
' . $this->get_download_tracking_script() . '
|
||||
}
|
||||
|
||||
// Mark as loaded
|
||||
window.tigerstyleHeatAnalyticsLoaded = true;
|
||||
|
||||
// Fire event for other plugins
|
||||
if (typeof window.CustomEvent === "function") {
|
||||
document.dispatchEvent(new CustomEvent("tigerstyleHeatAnalyticsInitialized", {
|
||||
detail: { analyticsId: analyticsId, config: config }
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for Whiskers consent changes
|
||||
document.addEventListener("tigerstyleWhiskersConsentChanged", function(event) {
|
||||
if (event.detail && event.detail.analytics === true) {
|
||||
console.log("🐱 TigerStyle Whiskers: Analytics consent granted, initializing Heat analytics!");
|
||||
window.tigerstyleHeatInitAnalytics();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if consent already exists on page load
|
||||
if (window.tigerstyleWhiskersConsent && window.tigerstyleWhiskersConsent.hasAnalyticsConsent()) {
|
||||
console.log("🔥 TigerStyle Heat: Existing analytics consent detected, initializing!");
|
||||
window.tigerstyleHeatInitAnalytics();
|
||||
}
|
||||
';
|
||||
|
||||
echo '</script>' . "\n";
|
||||
echo "<!-- End TigerStyle Heat + Whiskers Integration -->\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Google Tag Manager head code
|
||||
*/
|
||||
public function inject_google_tag_manager_head() {
|
||||
$gtm_id = get_option('google_tag_manager_id', '');
|
||||
|
||||
if (empty($gtm_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo "\n<!-- Google Tag Manager by TigerStyle Heat -->\n";
|
||||
echo '<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({\'gtm.start\':' . "\n";
|
||||
echo 'new Date().getTime(),event:\'gtm.js\'});var f=d.getElementsByTagName(s)[0],' . "\n";
|
||||
echo 'j=d.createElement(s),dl=l!=\'dataLayer\'?\'&l=\'+l:\'\';j.async=true;j.src=' . "\n";
|
||||
echo '\'https://www.googletagmanager.com/gtm.js?id=\'+i+dl;f.parentNode.insertBefore(j,f);' . "\n";
|
||||
echo '})(window,document,\'script\',\'dataLayer\',\'' . esc_attr($gtm_id) . '\');</script>' . "\n";
|
||||
echo "<!-- End Google Tag Manager -->\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Google Tag Manager body code
|
||||
*/
|
||||
public function inject_google_tag_manager_body() {
|
||||
$gtm_id = get_option('google_tag_manager_id', '');
|
||||
|
||||
if (empty($gtm_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo "\n<!-- Google Tag Manager (noscript) by TigerStyle Heat -->\n";
|
||||
echo '<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=' . esc_attr($gtm_id) . '"' . "\n";
|
||||
echo 'height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>' . "\n";
|
||||
echo "<!-- End Google Tag Manager (noscript) -->\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Google Site Verification meta tag
|
||||
*/
|
||||
public function inject_site_verification() {
|
||||
$verification_code = get_option('google_site_verification', '');
|
||||
|
||||
if (empty($verification_code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<meta name="google-site-verification" content="' . esc_attr($verification_code) . '" />' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outbound link tracking script
|
||||
*/
|
||||
private function get_outbound_link_tracking_script() {
|
||||
return '
|
||||
// Outbound link tracking
|
||||
document.addEventListener("click", function(e) {
|
||||
var link = e.target.closest("a");
|
||||
if (link && link.hostname !== window.location.hostname) {
|
||||
gtag("event", "click", {
|
||||
event_category: "outbound",
|
||||
event_label: link.href,
|
||||
transport_type: "beacon"
|
||||
});
|
||||
}
|
||||
});
|
||||
';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download tracking script
|
||||
*/
|
||||
private function get_download_tracking_script() {
|
||||
return '
|
||||
// Download tracking
|
||||
document.addEventListener("click", function(e) {
|
||||
var link = e.target.closest("a");
|
||||
if (link && link.href) {
|
||||
var filePath = link.pathname;
|
||||
var fileExtension = filePath.split(".").pop().toLowerCase();
|
||||
var downloadExtensions = ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "zip", "rar", "mp3", "mp4", "avi", "mov"];
|
||||
|
||||
if (downloadExtensions.includes(fileExtension)) {
|
||||
gtag("event", "file_download", {
|
||||
event_category: "downloads",
|
||||
event_label: link.href,
|
||||
value: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
// Get current settings
|
||||
$analytics_id = get_option('google_analytics_id', '');
|
||||
$gtm_id = get_option('google_tag_manager_id', '');
|
||||
$site_verification = get_option('google_site_verification', '');
|
||||
$adsense_id = get_option('adsense_publisher_id', '');
|
||||
$business_id = get_option('google_my_business_id', '');
|
||||
$enhanced_ecommerce = get_option('google_enhanced_ecommerce', 0);
|
||||
$track_outbound = get_option('google_track_outbound_links', 0);
|
||||
$track_downloads = get_option('google_track_downloads', 0);
|
||||
?>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Google Services Integration', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Connect your website with essential Google services for better SEO performance and analytics. This includes Google Analytics, Google Search Console, Google Tag Manager, and AdSense integration.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
|
||||
<?php if (class_exists('TigerStyleWhiskers') || class_exists('TigerStyleWhiskers_CookieConsent')): ?>
|
||||
<div style="background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%); color: white; padding: 15px; border-radius: 8px; margin-top: 15px;">
|
||||
<strong>🐱 TigerStyle Whiskers Integration Active!</strong><br>
|
||||
<small>Analytics will automatically respect user privacy consent. Users must grant analytics consent before Google Analytics loads, ensuring GDPR compliance.</small>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="update_google_setup">
|
||||
<?php wp_nonce_field('update_google_setup', 'google_setup_nonce'); ?>
|
||||
|
||||
<h2><?php _e('Analytics & Tracking', 'tigerstyle-heat'); ?></h2>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="google_analytics_id"><?php _e('Google Analytics ID', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="google_analytics_id"
|
||||
name="google_analytics_id"
|
||||
value="<?php echo esc_attr($analytics_id); ?>"
|
||||
placeholder="G-XXXXXXXXXX or UA-XXXXXXXX-X"
|
||||
class="regular-text"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Enter your Google Analytics tracking ID. Use GA4 format (G-XXXXXXXXXX) for new properties or Universal Analytics format (UA-XXXXXXXX-X) for legacy properties.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="google_tag_manager_id"><?php _e('Google Tag Manager ID', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="google_tag_manager_id"
|
||||
name="google_tag_manager_id"
|
||||
value="<?php echo esc_attr($gtm_id); ?>"
|
||||
placeholder="GTM-XXXXXXX"
|
||||
class="regular-text"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Enter your Google Tag Manager container ID (GTM-XXXXXXX). Note: This will override individual tracking codes if both are configured.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enhanced Tracking', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input type="checkbox" name="enhanced_ecommerce" value="1" <?php checked($enhanced_ecommerce); ?>>
|
||||
<?php _e('Enable Enhanced Ecommerce tracking', 'tigerstyle-heat'); ?>
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="track_outbound_links" value="1" <?php checked($track_outbound); ?>>
|
||||
<?php _e('Track outbound link clicks', 'tigerstyle-heat'); ?>
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="track_downloads" value="1" <?php checked($track_downloads); ?>>
|
||||
<?php _e('Track file downloads', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
</fieldset>
|
||||
<p class="description">
|
||||
<?php _e('Enable additional tracking features for better analytics insights.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php _e('Search Console & Verification', 'tigerstyle-heat'); ?></h2>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="google_site_verification"><?php _e('Google Site Verification', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="google_site_verification"
|
||||
name="google_site_verification"
|
||||
value="<?php echo esc_attr($site_verification); ?>"
|
||||
placeholder="aBcDeFgHiJkLmNoPqRsTuVwXyZ123456789"
|
||||
class="regular-text"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Enter your Google Search Console verification code (content value from meta tag). This enables your site in Google Search Console.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php _e('Other Google Services', 'tigerstyle-heat'); ?></h2>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="adsense_publisher_id"><?php _e('AdSense Publisher ID', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="adsense_publisher_id"
|
||||
name="adsense_publisher_id"
|
||||
value="<?php echo esc_attr($adsense_id); ?>"
|
||||
placeholder="pub-XXXXXXXXXXXXXXXX"
|
||||
class="regular-text"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Enter your Google AdSense Publisher ID for auto ads integration.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="google_my_business_id"><?php _e('Google My Business ID', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="google_my_business_id"
|
||||
name="google_my_business_id"
|
||||
value="<?php echo esc_attr($business_id); ?>"
|
||||
placeholder="ChIJXXXXXXXXXXXXXXXXXXXXXX"
|
||||
class="regular-text"
|
||||
>
|
||||
<p class="description">
|
||||
<?php _e('Enter your Google My Business Place ID for local business integration.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(__('Update Google Setup', 'tigerstyle-heat')); ?>
|
||||
</form>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Setup Instructions', 'tigerstyle-heat'); ?></h3>
|
||||
<div class="seo-setup-steps">
|
||||
<h4><?php _e('Google Analytics Setup:', 'tigerstyle-heat'); ?></h4>
|
||||
<ol>
|
||||
<li><?php _e('Visit <a href="https://analytics.google.com/" target="_blank">Google Analytics</a> and sign in', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Create a new GA4 property for your website', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Copy the Measurement ID (starts with G-) from the Data Streams section', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Paste the ID in the Google Analytics ID field above', 'tigerstyle-heat'); ?></li>
|
||||
</ol>
|
||||
|
||||
<h4><?php _e('Google Search Console Setup:', 'tigerstyle-heat'); ?></h4>
|
||||
<ol>
|
||||
<li><?php _e('Visit <a href="https://search.google.com/search-console/" target="_blank">Google Search Console</a>', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Add your property using the URL prefix method', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Select "HTML tag" verification method', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Copy the content value from the meta tag (everything between the quotes after content=)', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Paste it in the Google Site Verification field above and save', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Return to Search Console and click "Verify"', 'tigerstyle-heat'); ?></li>
|
||||
</ol>
|
||||
|
||||
<h4><?php _e('Google Tag Manager Setup:', 'tigerstyle-heat'); ?></h4>
|
||||
<ol>
|
||||
<li><?php _e('Visit <a href="https://tagmanager.google.com/" target="_blank">Google Tag Manager</a>', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Create a new container for your website', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Copy the Container ID (GTM-XXXXXXX)', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Paste it in the Google Tag Manager ID field above', 'tigerstyle-heat'); ?></li>
|
||||
</ol>
|
||||
|
||||
<h4><?php _e('Sitemap Submission:', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Once Google Search Console is verified, submit your sitemap:', 'tigerstyle-heat'); ?></p>
|
||||
<ol>
|
||||
<li><?php _e('Go to Sitemaps in the left sidebar of Search Console', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php printf(__('Enter your sitemap URL: %s', 'tigerstyle-heat'), '<code>' . home_url('/sitemap.xml') . '</code>'); ?></li>
|
||||
<li><?php _e('Click Submit to notify Google about your content', 'tigerstyle-heat'); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($analytics_id) || !empty($gtm_id) || !empty($site_verification)): ?>
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Current Configuration Status', 'tigerstyle-heat'); ?></h3>
|
||||
<ul>
|
||||
<?php if (!empty($analytics_id)): ?>
|
||||
<li><strong><?php _e('Google Analytics:', 'tigerstyle-heat'); ?></strong>
|
||||
<span style="color: green;">✓ Configured (<?php echo esc_html($analytics_id); ?>)</span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($gtm_id)): ?>
|
||||
<li><strong><?php _e('Google Tag Manager:', 'tigerstyle-heat'); ?></strong>
|
||||
<span style="color: green;">✓ Configured (<?php echo esc_html($gtm_id); ?>)</span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($site_verification)): ?>
|
||||
<li><strong><?php _e('Site Verification:', 'tigerstyle-heat'); ?></strong>
|
||||
<span style="color: green;">✓ Meta tag added</span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($adsense_id)): ?>
|
||||
<li><strong><?php _e('Google AdSense:', 'tigerstyle-heat'); ?></strong>
|
||||
<span style="color: green;">✓ Configured (<?php echo esc_html($adsense_id); ?>)</span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
}
|
||||
}
|
||||
28
includes/modules/class-head-footer.php
Normal file
28
includes/modules/class-head-footer.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Head Footer Module for TigerStyle Heat
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Head_footer {
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
// Initialize hooks
|
||||
}
|
||||
|
||||
public function render_admin_page() {
|
||||
echo '<div class="seo-info-box"><h3>Head Footer Configuration</h3><p>Configure custom code injection for your site\'s header and footer areas.</p></div>';
|
||||
}
|
||||
}
|
||||
172
includes/modules/class-llms-txt.php
Normal file
172
includes/modules/class-llms-txt.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* LLMs.txt Module for TigerStyle Heat
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Llms_txt {
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
*/
|
||||
private function init_hooks() {
|
||||
add_action('template_redirect', array($this, 'handle_llmstxt_request'), 1);
|
||||
add_action('admin_post_update_llmstxt', array($this, 'handle_llmstxt_update'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle llms.txt requests
|
||||
*/
|
||||
public function handle_llmstxt_request() {
|
||||
if (!$this->is_llmstxt_request()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if llms.txt is enabled
|
||||
if (!TigerStyleSEO_Utils::get_option('llmstxt_enabled', false)) {
|
||||
status_header(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $this->get_llmstxt_content();
|
||||
|
||||
// Set headers for markdown content
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
header('X-Robots-Tag: noindex, nofollow, nosnippet, noarchive');
|
||||
|
||||
echo $content;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current request is for llms.txt
|
||||
*/
|
||||
private function is_llmstxt_request() {
|
||||
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
return (strpos($request_uri, '/llms.txt') !== false) ||
|
||||
(isset($_GET['llms']) && $_GET['llms'] === 'txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get llms.txt content
|
||||
*/
|
||||
private function get_llmstxt_content() {
|
||||
$custom_content = TigerStyleSEO_Utils::get_option('llmstxt_content', '');
|
||||
if (!empty($custom_content)) {
|
||||
return $custom_content;
|
||||
}
|
||||
|
||||
// Generate default content
|
||||
return $this->get_default_llmstxt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate default llms.txt content based on WordPress site
|
||||
*/
|
||||
private function get_default_llmstxt() {
|
||||
$site_name = get_bloginfo('name');
|
||||
$site_description = get_bloginfo('description');
|
||||
|
||||
$content = "# {$site_name}\n\n";
|
||||
|
||||
if (!empty($site_description)) {
|
||||
$content .= "> {$site_description}\n\n";
|
||||
}
|
||||
|
||||
$content .= "This is a WordPress website";
|
||||
if (!empty($site_description)) {
|
||||
$content .= " focused on " . strtolower($site_description);
|
||||
}
|
||||
$content .= ". Below are the key resources for understanding this site:\n\n";
|
||||
|
||||
// Add main navigation links
|
||||
$content .= "## Main Pages\n";
|
||||
|
||||
// Get key pages
|
||||
$pages = get_pages(array(
|
||||
'sort_order' => 'ASC',
|
||||
'sort_column' => 'menu_order',
|
||||
'number' => 10
|
||||
));
|
||||
|
||||
foreach ($pages as $page) {
|
||||
$content .= "- [{$page->post_title}](" . get_permalink($page) . ")";
|
||||
if (!empty($page->post_excerpt)) {
|
||||
$content .= ": " . wp_strip_all_tags($page->post_excerpt);
|
||||
}
|
||||
$content .= "\n";
|
||||
}
|
||||
|
||||
// Add recent posts
|
||||
$posts = get_posts(array(
|
||||
'numberposts' => 5,
|
||||
'post_status' => 'publish'
|
||||
));
|
||||
|
||||
if (!empty($posts)) {
|
||||
$content .= "\n## Recent Content\n";
|
||||
foreach ($posts as $post) {
|
||||
$content .= "- [{$post->post_title}](" . get_permalink($post) . ")";
|
||||
if (!empty($post->post_excerpt)) {
|
||||
$content .= ": " . wp_strip_all_tags($post->post_excerpt);
|
||||
}
|
||||
$content .= "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Add contact and additional info
|
||||
$content .= "\n## Technical Information\n";
|
||||
$content .= "- WordPress Version: " . get_bloginfo('version') . "\n";
|
||||
$content .= "- Site URL: " . get_bloginfo('url') . "\n";
|
||||
$content .= "- Admin Email: " . get_bloginfo('admin_email') . "\n";
|
||||
$content .= "- Language: " . get_bloginfo('language') . "\n";
|
||||
$content .= "- Content generated: " . current_time('Y-m-d H:i:s T') . "\n\n";
|
||||
$content .= "---\n";
|
||||
$content .= "*This llms.txt file is automatically generated by TigerStyle Heat Manager*";
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle llms.txt settings update
|
||||
*/
|
||||
public function handle_llmstxt_update() {
|
||||
// Verify nonce and permissions
|
||||
if (!TigerStyleSEO_Utils::verify_nonce('llmstxt_nonce', 'update_llmstxt') ||
|
||||
!TigerStyleSEO_Utils::current_user_can_manage()) {
|
||||
wp_die(__('Security check failed.', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Update settings
|
||||
$enabled = isset($_POST['llmstxt_enabled']) ? 1 : 0;
|
||||
$content = isset($_POST['llmstxt_content']) ? TigerStyleSEO_Utils::sanitize_textarea($_POST['llmstxt_content']) : '';
|
||||
|
||||
TigerStyleSEO_Utils::update_option('llmstxt_enabled', $enabled);
|
||||
TigerStyleSEO_Utils::update_option('llmstxt_content', $content);
|
||||
|
||||
TigerStyleSEO_Utils::redirect_with_message('llmstxt_updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
include TIGERSTYLE_HEAT_PLUGIN_DIR . 'admin/pages/llms-txt.php';
|
||||
}
|
||||
}
|
||||
257
includes/modules/class-meta-tags.php
Normal file
257
includes/modules/class-meta-tags.php
Normal file
@ -0,0 +1,257 @@
|
||||
<?php
|
||||
/**
|
||||
* Meta Tags Module for TigerStyle Heat
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Meta_tags {
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
*/
|
||||
private function init_hooks() {
|
||||
add_action('wp_head', array($this, 'inject_meta_tags'), 1);
|
||||
add_action('admin_post_update_meta_tags', array($this, 'handle_meta_tags_update'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject meta tags into head section
|
||||
*/
|
||||
public function inject_meta_tags() {
|
||||
global $post;
|
||||
|
||||
echo "\n<!-- TigerStyle Heat Meta Tags -->\n";
|
||||
|
||||
// Get current URL and post type for pattern matching
|
||||
$current_url = $_SERVER['REQUEST_URI'];
|
||||
$post_type = get_post_type();
|
||||
|
||||
// Check for pattern-based overrides
|
||||
$patterns = TigerStyleSEO_Utils::get_option('meta_tag_patterns', array());
|
||||
$pattern_match = null;
|
||||
|
||||
foreach ($patterns as $pattern_data) {
|
||||
$pattern = $pattern_data['pattern'];
|
||||
|
||||
// Handle post_type: patterns
|
||||
if (strpos($pattern, 'post_type:') === 0) {
|
||||
$target_post_type = substr($pattern, 10);
|
||||
if ($post_type === $target_post_type) {
|
||||
$pattern_match = $pattern_data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Handle URL patterns with wildcards
|
||||
else {
|
||||
$regex_pattern = str_replace('*', '.*', preg_quote($pattern, '/'));
|
||||
if (preg_match('/^' . $regex_pattern . '/', $current_url)) {
|
||||
$pattern_match = $pattern_data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Meta description
|
||||
$description = '';
|
||||
if ($pattern_match && !empty($pattern_match['description'])) {
|
||||
$description = $pattern_match['description'];
|
||||
} elseif (is_single() && !empty($post->post_excerpt)) {
|
||||
$description = wp_strip_all_tags($post->post_excerpt);
|
||||
} elseif (is_single() && !empty($post->post_content)) {
|
||||
$description = wp_strip_all_tags(wp_trim_words($post->post_content, 25));
|
||||
} else {
|
||||
$description = TigerStyleSEO_Utils::get_option('meta_description_default', '');
|
||||
}
|
||||
|
||||
if (!empty($description)) {
|
||||
echo '<meta name="description" content="' . esc_attr($description) . '">' . "\n";
|
||||
}
|
||||
|
||||
// Meta keywords
|
||||
$keywords = TigerStyleSEO_Utils::get_option('meta_keywords_default', '');
|
||||
if (!empty($keywords)) {
|
||||
echo '<meta name="keywords" content="' . esc_attr($keywords) . '">' . "\n";
|
||||
}
|
||||
|
||||
// Meta author
|
||||
$author = TigerStyleSEO_Utils::get_option('meta_author_default', '');
|
||||
if (!empty($author)) {
|
||||
echo '<meta name="author" content="' . esc_attr($author) . '">' . "\n";
|
||||
}
|
||||
|
||||
// Robots directive
|
||||
$robots = '';
|
||||
if ($pattern_match && !empty($pattern_match['robots'])) {
|
||||
$robots = $pattern_match['robots'];
|
||||
} else {
|
||||
$robots = TigerStyleSEO_Utils::get_option('meta_robots_default', 'index,follow');
|
||||
}
|
||||
|
||||
// Build enhanced robots directive with Google-recommended controls
|
||||
$robots_directives = array($robots);
|
||||
|
||||
// Add nosnippet if enabled
|
||||
if (TigerStyleSEO_Utils::get_option('meta_nosnippet_enabled', false)) {
|
||||
$robots_directives[] = 'nosnippet';
|
||||
}
|
||||
|
||||
// Add max-snippet if set
|
||||
$max_snippet = TigerStyleSEO_Utils::get_option('meta_max_snippet', '');
|
||||
if (!empty($max_snippet) && is_numeric($max_snippet)) {
|
||||
$robots_directives[] = 'max-snippet:' . intval($max_snippet);
|
||||
}
|
||||
|
||||
// Add max-image-preview if set
|
||||
$max_image_preview = TigerStyleSEO_Utils::get_option('meta_max_image_preview', '');
|
||||
if (!empty($max_image_preview)) {
|
||||
$robots_directives[] = 'max-image-preview:' . $max_image_preview;
|
||||
}
|
||||
|
||||
echo '<meta name="robots" content="' . esc_attr(implode(',', $robots_directives)) . '">' . "\n";
|
||||
|
||||
// Charset (if enabled) - should be early in head but after robots for our structure
|
||||
if (TigerStyleSEO_Utils::get_option('meta_charset_enabled', true)) {
|
||||
echo '<meta charset="UTF-8">' . "\n";
|
||||
}
|
||||
|
||||
// Viewport (if enabled)
|
||||
if (TigerStyleSEO_Utils::get_option('meta_viewport_enabled', true)) {
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1">' . "\n";
|
||||
}
|
||||
|
||||
// Theme color
|
||||
$theme_color = TigerStyleSEO_Utils::get_option('meta_theme_color', '#000000');
|
||||
if (!empty($theme_color)) {
|
||||
echo '<meta name="theme-color" content="' . esc_attr($theme_color) . '">' . "\n";
|
||||
}
|
||||
|
||||
// Google Site Verification
|
||||
$google_verification = TigerStyleSEO_Utils::get_option('google_site_verification', '');
|
||||
if (!empty($google_verification)) {
|
||||
echo '<meta name="google-site-verification" content="' . esc_attr($google_verification) . '">' . "\n";
|
||||
}
|
||||
|
||||
// Open Graph tags
|
||||
$og_site_name = TigerStyleSEO_Utils::get_option('og_site_name', get_bloginfo('name'));
|
||||
if (!empty($og_site_name)) {
|
||||
echo '<meta property="og:site_name" content="' . esc_attr($og_site_name) . '">' . "\n";
|
||||
}
|
||||
|
||||
// OG Title
|
||||
$og_title = is_single() ? get_the_title() : get_bloginfo('name');
|
||||
echo '<meta property="og:title" content="' . esc_attr($og_title) . '">' . "\n";
|
||||
|
||||
// OG Description (reuse meta description)
|
||||
if (!empty($description)) {
|
||||
echo '<meta property="og:description" content="' . esc_attr($description) . '">' . "\n";
|
||||
}
|
||||
|
||||
// OG URL
|
||||
$og_url = is_single() ? get_permalink() : home_url($_SERVER['REQUEST_URI']);
|
||||
echo '<meta property="og:url" content="' . esc_url($og_url) . '">' . "\n";
|
||||
|
||||
// OG Type
|
||||
$og_type = is_single() ? 'article' : 'website';
|
||||
echo '<meta property="og:type" content="' . esc_attr($og_type) . '">' . "\n";
|
||||
|
||||
// OG Image
|
||||
$og_image = '';
|
||||
if (is_single() && has_post_thumbnail()) {
|
||||
$og_image = get_the_post_thumbnail_url($post, 'large');
|
||||
} else {
|
||||
$og_image = TigerStyleSEO_Utils::get_option('og_default_image', '');
|
||||
}
|
||||
if (!empty($og_image)) {
|
||||
echo '<meta property="og:image" content="' . esc_url($og_image) . '">' . "\n";
|
||||
}
|
||||
|
||||
// Twitter Card tags
|
||||
echo '<meta name="twitter:card" content="summary_large_image">' . "\n";
|
||||
|
||||
$twitter_site = TigerStyleSEO_Utils::get_option('twitter_site', '');
|
||||
if (!empty($twitter_site)) {
|
||||
echo '<meta name="twitter:site" content="' . esc_attr($twitter_site) . '">' . "\n";
|
||||
}
|
||||
|
||||
// Twitter title and description (reuse OG values)
|
||||
echo '<meta name="twitter:title" content="' . esc_attr($og_title) . '">' . "\n";
|
||||
if (!empty($description)) {
|
||||
echo '<meta name="twitter:description" content="' . esc_attr($description) . '">' . "\n";
|
||||
}
|
||||
if (!empty($og_image)) {
|
||||
echo '<meta name="twitter:image" content="' . esc_url($og_image) . '">' . "\n";
|
||||
}
|
||||
|
||||
echo "<!-- /TigerStyle Heat Meta Tags -->\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle meta tags settings update
|
||||
*/
|
||||
public function handle_meta_tags_update() {
|
||||
// Verify nonce and permissions
|
||||
if (!TigerStyleSEO_Utils::verify_nonce('meta_tags_nonce', 'update_meta_tags') ||
|
||||
!TigerStyleSEO_Utils::current_user_can_manage()) {
|
||||
wp_die(__('Security check failed.', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Update default meta tags
|
||||
TigerStyleSEO_Utils::update_option('meta_description_default', sanitize_textarea_field($_POST['meta_description_default'] ?? ''));
|
||||
TigerStyleSEO_Utils::update_option('meta_keywords_default', sanitize_text_field($_POST['meta_keywords_default'] ?? ''));
|
||||
TigerStyleSEO_Utils::update_option('meta_author_default', sanitize_text_field($_POST['meta_author_default'] ?? ''));
|
||||
TigerStyleSEO_Utils::update_option('meta_robots_default', sanitize_text_field($_POST['meta_robots_default'] ?? 'index,follow'));
|
||||
|
||||
// Update enhanced robots options
|
||||
TigerStyleSEO_Utils::update_option('meta_charset_enabled', isset($_POST['meta_charset_enabled']) ? 1 : 0);
|
||||
TigerStyleSEO_Utils::update_option('meta_viewport_enabled', isset($_POST['meta_viewport_enabled']) ? 1 : 0);
|
||||
TigerStyleSEO_Utils::update_option('meta_nosnippet_enabled', isset($_POST['meta_nosnippet_enabled']) ? 1 : 0);
|
||||
TigerStyleSEO_Utils::update_option('meta_max_snippet', sanitize_text_field($_POST['meta_max_snippet'] ?? ''));
|
||||
TigerStyleSEO_Utils::update_option('meta_max_image_preview', sanitize_text_field($_POST['meta_max_image_preview'] ?? ''));
|
||||
TigerStyleSEO_Utils::update_option('meta_theme_color', sanitize_hex_color($_POST['meta_theme_color'] ?? '#000000'));
|
||||
|
||||
// Update social media options
|
||||
TigerStyleSEO_Utils::update_option('og_site_name', sanitize_text_field($_POST['og_site_name'] ?? ''));
|
||||
TigerStyleSEO_Utils::update_option('og_default_image', esc_url_raw($_POST['og_default_image'] ?? ''));
|
||||
TigerStyleSEO_Utils::update_option('twitter_site', sanitize_text_field($_POST['twitter_site'] ?? ''));
|
||||
|
||||
// Handle pattern-based overrides
|
||||
$patterns = array();
|
||||
if (isset($_POST['patterns']) && is_array($_POST['patterns'])) {
|
||||
foreach ($_POST['patterns'] as $pattern_data) {
|
||||
if (!empty($pattern_data['pattern'])) {
|
||||
$patterns[] = array(
|
||||
'pattern' => sanitize_text_field($pattern_data['pattern']),
|
||||
'description' => sanitize_textarea_field($pattern_data['description'] ?? ''),
|
||||
'robots' => sanitize_text_field($pattern_data['robots'] ?? '')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
TigerStyleSEO_Utils::update_option('meta_tag_patterns', $patterns);
|
||||
|
||||
TigerStyleSEO_Utils::redirect_with_message('meta_tags_updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
include TIGERSTYLE_HEAT_PLUGIN_DIR . 'admin/pages/meta-tags.php';
|
||||
}
|
||||
}
|
||||
564
includes/modules/class-opengraph.php
Normal file
564
includes/modules/class-opengraph.php
Normal file
@ -0,0 +1,564 @@
|
||||
<?php
|
||||
/**
|
||||
* OpenGraph Module for TigerStyle Heat
|
||||
* Implements Facebook OpenGraph meta tags for social sharing optimization
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_OpenGraph {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module
|
||||
*/
|
||||
private function init() {
|
||||
// Frontend hooks
|
||||
add_action('wp_head', array($this, 'inject_opengraph_tags'), 5);
|
||||
|
||||
// Admin hooks
|
||||
if (is_admin()) {
|
||||
add_action('admin_post_update_opengraph_settings', array($this, 'handle_form_submission'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject OpenGraph meta tags into head section
|
||||
*/
|
||||
public function inject_opengraph_tags() {
|
||||
$settings = $this->get_opengraph_settings();
|
||||
|
||||
if (!$settings['enabled']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tags = $this->generate_opengraph_tags();
|
||||
|
||||
if (!empty($tags)) {
|
||||
echo "\n<!-- TigerStyle Heat - OpenGraph Meta Tags -->\n";
|
||||
foreach ($tags as $tag) {
|
||||
echo $tag . "\n";
|
||||
}
|
||||
echo "<!-- End TigerStyle Heat OpenGraph -->\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OpenGraph meta tags based on current page
|
||||
*/
|
||||
private function generate_opengraph_tags() {
|
||||
$tags = array();
|
||||
$settings = $this->get_opengraph_settings();
|
||||
|
||||
// Required tags
|
||||
$tags[] = '<meta property="og:url" content="' . esc_attr($this->get_canonical_url()) . '" />';
|
||||
$tags[] = '<meta property="og:title" content="' . esc_attr($this->get_page_title()) . '" />';
|
||||
$tags[] = '<meta property="og:description" content="' . esc_attr($this->get_page_description()) . '" />';
|
||||
$tags[] = '<meta property="og:image" content="' . esc_attr($this->get_page_image()) . '" />';
|
||||
|
||||
// Optional but recommended tags
|
||||
$tags[] = '<meta property="og:type" content="' . esc_attr($this->get_page_type()) . '" />';
|
||||
$tags[] = '<meta property="og:locale" content="' . esc_attr($this->get_locale()) . '" />';
|
||||
|
||||
// Site name
|
||||
if (!empty($settings['site_name'])) {
|
||||
$tags[] = '<meta property="og:site_name" content="' . esc_attr($settings['site_name']) . '" />';
|
||||
}
|
||||
|
||||
// Facebook App ID
|
||||
if (!empty($settings['app_id'])) {
|
||||
$tags[] = '<meta property="fb:app_id" content="' . esc_attr($settings['app_id']) . '" />';
|
||||
}
|
||||
|
||||
// Image dimensions if available
|
||||
$image_data = $this->get_image_data($this->get_page_image());
|
||||
if ($image_data) {
|
||||
$tags[] = '<meta property="og:image:width" content="' . esc_attr($image_data['width']) . '" />';
|
||||
$tags[] = '<meta property="og:image:height" content="' . esc_attr($image_data['height']) . '" />';
|
||||
if (!empty($image_data['type'])) {
|
||||
$tags[] = '<meta property="og:image:type" content="' . esc_attr($image_data['type']) . '" />';
|
||||
}
|
||||
}
|
||||
|
||||
// Article-specific tags for posts
|
||||
if (is_single() && get_post_type() === 'post') {
|
||||
$post = get_post();
|
||||
$tags[] = '<meta property="article:published_time" content="' . esc_attr(get_the_date('c')) . '" />';
|
||||
$tags[] = '<meta property="article:modified_time" content="' . esc_attr(get_the_modified_date('c')) . '" />';
|
||||
|
||||
// Author
|
||||
$author = get_the_author_meta('display_name', $post->post_author);
|
||||
if ($author) {
|
||||
$tags[] = '<meta property="article:author" content="' . esc_attr($author) . '" />';
|
||||
}
|
||||
|
||||
// Categories as sections
|
||||
$categories = get_the_category();
|
||||
foreach ($categories as $category) {
|
||||
$tags[] = '<meta property="article:section" content="' . esc_attr($category->name) . '" />';
|
||||
}
|
||||
|
||||
// Tags
|
||||
$post_tags = get_the_tags();
|
||||
if ($post_tags) {
|
||||
foreach ($post_tags as $tag) {
|
||||
$tags[] = '<meta property="article:tag" content="' . esc_attr($tag->name) . '" />';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return apply_filters('tigerstyle_heat_opengraph_tags', $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canonical URL for current page
|
||||
*/
|
||||
private function get_canonical_url() {
|
||||
if (is_home() || is_front_page()) {
|
||||
return home_url('/');
|
||||
}
|
||||
|
||||
if (is_singular()) {
|
||||
return get_permalink();
|
||||
}
|
||||
|
||||
if (is_category()) {
|
||||
return get_category_link(get_queried_object_id());
|
||||
}
|
||||
|
||||
if (is_tag()) {
|
||||
return get_tag_link(get_queried_object_id());
|
||||
}
|
||||
|
||||
if (is_author()) {
|
||||
return get_author_posts_url(get_queried_object_id());
|
||||
}
|
||||
|
||||
// Fallback to current URL
|
||||
global $wp;
|
||||
return home_url(add_query_arg(array(), $wp->request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page title optimized for social sharing
|
||||
*/
|
||||
private function get_page_title() {
|
||||
if (is_singular()) {
|
||||
return get_the_title();
|
||||
}
|
||||
|
||||
if (is_home() || is_front_page()) {
|
||||
return get_bloginfo('name');
|
||||
}
|
||||
|
||||
if (is_category()) {
|
||||
return single_cat_title('', false);
|
||||
}
|
||||
|
||||
if (is_tag()) {
|
||||
return single_tag_title('', false);
|
||||
}
|
||||
|
||||
if (is_author()) {
|
||||
return get_the_author_meta('display_name', get_queried_object_id());
|
||||
}
|
||||
|
||||
if (is_archive()) {
|
||||
return get_the_archive_title();
|
||||
}
|
||||
|
||||
return get_bloginfo('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page description for social sharing
|
||||
*/
|
||||
private function get_page_description() {
|
||||
$settings = $this->get_opengraph_settings();
|
||||
|
||||
if (is_singular()) {
|
||||
$post = get_post();
|
||||
|
||||
// Try excerpt first
|
||||
if (!empty($post->post_excerpt)) {
|
||||
return wp_strip_all_tags($post->post_excerpt);
|
||||
}
|
||||
|
||||
// Try content excerpt
|
||||
$content = wp_strip_all_tags($post->post_content);
|
||||
if ($content) {
|
||||
return wp_trim_words($content, 30);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_category()) {
|
||||
$description = category_description();
|
||||
if ($description) {
|
||||
return wp_strip_all_tags($description);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_tag()) {
|
||||
$description = tag_description();
|
||||
if ($description) {
|
||||
return wp_strip_all_tags($description);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to site description
|
||||
return get_bloginfo('description') ?: $settings['default_description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate image for the page
|
||||
*/
|
||||
private function get_page_image() {
|
||||
$settings = $this->get_opengraph_settings();
|
||||
|
||||
// Featured image for posts/pages
|
||||
if (is_singular() && has_post_thumbnail()) {
|
||||
$image_id = get_post_thumbnail_id();
|
||||
$image_url = wp_get_attachment_image_url($image_id, 'large');
|
||||
if ($image_url) {
|
||||
return $image_url;
|
||||
}
|
||||
}
|
||||
|
||||
// Find first image in content
|
||||
if (is_singular()) {
|
||||
$content = get_post_field('post_content');
|
||||
preg_match('/<img[^>]+src="([^">]+)"/', $content, $matches);
|
||||
if (!empty($matches[1])) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Default image from settings
|
||||
if (!empty($settings['default_image'])) {
|
||||
return $settings['default_image'];
|
||||
}
|
||||
|
||||
// Site logo as fallback
|
||||
$custom_logo_id = get_theme_mod('custom_logo');
|
||||
if ($custom_logo_id) {
|
||||
$logo_url = wp_get_attachment_image_url($custom_logo_id, 'large');
|
||||
if ($logo_url) {
|
||||
return $logo_url;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenGraph type for current page
|
||||
*/
|
||||
private function get_page_type() {
|
||||
if (is_single() && get_post_type() === 'post') {
|
||||
return 'article';
|
||||
}
|
||||
|
||||
if (is_page()) {
|
||||
return 'website';
|
||||
}
|
||||
|
||||
if (is_author()) {
|
||||
return 'profile';
|
||||
}
|
||||
|
||||
return 'website';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locale for OpenGraph
|
||||
*/
|
||||
private function get_locale() {
|
||||
$locale = get_locale();
|
||||
|
||||
// Convert WordPress locale to OpenGraph format
|
||||
$locale_map = array(
|
||||
'en_US' => 'en_US',
|
||||
'en_GB' => 'en_GB',
|
||||
'es_ES' => 'es_ES',
|
||||
'fr_FR' => 'fr_FR',
|
||||
'de_DE' => 'de_DE',
|
||||
'it_IT' => 'it_IT',
|
||||
'pt_BR' => 'pt_BR',
|
||||
'nl_NL' => 'nl_NL',
|
||||
'ru_RU' => 'ru_RU',
|
||||
'ja' => 'ja_JP',
|
||||
'zh_CN' => 'zh_CN',
|
||||
);
|
||||
|
||||
return isset($locale_map[$locale]) ? $locale_map[$locale] : 'en_US';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image dimensions and type
|
||||
*/
|
||||
private function get_image_data($image_url) {
|
||||
if (empty($image_url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to get attachment ID from URL
|
||||
$attachment_id = attachment_url_to_postid($image_url);
|
||||
if ($attachment_id) {
|
||||
$metadata = wp_get_attachment_metadata($attachment_id);
|
||||
if ($metadata && isset($metadata['width'], $metadata['height'])) {
|
||||
return array(
|
||||
'width' => $metadata['width'],
|
||||
'height' => $metadata['height'],
|
||||
'type' => get_post_mime_type($attachment_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get image size from URL (if local)
|
||||
if (strpos($image_url, home_url()) === 0) {
|
||||
$upload_dir = wp_upload_dir();
|
||||
$image_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $image_url);
|
||||
|
||||
if (file_exists($image_path)) {
|
||||
$image_info = getimagesize($image_path);
|
||||
if ($image_info) {
|
||||
return array(
|
||||
'width' => $image_info[0],
|
||||
'height' => $image_info[1],
|
||||
'type' => $image_info['mime']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenGraph settings
|
||||
*/
|
||||
private function get_opengraph_settings() {
|
||||
$defaults = array(
|
||||
'enabled' => true,
|
||||
'site_name' => get_bloginfo('name'),
|
||||
'app_id' => '',
|
||||
'default_description' => get_bloginfo('description'),
|
||||
'default_image' => '',
|
||||
'include_article_tags' => true,
|
||||
'include_author_info' => true
|
||||
);
|
||||
|
||||
$settings = get_option('tigerstyle_opengraph_settings', array());
|
||||
return wp_parse_args($settings, $defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission
|
||||
*/
|
||||
public function handle_form_submission() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('You do not have sufficient permissions to access this page.'));
|
||||
}
|
||||
|
||||
if (!wp_verify_nonce($_POST['_wpnonce'], 'tigerstyle_opengraph_settings')) {
|
||||
wp_die(__('Security check failed. Please try again.'));
|
||||
}
|
||||
|
||||
$settings = array(
|
||||
'enabled' => isset($_POST['og_enabled']) ? 1 : 0,
|
||||
'site_name' => sanitize_text_field($_POST['og_site_name'] ?? ''),
|
||||
'app_id' => sanitize_text_field($_POST['og_app_id'] ?? ''),
|
||||
'default_description' => sanitize_textarea_field($_POST['og_default_description'] ?? ''),
|
||||
'default_image' => esc_url_raw($_POST['og_default_image'] ?? ''),
|
||||
'include_article_tags' => isset($_POST['og_include_article_tags']) ? 1 : 0,
|
||||
'include_author_info' => isset($_POST['og_include_author_info']) ? 1 : 0
|
||||
);
|
||||
|
||||
update_option('tigerstyle_opengraph_settings', $settings);
|
||||
|
||||
wp_redirect(add_query_arg(array('page' => 'tigerstyle-heat', 'tab' => 'opengraph', 'updated' => 'true'), admin_url('admin.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information for current page
|
||||
*/
|
||||
public function get_debug_info() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array(
|
||||
'url' => $this->get_canonical_url(),
|
||||
'title' => $this->get_page_title(),
|
||||
'description' => $this->get_page_description(),
|
||||
'image' => $this->get_page_image(),
|
||||
'type' => $this->get_page_type(),
|
||||
'locale' => $this->get_locale(),
|
||||
'tags' => $this->generate_opengraph_tags()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
$settings = $this->get_opengraph_settings();
|
||||
?>
|
||||
<div class="seo-info-box">
|
||||
<h2><?php _e('Facebook OpenGraph Configuration', 'tigerstyle-heat'); ?></h2>
|
||||
<p><?php _e('Configure OpenGraph meta tags to optimize how your content appears when shared on Facebook, LinkedIn, Twitter, and other social platforms.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<div class="seo-setup-steps">
|
||||
<h3><?php _e('What is OpenGraph?', 'tigerstyle-heat'); ?></h3>
|
||||
<p><?php _e('OpenGraph is a protocol that enables any web page to become a rich object in a social graph. It allows you to control how your content appears when shared on social media platforms.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<h4><?php _e('Key OpenGraph Meta Tags:', 'tigerstyle-heat'); ?></h4>
|
||||
<ul>
|
||||
<li><strong>og:title</strong> - <?php _e('The title of your content', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong>og:description</strong> - <?php _e('A brief description of your content', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong>og:image</strong> - <?php _e('An image representing your content', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong>og:url</strong> - <?php _e('The canonical URL of your content', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong>og:type</strong> - <?php _e('The type of object (article, website, etc.)', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
|
||||
<p><a href="https://developers.facebook.com/docs/sharing/webmasters/" target="_blank"><?php _e('Learn more about OpenGraph protocol', 'tigerstyle-heat'); ?></a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<?php wp_nonce_field('tigerstyle_opengraph_settings'); ?>
|
||||
<input type="hidden" name="action" value="update_opengraph_settings">
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enable OpenGraph', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="og_enabled" value="1" <?php checked($settings['enabled']); ?>>
|
||||
<?php _e('Enable OpenGraph meta tags', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Enable automatic generation of OpenGraph meta tags for social sharing.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Site Name', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="og_site_name" value="<?php echo esc_attr($settings['site_name']); ?>" class="regular-text">
|
||||
<p class="description"><?php _e('The name of your website. Defaults to site title.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Facebook App ID', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="og_app_id" value="<?php echo esc_attr($settings['app_id']); ?>" class="regular-text">
|
||||
<p class="description"><?php _e('Optional: Your Facebook App ID for enhanced social insights.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Default Description', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<textarea name="og_default_description" rows="3" class="large-text"><?php echo esc_textarea($settings['default_description']); ?></textarea>
|
||||
<p class="description"><?php _e('Default description used when no specific content description is available.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Default Image URL', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="url" name="og_default_image" value="<?php echo esc_attr($settings['default_image']); ?>" class="large-text">
|
||||
<p class="description"><?php _e('Default image used when no featured image is available. Recommended size: 1200x630 pixels.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Article Options', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="og_include_article_tags" value="1" <?php checked($settings['include_article_tags']); ?>>
|
||||
<?php _e('Include article tags and categories', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
<input type="checkbox" name="og_include_author_info" value="1" <?php checked($settings['include_author_info']); ?>>
|
||||
<?php _e('Include article author information', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Enhanced metadata for blog posts and articles.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(__('Save OpenGraph Settings', 'tigerstyle-heat')); ?>
|
||||
</form>
|
||||
|
||||
<?php if ($settings['enabled'] && current_user_can('manage_options')): ?>
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Current Page OpenGraph Preview', 'tigerstyle-heat'); ?></h3>
|
||||
<p><?php _e('Preview how the current page would appear with OpenGraph tags:', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<?php
|
||||
$debug_info = $this->get_debug_info();
|
||||
if (!empty($debug_info)):
|
||||
?>
|
||||
<div style="border: 1px solid #ddd; padding: 15px; margin: 15px 0; background: #f9f9f9;">
|
||||
<h4><?php _e('OpenGraph Data:', 'tigerstyle-heat'); ?></h4>
|
||||
<p><strong><?php _e('Title:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($debug_info['title']); ?></p>
|
||||
<p><strong><?php _e('Description:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($debug_info['description']); ?></p>
|
||||
<p><strong><?php _e('URL:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($debug_info['url']); ?></p>
|
||||
<p><strong><?php _e('Type:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($debug_info['type']); ?></p>
|
||||
<?php if (!empty($debug_info['image'])): ?>
|
||||
<p><strong><?php _e('Image:', 'tigerstyle-heat'); ?></strong> <br>
|
||||
<img src="<?php echo esc_url($debug_info['image']); ?>" style="max-width: 300px; max-height: 150px; border: 1px solid #ddd;" alt="OpenGraph Preview">
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<h4><?php _e('Generated Meta Tags:', 'tigerstyle-heat'); ?></h4>
|
||||
<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto; font-size: 12px;"><?php
|
||||
foreach ($debug_info['tags'] as $tag) {
|
||||
echo esc_html($tag) . "\n";
|
||||
}
|
||||
?></pre>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Testing Your OpenGraph Tags', 'tigerstyle-heat'); ?></h3>
|
||||
<p><?php _e('Use these tools to validate and preview your OpenGraph implementation:', 'tigerstyle-heat'); ?></p>
|
||||
<ul>
|
||||
<li><a href="https://developers.facebook.com/tools/debug/" target="_blank"><?php _e('Facebook Sharing Debugger', 'tigerstyle-heat'); ?></a></li>
|
||||
<li><a href="https://www.linkedin.com/post-inspector/" target="_blank"><?php _e('LinkedIn Post Inspector', 'tigerstyle-heat'); ?></a></li>
|
||||
<li><a href="https://cards-dev.twitter.com/validator" target="_blank"><?php _e('Twitter Card Validator', 'tigerstyle-heat'); ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
815
includes/modules/class-performance.php
Normal file
815
includes/modules/class-performance.php
Normal file
@ -0,0 +1,815 @@
|
||||
<?php
|
||||
/**
|
||||
* Performance Module for TigerStyle Heat
|
||||
* WP Rocket-inspired compression system with Brotli + Gzip fallbacks
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Performance {
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
$this->init_compression();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize compression hooks and filters
|
||||
*/
|
||||
public function init_compression() {
|
||||
// Admin hooks (always register to handle form submissions)
|
||||
if (is_admin()) {
|
||||
add_action('admin_post_update_compression_settings', array($this, 'handle_form_submission'));
|
||||
add_action('wp_ajax_tigerstyle_analyze_cache', array($this, 'ajax_analyze_cache'));
|
||||
}
|
||||
|
||||
if (!get_option('tigerstyle_compression_enabled', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hook into output buffering for automatic compression
|
||||
add_action('template_redirect', array($this, 'start_compression_buffer'), 1);
|
||||
|
||||
// Hook into post updates to clear relevant cache
|
||||
add_action('save_post', array($this, 'clear_post_compression_cache'));
|
||||
add_action('wp_update_nav_menu', array($this, 'clear_compression_cache'));
|
||||
add_action('switch_theme', array($this, 'clear_compression_cache'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start output buffering for compression
|
||||
*/
|
||||
public function start_compression_buffer() {
|
||||
// Only compress HTML pages, not admin or AJAX requests
|
||||
if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if content type should be compressed
|
||||
$content_type = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
if (!empty($content_type) && strpos($content_type, 'text/html') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob_start(array($this, 'compress_output_buffer'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress output buffer callback
|
||||
*/
|
||||
public function compress_output_buffer($content) {
|
||||
// Only compress if content is substantial and HTML
|
||||
if (strlen($content) < 1000 || strpos($content, '<html') === false) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
return $this->compress_and_cache($content, $_SERVER['REQUEST_URI'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Main compression function with Brotli + Gzip fallbacks
|
||||
*/
|
||||
public function compress_and_cache($content, $url = '') {
|
||||
if (!get_option('tigerstyle_compression_enabled', false)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$method = get_option('tigerstyle_compression_method', 'auto');
|
||||
$level = get_option('tigerstyle_compression_level', 6);
|
||||
$cache_enabled = get_option('tigerstyle_compression_cache_enabled', true);
|
||||
|
||||
$accept_encoding = $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '';
|
||||
$compressed_content = $content;
|
||||
$compression_used = 'none';
|
||||
$original_size = strlen($content);
|
||||
|
||||
// Determine best compression method
|
||||
if ($method === 'auto') {
|
||||
if (strpos($accept_encoding, 'br') !== false && function_exists('brotli_compress')) {
|
||||
$compressed_content = brotli_compress($content, $level);
|
||||
$compression_used = 'brotli';
|
||||
header('Content-Encoding: br');
|
||||
} elseif (strpos($accept_encoding, 'gzip') !== false) {
|
||||
$compressed_content = gzencode($content, $level);
|
||||
$compression_used = 'gzip';
|
||||
header('Content-Encoding: gzip');
|
||||
}
|
||||
} elseif ($method === 'brotli' && function_exists('brotli_compress')) {
|
||||
if (strpos($accept_encoding, 'br') !== false) {
|
||||
$compressed_content = brotli_compress($content, $level);
|
||||
$compression_used = 'brotli';
|
||||
header('Content-Encoding: br');
|
||||
}
|
||||
} elseif ($method === 'gzip') {
|
||||
if (strpos($accept_encoding, 'gzip') !== false) {
|
||||
$compressed_content = gzencode($content, $level);
|
||||
$compression_used = 'gzip';
|
||||
header('Content-Encoding: gzip');
|
||||
}
|
||||
}
|
||||
|
||||
// Cache compressed content if enabled
|
||||
if ($cache_enabled && !empty($url) && $compression_used !== 'none') {
|
||||
$this->cache_compressed_content($url, $compressed_content, $compression_used);
|
||||
}
|
||||
|
||||
// Update compression statistics
|
||||
$this->update_compression_stats($original_size, strlen($compressed_content), $compression_used);
|
||||
|
||||
// Set additional performance headers
|
||||
header('Vary: Accept-Encoding');
|
||||
header('Cache-Control: public, max-age=31536000');
|
||||
|
||||
return $compressed_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache compressed content to filesystem
|
||||
*/
|
||||
private function cache_compressed_content($url, $content, $compression_type) {
|
||||
$cache_dir = WP_CONTENT_DIR . '/cache/tigerstyle-compression/';
|
||||
if (!is_dir($cache_dir)) {
|
||||
wp_mkdir_p($cache_dir);
|
||||
}
|
||||
|
||||
$cache_key = md5($url);
|
||||
$cache_file = $cache_dir . $cache_key . '.' . $compression_type;
|
||||
|
||||
file_put_contents($cache_file, $content);
|
||||
|
||||
// Store metadata
|
||||
$metadata = array(
|
||||
'url' => $url,
|
||||
'compression' => $compression_type,
|
||||
'size' => strlen($content),
|
||||
'created' => time()
|
||||
);
|
||||
file_put_contents($cache_file . '.meta', json_encode($metadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update compression statistics
|
||||
*/
|
||||
private function update_compression_stats($original_size, $compressed_size, $compression_used) {
|
||||
$stats = get_option('tigerstyle_compression_stats', array(
|
||||
'total_files' => 0,
|
||||
'total_original_size' => 0,
|
||||
'total_compressed_size' => 0,
|
||||
'compression_methods' => array()
|
||||
));
|
||||
|
||||
$stats['total_files']++;
|
||||
$stats['total_original_size'] += $original_size;
|
||||
$stats['total_compressed_size'] += $compressed_size;
|
||||
|
||||
if (!isset($stats['compression_methods'][$compression_used])) {
|
||||
$stats['compression_methods'][$compression_used] = 0;
|
||||
}
|
||||
$stats['compression_methods'][$compression_used]++;
|
||||
|
||||
// Calculate derived stats
|
||||
$bandwidth_saved = $stats['total_original_size'] - $stats['total_compressed_size'];
|
||||
$avg_ratio = $stats['total_original_size'] > 0 ?
|
||||
round((1 - $stats['total_compressed_size'] / $stats['total_original_size']) * 100, 1) : 0;
|
||||
|
||||
$stats['bandwidth_saved'] = $this->format_bytes($bandwidth_saved);
|
||||
$stats['avg_ratio'] = $avg_ratio . '%';
|
||||
$stats['files_compressed'] = number_format($stats['total_files']);
|
||||
|
||||
update_option('tigerstyle_compression_stats', $stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes into human readable format
|
||||
*/
|
||||
private function format_bytes($bytes, $precision = 2) {
|
||||
$units = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear compression cache
|
||||
*/
|
||||
public function clear_compression_cache() {
|
||||
$cache_dir = WP_CONTENT_DIR . '/cache/tigerstyle-compression/';
|
||||
if (is_dir($cache_dir)) {
|
||||
$this->delete_directory_contents($cache_dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warm compression cache for popular pages
|
||||
*/
|
||||
public function warm_compression_cache() {
|
||||
// Get popular posts and pages
|
||||
$popular_posts = get_posts(array(
|
||||
'numberposts' => 10,
|
||||
'meta_key' => 'views',
|
||||
'orderby' => 'meta_value_num',
|
||||
'order' => 'DESC'
|
||||
));
|
||||
|
||||
$home_url = home_url('/');
|
||||
$urls_to_warm = array($home_url);
|
||||
|
||||
foreach ($popular_posts as $post) {
|
||||
$urls_to_warm[] = get_permalink($post->ID);
|
||||
}
|
||||
|
||||
// Pre-compress popular URLs
|
||||
foreach ($urls_to_warm as $url) {
|
||||
$this->precompress_url($url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-compress a specific URL
|
||||
*/
|
||||
private function precompress_url($url) {
|
||||
$response = wp_remote_get($url, array(
|
||||
'timeout' => 10,
|
||||
'headers' => array(
|
||||
'Accept-Encoding' => 'gzip, br'
|
||||
)
|
||||
));
|
||||
|
||||
if (!is_wp_error($response)) {
|
||||
$content = wp_remote_retrieve_body($response);
|
||||
$this->compress_and_cache($content, $url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear compression cache for a specific post
|
||||
*/
|
||||
public function clear_post_compression_cache($post_id) {
|
||||
$post_url = get_permalink($post_id);
|
||||
$cache_key = md5($post_url);
|
||||
$cache_dir = WP_CONTENT_DIR . '/cache/tigerstyle-compression/';
|
||||
|
||||
// Remove all cached versions of this post
|
||||
$files = glob($cache_dir . $cache_key . '.*');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
// Also clear home page cache as it might include this post
|
||||
$home_cache_key = md5(home_url('/'));
|
||||
$home_files = glob($cache_dir . $home_cache_key . '.*');
|
||||
foreach ($home_files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete directory contents recursively
|
||||
*/
|
||||
private function delete_directory_contents($dir) {
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = array_diff(scandir($dir), array('.', '..'));
|
||||
foreach ($files as $file) {
|
||||
$path = $dir . '/' . $file;
|
||||
if (is_dir($path)) {
|
||||
$this->delete_directory_contents($path);
|
||||
rmdir($path);
|
||||
} else {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for cache analysis
|
||||
*/
|
||||
public function ajax_analyze_cache() {
|
||||
check_ajax_referer('tigerstyle_cache_analysis', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Unauthorized access');
|
||||
}
|
||||
|
||||
$analysis = $this->analyze_cacheable_content();
|
||||
wp_send_json_success($analysis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze cacheable content and calculate potential savings
|
||||
*/
|
||||
public function analyze_cacheable_content() {
|
||||
$analysis = array(
|
||||
'pages' => array(),
|
||||
'assets' => array(),
|
||||
'total_size' => 0,
|
||||
'potential_savings' => 0,
|
||||
'compression_estimates' => array()
|
||||
);
|
||||
|
||||
// Analyze main pages
|
||||
$pages_analysis = $this->analyze_pages();
|
||||
$analysis['pages'] = $pages_analysis['pages'];
|
||||
$analysis['total_size'] += $pages_analysis['total_size'];
|
||||
$analysis['potential_savings'] += $pages_analysis['potential_savings'];
|
||||
|
||||
// Analyze static assets
|
||||
$assets_analysis = $this->analyze_static_assets();
|
||||
$analysis['assets'] = $assets_analysis['assets'];
|
||||
$analysis['total_size'] += $assets_analysis['total_size'];
|
||||
$analysis['potential_savings'] += $assets_analysis['potential_savings'];
|
||||
|
||||
// Calculate compression estimates
|
||||
$analysis['compression_estimates'] = $this->calculate_compression_estimates($analysis['total_size']);
|
||||
|
||||
// Format results
|
||||
$analysis['total_size_formatted'] = $this->format_bytes($analysis['total_size']);
|
||||
$analysis['potential_savings_formatted'] = $this->format_bytes($analysis['potential_savings']);
|
||||
$analysis['savings_percentage'] = $analysis['total_size'] > 0 ?
|
||||
round(($analysis['potential_savings'] / $analysis['total_size']) * 100, 1) : 0;
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze main pages for caching potential
|
||||
*/
|
||||
private function analyze_pages() {
|
||||
$pages = array();
|
||||
$total_size = 0;
|
||||
$potential_savings = 0;
|
||||
|
||||
// Get homepage
|
||||
$home_url = home_url('/');
|
||||
$home_analysis = $this->analyze_single_page($home_url, 'Homepage');
|
||||
if ($home_analysis) {
|
||||
$pages[] = $home_analysis;
|
||||
$total_size += $home_analysis['size'];
|
||||
$potential_savings += $home_analysis['potential_savings'];
|
||||
}
|
||||
|
||||
// Get recent posts
|
||||
$recent_posts = get_posts(array(
|
||||
'numberposts' => 5,
|
||||
'post_status' => 'publish'
|
||||
));
|
||||
|
||||
foreach ($recent_posts as $post) {
|
||||
$post_url = get_permalink($post->ID);
|
||||
$post_analysis = $this->analyze_single_page($post_url, $post->post_title);
|
||||
if ($post_analysis) {
|
||||
$pages[] = $post_analysis;
|
||||
$total_size += $post_analysis['size'];
|
||||
$potential_savings += $post_analysis['potential_savings'];
|
||||
}
|
||||
}
|
||||
|
||||
// Get recent pages
|
||||
$recent_pages = get_posts(array(
|
||||
'numberposts' => 3,
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'publish'
|
||||
));
|
||||
|
||||
foreach ($recent_pages as $page) {
|
||||
$page_url = get_permalink($page->ID);
|
||||
$page_analysis = $this->analyze_single_page($page_url, $page->post_title);
|
||||
if ($page_analysis) {
|
||||
$pages[] = $page_analysis;
|
||||
$total_size += $page_analysis['size'];
|
||||
$potential_savings += $page_analysis['potential_savings'];
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'pages' => $pages,
|
||||
'total_size' => $total_size,
|
||||
'potential_savings' => $potential_savings
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a single page for compression potential
|
||||
*/
|
||||
private function analyze_single_page($url, $title) {
|
||||
// Use WordPress HTTP API to fetch the page
|
||||
$response = wp_remote_get($url, array(
|
||||
'timeout' => 15,
|
||||
'user-agent' => 'TigerStyle-SEO-Cache-Analyzer/1.0'
|
||||
));
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = wp_remote_retrieve_body($response);
|
||||
$original_size = strlen($content);
|
||||
|
||||
// Skip if too small or not HTML
|
||||
if ($original_size < 500 || strpos($content, '<html') === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Estimate compression savings
|
||||
$gzip_size = strlen(gzcompress($content, 6));
|
||||
$brotli_size = function_exists('brotli_compress') ?
|
||||
strlen(brotli_compress($content, 6)) : $gzip_size * 0.85; // Estimate 15% better
|
||||
|
||||
$best_compressed_size = min($gzip_size, $brotli_size);
|
||||
$potential_savings = $original_size - $best_compressed_size;
|
||||
|
||||
return array(
|
||||
'title' => $title,
|
||||
'url' => $url,
|
||||
'size' => $original_size,
|
||||
'gzip_size' => $gzip_size,
|
||||
'brotli_size' => $brotli_size,
|
||||
'potential_savings' => $potential_savings,
|
||||
'compression_ratio' => round((1 - $best_compressed_size / $original_size) * 100, 1),
|
||||
'size_formatted' => $this->format_bytes($original_size),
|
||||
'savings_formatted' => $this->format_bytes($potential_savings)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze static assets for compression potential
|
||||
*/
|
||||
private function analyze_static_assets() {
|
||||
$assets = array();
|
||||
$total_size = 0;
|
||||
$potential_savings = 0;
|
||||
|
||||
// Analyze CSS files
|
||||
$css_files = $this->find_theme_files('*.css');
|
||||
foreach ($css_files as $file) {
|
||||
$asset_analysis = $this->analyze_static_file($file, 'CSS');
|
||||
if ($asset_analysis) {
|
||||
$assets[] = $asset_analysis;
|
||||
$total_size += $asset_analysis['size'];
|
||||
$potential_savings += $asset_analysis['potential_savings'];
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze JS files
|
||||
$js_files = $this->find_theme_files('*.js');
|
||||
foreach ($js_files as $file) {
|
||||
$asset_analysis = $this->analyze_static_file($file, 'JavaScript');
|
||||
if ($asset_analysis) {
|
||||
$assets[] = $asset_analysis;
|
||||
$total_size += $asset_analysis['size'];
|
||||
$potential_savings += $asset_analysis['potential_savings'];
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'assets' => $assets,
|
||||
'total_size' => $total_size,
|
||||
'potential_savings' => $potential_savings
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find theme files by pattern
|
||||
*/
|
||||
private function find_theme_files($pattern) {
|
||||
$theme_dir = get_stylesheet_directory();
|
||||
$files = glob($theme_dir . '/' . $pattern);
|
||||
|
||||
// Also check parent theme if using child theme
|
||||
if (is_child_theme()) {
|
||||
$parent_theme_dir = get_template_directory();
|
||||
$parent_files = glob($parent_theme_dir . '/' . $pattern);
|
||||
$files = array_merge($files, $parent_files);
|
||||
}
|
||||
|
||||
// Limit to reasonable number of files
|
||||
return array_slice($files, 0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a static file for compression potential
|
||||
*/
|
||||
private function analyze_static_file($file_path, $type) {
|
||||
if (!file_exists($file_path) || !is_readable($file_path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = file_get_contents($file_path);
|
||||
$original_size = strlen($content);
|
||||
|
||||
// Skip small files
|
||||
if ($original_size < 1000) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Estimate compression savings
|
||||
$gzip_size = strlen(gzcompress($content, 6));
|
||||
$brotli_size = function_exists('brotli_compress') ?
|
||||
strlen(brotli_compress($content, 6)) : $gzip_size * 0.85;
|
||||
|
||||
$best_compressed_size = min($gzip_size, $brotli_size);
|
||||
$potential_savings = $original_size - $best_compressed_size;
|
||||
|
||||
$filename = basename($file_path);
|
||||
|
||||
return array(
|
||||
'filename' => $filename,
|
||||
'type' => $type,
|
||||
'path' => $file_path,
|
||||
'size' => $original_size,
|
||||
'gzip_size' => $gzip_size,
|
||||
'brotli_size' => $brotli_size,
|
||||
'potential_savings' => $potential_savings,
|
||||
'compression_ratio' => round((1 - $best_compressed_size / $original_size) * 100, 1),
|
||||
'size_formatted' => $this->format_bytes($original_size),
|
||||
'savings_formatted' => $this->format_bytes($potential_savings)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate compression estimates for different scenarios
|
||||
*/
|
||||
private function calculate_compression_estimates($total_size) {
|
||||
return array(
|
||||
'gzip_only' => array(
|
||||
'method' => 'Gzip only',
|
||||
'ratio' => 70, // Conservative estimate
|
||||
'savings' => round($total_size * 0.30),
|
||||
'savings_formatted' => $this->format_bytes(round($total_size * 0.30))
|
||||
),
|
||||
'brotli_only' => array(
|
||||
'method' => 'Brotli only',
|
||||
'ratio' => 78, // Better compression
|
||||
'savings' => round($total_size * 0.22),
|
||||
'savings_formatted' => $this->format_bytes(round($total_size * 0.22))
|
||||
),
|
||||
'auto_best' => array(
|
||||
'method' => 'Auto (Brotli + Gzip fallback)',
|
||||
'ratio' => 75, // Average between both
|
||||
'savings' => round($total_size * 0.25),
|
||||
'savings_formatted' => $this->format_bytes(round($total_size * 0.25))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function render_admin_page() {
|
||||
// Get current settings
|
||||
$compression_enabled = get_option('tigerstyle_compression_enabled', false);
|
||||
$compression_method = get_option('tigerstyle_compression_method', 'auto');
|
||||
$compression_level = get_option('tigerstyle_compression_level', 6);
|
||||
$compression_cache_enabled = get_option('tigerstyle_compression_cache_enabled', true);
|
||||
$compression_stats = get_option('tigerstyle_compression_stats', array());
|
||||
|
||||
?>
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Performance & Compression Status', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Boost your website performance with intelligent compression. Our WP Rocket-inspired system provides Brotli and Gzip compression with smart caching, bandwidth monitoring, and automatic optimization.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php _e('Compression Status:', 'tigerstyle-heat'); ?></strong>
|
||||
<?php if ($compression_enabled): ?>
|
||||
<span style="color: green;"><?php _e('Active', 'tigerstyle-heat'); ?></span>
|
||||
<?php
|
||||
$method_names = array(
|
||||
'auto' => __('Auto (Brotli + Gzip fallback)', 'tigerstyle-heat'),
|
||||
'brotli' => __('Brotli only', 'tigerstyle-heat'),
|
||||
'gzip' => __('Gzip only', 'tigerstyle-heat')
|
||||
);
|
||||
?>
|
||||
<span style="color: #666;"> - <?php echo esc_html($method_names[$compression_method]); ?></span>
|
||||
<?php else: ?>
|
||||
<span style="color: red;"><?php _e('Disabled', 'tigerstyle-heat'); ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
<?php if (!empty($compression_stats)): ?>
|
||||
<div class="compression-stats" style="margin-top: 15px;">
|
||||
<h4><?php _e('Performance Statistics', 'tigerstyle-heat'); ?></h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 10px;">
|
||||
<div class="stat-box" style="padding: 10px; background: #f1f1f1; border-radius: 4px;">
|
||||
<strong><?php _e('Bandwidth Saved:', 'tigerstyle-heat'); ?></strong><br>
|
||||
<span style="color: #007cba; font-size: 18px;"><?php echo esc_html($compression_stats['bandwidth_saved'] ?? '0 MB'); ?></span>
|
||||
</div>
|
||||
<div class="stat-box" style="padding: 10px; background: #f1f1f1; border-radius: 4px;">
|
||||
<strong><?php _e('Avg. Compression Ratio:', 'tigerstyle-heat'); ?></strong><br>
|
||||
<span style="color: #007cba; font-size: 18px;"><?php echo esc_html($compression_stats['avg_ratio'] ?? '0%'); ?></span>
|
||||
</div>
|
||||
<div class="stat-box" style="padding: 10px; background: #f1f1f1; border-radius: 4px;">
|
||||
<strong><?php _e('Files Compressed:', 'tigerstyle-heat'); ?></strong><br>
|
||||
<span style="color: #007cba; font-size: 18px;"><?php echo esc_html($compression_stats['files_compressed'] ?? '0'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="update_compression_settings">
|
||||
<?php wp_nonce_field('update_compression_settings', 'compression_nonce'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enable Compression', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="compression_enabled" value="1" <?php checked($compression_enabled); ?>>
|
||||
<?php _e('Enable intelligent compression system', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php _e('Activate smart compression with automatic Brotli + Gzip fallbacks for maximum performance and compatibility.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="compression_method"><?php _e('Compression Method', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="compression_method" name="compression_method">
|
||||
<option value="auto" <?php selected($compression_method, 'auto'); ?>><?php _e('Auto (Recommended) - Brotli with Gzip fallback', 'tigerstyle-heat'); ?></option>
|
||||
<option value="brotli" <?php selected($compression_method, 'brotli'); ?>><?php _e('Brotli only (Better compression)', 'tigerstyle-heat'); ?></option>
|
||||
<option value="gzip" <?php selected($compression_method, 'gzip'); ?>><?php _e('Gzip only (Maximum compatibility)', 'tigerstyle-heat'); ?></option>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php _e('Auto mode provides best performance with maximum browser compatibility. Brotli typically achieves 15-25% better compression than Gzip.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="compression_level"><?php _e('Compression Level', 'tigerstyle-heat'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="range" id="compression_level" name="compression_level" min="1" max="9" value="<?php echo esc_attr($compression_level); ?>" oninput="updateCompressionLevel(this.value)">
|
||||
<output id="compression_level_display"><?php echo esc_html($compression_level); ?></output>
|
||||
<p class="description">
|
||||
<?php _e('Higher levels provide better compression but use more CPU. Level 6 offers the best balance of speed and compression.', 'tigerstyle-heat'); ?>
|
||||
<br><span id="compression_level_desc"></span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Smart Caching', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="compression_cache_enabled" value="1" <?php checked($compression_cache_enabled); ?>>
|
||||
<?php _e('Enable intelligent cache warming and purging', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php _e('Automatically pre-compress popular content and clear cache when posts are updated. Reduces server load and improves response times.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(__('Update Compression Settings', 'tigerstyle-heat')); ?>
|
||||
</form>
|
||||
|
||||
<!-- Cache Analysis Section -->
|
||||
<div class="seo-info-box" style="margin-top: 25px;">
|
||||
<h3><?php _e('🔍 Cache Analysis & Potential Savings', 'tigerstyle-heat'); ?></h3>
|
||||
<p class="description">
|
||||
<?php _e('Analyze your website content to see what could be cached and estimate potential bandwidth savings with compression.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
|
||||
<button type="button" id="analyze-cache-btn" class="button button-primary" style="margin: 15px 0;">
|
||||
<?php _e('Analyze Cacheable Content', 'tigerstyle-heat'); ?>
|
||||
</button>
|
||||
|
||||
<div id="cache-analysis-results" style="display: none; margin-top: 20px;">
|
||||
<!-- Results will be loaded here via AJAX -->
|
||||
</div>
|
||||
|
||||
<div id="cache-analysis-loading" style="display: none; margin-top: 15px; text-align: center;">
|
||||
<div style="display: inline-block; vertical-align: middle;">
|
||||
<div class="spinner is-active" style="float: none; margin: 0 10px 0 0;"></div>
|
||||
<span><?php _e('Analyzing content and calculating compression savings...', 'tigerstyle-heat'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Compression Benefits', 'tigerstyle-heat'); ?></h3>
|
||||
<div class="seo-benefits-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-top: 15px;">
|
||||
<div>
|
||||
<h4><?php _e('🚀 Performance Boost', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Reduce file sizes by 60-80%, dramatically improving page load times and Core Web Vitals scores.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<h4><?php _e('💰 Bandwidth Savings', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Lower hosting costs and faster loading for users on limited data plans or slow connections.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<h4><?php _e('🎯 SEO Improvement', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Google rewards fast-loading sites with higher rankings. Compression is a ranking factor.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<h4><?php _e('🔧 Smart Automation', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('Set it and forget it! Our system handles everything automatically with intelligent optimizations.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Compression level slider functionality
|
||||
function updateCompressionLevel(value) {
|
||||
document.getElementById('compression_level_display').textContent = value;
|
||||
const descriptions = {
|
||||
1: '<?php echo esc_js(__("Fastest - Minimal compression", "tigerstyle-heat")); ?>',
|
||||
2: '<?php echo esc_js(__("Very fast - Light compression", "tigerstyle-heat")); ?>',
|
||||
3: '<?php echo esc_js(__("Fast - Basic compression", "tigerstyle-heat")); ?>',
|
||||
4: '<?php echo esc_js(__("Balanced - Good compression", "tigerstyle-heat")); ?>',
|
||||
5: '<?php echo esc_js(__("Good - Better compression", "tigerstyle-heat")); ?>',
|
||||
6: '<?php echo esc_js(__("Recommended - Optimal balance", "tigerstyle-heat")); ?>',
|
||||
7: '<?php echo esc_js(__("High - Strong compression", "tigerstyle-heat")); ?>',
|
||||
8: '<?php echo esc_js(__("Maximum - Slow but thorough", "tigerstyle-heat")); ?>',
|
||||
9: '<?php echo esc_js(__("Ultra - Slowest, best compression", "tigerstyle-heat")); ?>'
|
||||
};
|
||||
document.getElementById('compression_level_desc').textContent = descriptions[value] || '';
|
||||
}
|
||||
|
||||
// Initialize compression level description on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const levelSlider = document.getElementById('compression_level');
|
||||
if (levelSlider) {
|
||||
updateCompressionLevel(levelSlider.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle compression settings form submission
|
||||
*/
|
||||
public function handle_form_submission() {
|
||||
// Verify nonce and permissions
|
||||
if (!wp_verify_nonce($_POST['compression_nonce'], 'update_compression_settings')) {
|
||||
wp_die(__('Security check failed.', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('You do not have sufficient permissions.', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Sanitize and update compression settings
|
||||
$compression_enabled = isset($_POST['compression_enabled']) ? 1 : 0;
|
||||
$compression_method = sanitize_text_field($_POST['compression_method'] ?? 'auto');
|
||||
$compression_level = intval($_POST['compression_level'] ?? 6);
|
||||
$compression_cache_enabled = isset($_POST['compression_cache_enabled']) ? 1 : 0;
|
||||
|
||||
// Validate compression level
|
||||
if ($compression_level < 1 || $compression_level > 9) {
|
||||
$compression_level = 6;
|
||||
}
|
||||
|
||||
// Validate compression method
|
||||
$valid_methods = array('auto', 'gzip', 'brotli');
|
||||
if (!in_array($compression_method, $valid_methods)) {
|
||||
$compression_method = 'auto';
|
||||
}
|
||||
|
||||
// Update options
|
||||
update_option('tigerstyle_compression_enabled', $compression_enabled);
|
||||
update_option('tigerstyle_compression_method', $compression_method);
|
||||
update_option('tigerstyle_compression_level', $compression_level);
|
||||
update_option('tigerstyle_compression_cache_enabled', $compression_cache_enabled);
|
||||
|
||||
// Clear compression cache when settings change
|
||||
$this->clear_compression_cache();
|
||||
|
||||
// Re-initialize compression hooks if enabled
|
||||
if ($compression_enabled) {
|
||||
remove_action('template_redirect', array($this, 'start_compression_buffer'), 1);
|
||||
add_action('template_redirect', array($this, 'start_compression_buffer'), 1);
|
||||
}
|
||||
|
||||
// Redirect with success message
|
||||
wp_redirect(add_query_arg(array(
|
||||
'page' => 'tigerstyle-heat',
|
||||
'tab' => 'performance',
|
||||
'message' => 'compression_settings_updated'
|
||||
), admin_url('admin.php')));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
461
includes/modules/class-robots-txt.php
Normal file
461
includes/modules/class-robots-txt.php
Normal file
@ -0,0 +1,461 @@
|
||||
<?php
|
||||
/**
|
||||
* Robots.txt Module for TigerStyle Heat
|
||||
*
|
||||
* Provides comprehensive robots.txt management with dynamic generation,
|
||||
* sitemap integration, and intelligent defaults for WordPress sites.
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_RobotsTxt {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Option name for robots.txt settings
|
||||
*/
|
||||
private $option_name = 'tigerstyle_heat_robots_txt';
|
||||
|
||||
/**
|
||||
* Default robots.txt rules
|
||||
*/
|
||||
private $default_rules = array();
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_default_rules();
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WordPress hooks
|
||||
*/
|
||||
private function init_hooks() {
|
||||
// Handle robots.txt generation
|
||||
add_action('do_robotstxt', array($this, 'generate_robotstxt'), 10, 1);
|
||||
|
||||
// Override WordPress default robots.txt
|
||||
add_filter('robots_txt', array($this, 'filter_robots_txt'), 10, 2);
|
||||
|
||||
// Handle admin form submissions
|
||||
add_action('admin_post_tigerstyle_save_robots_txt', array($this, 'save_settings'));
|
||||
|
||||
// Add admin scripts
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default robots.txt rules
|
||||
*/
|
||||
private function init_default_rules() {
|
||||
$this->default_rules = array(
|
||||
'user_agent_all' => array(
|
||||
'user_agent' => '*',
|
||||
'disallow' => array(
|
||||
'/wp-admin/',
|
||||
'/wp-includes/',
|
||||
'/wp-content/plugins/',
|
||||
'/wp-content/themes/',
|
||||
'/wp-json/',
|
||||
'/xmlrpc.php',
|
||||
'/wp-*.php',
|
||||
'/readme.html',
|
||||
'/license.txt',
|
||||
'/?s=',
|
||||
'/search/',
|
||||
'/author/',
|
||||
'/feed/',
|
||||
'/comments/',
|
||||
'/trackback/',
|
||||
'/wp-login.php',
|
||||
'/wp-register.php'
|
||||
),
|
||||
'allow' => array(
|
||||
'/wp-content/uploads/',
|
||||
'/wp-content/themes/*/css/',
|
||||
'/wp-content/themes/*/js/',
|
||||
'/wp-content/themes/*/images/'
|
||||
)
|
||||
),
|
||||
'user_agent_google_images' => array(
|
||||
'user_agent' => 'Googlebot-Image',
|
||||
'allow' => array(
|
||||
'/wp-content/uploads/'
|
||||
)
|
||||
),
|
||||
'crawl_delay' => 1,
|
||||
'include_sitemap' => true,
|
||||
'custom_rules' => ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current settings
|
||||
*/
|
||||
private function get_settings() {
|
||||
$settings = get_option($this->option_name, array());
|
||||
return wp_parse_args($settings, $this->default_rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate robots.txt content
|
||||
*/
|
||||
public function generate_robotstxt($is_public) {
|
||||
if (!$is_public) {
|
||||
echo "User-agent: *\nDisallow: /\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $this->get_settings();
|
||||
$output = array();
|
||||
|
||||
// Add header comment
|
||||
$output[] = '# Robots.txt generated by TigerStyle Heat Plugin';
|
||||
$output[] = '# ' . home_url('/robots.txt');
|
||||
$output[] = '# Generated on: ' . current_time('Y-m-d H:i:s T');
|
||||
$output[] = '';
|
||||
|
||||
// Main user agent rules
|
||||
if (isset($settings['user_agent_all'])) {
|
||||
$rules = $settings['user_agent_all'];
|
||||
$output[] = 'User-agent: ' . $rules['user_agent'];
|
||||
|
||||
// Disallow rules
|
||||
if (!empty($rules['disallow'])) {
|
||||
foreach ($rules['disallow'] as $path) {
|
||||
if (!empty(trim($path))) {
|
||||
$output[] = 'Disallow: ' . trim($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow rules
|
||||
if (!empty($rules['allow'])) {
|
||||
foreach ($rules['allow'] as $path) {
|
||||
if (!empty(trim($path))) {
|
||||
$output[] = 'Allow: ' . trim($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$output[] = '';
|
||||
}
|
||||
|
||||
// Google Images specific rules
|
||||
if (isset($settings['user_agent_google_images'])) {
|
||||
$rules = $settings['user_agent_google_images'];
|
||||
$output[] = 'User-agent: ' . $rules['user_agent'];
|
||||
|
||||
if (!empty($rules['allow'])) {
|
||||
foreach ($rules['allow'] as $path) {
|
||||
if (!empty(trim($path))) {
|
||||
$output[] = 'Allow: ' . trim($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$output[] = '';
|
||||
}
|
||||
|
||||
// Crawl delay
|
||||
if (!empty($settings['crawl_delay']) && $settings['crawl_delay'] > 0) {
|
||||
$output[] = 'User-agent: *';
|
||||
$output[] = 'Crawl-delay: ' . intval($settings['crawl_delay']);
|
||||
$output[] = '';
|
||||
}
|
||||
|
||||
// Custom rules
|
||||
if (!empty($settings['custom_rules'])) {
|
||||
$output[] = '# Custom Rules';
|
||||
$output[] = trim($settings['custom_rules']);
|
||||
$output[] = '';
|
||||
}
|
||||
|
||||
// Sitemap references
|
||||
if (!empty($settings['include_sitemap'])) {
|
||||
$output[] = '# Sitemaps';
|
||||
$output[] = 'Sitemap: ' . home_url('/sitemap.xml');
|
||||
$output[] = 'Sitemap: ' . home_url('/sitemap_index.xml');
|
||||
|
||||
// Add WordPress default sitemaps if they exist
|
||||
if (function_exists('wp_sitemaps_get_server')) {
|
||||
$output[] = 'Sitemap: ' . home_url('/wp-sitemap.xml');
|
||||
}
|
||||
}
|
||||
|
||||
echo implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter WordPress default robots.txt
|
||||
*/
|
||||
public function filter_robots_txt($output, $public) {
|
||||
// Clear default output and use our custom generation
|
||||
ob_start();
|
||||
$this->generate_robotstxt($public);
|
||||
$custom_output = ob_get_clean();
|
||||
|
||||
return $custom_output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings from admin form
|
||||
*/
|
||||
public function save_settings() {
|
||||
// Verify nonce
|
||||
if (!wp_verify_nonce($_POST['tigerstyle_robots_nonce'], 'tigerstyle_robots_save')) {
|
||||
wp_die('Security check failed');
|
||||
}
|
||||
|
||||
// Check user permissions
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Insufficient permissions');
|
||||
}
|
||||
|
||||
$settings = array();
|
||||
|
||||
// Process main user agent rules
|
||||
$settings['user_agent_all'] = array(
|
||||
'user_agent' => '*',
|
||||
'disallow' => array_filter(array_map('trim', explode("\n", sanitize_textarea_field($_POST['disallow_rules'])))),
|
||||
'allow' => array_filter(array_map('trim', explode("\n", sanitize_textarea_field($_POST['allow_rules']))))
|
||||
);
|
||||
|
||||
// Google Images rules
|
||||
$settings['user_agent_google_images'] = array(
|
||||
'user_agent' => 'Googlebot-Image',
|
||||
'allow' => array_filter(array_map('trim', explode("\n", sanitize_textarea_field($_POST['google_images_allow']))))
|
||||
);
|
||||
|
||||
// Crawl delay
|
||||
$settings['crawl_delay'] = intval($_POST['crawl_delay']);
|
||||
|
||||
// Include sitemap
|
||||
$settings['include_sitemap'] = isset($_POST['include_sitemap']) ? true : false;
|
||||
|
||||
// Custom rules
|
||||
$settings['custom_rules'] = sanitize_textarea_field($_POST['custom_rules']);
|
||||
|
||||
// Save settings
|
||||
update_option($this->option_name, $settings);
|
||||
|
||||
// Redirect back with success message
|
||||
wp_redirect(add_query_arg(array(
|
||||
'page' => 'tigerstyle-heat',
|
||||
'message' => 'robots_txt_updated'
|
||||
), admin_url('admin.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin scripts
|
||||
*/
|
||||
public function enqueue_admin_scripts($hook) {
|
||||
if ($hook !== 'toplevel_page_tigerstyle-heat') {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script('tigerstyle-robots-admin', TIGERSTYLE_HEAT_PLUGIN_URL . 'admin/js/robots-admin.js', array('jquery'), TIGERSTYLE_HEAT_VERSION, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
$settings = $this->get_settings();
|
||||
$current_robots_url = home_url('/robots.txt');
|
||||
|
||||
?>
|
||||
<div class="tigerstyle-robots-container">
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Robots.txt Configuration', 'tigerstyle-heat'); ?></h3>
|
||||
<p><?php _e('Configure your site\'s robots.txt file to control how search engines crawl your website.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<div class="robots-current-status">
|
||||
<h4><?php _e('Current Robots.txt Status', 'tigerstyle-heat'); ?></h4>
|
||||
<p>
|
||||
<strong><?php _e('URL:', 'tigerstyle-heat'); ?></strong>
|
||||
<a href="<?php echo esc_url($current_robots_url); ?>" target="_blank"><?php echo esc_url($current_robots_url); ?></a>
|
||||
<span class="dashicons dashicons-external"></span>
|
||||
</p>
|
||||
<button type="button" id="test-robots-btn" class="button"><?php _e('Test Current Robots.txt', 'tigerstyle-heat'); ?></button>
|
||||
<div id="robots-test-result" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="tigerstyle_save_robots_txt">
|
||||
<?php wp_nonce_field('tigerstyle_robots_save', 'tigerstyle_robots_nonce'); ?>
|
||||
|
||||
<div class="robots-config-grid">
|
||||
<div class="robots-config-section">
|
||||
<h4><?php _e('Disallow Rules (one per line)', 'tigerstyle-heat'); ?></h4>
|
||||
<textarea name="disallow_rules" rows="10" cols="50" class="large-text code"><?php echo esc_textarea(implode("\n", $settings['user_agent_all']['disallow'])); ?></textarea>
|
||||
<p class="description"><?php _e('Paths that should be blocked from crawling. Start each path with /', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="robots-config-section">
|
||||
<h4><?php _e('Allow Rules (one per line)', 'tigerstyle-heat'); ?></h4>
|
||||
<textarea name="allow_rules" rows="10" cols="50" class="large-text code"><?php echo esc_textarea(implode("\n", $settings['user_agent_all']['allow'])); ?></textarea>
|
||||
<p class="description"><?php _e('Paths that should be explicitly allowed for crawling.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="robots-config-grid">
|
||||
<div class="robots-config-section">
|
||||
<h4><?php _e('Google Images Allow Rules', 'tigerstyle-heat'); ?></h4>
|
||||
<textarea name="google_images_allow" rows="5" cols="50" class="large-text code"><?php echo esc_textarea(implode("\n", $settings['user_agent_google_images']['allow'])); ?></textarea>
|
||||
<p class="description"><?php _e('Paths specifically allowed for Google Images crawler.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="robots-config-section">
|
||||
<h4><?php _e('Crawl Settings', 'tigerstyle-heat'); ?></h4>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Crawl Delay (seconds)', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="number" name="crawl_delay" value="<?php echo esc_attr($settings['crawl_delay']); ?>" min="0" max="60" step="1" class="small-text">
|
||||
<p class="description"><?php _e('Delay between crawler requests. 0 = no delay.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Include Sitemap References', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="include_sitemap" value="1" <?php checked($settings['include_sitemap']); ?>>
|
||||
<?php _e('Automatically include sitemap URLs in robots.txt', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="robots-config-section">
|
||||
<h4><?php _e('Custom Rules', 'tigerstyle-heat'); ?></h4>
|
||||
<textarea name="custom_rules" rows="8" cols="80" class="large-text code" placeholder="# Add custom robots.txt rules here"><?php echo esc_textarea($settings['custom_rules']); ?></textarea>
|
||||
<p class="description"><?php _e('Add any custom robots.txt rules. These will be added after the standard rules.', 'tigerstyle-heat'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="robots-preview-section">
|
||||
<h4><?php _e('Live Preview', 'tigerstyle-heat'); ?></h4>
|
||||
<button type="button" id="preview-robots-btn" class="button"><?php _e('Generate Preview', 'tigerstyle-heat'); ?></button>
|
||||
<pre id="robots-preview" class="robots-preview-content"></pre>
|
||||
</div>
|
||||
|
||||
<?php submit_button(__('Save Robots.txt Configuration', 'tigerstyle-heat'), 'primary', 'submit'); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tigerstyle-robots-container { max-width: 1200px; }
|
||||
.robots-config-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 20px 0; }
|
||||
.robots-config-section { background: #f9f9f9; padding: 15px; border-radius: 5px; }
|
||||
.robots-current-status { background: #e7f3ff; padding: 15px; border-left: 4px solid #0073aa; margin: 15px 0; }
|
||||
.robots-preview-content { background: #f1f1f1; padding: 15px; border: 1px solid #ddd; max-height: 300px; overflow-y: auto; }
|
||||
.robots-preview-section { margin: 20px 0; }
|
||||
@media (max-width: 768px) { .robots-config-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
// Test current robots.txt
|
||||
$('#test-robots-btn').click(function() {
|
||||
var btn = $(this);
|
||||
var result = $('#robots-test-result');
|
||||
|
||||
btn.prop('disabled', true).text('<?php _e('Testing...', 'tigerstyle-heat'); ?>');
|
||||
|
||||
$.get('<?php echo esc_js($current_robots_url); ?>')
|
||||
.done(function(data) {
|
||||
result.html('<div class="notice notice-success inline"><p><strong><?php _e('Success!', 'tigerstyle-heat'); ?></strong> <?php _e('Robots.txt is accessible and working.', 'tigerstyle-heat'); ?></p></div>');
|
||||
})
|
||||
.fail(function() {
|
||||
result.html('<div class="notice notice-error inline"><p><strong><?php _e('Error!', 'tigerstyle-heat'); ?></strong> <?php _e('Could not access robots.txt file.', 'tigerstyle-heat'); ?></p></div>');
|
||||
})
|
||||
.always(function() {
|
||||
btn.prop('disabled', false).text('<?php _e('Test Current Robots.txt', 'tigerstyle-heat'); ?>');
|
||||
});
|
||||
});
|
||||
|
||||
// Generate preview
|
||||
$('#preview-robots-btn').click(function() {
|
||||
var formData = $('form').serialize();
|
||||
var btn = $(this);
|
||||
var preview = $('#robots-preview');
|
||||
|
||||
btn.prop('disabled', true).text('<?php _e('Generating...', 'tigerstyle-heat'); ?>');
|
||||
|
||||
// Simulate robots.txt generation
|
||||
var disallowRules = $('textarea[name="disallow_rules"]').val().split('\n').filter(line => line.trim());
|
||||
var allowRules = $('textarea[name="allow_rules"]').val().split('\n').filter(line => line.trim());
|
||||
var googleImagesAllow = $('textarea[name="google_images_allow"]').val().split('\n').filter(line => line.trim());
|
||||
var crawlDelay = $('input[name="crawl_delay"]').val();
|
||||
var includeSitemap = $('input[name="include_sitemap"]').is(':checked');
|
||||
var customRules = $('textarea[name="custom_rules"]').val();
|
||||
|
||||
var previewContent = '# Robots.txt generated by TigerStyle Heat Plugin\n';
|
||||
previewContent += '# <?php echo esc_js($current_robots_url); ?>\n';
|
||||
previewContent += '# Generated on: ' + new Date().toLocaleString() + '\n\n';
|
||||
|
||||
previewContent += 'User-agent: *\n';
|
||||
disallowRules.forEach(rule => {
|
||||
if (rule.trim()) previewContent += 'Disallow: ' + rule.trim() + '\n';
|
||||
});
|
||||
allowRules.forEach(rule => {
|
||||
if (rule.trim()) previewContent += 'Allow: ' + rule.trim() + '\n';
|
||||
});
|
||||
previewContent += '\n';
|
||||
|
||||
if (googleImagesAllow.length > 0) {
|
||||
previewContent += 'User-agent: Googlebot-Image\n';
|
||||
googleImagesAllow.forEach(rule => {
|
||||
if (rule.trim()) previewContent += 'Allow: ' + rule.trim() + '\n';
|
||||
});
|
||||
previewContent += '\n';
|
||||
}
|
||||
|
||||
if (crawlDelay && crawlDelay > 0) {
|
||||
previewContent += 'User-agent: *\n';
|
||||
previewContent += 'Crawl-delay: ' + crawlDelay + '\n\n';
|
||||
}
|
||||
|
||||
if (customRules.trim()) {
|
||||
previewContent += '# Custom Rules\n';
|
||||
previewContent += customRules.trim() + '\n\n';
|
||||
}
|
||||
|
||||
if (includeSitemap) {
|
||||
previewContent += '# Sitemaps\n';
|
||||
previewContent += 'Sitemap: <?php echo esc_js(home_url('/sitemap.xml')); ?>\n';
|
||||
previewContent += 'Sitemap: <?php echo esc_js(home_url('/sitemap_index.xml')); ?>\n';
|
||||
previewContent += 'Sitemap: <?php echo esc_js(home_url('/wp-sitemap.xml')); ?>\n';
|
||||
}
|
||||
|
||||
preview.text(previewContent);
|
||||
btn.prop('disabled', false).text('<?php _e('Generate Preview', 'tigerstyle-heat'); ?>');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
28
includes/modules/class-seo-health.php
Normal file
28
includes/modules/class-seo-health.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Seo Health Module for TigerStyle Heat
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Seo_health {
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
// Initialize hooks
|
||||
}
|
||||
|
||||
public function render_admin_page() {
|
||||
echo '<div class="seo-info-box"><h3>SEO Health Configuration</h3><p>Monitor and analyze your website\'s SEO health with comprehensive auditing tools.</p></div>';
|
||||
}
|
||||
}
|
||||
756
includes/modules/class-sitemap-xml.php
Normal file
756
includes/modules/class-sitemap-xml.php
Normal file
@ -0,0 +1,756 @@
|
||||
<?php
|
||||
/**
|
||||
* Sitemap XML Module for TigerStyle Heat
|
||||
*
|
||||
* Provides comprehensive XML sitemap generation and management with support for
|
||||
* posts, pages, categories, tags, and custom post types with intelligent defaults.
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_Sitemap_xml {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Option name for sitemap settings
|
||||
*/
|
||||
private $option_name = 'tigerstyle_heat_sitemap_xml';
|
||||
|
||||
/**
|
||||
* Default sitemap settings
|
||||
*/
|
||||
private $default_settings = array();
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_default_settings();
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WordPress hooks
|
||||
*/
|
||||
private function init_hooks() {
|
||||
// Handle sitemap generation
|
||||
add_action('init', array($this, 'register_sitemap_endpoints'));
|
||||
|
||||
// Handle admin form submissions
|
||||
add_action('admin_post_tigerstyle_save_sitemap_xml', array($this, 'save_settings'));
|
||||
|
||||
// Add admin scripts
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
|
||||
|
||||
// Flush rewrite rules when settings change
|
||||
add_action('update_option_' . $this->option_name, array($this, 'flush_rewrite_rules'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default sitemap settings
|
||||
*/
|
||||
private function init_default_settings() {
|
||||
$this->default_settings = array(
|
||||
'enabled' => true,
|
||||
'include_posts' => true,
|
||||
'include_pages' => true,
|
||||
'include_categories' => true,
|
||||
'include_tags' => true,
|
||||
'include_custom_post_types' => array(),
|
||||
'exclude_post_ids' => '',
|
||||
'max_entries' => 50000,
|
||||
'split_by_post_type' => false,
|
||||
'include_images' => true,
|
||||
'include_lastmod' => true,
|
||||
'include_changefreq' => true,
|
||||
'include_priority' => true,
|
||||
'default_changefreq' => 'monthly',
|
||||
'ping_search_engines' => true,
|
||||
'cache_duration' => 24, // hours
|
||||
'exclude_noindex' => true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register sitemap endpoints
|
||||
*/
|
||||
public function register_sitemap_endpoints() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
if (!$settings['enabled']) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Main sitemap index
|
||||
add_rewrite_rule('^sitemap\.xml$', 'index.php?tigerstyle_sitemap=index', 'top');
|
||||
add_rewrite_rule('^sitemap_index\.xml$', 'index.php?tigerstyle_sitemap=index', 'top');
|
||||
|
||||
// Individual sitemaps
|
||||
add_rewrite_rule('^sitemap-([^/]+)\.xml$', 'index.php?tigerstyle_sitemap=$matches[1]', 'top');
|
||||
|
||||
// Add query vars
|
||||
add_filter('query_vars', array($this, 'add_query_vars'));
|
||||
|
||||
// Handle template redirects
|
||||
add_action('template_redirect', array($this, 'handle_sitemap_request'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add query vars
|
||||
*/
|
||||
public function add_query_vars($vars) {
|
||||
$vars[] = 'tigerstyle_sitemap';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle sitemap requests
|
||||
*/
|
||||
public function handle_sitemap_request() {
|
||||
$sitemap = get_query_var('tigerstyle_sitemap');
|
||||
|
||||
if (empty($sitemap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set XML headers
|
||||
header('Content-Type: application/xml; charset=utf-8');
|
||||
|
||||
if ($sitemap === 'index') {
|
||||
$this->generate_sitemap_index();
|
||||
} else {
|
||||
$this->generate_sitemap($sitemap);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate sitemap index
|
||||
*/
|
||||
private function generate_sitemap_index() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
||||
echo '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
|
||||
|
||||
// Posts sitemap
|
||||
if ($settings['include_posts']) {
|
||||
$this->add_sitemap_to_index('posts');
|
||||
}
|
||||
|
||||
// Pages sitemap
|
||||
if ($settings['include_pages']) {
|
||||
$this->add_sitemap_to_index('pages');
|
||||
}
|
||||
|
||||
// Categories sitemap
|
||||
if ($settings['include_categories']) {
|
||||
$this->add_sitemap_to_index('categories');
|
||||
}
|
||||
|
||||
// Tags sitemap
|
||||
if ($settings['include_tags']) {
|
||||
$this->add_sitemap_to_index('tags');
|
||||
}
|
||||
|
||||
// Custom post types
|
||||
if (!empty($settings['include_custom_post_types'])) {
|
||||
foreach ($settings['include_custom_post_types'] as $post_type) {
|
||||
$this->add_sitemap_to_index($post_type);
|
||||
}
|
||||
}
|
||||
|
||||
echo '</sitemapindex>' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Add sitemap to index
|
||||
*/
|
||||
private function add_sitemap_to_index($type) {
|
||||
$url = home_url("/sitemap-{$type}.xml");
|
||||
$lastmod = date('c');
|
||||
|
||||
echo " <sitemap>\n";
|
||||
echo " <loc>" . esc_xml($url) . "</loc>\n";
|
||||
echo " <lastmod>" . esc_xml($lastmod) . "</lastmod>\n";
|
||||
echo " </sitemap>\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate individual sitemap
|
||||
*/
|
||||
private function generate_sitemap($type) {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
||||
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
|
||||
|
||||
if ($settings['include_images']) {
|
||||
echo ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"';
|
||||
}
|
||||
|
||||
echo '>' . "\n";
|
||||
|
||||
switch ($type) {
|
||||
case 'posts':
|
||||
$this->generate_posts_sitemap();
|
||||
break;
|
||||
case 'pages':
|
||||
$this->generate_pages_sitemap();
|
||||
break;
|
||||
case 'categories':
|
||||
$this->generate_categories_sitemap();
|
||||
break;
|
||||
case 'tags':
|
||||
$this->generate_tags_sitemap();
|
||||
break;
|
||||
default:
|
||||
// Handle custom post types
|
||||
$this->generate_custom_post_type_sitemap($type);
|
||||
break;
|
||||
}
|
||||
|
||||
echo '</urlset>' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate posts sitemap
|
||||
*/
|
||||
private function generate_posts_sitemap() {
|
||||
$settings = $this->get_settings();
|
||||
$exclude_ids = array_filter(array_map('trim', explode(',', $settings['exclude_post_ids'])));
|
||||
|
||||
$posts = get_posts(array(
|
||||
'post_type' => 'post',
|
||||
'post_status' => 'publish',
|
||||
'numberposts' => $settings['max_entries'],
|
||||
'exclude' => $exclude_ids,
|
||||
'meta_query' => $settings['exclude_noindex'] ? array(
|
||||
array(
|
||||
'key' => '_yoast_wpseo_meta-robots-noindex',
|
||||
'value' => '1',
|
||||
'compare' => '!='
|
||||
)
|
||||
) : array()
|
||||
));
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$this->add_url_to_sitemap($post);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate pages sitemap
|
||||
*/
|
||||
private function generate_pages_sitemap() {
|
||||
$settings = $this->get_settings();
|
||||
$exclude_ids = array_filter(array_map('trim', explode(',', $settings['exclude_post_ids'])));
|
||||
|
||||
$pages = get_posts(array(
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'publish',
|
||||
'numberposts' => $settings['max_entries'],
|
||||
'exclude' => $exclude_ids,
|
||||
'meta_query' => $settings['exclude_noindex'] ? array(
|
||||
array(
|
||||
'key' => '_yoast_wpseo_meta-robots-noindex',
|
||||
'value' => '1',
|
||||
'compare' => '!='
|
||||
)
|
||||
) : array()
|
||||
));
|
||||
|
||||
foreach ($pages as $page) {
|
||||
$this->add_url_to_sitemap($page);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate categories sitemap
|
||||
*/
|
||||
private function generate_categories_sitemap() {
|
||||
$categories = get_categories(array(
|
||||
'hide_empty' => true,
|
||||
'number' => $this->get_settings()['max_entries']
|
||||
));
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$this->add_taxonomy_url_to_sitemap($category);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tags sitemap
|
||||
*/
|
||||
private function generate_tags_sitemap() {
|
||||
$tags = get_tags(array(
|
||||
'hide_empty' => true,
|
||||
'number' => $this->get_settings()['max_entries']
|
||||
));
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$this->add_taxonomy_url_to_sitemap($tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate custom post type sitemap
|
||||
*/
|
||||
private function generate_custom_post_type_sitemap($post_type) {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
if (!in_array($post_type, $settings['include_custom_post_types'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$posts = get_posts(array(
|
||||
'post_type' => $post_type,
|
||||
'post_status' => 'publish',
|
||||
'numberposts' => $settings['max_entries']
|
||||
));
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$this->add_url_to_sitemap($post);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add URL to sitemap for posts/pages
|
||||
*/
|
||||
private function add_url_to_sitemap($post) {
|
||||
$settings = $this->get_settings();
|
||||
$url = get_permalink($post);
|
||||
|
||||
echo " <url>\n";
|
||||
echo " <loc>" . esc_xml($url) . "</loc>\n";
|
||||
|
||||
if ($settings['include_lastmod']) {
|
||||
echo " <lastmod>" . esc_xml(date('c', strtotime($post->post_modified))) . "</lastmod>\n";
|
||||
}
|
||||
|
||||
if ($settings['include_changefreq']) {
|
||||
echo " <changefreq>" . esc_xml($settings['default_changefreq']) . "</changefreq>\n";
|
||||
}
|
||||
|
||||
if ($settings['include_priority']) {
|
||||
$priority = ($post->post_type === 'page') ? '0.8' : '0.6';
|
||||
echo " <priority>" . esc_xml($priority) . "</priority>\n";
|
||||
}
|
||||
|
||||
// Add images if enabled
|
||||
if ($settings['include_images']) {
|
||||
$this->add_post_images_to_sitemap($post);
|
||||
}
|
||||
|
||||
echo " </url>\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Add URL to sitemap for taxonomy terms
|
||||
*/
|
||||
private function add_taxonomy_url_to_sitemap($term) {
|
||||
$settings = $this->get_settings();
|
||||
$url = get_term_link($term);
|
||||
|
||||
if (is_wp_error($url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo " <url>\n";
|
||||
echo " <loc>" . esc_xml($url) . "</loc>\n";
|
||||
|
||||
if ($settings['include_changefreq']) {
|
||||
echo " <changefreq>weekly</changefreq>\n";
|
||||
}
|
||||
|
||||
if ($settings['include_priority']) {
|
||||
echo " <priority>0.5</priority>\n";
|
||||
}
|
||||
|
||||
echo " </url>\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Add post images to sitemap
|
||||
*/
|
||||
private function add_post_images_to_sitemap($post) {
|
||||
// Get featured image
|
||||
$featured_image_id = get_post_thumbnail_id($post->ID);
|
||||
if ($featured_image_id) {
|
||||
$image_url = wp_get_attachment_image_url($featured_image_id, 'full');
|
||||
if ($image_url) {
|
||||
echo " <image:image>\n";
|
||||
echo " <image:loc>" . esc_xml($image_url) . "</image:loc>\n";
|
||||
echo " </image:image>\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Get images from content
|
||||
preg_match_all('/<img[^>]+src=[\'"]([^\'"]+)[\'"][^>]*>/i', $post->post_content, $matches);
|
||||
if (!empty($matches[1])) {
|
||||
foreach (array_slice($matches[1], 0, 10) as $image_url) {
|
||||
if (filter_var($image_url, FILTER_VALIDATE_URL)) {
|
||||
echo " <image:image>\n";
|
||||
echo " <image:loc>" . esc_xml($image_url) . "</image:loc>\n";
|
||||
echo " </image:image>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current settings
|
||||
*/
|
||||
private function get_settings() {
|
||||
$settings = get_option($this->option_name, array());
|
||||
return wp_parse_args($settings, $this->default_settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings from admin form
|
||||
*/
|
||||
public function save_settings() {
|
||||
// Verify nonce
|
||||
if (!wp_verify_nonce($_POST['tigerstyle_sitemap_nonce'], 'tigerstyle_sitemap_save')) {
|
||||
wp_die('Security check failed');
|
||||
}
|
||||
|
||||
// Check user permissions
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Insufficient permissions');
|
||||
}
|
||||
|
||||
$settings = array();
|
||||
|
||||
// Basic settings
|
||||
$settings['enabled'] = isset($_POST['enabled']) ? true : false;
|
||||
$settings['include_posts'] = isset($_POST['include_posts']) ? true : false;
|
||||
$settings['include_pages'] = isset($_POST['include_pages']) ? true : false;
|
||||
$settings['include_categories'] = isset($_POST['include_categories']) ? true : false;
|
||||
$settings['include_tags'] = isset($_POST['include_tags']) ? true : false;
|
||||
$settings['include_custom_post_types'] = isset($_POST['include_custom_post_types']) ? $_POST['include_custom_post_types'] : array();
|
||||
$settings['exclude_post_ids'] = sanitize_text_field($_POST['exclude_post_ids']);
|
||||
$settings['max_entries'] = intval($_POST['max_entries']);
|
||||
$settings['split_by_post_type'] = isset($_POST['split_by_post_type']) ? true : false;
|
||||
$settings['include_images'] = isset($_POST['include_images']) ? true : false;
|
||||
$settings['include_lastmod'] = isset($_POST['include_lastmod']) ? true : false;
|
||||
$settings['include_changefreq'] = isset($_POST['include_changefreq']) ? true : false;
|
||||
$settings['include_priority'] = isset($_POST['include_priority']) ? true : false;
|
||||
$settings['default_changefreq'] = sanitize_text_field($_POST['default_changefreq']);
|
||||
$settings['ping_search_engines'] = isset($_POST['ping_search_engines']) ? true : false;
|
||||
$settings['cache_duration'] = intval($_POST['cache_duration']);
|
||||
$settings['exclude_noindex'] = isset($_POST['exclude_noindex']) ? true : false;
|
||||
|
||||
// Save settings
|
||||
update_option($this->option_name, $settings);
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
|
||||
// Redirect back with success message
|
||||
wp_redirect(add_query_arg(array(
|
||||
'page' => 'tigerstyle-heat',
|
||||
'message' => 'sitemap_xml_updated'
|
||||
), admin_url('admin.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush rewrite rules when settings change
|
||||
*/
|
||||
public function flush_rewrite_rules() {
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin scripts
|
||||
*/
|
||||
public function enqueue_admin_scripts($hook) {
|
||||
if ($hook !== 'toplevel_page_tigerstyle-heat') {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script('tigerstyle-sitemap-admin', TIGERSTYLE_HEAT_PLUGIN_URL . 'admin/js/sitemap-admin.js', array('jquery'), TIGERSTYLE_HEAT_VERSION, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
$settings = $this->get_settings();
|
||||
$sitemap_index_url = home_url('/sitemap.xml');
|
||||
$available_post_types = get_post_types(array('public' => true), 'objects');
|
||||
unset($available_post_types['attachment']);
|
||||
|
||||
?>
|
||||
<div class="tigerstyle-sitemap-container">
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('XML Sitemap Configuration', 'tigerstyle-heat'); ?></h3>
|
||||
<p><?php _e('Generate and configure XML sitemaps to help search engines discover and index your content efficiently.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<div class="sitemap-current-status">
|
||||
<h4><?php _e('Current Sitemap Status', 'tigerstyle-heat'); ?></h4>
|
||||
<p>
|
||||
<strong><?php _e('Sitemap Index URL:', 'tigerstyle-heat'); ?></strong>
|
||||
<a href="<?php echo esc_url($sitemap_index_url); ?>" target="_blank"><?php echo esc_url($sitemap_index_url); ?></a>
|
||||
<span class="dashicons dashicons-external"></span>
|
||||
</p>
|
||||
<button type="button" id="test-sitemap-btn" class="button"><?php _e('Test Sitemap Access', 'tigerstyle-heat'); ?></button>
|
||||
<button type="button" id="validate-sitemap-btn" class="button"><?php _e('Validate XML Structure', 'tigerstyle-heat'); ?></button>
|
||||
<div id="sitemap-test-result" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
|
||||
<input type="hidden" name="action" value="tigerstyle_save_sitemap_xml">
|
||||
<?php wp_nonce_field('tigerstyle_sitemap_save', 'tigerstyle_sitemap_nonce'); ?>
|
||||
|
||||
<div class="sitemap-config-grid">
|
||||
<div class="sitemap-config-section">
|
||||
<h4><?php _e('Basic Settings', 'tigerstyle-heat'); ?></h4>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enable XML Sitemaps', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enabled" value="1" <?php checked($settings['enabled']); ?>>
|
||||
<?php _e('Generate XML sitemaps for this website', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Maximum Entries per Sitemap', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="number" name="max_entries" value="<?php echo esc_attr($settings['max_entries']); ?>" min="100" max="50000" step="100" class="regular-text">
|
||||
<p class="description"><?php _e('Maximum number of URLs in each sitemap file (recommended: 50,000 or less)', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="sitemap-config-section">
|
||||
<h4><?php _e('Content Types', 'tigerstyle-heat'); ?></h4>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Include in Sitemap', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label><input type="checkbox" name="include_posts" value="1" <?php checked($settings['include_posts']); ?>> <?php _e('Posts', 'tigerstyle-heat'); ?></label><br>
|
||||
<label><input type="checkbox" name="include_pages" value="1" <?php checked($settings['include_pages']); ?>> <?php _e('Pages', 'tigerstyle-heat'); ?></label><br>
|
||||
<label><input type="checkbox" name="include_categories" value="1" <?php checked($settings['include_categories']); ?>> <?php _e('Categories', 'tigerstyle-heat'); ?></label><br>
|
||||
<label><input type="checkbox" name="include_tags" value="1" <?php checked($settings['include_tags']); ?>> <?php _e('Tags', 'tigerstyle-heat'); ?></label><br>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if (!empty($available_post_types)): ?>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Custom Post Types', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<?php foreach ($available_post_types as $post_type): ?>
|
||||
<?php if (!in_array($post_type->name, array('post', 'page'))): ?>
|
||||
<label>
|
||||
<input type="checkbox" name="include_custom_post_types[]" value="<?php echo esc_attr($post_type->name); ?>" <?php checked(in_array($post_type->name, $settings['include_custom_post_types'])); ?>>
|
||||
<?php echo esc_html($post_type->label); ?>
|
||||
</label><br>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sitemap-config-grid">
|
||||
<div class="sitemap-config-section">
|
||||
<h4><?php _e('XML Structure Settings', 'tigerstyle-heat'); ?></h4>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Include Additional Data', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label><input type="checkbox" name="include_lastmod" value="1" <?php checked($settings['include_lastmod']); ?>> <?php _e('Last Modified Date', 'tigerstyle-heat'); ?></label><br>
|
||||
<label><input type="checkbox" name="include_changefreq" value="1" <?php checked($settings['include_changefreq']); ?>> <?php _e('Change Frequency', 'tigerstyle-heat'); ?></label><br>
|
||||
<label><input type="checkbox" name="include_priority" value="1" <?php checked($settings['include_priority']); ?>> <?php _e('Priority Values', 'tigerstyle-heat'); ?></label><br>
|
||||
<label><input type="checkbox" name="include_images" value="1" <?php checked($settings['include_images']); ?>> <?php _e('Images (Google Image Sitemap)', 'tigerstyle-heat'); ?></label><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Default Change Frequency', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<select name="default_changefreq">
|
||||
<option value="always" <?php selected($settings['default_changefreq'], 'always'); ?>><?php _e('Always', 'tigerstyle-heat'); ?></option>
|
||||
<option value="hourly" <?php selected($settings['default_changefreq'], 'hourly'); ?>><?php _e('Hourly', 'tigerstyle-heat'); ?></option>
|
||||
<option value="daily" <?php selected($settings['default_changefreq'], 'daily'); ?>><?php _e('Daily', 'tigerstyle-heat'); ?></option>
|
||||
<option value="weekly" <?php selected($settings['default_changefreq'], 'weekly'); ?>><?php _e('Weekly', 'tigerstyle-heat'); ?></option>
|
||||
<option value="monthly" <?php selected($settings['default_changefreq'], 'monthly'); ?>><?php _e('Monthly', 'tigerstyle-heat'); ?></option>
|
||||
<option value="yearly" <?php selected($settings['default_changefreq'], 'yearly'); ?>><?php _e('Yearly', 'tigerstyle-heat'); ?></option>
|
||||
<option value="never" <?php selected($settings['default_changefreq'], 'never'); ?>><?php _e('Never', 'tigerstyle-heat'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="sitemap-config-section">
|
||||
<h4><?php _e('Advanced Options', 'tigerstyle-heat'); ?></h4>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Exclude Posts/Pages', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="exclude_post_ids" value="<?php echo esc_attr($settings['exclude_post_ids']); ?>" class="regular-text" placeholder="1,2,3">
|
||||
<p class="description"><?php _e('Comma-separated list of post/page IDs to exclude from sitemap', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Additional Settings', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label><input type="checkbox" name="exclude_noindex" value="1" <?php checked($settings['exclude_noindex']); ?>> <?php _e('Exclude noindex pages', 'tigerstyle-heat'); ?></label><br>
|
||||
<label><input type="checkbox" name="ping_search_engines" value="1" <?php checked($settings['ping_search_engines']); ?>> <?php _e('Ping search engines when sitemap updates', 'tigerstyle-heat'); ?></label><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Cache Duration (hours)', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="number" name="cache_duration" value="<?php echo esc_attr($settings['cache_duration']); ?>" min="1" max="168" step="1" class="small-text">
|
||||
<p class="description"><?php _e('How long to cache sitemap files before regenerating', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sitemap-preview-section">
|
||||
<h4><?php _e('Sitemap URLs', 'tigerstyle-heat'); ?></h4>
|
||||
<button type="button" id="generate-sitemap-urls-btn" class="button"><?php _e('Generate Sitemap URLs', 'tigerstyle-heat'); ?></button>
|
||||
<div id="sitemap-urls-preview" class="sitemap-preview-content"></div>
|
||||
</div>
|
||||
|
||||
<?php submit_button(__('Save Sitemap Configuration', 'tigerstyle-heat'), 'primary', 'submit'); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tigerstyle-sitemap-container { max-width: 1200px; }
|
||||
.sitemap-config-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 20px 0; }
|
||||
.sitemap-config-section { background: #f9f9f9; padding: 15px; border-radius: 5px; }
|
||||
.sitemap-current-status { background: #e7f3ff; padding: 15px; border-left: 4px solid #0073aa; margin: 15px 0; }
|
||||
.sitemap-preview-content { background: #f1f1f1; padding: 15px; border: 1px solid #ddd; max-height: 300px; overflow-y: auto; }
|
||||
.sitemap-preview-section { margin: 20px 0; }
|
||||
@media (max-width: 768px) { .sitemap-config-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
// Test sitemap access
|
||||
$('#test-sitemap-btn').click(function() {
|
||||
var btn = $(this);
|
||||
var result = $('#sitemap-test-result');
|
||||
|
||||
btn.prop('disabled', true).text('<?php _e('Testing...', 'tigerstyle-heat'); ?>');
|
||||
|
||||
$.get('<?php echo esc_js($sitemap_index_url); ?>')
|
||||
.done(function(data) {
|
||||
result.html('<div class="notice notice-success inline"><p><strong><?php _e('Success!', 'tigerstyle-heat'); ?></strong> <?php _e('Sitemap is accessible and working.', 'tigerstyle-heat'); ?></p></div>');
|
||||
})
|
||||
.fail(function() {
|
||||
result.html('<div class="notice notice-error inline"><p><strong><?php _e('Error!', 'tigerstyle-heat'); ?></strong> <?php _e('Could not access sitemap file.', 'tigerstyle-heat'); ?></p></div>');
|
||||
})
|
||||
.always(function() {
|
||||
btn.prop('disabled', false).text('<?php _e('Test Sitemap Access', 'tigerstyle-heat'); ?>');
|
||||
});
|
||||
});
|
||||
|
||||
// Validate XML structure
|
||||
$('#validate-sitemap-btn').click(function() {
|
||||
var btn = $(this);
|
||||
var result = $('#sitemap-test-result');
|
||||
|
||||
btn.prop('disabled', true).text('<?php _e('Validating...', 'tigerstyle-heat'); ?>');
|
||||
|
||||
$.get('<?php echo esc_js($sitemap_index_url); ?>')
|
||||
.done(function(data) {
|
||||
try {
|
||||
var parser = new DOMParser();
|
||||
var xmlDoc = parser.parseFromString(data, "text/xml");
|
||||
var parseError = xmlDoc.getElementsByTagName("parsererror");
|
||||
|
||||
if (parseError.length > 0) {
|
||||
result.html('<div class="notice notice-error inline"><p><strong><?php _e('Invalid XML!', 'tigerstyle-heat'); ?></strong> <?php _e('The sitemap contains XML syntax errors.', 'tigerstyle-heat'); ?></p></div>');
|
||||
} else {
|
||||
result.html('<div class="notice notice-success inline"><p><strong><?php _e('Valid XML!', 'tigerstyle-heat'); ?></strong> <?php _e('The sitemap XML structure is valid.', 'tigerstyle-heat'); ?></p></div>');
|
||||
}
|
||||
} catch (e) {
|
||||
result.html('<div class="notice notice-error inline"><p><strong><?php _e('Validation Error!', 'tigerstyle-heat'); ?></strong> <?php _e('Could not validate XML structure.', 'tigerstyle-heat'); ?></p></div>');
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
result.html('<div class="notice notice-error inline"><p><strong><?php _e('Error!', 'tigerstyle-heat'); ?></strong> <?php _e('Could not access sitemap for validation.', 'tigerstyle-heat'); ?></p></div>');
|
||||
})
|
||||
.always(function() {
|
||||
btn.prop('disabled', false).text('<?php _e('Validate XML Structure', 'tigerstyle-heat'); ?>');
|
||||
});
|
||||
});
|
||||
|
||||
// Generate sitemap URLs preview
|
||||
$('#generate-sitemap-urls-btn').click(function() {
|
||||
var btn = $(this);
|
||||
var preview = $('#sitemap-urls-preview');
|
||||
var enabled = $('input[name="enabled"]').is(':checked');
|
||||
|
||||
btn.prop('disabled', true).text('<?php _e('Generating...', 'tigerstyle-heat'); ?>');
|
||||
|
||||
if (!enabled) {
|
||||
preview.html('<p><?php _e('XML Sitemaps are disabled. Enable them to see sitemap URLs.', 'tigerstyle-heat'); ?></p>');
|
||||
btn.prop('disabled', false).text('<?php _e('Generate Sitemap URLs', 'tigerstyle-heat'); ?>');
|
||||
return;
|
||||
}
|
||||
|
||||
var urls = ['<?php echo esc_js($sitemap_index_url); ?>'];
|
||||
|
||||
if ($('input[name="include_posts"]').is(':checked')) {
|
||||
urls.push('<?php echo esc_js(home_url('/sitemap-posts.xml')); ?>');
|
||||
}
|
||||
if ($('input[name="include_pages"]').is(':checked')) {
|
||||
urls.push('<?php echo esc_js(home_url('/sitemap-pages.xml')); ?>');
|
||||
}
|
||||
if ($('input[name="include_categories"]').is(':checked')) {
|
||||
urls.push('<?php echo esc_js(home_url('/sitemap-categories.xml')); ?>');
|
||||
}
|
||||
if ($('input[name="include_tags"]').is(':checked')) {
|
||||
urls.push('<?php echo esc_js(home_url('/sitemap-tags.xml')); ?>');
|
||||
}
|
||||
|
||||
$('input[name="include_custom_post_types[]"]:checked').each(function() {
|
||||
urls.push('<?php echo esc_js(home_url('/sitemap-')); ?>' + $(this).val() + '.xml');
|
||||
});
|
||||
|
||||
var html = '<h5><?php _e('Generated Sitemap URLs:', 'tigerstyle-heat'); ?></h5><ul>';
|
||||
urls.forEach(function(url) {
|
||||
html += '<li><a href="' + url + '" target="_blank">' + url + '</a></li>';
|
||||
});
|
||||
html += '</ul>';
|
||||
|
||||
preview.html(html);
|
||||
btn.prop('disabled', false).text('<?php _e('Generate Sitemap URLs', 'tigerstyle-heat'); ?>');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
3059
includes/modules/class-structured-data.php
Normal file
3059
includes/modules/class-structured-data.php
Normal file
File diff suppressed because it is too large
Load Diff
880
includes/modules/class-twitter.php
Normal file
880
includes/modules/class-twitter.php
Normal file
@ -0,0 +1,880 @@
|
||||
<?php
|
||||
/**
|
||||
* Twitter/X Integration Module
|
||||
*
|
||||
* Handles Twitter Cards optimization, meta tags generation,
|
||||
* and social sharing optimization for X platform.
|
||||
*
|
||||
* @package TigerStyleSEO
|
||||
* @subpackage Modules
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Twitter/X Integration Class
|
||||
*/
|
||||
class TigerStyleSEO_Twitter {
|
||||
|
||||
/**
|
||||
* Single instance of the class
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Option name for Twitter settings
|
||||
*/
|
||||
private $option_name = 'tigerstyle_heat_twitter';
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
private $defaults = array(
|
||||
'enable_twitter_cards' => true,
|
||||
'default_card_type' => 'summary_large_image',
|
||||
'site_username' => '',
|
||||
'creator_username' => '',
|
||||
'default_image' => '',
|
||||
'image_alt_text' => '',
|
||||
'enable_creator_tags' => true,
|
||||
'enable_fallback_to_og' => true,
|
||||
'card_title_length' => 70,
|
||||
'card_description_length' => 200,
|
||||
'optimize_for_engagement' => true
|
||||
);
|
||||
|
||||
/**
|
||||
* Available Twitter Card types
|
||||
*/
|
||||
private $card_types = array(
|
||||
'summary' => 'Summary Card',
|
||||
'summary_large_image' => 'Summary Card with Large Image',
|
||||
'app' => 'App Card',
|
||||
'player' => 'Player Card'
|
||||
);
|
||||
|
||||
/**
|
||||
* Get single instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module
|
||||
*/
|
||||
private function init() {
|
||||
// Frontend hooks
|
||||
add_action('wp_head', array($this, 'output_twitter_card_tags'), 6);
|
||||
|
||||
// Admin hooks
|
||||
if (is_admin()) {
|
||||
add_action('admin_init', array($this, 'register_settings'));
|
||||
}
|
||||
|
||||
// REST API endpoints for testing
|
||||
add_action('rest_api_init', array($this, 'register_rest_routes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twitter settings
|
||||
*/
|
||||
public function get_settings() {
|
||||
$settings = get_option($this->option_name, array());
|
||||
return wp_parse_args($settings, $this->defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Twitter settings
|
||||
*/
|
||||
public function update_settings($new_settings) {
|
||||
$settings = $this->get_settings();
|
||||
$settings = wp_parse_args($new_settings, $settings);
|
||||
return update_option($this->option_name, $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output Twitter Card meta tags
|
||||
*/
|
||||
public function output_twitter_card_tags() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
if (!$settings['enable_twitter_cards']) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Twitter Card data
|
||||
$card_data = $this->get_twitter_card_data();
|
||||
|
||||
// Output Twitter Card tags
|
||||
foreach ($card_data as $name => $content) {
|
||||
if (!empty($content)) {
|
||||
printf(
|
||||
'<meta name="%s" content="%s" />' . "\n",
|
||||
esc_attr($name),
|
||||
esc_attr($content)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twitter Card data for current page
|
||||
*/
|
||||
private function get_twitter_card_data() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
$card_data = array();
|
||||
|
||||
// Basic required tags
|
||||
$card_data['twitter:card'] = $this->get_card_type();
|
||||
$card_data['twitter:site'] = $this->get_site_username();
|
||||
|
||||
// Content tags
|
||||
$card_data['twitter:title'] = $this->get_twitter_title();
|
||||
$card_data['twitter:description'] = $this->get_twitter_description();
|
||||
|
||||
// Image tags
|
||||
$image_data = $this->get_twitter_image();
|
||||
if ($image_data) {
|
||||
$card_data['twitter:image'] = $image_data['url'];
|
||||
if (!empty($image_data['alt'])) {
|
||||
$card_data['twitter:image:alt'] = $image_data['alt'];
|
||||
}
|
||||
}
|
||||
|
||||
// Creator tag
|
||||
if ($settings['enable_creator_tags']) {
|
||||
$creator = $this->get_creator_username();
|
||||
if ($creator) {
|
||||
$card_data['twitter:creator'] = $creator;
|
||||
}
|
||||
}
|
||||
|
||||
// App card specific tags
|
||||
if ($card_data['twitter:card'] === 'app') {
|
||||
$app_data = $this->get_app_card_data();
|
||||
$card_data = array_merge($card_data, $app_data);
|
||||
}
|
||||
|
||||
// Player card specific tags
|
||||
if ($card_data['twitter:card'] === 'player') {
|
||||
$player_data = $this->get_player_card_data();
|
||||
$card_data = array_merge($card_data, $player_data);
|
||||
}
|
||||
|
||||
return apply_filters('tigerstyle_heat_twitter_card_data', $card_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twitter Card type
|
||||
*/
|
||||
private function get_card_type() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
// Check for post-specific card type
|
||||
if (is_singular()) {
|
||||
$post_card_type = get_post_meta(get_the_ID(), '_twitter_card_type', true);
|
||||
if ($post_card_type && isset($this->card_types[$post_card_type])) {
|
||||
return $post_card_type;
|
||||
}
|
||||
}
|
||||
|
||||
return $settings['default_card_type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get site username (with @ prefix)
|
||||
*/
|
||||
private function get_site_username() {
|
||||
$settings = $this->get_settings();
|
||||
$username = $settings['site_username'];
|
||||
|
||||
if (!empty($username)) {
|
||||
// Ensure @ prefix
|
||||
return (strpos($username, '@') === 0) ? $username : '@' . $username;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get creator username (with @ prefix)
|
||||
*/
|
||||
private function get_creator_username() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
// Check for post-specific creator
|
||||
if (is_singular()) {
|
||||
$post_creator = get_post_meta(get_the_ID(), '_twitter_creator', true);
|
||||
if ($post_creator) {
|
||||
return (strpos($post_creator, '@') === 0) ? $post_creator : '@' . $post_creator;
|
||||
}
|
||||
|
||||
// Try to get author's Twitter handle
|
||||
$author_twitter = get_the_author_meta('twitter');
|
||||
if ($author_twitter) {
|
||||
return (strpos($author_twitter, '@') === 0) ? $author_twitter : '@' . $author_twitter;
|
||||
}
|
||||
}
|
||||
|
||||
$username = $settings['creator_username'];
|
||||
if (!empty($username)) {
|
||||
return (strpos($username, '@') === 0) ? $username : '@' . $username;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twitter title
|
||||
*/
|
||||
private function get_twitter_title() {
|
||||
$settings = $this->get_settings();
|
||||
$max_length = $settings['card_title_length'];
|
||||
|
||||
// Check for custom Twitter title
|
||||
if (is_singular()) {
|
||||
$custom_title = get_post_meta(get_the_ID(), '_twitter_title', true);
|
||||
if ($custom_title) {
|
||||
return $this->truncate_text($custom_title, $max_length);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to OpenGraph or default title logic
|
||||
if (is_single() || is_page()) {
|
||||
$title = get_the_title();
|
||||
} elseif (is_category()) {
|
||||
$title = single_cat_title('', false);
|
||||
} elseif (is_tag()) {
|
||||
$title = single_tag_title('', false);
|
||||
} elseif (is_author()) {
|
||||
$title = get_the_author();
|
||||
} elseif (is_search()) {
|
||||
$title = sprintf(__('Search Results for: %s', 'tigerstyle-heat'), get_search_query());
|
||||
} elseif (is_archive()) {
|
||||
$title = get_the_archive_title();
|
||||
} else {
|
||||
$title = get_bloginfo('name');
|
||||
}
|
||||
|
||||
return $this->truncate_text($title, $max_length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twitter description
|
||||
*/
|
||||
private function get_twitter_description() {
|
||||
$settings = $this->get_settings();
|
||||
$max_length = $settings['card_description_length'];
|
||||
|
||||
// Check for custom Twitter description
|
||||
if (is_singular()) {
|
||||
$custom_description = get_post_meta(get_the_ID(), '_twitter_description', true);
|
||||
if ($custom_description) {
|
||||
return $this->truncate_text($custom_description, $max_length);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback logic
|
||||
if (is_single() || is_page()) {
|
||||
$excerpt = get_the_excerpt();
|
||||
if ($excerpt) {
|
||||
return $this->truncate_text($excerpt, $max_length);
|
||||
}
|
||||
} elseif (is_category()) {
|
||||
$description = category_description();
|
||||
if ($description) {
|
||||
return $this->truncate_text(strip_tags($description), $max_length);
|
||||
}
|
||||
} elseif (is_tag()) {
|
||||
$description = tag_description();
|
||||
if ($description) {
|
||||
return $this->truncate_text(strip_tags($description), $max_length);
|
||||
}
|
||||
}
|
||||
|
||||
$site_description = get_bloginfo('description');
|
||||
return $this->truncate_text($site_description, $max_length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twitter image data
|
||||
*/
|
||||
private function get_twitter_image() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
// Check for custom Twitter image
|
||||
if (is_singular()) {
|
||||
$custom_image = get_post_meta(get_the_ID(), '_twitter_image', true);
|
||||
if ($custom_image) {
|
||||
return array(
|
||||
'url' => $custom_image,
|
||||
'alt' => get_post_meta(get_the_ID(), '_twitter_image_alt', true) ?: $settings['image_alt_text']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Try featured image
|
||||
if (is_singular() && has_post_thumbnail()) {
|
||||
$image_id = get_post_thumbnail_id();
|
||||
$image = wp_get_attachment_image_src($image_id, 'full');
|
||||
|
||||
if ($image) {
|
||||
return array(
|
||||
'url' => $image[0],
|
||||
'alt' => get_post_meta($image_id, '_wp_attachment_image_alt', true) ?: get_the_title()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default image
|
||||
if (!empty($settings['default_image'])) {
|
||||
return array(
|
||||
'url' => $settings['default_image'],
|
||||
'alt' => $settings['image_alt_text'] ?: get_bloginfo('name') . ' - Default Image'
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get App Card specific data
|
||||
*/
|
||||
private function get_app_card_data() {
|
||||
$app_data = array();
|
||||
|
||||
if (is_singular()) {
|
||||
$post_id = get_the_ID();
|
||||
|
||||
// iOS app data
|
||||
$ios_id = get_post_meta($post_id, '_twitter_app_id_iphone', true);
|
||||
if ($ios_id) {
|
||||
$app_data['twitter:app:id:iphone'] = $ios_id;
|
||||
$app_data['twitter:app:id:ipad'] = $ios_id;
|
||||
|
||||
$ios_url = get_post_meta($post_id, '_twitter_app_url_iphone', true);
|
||||
if ($ios_url) {
|
||||
$app_data['twitter:app:url:iphone'] = $ios_url;
|
||||
$app_data['twitter:app:url:ipad'] = $ios_url;
|
||||
}
|
||||
|
||||
$ios_name = get_post_meta($post_id, '_twitter_app_name_iphone', true);
|
||||
if ($ios_name) {
|
||||
$app_data['twitter:app:name:iphone'] = $ios_name;
|
||||
$app_data['twitter:app:name:ipad'] = $ios_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Android app data
|
||||
$android_id = get_post_meta($post_id, '_twitter_app_id_googleplay', true);
|
||||
if ($android_id) {
|
||||
$app_data['twitter:app:id:googleplay'] = $android_id;
|
||||
|
||||
$android_url = get_post_meta($post_id, '_twitter_app_url_googleplay', true);
|
||||
if ($android_url) {
|
||||
$app_data['twitter:app:url:googleplay'] = $android_url;
|
||||
}
|
||||
|
||||
$android_name = get_post_meta($post_id, '_twitter_app_name_googleplay', true);
|
||||
if ($android_name) {
|
||||
$app_data['twitter:app:name:googleplay'] = $android_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $app_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Player Card specific data
|
||||
*/
|
||||
private function get_player_card_data() {
|
||||
$player_data = array();
|
||||
|
||||
if (is_singular()) {
|
||||
$post_id = get_the_ID();
|
||||
|
||||
$player_url = get_post_meta($post_id, '_twitter_player_url', true);
|
||||
if ($player_url) {
|
||||
$player_data['twitter:player'] = $player_url;
|
||||
|
||||
$player_width = get_post_meta($post_id, '_twitter_player_width', true);
|
||||
if ($player_width) {
|
||||
$player_data['twitter:player:width'] = $player_width;
|
||||
}
|
||||
|
||||
$player_height = get_post_meta($post_id, '_twitter_player_height', true);
|
||||
if ($player_height) {
|
||||
$player_data['twitter:player:height'] = $player_height;
|
||||
}
|
||||
|
||||
$player_stream = get_post_meta($post_id, '_twitter_player_stream', true);
|
||||
if ($player_stream) {
|
||||
$player_data['twitter:player:stream'] = $player_stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $player_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text to specified length
|
||||
*/
|
||||
private function truncate_text($text, $max_length) {
|
||||
if (strlen($text) <= $max_length) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
return substr($text, 0, $max_length - 3) . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Twitter image requirements
|
||||
*/
|
||||
public function validate_image($image_url) {
|
||||
$errors = array();
|
||||
|
||||
// Check if image exists and get dimensions
|
||||
$image_info = getimagesize($image_url);
|
||||
|
||||
if (!$image_info) {
|
||||
$errors[] = __('Unable to access image or invalid image format.', 'tigerstyle-heat');
|
||||
return $errors;
|
||||
}
|
||||
|
||||
$width = $image_info[0];
|
||||
$height = $image_info[1];
|
||||
$mime_type = $image_info['mime'];
|
||||
|
||||
// Get file size
|
||||
$headers = get_headers($image_url, 1);
|
||||
$size = isset($headers['Content-Length']) ? $headers['Content-Length'] : 0;
|
||||
|
||||
// Check file size (5MB limit)
|
||||
if ($size > 5 * 1024 * 1024) {
|
||||
$errors[] = __('Image file size must be under 5MB for Twitter Cards.', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
// Check supported formats
|
||||
$supported_formats = array('image/jpeg', 'image/png', 'image/webp', 'image/gif');
|
||||
if (!in_array($mime_type, $supported_formats)) {
|
||||
$errors[] = __('Image format not supported. Use JPG, PNG, WEBP, or GIF.', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
// Check minimum dimensions for summary_large_image
|
||||
if ($width < 300 || $height < 157) {
|
||||
$errors[] = __('Warning: For best results, images should be at least 300x157 pixels.', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
// Check recommended dimensions
|
||||
if ($width < 1200 || $height < 628) {
|
||||
$errors[] = __('Recommendation: Use 1200x628 pixels for optimal display.', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
// Check aspect ratio for large image cards
|
||||
$ratio = $width / $height;
|
||||
if ($ratio < 1.8 || $ratio > 2.1) {
|
||||
$errors[] = __('Warning: Recommended aspect ratio is 1.91:1 for large image cards.', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin settings
|
||||
*/
|
||||
public function register_settings() {
|
||||
register_setting(
|
||||
'tigerstyle_heat_twitter',
|
||||
$this->option_name,
|
||||
array(
|
||||
'sanitize_callback' => array($this, 'sanitize_settings')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize settings
|
||||
*/
|
||||
public function sanitize_settings($settings) {
|
||||
$clean_settings = array();
|
||||
|
||||
// Boolean settings
|
||||
$boolean_fields = array('enable_twitter_cards', 'enable_creator_tags', 'enable_fallback_to_og', 'optimize_for_engagement');
|
||||
foreach ($boolean_fields as $field) {
|
||||
$clean_settings[$field] = !empty($settings[$field]);
|
||||
}
|
||||
|
||||
// Text settings
|
||||
$clean_settings['default_card_type'] = sanitize_text_field($settings['default_card_type'] ?? 'summary_large_image');
|
||||
$clean_settings['site_username'] = sanitize_text_field($settings['site_username'] ?? '');
|
||||
$clean_settings['creator_username'] = sanitize_text_field($settings['creator_username'] ?? '');
|
||||
$clean_settings['default_image'] = esc_url_raw($settings['default_image'] ?? '');
|
||||
$clean_settings['image_alt_text'] = sanitize_text_field($settings['image_alt_text'] ?? '');
|
||||
|
||||
// Numeric settings
|
||||
$clean_settings['card_title_length'] = min(70, max(10, absint($settings['card_title_length'] ?? 70)));
|
||||
$clean_settings['card_description_length'] = min(200, max(50, absint($settings['card_description_length'] ?? 200)));
|
||||
|
||||
return $clean_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes for testing
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
register_rest_route('tigerstyle-heat/v1', '/twitter/debug', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'debug_twitter_cards'),
|
||||
'permission_callback' => function() {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug Twitter Card data (REST endpoint)
|
||||
*/
|
||||
public function debug_twitter_cards($request) {
|
||||
$url = $request->get_param('url');
|
||||
|
||||
if (empty($url)) {
|
||||
$url = home_url();
|
||||
}
|
||||
|
||||
// Get Twitter Card data for the URL
|
||||
$card_data = $this->get_twitter_card_data();
|
||||
|
||||
return rest_ensure_response(array(
|
||||
'url' => $url,
|
||||
'twitter_card_data' => $card_data,
|
||||
'card_validator_urls' => $this->get_card_validator_urls(),
|
||||
'validation_notes' => $this->get_validation_notes($card_data)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation notes for Twitter Card data
|
||||
*/
|
||||
private function get_validation_notes($card_data) {
|
||||
$notes = array();
|
||||
|
||||
// Check required fields
|
||||
$required_fields = array('twitter:card', 'twitter:title', 'twitter:description');
|
||||
foreach ($required_fields as $field) {
|
||||
if (empty($card_data[$field])) {
|
||||
$notes[] = sprintf(__('Missing required field: %s', 'tigerstyle-heat'), $field);
|
||||
}
|
||||
}
|
||||
|
||||
// Check site username
|
||||
if (empty($card_data['twitter:site'])) {
|
||||
$notes[] = __('Missing twitter:site - highly recommended for attribution', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
// Check image
|
||||
if (!empty($card_data['twitter:image'])) {
|
||||
$image_errors = $this->validate_image($card_data['twitter:image']);
|
||||
$notes = array_merge($notes, $image_errors);
|
||||
} else {
|
||||
$notes[] = __('No Twitter image specified - cards may not display optimally', 'tigerstyle-heat');
|
||||
}
|
||||
|
||||
return $notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twitter Card validator URLs
|
||||
*/
|
||||
public function get_card_validator_urls() {
|
||||
return array(
|
||||
'twitter_official' => 'https://cards-dev.twitter.com/validator',
|
||||
'threadcreator' => 'https://threadcreator.com/tools/twitter-card-validator',
|
||||
'tweetpik' => 'https://tweethunter.io/tweetpik/twitter-card-validator',
|
||||
'boilerplate' => 'https://boilerplatehq.com/tools/twitter-card-validator',
|
||||
'brandbird' => 'https://www.brandbird.app/tools/twitter-card-validator',
|
||||
'typefully' => 'https://typefully.com/tools/twitter-card-validator'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twitter/X developer documentation URLs
|
||||
*/
|
||||
public function get_twitter_docs_urls() {
|
||||
return array(
|
||||
'cards_overview' => 'https://developer.x.com/en/docs/x-for-websites/cards/overview/markup',
|
||||
'troubleshooting' => 'https://developer.x.com/en/docs/x-for-websites/cards/guides/troubleshooting-cards',
|
||||
'getting_started' => 'https://developer.x.com/en/docs/x-for-websites/cards/guides/getting-started',
|
||||
'card_types' => 'https://developer.x.com/en/docs/x-for-websites/cards/overview/abouts-cards'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
// Handle form submission
|
||||
if (isset($_POST['submit_twitter_settings']) && wp_verify_nonce($_POST['twitter_nonce'], 'twitter_settings')) {
|
||||
$this->handle_settings_update();
|
||||
}
|
||||
|
||||
$settings = $this->get_settings();
|
||||
$validator_urls = $this->get_card_validator_urls();
|
||||
$docs_urls = $this->get_twitter_docs_urls();
|
||||
?>
|
||||
|
||||
<div class="seo-info-box">
|
||||
<h3><?php _e('Twitter/✖️ Cards & Social Optimization', 'tigerstyle-heat'); ?></h3>
|
||||
<p><?php _e('Configure Twitter Cards for optimal sharing on X (formerly Twitter). Enhance your social media presence with rich media previews.', 'tigerstyle-heat'); ?></p>
|
||||
|
||||
<div class="seo-setup-steps">
|
||||
<h4><?php _e('Quick Setup Steps:', 'tigerstyle-heat'); ?></h4>
|
||||
<ol>
|
||||
<li><?php _e('Enable Twitter Cards below', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Add your @username for site attribution', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Set a default card image (1200x628px recommended)', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Configure creator username for content attribution', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Test your cards using Twitter Card validators', 'tigerstyle-heat'); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<strong><?php _e('Twitter Card Validators & Tools:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin-left: 20px;">
|
||||
<li><a href="<?php echo esc_url($validator_urls['threadcreator']); ?>" target="_blank"><?php _e('ThreadCreator Validator', 'tigerstyle-heat'); ?></a> - <?php _e('Preview how your website will look when shared', 'tigerstyle-heat'); ?></li>
|
||||
<li><a href="<?php echo esc_url($validator_urls['tweetpik']); ?>" target="_blank"><?php _e('TweetPik Validator', 'tigerstyle-heat'); ?></a> - <?php _e('Validate how your site appears in tweets', 'tigerstyle-heat'); ?></li>
|
||||
<li><a href="<?php echo esc_url($validator_urls['brandbird']); ?>" target="_blank"><?php _e('BrandBird Validator', 'tigerstyle-heat'); ?></a> - <?php _e('Test and preview Twitter Cards online', 'tigerstyle-heat'); ?></li>
|
||||
<li><a href="<?php echo esc_url($validator_urls['typefully']); ?>" target="_blank"><?php _e('Typefully Validator', 'tigerstyle-heat'); ?></a> - <?php _e('Optimize tweets with card validation', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
|
||||
<strong style="margin-top: 15px; display: block;"><?php _e('Official X/Twitter Documentation:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin-left: 20px;">
|
||||
<li><a href="<?php echo esc_url($docs_urls['cards_overview']); ?>" target="_blank"><?php _e('Twitter Cards Markup Guide', 'tigerstyle-heat'); ?></a></li>
|
||||
<li><a href="<?php echo esc_url($docs_urls['troubleshooting']); ?>" target="_blank"><?php _e('Troubleshooting Cards', 'tigerstyle-heat'); ?></a></li>
|
||||
<li><a href="<?php echo esc_url($docs_urls['getting_started']); ?>" target="_blank"><?php _e('Getting Started Guide', 'tigerstyle-heat'); ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('twitter_settings', 'twitter_nonce'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Enable Twitter Cards', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_twitter_cards" value="1" <?php checked($settings['enable_twitter_cards']); ?> />
|
||||
<?php _e('Generate Twitter Card meta tags for X/Twitter sharing', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Automatically adds twitter:card, twitter:title, twitter:description, and other Twitter Card tags.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Default Card Type', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<select name="default_card_type">
|
||||
<?php foreach ($this->card_types as $value => $label): ?>
|
||||
<option value="<?php echo esc_attr($value); ?>" <?php selected($settings['default_card_type'], $value); ?>>
|
||||
<?php echo esc_html($label); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php _e('Default Twitter Card type for your content.', 'tigerstyle-heat'); ?><br>
|
||||
<strong><?php _e('Card Types:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin: 5px 0 0 20px;">
|
||||
<li><strong><?php _e('Summary:', 'tigerstyle-heat'); ?></strong> <?php _e('Default card with small image', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong><?php _e('Summary Large Image:', 'tigerstyle-heat'); ?></strong> <?php _e('Card with prominent image (recommended)', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong><?php _e('App:', 'tigerstyle-heat'); ?></strong> <?php _e('Mobile app download card', 'tigerstyle-heat'); ?></li>
|
||||
<li><strong><?php _e('Player:', 'tigerstyle-heat'); ?></strong> <?php _e('Video/audio content card', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Site Username', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="site_username" value="<?php echo esc_attr($settings['site_username']); ?>" class="regular-text" placeholder="@yoursite" />
|
||||
<p class="description">
|
||||
<?php _e('Your website\'s Twitter/X username (with or without @). This appears as attribution when content is shared.', 'tigerstyle-heat'); ?><br>
|
||||
<strong><?php _e('Example:', 'tigerstyle-heat'); ?></strong> @yourcompany
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Creator Username', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="creator_username" value="<?php echo esc_attr($settings['creator_username']); ?>" class="regular-text" placeholder="@creator" />
|
||||
<p class="description">
|
||||
<?php _e('Default content creator\'s Twitter/X username. Can be overridden per post.', 'tigerstyle-heat'); ?><br>
|
||||
<?php _e('This is used for individual content attribution alongside the site attribution.', 'tigerstyle-heat'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Default Card Image', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="url" name="default_image" value="<?php echo esc_attr($settings['default_image']); ?>" class="regular-text" placeholder="https://example.com/twitter-card.jpg" />
|
||||
<p class="description">
|
||||
<?php _e('Fallback image when no featured image is set. Used for Twitter Card previews.', 'tigerstyle-heat'); ?><br>
|
||||
<strong><?php _e('Twitter Image Requirements:', 'tigerstyle-heat'); ?></strong>
|
||||
<ul style="margin: 5px 0 0 20px;">
|
||||
<li><?php _e('Recommended: 1200x628px (1.91:1 ratio)', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Minimum: 300x157px', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Maximum file size: 5MB', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Supported formats: JPG, PNG, WEBP, GIF', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('SVG not supported', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Image Alt Text', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<input type="text" name="image_alt_text" value="<?php echo esc_attr($settings['image_alt_text']); ?>" class="regular-text" placeholder="Your site logo" />
|
||||
<p class="description"><?php _e('Default alt text for your Twitter Card images. Improves accessibility.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Text Length Limits', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label><?php _e('Title:', 'tigerstyle-heat'); ?>
|
||||
<input type="number" name="card_title_length" value="<?php echo esc_attr($settings['card_title_length']); ?>" min="10" max="70" style="width: 60px;" /> <?php _e('characters (max 70)', 'tigerstyle-heat'); ?>
|
||||
</label><br><br>
|
||||
<label><?php _e('Description:', 'tigerstyle-heat'); ?>
|
||||
<input type="number" name="card_description_length" value="<?php echo esc_attr($settings['card_description_length']); ?>" min="50" max="200" style="width: 60px;" /> <?php _e('characters (max 200)', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Twitter automatically truncates content that exceeds these limits.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Creator Attribution', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_creator_tags" value="1" <?php checked($settings['enable_creator_tags']); ?> />
|
||||
<?php _e('Include twitter:creator tags for content attribution', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Adds individual content creator attribution in addition to site attribution.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('OpenGraph Fallback', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_fallback_to_og" value="1" <?php checked($settings['enable_fallback_to_og']); ?> />
|
||||
<?php _e('Allow Twitter to use OpenGraph tags as fallback', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('If Twitter-specific tags are missing, X/Twitter may use og:title, og:description, and og:image tags.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Engagement Optimization', 'tigerstyle-heat'); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="optimize_for_engagement" value="1" <?php checked($settings['optimize_for_engagement']); ?> />
|
||||
<?php _e('Optimize card content for maximum engagement', 'tigerstyle-heat'); ?>
|
||||
</label>
|
||||
<p class="description"><?php _e('Ensures optimal card formatting and content structure for better click-through rates.', 'tigerstyle-heat'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="margin-top: 20px; padding: 20px; background: #f0f9ff; border: 1px solid #bfdbfe; border-radius: 5px;">
|
||||
<h4><?php _e('Twitter/X Platform Updates', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('X (formerly Twitter) continues to support Twitter Cards for rich media previews. Key features:', 'tigerstyle-heat'); ?></p>
|
||||
<ul style="margin-left: 20px;">
|
||||
<li><?php _e('Cards display rich previews when URLs are shared', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Images, titles, and descriptions improve engagement', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Proper attribution helps with brand recognition', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Works across all X/Twitter applications and platforms', 'tigerstyle-heat'); ?></li>
|
||||
</ul>
|
||||
|
||||
<h4 style="margin-top: 15px;"><?php _e('Testing Your Twitter Cards', 'tigerstyle-heat'); ?></h4>
|
||||
<p><?php _e('After saving these settings, validate your cards:', 'tigerstyle-heat'); ?></p>
|
||||
<ol style="margin-left: 20px;">
|
||||
<li><?php _e('Use any of the validator tools listed above', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Enter your page URL to see the card preview', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Share a test post on X/Twitter to verify appearance', 'tigerstyle-heat'); ?></li>
|
||||
<li><?php _e('Monitor engagement metrics to optimize content', 'tigerstyle-heat'); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<?php submit_button(__('Save Twitter Settings', 'tigerstyle-heat'), 'primary', 'submit_twitter_settings'); ?>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle settings update
|
||||
*/
|
||||
private function handle_settings_update() {
|
||||
// Validate and save settings
|
||||
$new_settings = array();
|
||||
|
||||
// Boolean settings
|
||||
$new_settings['enable_twitter_cards'] = !empty($_POST['enable_twitter_cards']);
|
||||
$new_settings['enable_creator_tags'] = !empty($_POST['enable_creator_tags']);
|
||||
$new_settings['enable_fallback_to_og'] = !empty($_POST['enable_fallback_to_og']);
|
||||
$new_settings['optimize_for_engagement'] = !empty($_POST['optimize_for_engagement']);
|
||||
|
||||
// Text settings
|
||||
$new_settings['default_card_type'] = sanitize_text_field($_POST['default_card_type'] ?? 'summary_large_image');
|
||||
$new_settings['site_username'] = sanitize_text_field($_POST['site_username'] ?? '');
|
||||
$new_settings['creator_username'] = sanitize_text_field($_POST['creator_username'] ?? '');
|
||||
$new_settings['default_image'] = esc_url_raw($_POST['default_image'] ?? '');
|
||||
$new_settings['image_alt_text'] = sanitize_text_field($_POST['image_alt_text'] ?? '');
|
||||
|
||||
// Numeric settings
|
||||
$new_settings['card_title_length'] = min(70, max(10, absint($_POST['card_title_length'] ?? 70)));
|
||||
$new_settings['card_description_length'] = min(200, max(50, absint($_POST['card_description_length'] ?? 200)));
|
||||
|
||||
// Validate image if provided
|
||||
if (!empty($new_settings['default_image'])) {
|
||||
$image_errors = $this->validate_image($new_settings['default_image']);
|
||||
if (!empty($image_errors)) {
|
||||
// Show validation warnings
|
||||
add_action('admin_notices', function() use ($image_errors) {
|
||||
echo '<div class="notice notice-warning"><p><strong>' . __('Image Validation Warnings:', 'tigerstyle-heat') . '</strong></p><ul>';
|
||||
foreach ($image_errors as $error) {
|
||||
echo '<li>' . esc_html($error) . '</li>';
|
||||
}
|
||||
echo '</ul></div>';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings
|
||||
$this->update_settings($new_settings);
|
||||
|
||||
// Redirect with success message
|
||||
wp_redirect(add_query_arg('message', 'twitter_updated', wp_get_referer()));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
516
includes/modules/class-visual-elements-gallery.php
Normal file
516
includes/modules/class-visual-elements-gallery.php
Normal file
@ -0,0 +1,516 @@
|
||||
<?php
|
||||
/**
|
||||
* Visual Elements Gallery Module for TigerStyle Heat
|
||||
* Implements Google's Visual Elements Gallery optimization with carousel support
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class TigerStyleSEO_VisualElementsGallery {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module
|
||||
*/
|
||||
private function init() {
|
||||
// Frontend hooks
|
||||
add_action('wp_head', array($this, 'inject_gallery_structured_data'), 3);
|
||||
|
||||
// Admin hooks
|
||||
if (is_admin()) {
|
||||
add_action('admin_post_update_visual_gallery', array($this, 'handle_form_submission'));
|
||||
}
|
||||
|
||||
// Gallery enhancement hooks
|
||||
add_filter('gallery_style', array($this, 'enhance_gallery_markup'), 10, 1);
|
||||
add_filter('post_gallery', array($this, 'enhance_gallery_shortcode'), 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject Visual Elements Gallery structured data
|
||||
*/
|
||||
public function inject_gallery_structured_data() {
|
||||
if (!TigerStyleSEO_Utils::get_option('visual_gallery_enabled', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $post;
|
||||
if (!$post) {
|
||||
return;
|
||||
}
|
||||
|
||||
$gallery_data = $this->get_gallery_structured_data($post->ID);
|
||||
|
||||
if (!empty($gallery_data)) {
|
||||
echo "\n<!-- TigerStyle Heat Visual Elements Gallery -->\n";
|
||||
foreach ($gallery_data as $schema) {
|
||||
echo '<script type="application/ld+json">';
|
||||
echo wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
echo '</script>' . "\n";
|
||||
}
|
||||
echo "<!-- /TigerStyle Heat Visual Elements Gallery -->\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery structured data for a post
|
||||
*/
|
||||
private function get_gallery_structured_data($post_id) {
|
||||
$schemas = array();
|
||||
|
||||
// Get post content
|
||||
$post = get_post($post_id);
|
||||
if (!$post) {
|
||||
return $schemas;
|
||||
}
|
||||
|
||||
// Detect galleries in content
|
||||
$galleries = $this->detect_galleries($post->post_content);
|
||||
|
||||
foreach ($galleries as $gallery) {
|
||||
$gallery_type = TigerStyleSEO_Utils::get_option('visual_gallery_type', 'ImageGallery');
|
||||
|
||||
if ($gallery_type === 'ItemList' && count($gallery['images']) >= 3) {
|
||||
// Generate ItemList schema for carousel optimization
|
||||
$schemas[] = $this->generate_itemlist_schema($gallery, $post);
|
||||
} elseif ($gallery_type === 'ImageGallery') {
|
||||
// Generate ImageGallery schema
|
||||
$schemas[] = $this->generate_imagegallery_schema($gallery, $post);
|
||||
}
|
||||
}
|
||||
|
||||
return $schemas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect galleries in post content
|
||||
*/
|
||||
private function detect_galleries($content) {
|
||||
$galleries = array();
|
||||
|
||||
// Detect WordPress [gallery] shortcode
|
||||
$pattern = '/\[gallery[^\]]*\]/';
|
||||
preg_match_all($pattern, $content, $matches);
|
||||
|
||||
foreach ($matches[0] as $shortcode) {
|
||||
$gallery = $this->parse_gallery_shortcode($shortcode);
|
||||
if (!empty($gallery['images'])) {
|
||||
$galleries[] = $gallery;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect Gutenberg gallery blocks
|
||||
$block_pattern = '/<!-- wp:gallery.*?-->.*?<!-- \/wp:gallery -->/s';
|
||||
preg_match_all($block_pattern, $content, $block_matches);
|
||||
|
||||
foreach ($block_matches[0] as $block) {
|
||||
$gallery = $this->parse_gallery_block($block);
|
||||
if (!empty($gallery['images'])) {
|
||||
$galleries[] = $gallery;
|
||||
}
|
||||
}
|
||||
|
||||
return $galleries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse gallery shortcode
|
||||
*/
|
||||
private function parse_gallery_shortcode($shortcode) {
|
||||
$atts = shortcode_parse_atts($shortcode);
|
||||
$gallery = array(
|
||||
'type' => 'shortcode',
|
||||
'images' => array(),
|
||||
'attributes' => $atts
|
||||
);
|
||||
|
||||
if (isset($atts['ids'])) {
|
||||
$ids = explode(',', $atts['ids']);
|
||||
foreach ($ids as $id) {
|
||||
$image_data = $this->get_image_structured_data(trim($id));
|
||||
if ($image_data) {
|
||||
$gallery['images'][] = $image_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $gallery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Gutenberg gallery block
|
||||
*/
|
||||
private function parse_gallery_block($block) {
|
||||
$gallery = array(
|
||||
'type' => 'gutenberg',
|
||||
'images' => array(),
|
||||
'attributes' => array()
|
||||
);
|
||||
|
||||
// Extract image IDs from block content
|
||||
preg_match_all('/wp-image-(\d+)/', $block, $matches);
|
||||
|
||||
if (!empty($matches[1])) {
|
||||
foreach ($matches[1] as $id) {
|
||||
$image_data = $this->get_image_structured_data($id);
|
||||
if ($image_data) {
|
||||
$gallery['images'][] = $image_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $gallery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get structured data for a single image or 3D model
|
||||
*/
|
||||
private function get_image_structured_data($attachment_id) {
|
||||
$attachment = get_post($attachment_id);
|
||||
if (!$attachment || $attachment->post_type !== 'attachment') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$attachment_url = wp_get_attachment_url($attachment_id);
|
||||
if (!$attachment_url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this is a glTF 3D model
|
||||
$gltf_module = tigerstyle_heat()->get_module('gltf_metadata');
|
||||
if ($gltf_module && $gltf_module->is_gltf_attachment($attachment_id)) {
|
||||
return $gltf_module->get_gltf_structured_data($attachment_id);
|
||||
}
|
||||
|
||||
// Handle regular images
|
||||
$image_data = wp_get_attachment_metadata($attachment_id);
|
||||
|
||||
$schema = array(
|
||||
'@type' => 'ImageObject',
|
||||
'contentUrl' => $attachment_url,
|
||||
'url' => $attachment_url,
|
||||
'name' => $attachment->post_title ?: basename($attachment_url),
|
||||
'description' => $attachment->post_content,
|
||||
'caption' => $attachment->post_excerpt
|
||||
);
|
||||
|
||||
// Add technical metadata for images
|
||||
if (!empty($image_data)) {
|
||||
$schema['width'] = $image_data['width'] ?? null;
|
||||
$schema['height'] = $image_data['height'] ?? null;
|
||||
$schema['encodingFormat'] = $this->get_image_format($attachment_url);
|
||||
}
|
||||
|
||||
// Add file size
|
||||
$file_path = get_attached_file($attachment_id);
|
||||
if ($file_path && file_exists($file_path)) {
|
||||
$schema['contentSize'] = filesize($file_path);
|
||||
}
|
||||
|
||||
// Add license information if available
|
||||
$license = get_post_meta($attachment_id, '_image_license', true);
|
||||
if ($license) {
|
||||
$schema['license'] = $license;
|
||||
}
|
||||
|
||||
// Add creator information
|
||||
$creator = get_post_meta($attachment_id, '_image_creator', true);
|
||||
if ($creator) {
|
||||
$schema['creator'] = array(
|
||||
'@type' => 'Person',
|
||||
'name' => $creator
|
||||
);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate ItemList schema for carousel optimization
|
||||
*/
|
||||
private function generate_itemlist_schema($gallery, $post) {
|
||||
$schema = array(
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'ItemList',
|
||||
'name' => $post->post_title . ' Gallery',
|
||||
'description' => $this->get_gallery_description($gallery, $post),
|
||||
'numberOfItems' => count($gallery['images']),
|
||||
'itemListElement' => array()
|
||||
);
|
||||
|
||||
$position = 1;
|
||||
foreach ($gallery['images'] as $image) {
|
||||
$schema['itemListElement'][] = array(
|
||||
'@type' => 'ListItem',
|
||||
'position' => $position,
|
||||
'item' => $image
|
||||
);
|
||||
$position++;
|
||||
}
|
||||
|
||||
// Add webpage context
|
||||
$schema['mainEntityOfPage'] = array(
|
||||
'@type' => 'WebPage',
|
||||
'@id' => get_permalink($post->ID)
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate ImageGallery schema
|
||||
*/
|
||||
private function generate_imagegallery_schema($gallery, $post) {
|
||||
$schema = array(
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'ImageGallery',
|
||||
'name' => $post->post_title . ' Gallery',
|
||||
'description' => $this->get_gallery_description($gallery, $post),
|
||||
'associatedMedia' => $gallery['images']
|
||||
);
|
||||
|
||||
// Add webpage context
|
||||
$schema['mainEntityOfPage'] = array(
|
||||
'@type' => 'WebPage',
|
||||
'@id' => get_permalink($post->ID)
|
||||
);
|
||||
|
||||
// Add creator information
|
||||
$author_id = $post->post_author;
|
||||
if ($author_id) {
|
||||
$author = get_userdata($author_id);
|
||||
$schema['creator'] = array(
|
||||
'@type' => 'Person',
|
||||
'name' => $author->display_name,
|
||||
'url' => get_author_posts_url($author_id)
|
||||
);
|
||||
}
|
||||
|
||||
// Add publication information
|
||||
$schema['datePublished'] = $post->post_date;
|
||||
$schema['dateModified'] = $post->post_modified;
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery description
|
||||
*/
|
||||
private function get_gallery_description($gallery, $post) {
|
||||
$custom_description = TigerStyleSEO_Utils::get_option('visual_gallery_description', '');
|
||||
|
||||
if ($custom_description) {
|
||||
return $custom_description;
|
||||
}
|
||||
|
||||
// Generate automatic description
|
||||
$count = count($gallery['images']);
|
||||
$description = sprintf(
|
||||
__('A collection of %d images from %s', 'tigerstyle-heat'),
|
||||
$count,
|
||||
$post->post_title
|
||||
);
|
||||
|
||||
if ($post->post_excerpt) {
|
||||
$description .= '. ' . $post->post_excerpt;
|
||||
}
|
||||
|
||||
return $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get media format from URL (supports both images and 3D models)
|
||||
*/
|
||||
private function get_image_format($url) {
|
||||
$extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
|
||||
|
||||
$format_map = array(
|
||||
// Image formats
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'svg' => 'image/svg+xml',
|
||||
'bmp' => 'image/bmp',
|
||||
'tiff' => 'image/tiff',
|
||||
'ico' => 'image/x-icon',
|
||||
// 3D model formats
|
||||
'gltf' => 'model/gltf+json',
|
||||
'glb' => 'model/gltf-binary'
|
||||
);
|
||||
|
||||
return $format_map[$extension] ?? 'image/jpeg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance gallery markup with microdata
|
||||
*/
|
||||
public function enhance_gallery_markup($css) {
|
||||
if (!TigerStyleSEO_Utils::get_option('visual_gallery_microdata', false)) {
|
||||
return $css;
|
||||
}
|
||||
|
||||
// Add microdata attributes to gallery wrapper
|
||||
$enhanced_css = str_replace(
|
||||
'<div class="gallery',
|
||||
'<div itemscope itemtype="https://schema.org/ImageGallery" class="gallery',
|
||||
$css
|
||||
);
|
||||
|
||||
return $enhanced_css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance gallery shortcode output
|
||||
*/
|
||||
public function enhance_gallery_shortcode($output, $atts, $instance) {
|
||||
if (!TigerStyleSEO_Utils::get_option('visual_gallery_enhanced_markup', false)) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
// Add structured data attributes to gallery items
|
||||
$enhanced_output = preg_replace_callback(
|
||||
'/<div class="gallery-item">.*?<\/div>/s',
|
||||
array($this, 'add_gallery_item_markup'),
|
||||
$output
|
||||
);
|
||||
|
||||
return $enhanced_output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add markup to gallery items
|
||||
*/
|
||||
private function add_gallery_item_markup($matches) {
|
||||
$item = $matches[0];
|
||||
|
||||
// Add ImageObject microdata
|
||||
$enhanced_item = str_replace(
|
||||
'<div class="gallery-item">',
|
||||
'<div class="gallery-item" itemscope itemtype="https://schema.org/ImageObject">',
|
||||
$item
|
||||
);
|
||||
|
||||
// Add itemprop to images
|
||||
$enhanced_item = str_replace(
|
||||
'<img ',
|
||||
'<img itemprop="contentUrl" ',
|
||||
$enhanced_item
|
||||
);
|
||||
|
||||
return $enhanced_item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission
|
||||
*/
|
||||
public function handle_form_submission() {
|
||||
// Verify nonce
|
||||
if (!TigerStyleSEO_Utils::verify_nonce('visual_gallery_nonce', 'update_visual_gallery')) {
|
||||
wp_die(__('Security check failed.', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
// Check user capabilities
|
||||
if (!TigerStyleSEO_Utils::current_user_can_manage()) {
|
||||
wp_die(__('You do not have sufficient permissions to access this page.', 'tigerstyle-heat'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Save Visual Elements Gallery settings
|
||||
TigerStyleSEO_Utils::update_option('visual_gallery_enabled', isset($_POST['visual_gallery_enabled']) && $_POST['visual_gallery_enabled'] === '1');
|
||||
TigerStyleSEO_Utils::update_option('visual_gallery_type', sanitize_text_field($_POST['visual_gallery_type'] ?? 'ImageGallery'));
|
||||
TigerStyleSEO_Utils::update_option('visual_gallery_description', sanitize_textarea_field($_POST['visual_gallery_description'] ?? ''));
|
||||
TigerStyleSEO_Utils::update_option('visual_gallery_microdata', isset($_POST['visual_gallery_microdata']) && $_POST['visual_gallery_microdata'] === '1');
|
||||
TigerStyleSEO_Utils::update_option('visual_gallery_enhanced_markup', isset($_POST['visual_gallery_enhanced_markup']) && $_POST['visual_gallery_enhanced_markup'] === '1');
|
||||
TigerStyleSEO_Utils::update_option('visual_gallery_min_images', intval($_POST['visual_gallery_min_images'] ?? 3));
|
||||
TigerStyleSEO_Utils::update_option('visual_gallery_carousel_enabled', isset($_POST['visual_gallery_carousel_enabled']) && $_POST['visual_gallery_carousel_enabled'] === '1');
|
||||
|
||||
TigerStyleSEO_Utils::redirect_with_message('visual_gallery_updated');
|
||||
|
||||
} catch (Exception $e) {
|
||||
TigerStyleSEO_Utils::redirect_with_message('error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if page has qualifying galleries for rich results
|
||||
*/
|
||||
public function has_qualifying_galleries($post_id = null) {
|
||||
if (!$post_id) {
|
||||
global $post;
|
||||
$post_id = $post ? $post->ID : 0;
|
||||
}
|
||||
|
||||
if (!$post_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post = get_post($post_id);
|
||||
if (!$post) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$galleries = $this->detect_galleries($post->post_content);
|
||||
$min_images = TigerStyleSEO_Utils::get_option('visual_gallery_min_images', 3);
|
||||
|
||||
foreach ($galleries as $gallery) {
|
||||
if (count($gallery['images']) >= $min_images) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery performance data
|
||||
*/
|
||||
public function get_gallery_performance_data($post_id = null) {
|
||||
if (!$post_id) {
|
||||
global $post;
|
||||
$post_id = $post ? $post->ID : 0;
|
||||
}
|
||||
|
||||
$post = get_post($post_id);
|
||||
if (!$post) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$galleries = $this->detect_galleries($post->post_content);
|
||||
|
||||
return array(
|
||||
'total_galleries' => count($galleries),
|
||||
'total_images' => array_sum(array_map(function($g) { return count($g['images']); }, $galleries)),
|
||||
'qualifying_galleries' => count(array_filter($galleries, function($g) {
|
||||
return count($g['images']) >= TigerStyleSEO_Utils::get_option('visual_gallery_min_images', 3);
|
||||
})),
|
||||
'has_structured_data' => TigerStyleSEO_Utils::get_option('visual_gallery_enabled', false),
|
||||
'schema_type' => TigerStyleSEO_Utils::get_option('visual_gallery_type', 'ImageGallery')
|
||||
);
|
||||
}
|
||||
}
|
||||
165
templates/amp-single.php
Normal file
165
templates/amp-single.php
Normal file
@ -0,0 +1,165 @@
|
||||
<!doctype html>
|
||||
<html ⚡>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?php echo esc_html($title ?? get_the_title()); ?></title>
|
||||
<link rel="canonical" href="<?php echo esc_url($canonical_url ?? get_permalink()); ?>">
|
||||
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
|
||||
|
||||
<!-- AMP Runtime -->
|
||||
<script async src="https://cdn.ampproject.org/v0.js"></script>
|
||||
|
||||
<!-- AMP Components -->
|
||||
<script async custom-element="amp-img" src="https://cdn.ampproject.org/v0/amp-img-0.1.js"></script>
|
||||
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
<?php echo wp_json_encode($structured_data ?? []); ?>
|
||||
</script>
|
||||
|
||||
<!-- AMP Styles -->
|
||||
<style amp-custom>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.amp-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.amp-title {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 15px 0;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.amp-meta {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.amp-meta time {
|
||||
color: #0073aa;
|
||||
}
|
||||
|
||||
.amp-content {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.amp-content h1,
|
||||
.amp-content h2,
|
||||
.amp-content h3 {
|
||||
color: #222;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.amp-content p {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.amp-content amp-img {
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
.amp-footer {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 20px;
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.amp-link {
|
||||
color: #0073aa;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.amp-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.amp-title {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.amp-content {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- AMP Boilerplate -->
|
||||
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
|
||||
</head>
|
||||
<body>
|
||||
<article class="amp-article">
|
||||
<header class="amp-header">
|
||||
<h1 class="amp-title"><?php echo esc_html($title ?? get_the_title()); ?></h1>
|
||||
<div class="amp-meta">
|
||||
<time datetime="<?php echo esc_attr(get_the_date('c')); ?>">
|
||||
<?php echo esc_html(get_the_date()); ?>
|
||||
</time>
|
||||
<?php if (get_the_author()): ?>
|
||||
by <span class="amp-author"><?php echo esc_html(get_the_author()); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="amp-content">
|
||||
<?php echo $content ?? apply_filters('the_content', get_the_content()); ?>
|
||||
</div>
|
||||
|
||||
<footer class="amp-footer">
|
||||
<p>
|
||||
<a href="<?php echo esc_url($canonical_url ?? get_permalink()); ?>" class="amp-link">
|
||||
View full version
|
||||
</a>
|
||||
|
|
||||
<a href="<?php echo esc_url(home_url()); ?>" class="amp-link">
|
||||
<?php echo esc_html(get_bloginfo('name')); ?>
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<!-- Analytics -->
|
||||
<?php
|
||||
$analytics_id = $this->get_option('analytics_id', '');
|
||||
if ($analytics_id):
|
||||
?>
|
||||
<amp-analytics type="gtag" data-credentials="include">
|
||||
<script type="application/json">
|
||||
{
|
||||
"vars" : {
|
||||
"gtag_id": "<?php echo esc_js($analytics_id); ?>",
|
||||
"config" : {
|
||||
"<?php echo esc_js($analytics_id); ?>": {
|
||||
"groups": "default"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</amp-analytics>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
</html>
|
||||
161
test-modular-system.php
Normal file
161
test-modular-system.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/**
|
||||
* Test script for TigerStyle Heat modular system
|
||||
* Simulates WordPress environment to test plugin initialization
|
||||
*/
|
||||
|
||||
// Mock WordPress functions for testing
|
||||
if (!function_exists('add_action')) {
|
||||
function add_action($hook, $callback) {
|
||||
echo "✓ Hook registered: $hook\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('add_filter')) {
|
||||
function add_filter($hook, $callback) {
|
||||
echo "✓ Filter registered: $hook\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('register_activation_hook')) {
|
||||
function register_activation_hook($file, $callback) {
|
||||
echo "✓ Activation hook registered\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('register_deactivation_hook')) {
|
||||
function register_deactivation_hook($file, $callback) {
|
||||
echo "✓ Deactivation hook registered\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('load_plugin_textdomain')) {
|
||||
function load_plugin_textdomain($domain, $deprecated = '', $plugin_rel_path = '') {
|
||||
echo "✓ Text domain loaded: $domain\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('is_admin')) {
|
||||
function is_admin() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('plugin_dir_path')) {
|
||||
function plugin_dir_path($file) {
|
||||
return dirname($file) . '/';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('plugin_dir_url')) {
|
||||
function plugin_dir_url($file) {
|
||||
return 'http://localhost/wp-content/plugins/' . basename(dirname($file)) . '/';
|
||||
}
|
||||
}
|
||||
|
||||
// Define constants
|
||||
if (!defined('ABSPATH')) {
|
||||
define('ABSPATH', '/tmp/wordpress/');
|
||||
}
|
||||
|
||||
echo "=== TigerStyle Heat Modular System Test ===\n\n";
|
||||
|
||||
echo "1. Testing Core Plugin Initialization...\n";
|
||||
try {
|
||||
require_once __DIR__ . '/tigerstyle-heat-new.php';
|
||||
echo "✓ Main plugin file loaded successfully\n";
|
||||
|
||||
// Test singleton instance
|
||||
$plugin = tigerstyle_heat();
|
||||
if ($plugin instanceof TigerStyleSEO) {
|
||||
echo "✓ Plugin singleton instance created\n";
|
||||
} else {
|
||||
echo "✗ Plugin singleton failed\n";
|
||||
}
|
||||
|
||||
// Test second instance (should be same)
|
||||
$plugin2 = tigerstyle_heat();
|
||||
if ($plugin === $plugin2) {
|
||||
echo "✓ Singleton pattern working correctly\n";
|
||||
} else {
|
||||
echo "✗ Singleton pattern failed\n";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "✗ Error loading main plugin: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "\n2. Testing Module Loading...\n";
|
||||
try {
|
||||
$modules = [
|
||||
'robots_txt',
|
||||
'sitemap_xml',
|
||||
'llms_txt',
|
||||
'google_setup',
|
||||
'structured_data',
|
||||
'meta_tags',
|
||||
'seo_health',
|
||||
'head_footer',
|
||||
'visual_elements_gallery'
|
||||
];
|
||||
|
||||
foreach ($modules as $module_name) {
|
||||
$module = $plugin->get_module($module_name);
|
||||
if ($module) {
|
||||
echo "✓ Module '$module_name' loaded successfully\n";
|
||||
} else {
|
||||
echo "✗ Module '$module_name' failed to load\n";
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "✗ Error testing modules: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "\n3. Testing Utils Class...\n";
|
||||
try {
|
||||
if (class_exists('TigerStyleSEO_Utils')) {
|
||||
echo "✓ Utils class loaded\n";
|
||||
|
||||
// Test business days parser
|
||||
$days = TigerStyleSEO_Utils::parse_business_days('Mo-Fr');
|
||||
if (is_array($days) && count($days) === 5) {
|
||||
echo "✓ Business days parser working\n";
|
||||
} else {
|
||||
echo "✗ Business days parser failed\n";
|
||||
}
|
||||
|
||||
// Test format_bytes
|
||||
$formatted = TigerStyleSEO_Utils::format_bytes(1024);
|
||||
if ($formatted === '1 KB') {
|
||||
echo "✓ Bytes formatter working\n";
|
||||
} else {
|
||||
echo "✗ Bytes formatter failed\n";
|
||||
}
|
||||
|
||||
} else {
|
||||
echo "✗ Utils class not found\n";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "✗ Error testing utils: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "\n4. Testing Admin Classes...\n";
|
||||
try {
|
||||
if (class_exists('TigerStyleSEO_Admin')) {
|
||||
echo "✓ Admin class loaded\n";
|
||||
} else {
|
||||
echo "✗ Admin class not found\n";
|
||||
}
|
||||
|
||||
if (class_exists('TigerStyleSEO_Admin_Pages')) {
|
||||
echo "✓ Admin Pages class loaded\n";
|
||||
} else {
|
||||
echo "✗ Admin Pages class not found\n";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "✗ Error testing admin classes: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "\n=== Test Complete ===\n";
|
||||
echo "If all tests show ✓, the modular system is working correctly!\n";
|
||||
198
tigerstyle-heat.php
Normal file
198
tigerstyle-heat.php
Normal file
@ -0,0 +1,198 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: TigerStyle Heat
|
||||
* Plugin URI: https://tigerstyle.com/heat-plugin
|
||||
* Description: Make your site irresistible! Natural SEO attraction with robots.txt, sitemap.xml, LLMs.txt, Google integration, structured data, and meta tags.
|
||||
* Version: 2.0.0
|
||||
* Author: TigerStyle
|
||||
* Author URI: https://tigerstyle.com
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: tigerstyle-heat
|
||||
* Domain Path: /languages
|
||||
* Requires at least: 5.0
|
||||
* Tested up to: 6.3
|
||||
* Requires PHP: 7.4
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Define plugin constants
|
||||
define('TIGERSTYLE_HEAT_VERSION', '2.0.0');
|
||||
define('TIGERSTYLE_HEAT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('TIGERSTYLE_HEAT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
define('TIGERSTYLE_HEAT_PLUGIN_FILE', __FILE__);
|
||||
|
||||
/**
|
||||
* Main TigerStyle Heat Plugin Class
|
||||
*/
|
||||
class TigerStyleSEO {
|
||||
|
||||
/**
|
||||
* Single instance of the class
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Plugin modules
|
||||
*/
|
||||
private $modules = array();
|
||||
|
||||
/**
|
||||
* Get single instance
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
*/
|
||||
private function init() {
|
||||
// Load dependencies
|
||||
$this->load_dependencies();
|
||||
|
||||
// Initialize hooks
|
||||
add_action('init', array($this, 'init_plugin'));
|
||||
|
||||
// Initialize admin components immediately if in admin area
|
||||
if (is_admin()) {
|
||||
$this->init_admin();
|
||||
}
|
||||
|
||||
// Load modules
|
||||
$this->load_modules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load required files
|
||||
*/
|
||||
private function load_dependencies() {
|
||||
// Core includes
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/class-core.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/class-utils.php';
|
||||
|
||||
// Admin includes
|
||||
if (is_admin()) {
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'admin/class-admin.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'admin/class-admin-pages.php';
|
||||
}
|
||||
|
||||
// Module includes
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-robots-txt.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-sitemap-xml.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-llms-txt.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-google-setup.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-meta-tags.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-structured-data.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-seo-health.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-head-footer.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-visual-elements-gallery.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-amp.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-ecosystem-coordinator.php';
|
||||
|
||||
// API includes
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/api/class-sxg-api-client.php';
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/api/class-ai-client.php';
|
||||
|
||||
// AI modules
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/modules/class-ai-provider.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugin components
|
||||
*/
|
||||
public function init_plugin() {
|
||||
// Load text domain
|
||||
load_plugin_textdomain('tigerstyle-heat', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize admin components
|
||||
*/
|
||||
public function init_admin() {
|
||||
TigerStyleSEO_Admin::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and initialize modules
|
||||
*/
|
||||
private function load_modules() {
|
||||
$this->modules = array(
|
||||
'robots_txt' => TigerStyleSEO_RobotsTxt::instance(),
|
||||
'sitemap_xml' => TigerStyleSEO_Sitemap_xml::instance(),
|
||||
'llms_txt' => TigerStyleSEO_Llms_txt::instance(),
|
||||
'google_setup' => TigerStyleSEO_Google_setup::instance(),
|
||||
'meta_tags' => TigerStyleSEO_Meta_tags::instance(),
|
||||
'structured_data' => TigerStyleSEO_StructuredData::instance(),
|
||||
'seo_health' => TigerStyleSEO_Seo_health::instance(),
|
||||
'head_footer' => TigerStyleSEO_Head_footer::instance(),
|
||||
'visual_elements_gallery' => TigerStyleSEO_VisualElementsGallery::instance(),
|
||||
'amp' => TigerStyleSEO_AMP::instance(),
|
||||
'ai_provider' => TigerStyleSEO_AI_Provider::instance(),
|
||||
'ecosystem_coordinator' => TigerStyleSEO_EcosystemCoordinator::instance(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific module
|
||||
*/
|
||||
public function get_module($module_name) {
|
||||
return isset($this->modules[$module_name]) ? $this->modules[$module_name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin activation
|
||||
*/
|
||||
public static function activate() {
|
||||
// Load core class for activation
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/class-core.php';
|
||||
|
||||
// Run activation hooks for all modules
|
||||
TigerStyleSEO_Core::activate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation
|
||||
*/
|
||||
public static function deactivate() {
|
||||
// Load core class for deactivation
|
||||
require_once TIGERSTYLE_HEAT_PLUGIN_DIR . 'includes/class-core.php';
|
||||
|
||||
TigerStyleSEO_Core::deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
// Register activation/deactivation hooks
|
||||
register_activation_hook(__FILE__, array('TigerStyleSEO', 'activate'));
|
||||
register_deactivation_hook(__FILE__, array('TigerStyleSEO', 'deactivate'));
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
*/
|
||||
function tigerstyle_heat_init() {
|
||||
return TigerStyleSEO::instance();
|
||||
}
|
||||
|
||||
// Start the plugin
|
||||
add_action('plugins_loaded', 'tigerstyle_heat_init');
|
||||
|
||||
/**
|
||||
* Helper function to get the main plugin instance
|
||||
*/
|
||||
function tigerstyle_heat() {
|
||||
return TigerStyleSEO::instance();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user