commit f2ee863afeefcb4e34db9d91be57250317c22bd4 Author: Jonas Date: Thu Jan 22 00:15:58 2026 +0100 feat: Implement the Instagram Gallery Sync Pro WordPress plugin. diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6e64feb Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7bb1d62 --- /dev/null +++ b/README.md @@ -0,0 +1,279 @@ +# Instagram Gallery Sync Pro + +[![WordPress](https://img.shields.io/badge/WordPress-5.0%2B-blue.svg)](https://wordpress.org/) +[![PHP](https://img.shields.io/badge/PHP-7.4%2B-purple.svg)](https://php.net/) +[![License](https://img.shields.io/badge/License-GPL--2.0%2B-green.svg)](https://www.gnu.org/licenses/gpl-2.0.html) + +Ein leistungsstarkes WordPress-Plugin zur Synchronisierung und Anzeige von Instagram-Fotos ohne offizielle API. + +**Entwickelt von [Keil & Schick](https://keil-schick.de)** + +--- + +## 🚀 Features + +- **Automatische Synchronisierung** – Holt regelmĂ€ĂŸig neue Bilder von öffentlichen Instagram-Profilen +- **Lokale Bildspeicherung** – Bilder werden auf deinem Server gespeichert (bessere Performance) +- **5 Layout-Optionen** – Grid, Masonry, Justified, Slider, List +- **VollstĂ€ndig responsive** – Separate Spalteneinstellungen fĂŒr Desktop, Tablet & Mobile +- **Lightbox-Integration** – GLightbox fĂŒr elegante Bildanzeige +- **Gutenberg-Block** – Nativer Block-Editor mit Live-Vorschau +- **Shortcode-UnterstĂŒtzung** – Flexibel mit zahlreichen Parametern +- **6 Hover-Effekte** – Zoom, Fade, Overlay, Grayscale, Lift, None +- **Custom CSS** – Eigene Styles direkt im Admin-Bereich +- **Activity Logs** – Detaillierte Protokollierung aller Sync-AktivitĂ€ten +- **i18n-ready** – Vorbereitet fĂŒr Übersetzungen (DE/EN) + +--- + +## 📋 Anforderungen + +- WordPress 5.0 oder höher +- PHP 7.4 oder höher +- Öffentliches Instagram-Profil + +--- + +## đŸ“„ Installation + +### Manuelle Installation + +1. Lade den `instagram-gallery-sync-pro` Ordner herunter +2. Kopiere ihn nach `/wp-content/plugins/` +3. Aktiviere das Plugin unter **Plugins** im WordPress-Admin +4. Gehe zu **Instagram Gallery** in der Sidebar + +### Via WordPress Admin + +1. Gehe zu **Plugins → Installieren** +2. Klicke auf **Plugin hochladen** +3. WĂ€hle die ZIP-Datei aus +4. Klicke auf **Jetzt installieren** und dann **Aktivieren** + +--- + +## ⚙ Konfiguration + +### Grundeinrichtung + +1. Navigiere zu **Instagram Gallery** im Admin-MenĂŒ +2. Gib deinen Instagram-Benutzernamen ein (ohne @) +3. WĂ€hle die gewĂŒnschte Bildanzahl und QualitĂ€t +4. Klicke auf **Sync Now** + +### Einstellungs-Tabs + +| Tab | Beschreibung | +|-----|--------------| +| **Instagram** | Username, Sync-Intervall, BildqualitĂ€t | +| **Layout** | Grid-Typ, Spalten, AbstĂ€nde, Hover-Effekte | +| **Anzeige** | Limit, Sortierung, Lightbox, Lazy Loading | +| **Styling** | Farben, SchriftgrĂ¶ĂŸen, Custom CSS | +| **Erweitert** | Debug-Modus, Cache, Proxy, Logs | + +--- + +## 🔧 Verwendung + +### Shortcode + +Einfachste Verwendung: + +``` +[instagram_gallery] +``` + +Mit Parametern: + +``` +[instagram_gallery layout="masonry" columns="4" limit="12" spacing="15"] +``` + +#### Alle Parameter + +| Parameter | Werte | Standard | +|-----------|-------|----------| +| `layout` | `grid`, `masonry`, `slider`, `justified`, `list` | `grid` | +| `columns` | `1` - `6` | `3` | +| `spacing` | `0` - `50` (px) | `10` | +| `limit` | `1` - `50` | `12` | +| `order` | `newest`, `oldest`, `random` | `newest` | +| `lightbox` | `yes`, `no` | `yes` | +| `captions` | `yes`, `no` | `no` | +| `autoplay` | `true`, `false` (nur Slider) | `false` | +| `class` | CSS-Klassenname | – | + +### Gutenberg Block + +1. Öffne den Block-Editor +2. Suche nach "Instagram Gallery" +3. FĂŒge den Block hinzu +4. Konfiguriere ĂŒber die Sidebar-Einstellungen + +### PHP Template + +```php + 'grid', + 'columns' => 3, + 'limit' => 9 + )); +} +?> +``` + +--- + +## 🎹 Styling + +### CSS-Variablen + +Das Plugin verwendet CSS Custom Properties fĂŒr einfache Anpassung: + +```css +:root { + --igsp-primary: #e1306c; + --igsp-hover: #c13584; + --igsp-text: #ffffff; + --igsp-font-size: 14px; + --igsp-radius: 0px; +} +``` + +### CSS-Klassen + +| Klasse | Beschreibung | +|--------|--------------| +| `.igsp-gallery` | Container | +| `.igsp-item` | Einzelnes Bild | +| `.igsp-image` | Bild-Element | +| `.igsp-overlay` | Caption-Overlay | +| `.igsp-follow-btn` | Instagram-Button | + +--- + +## 🔄 Synchronisierung + +### Automatisch + +Das Plugin synchronisiert automatisch basierend auf dem konfigurierten Intervall: +- Alle 30 Minuten +- StĂŒndlich +- Alle 6 Stunden +- TĂ€glich +- Wöchentlich + +### Manuell + +Klicke auf **Sync Now** im Instagram-Tab der Einstellungen. + +### Scraping-Methoden + +Das Plugin verwendet mehrere Fallback-Methoden: + +1. **Web Profile Info API** – Instagrams interne Web-API +2. **Embed Page Parsing** – Parst die Embed-Seiten +3. **Profile Page Parsing** – HTML-Analyse mit mehreren Strategien + +--- + +## ⚠ Wichtige Hinweise + +> **Hinweis:** Instagram blockiert Scraping aktiv. Das Plugin enthĂ€lt Anti-Blocking-Maßnahmen, aber 100% ZuverlĂ€ssigkeit kann nicht garantiert werden. + +### Best Practices + +- Verwende **lange Sync-Intervalle** (6h+) +- Aktiviere **lokale Bildspeicherung** +- Nutze **Caching** fĂŒr bessere Performance +- Halte die **Bildanzahl** moderat (12-24) + +### Problembehandlung + +| Problem | Lösung | +|---------|--------| +| Keine Bilder | PrĂŒfe ob Profil öffentlich ist | +| Rate Limiting | Erhöhe Sync-Intervall | +| Timeout | Erhöhe Request-Timeout in Erweitert | +| Parsing-Fehler | PrĂŒfe Logs, warte und versuche erneut | + +--- + +## đŸ—„ïž Datenbank + +Das Plugin erstellt zwei Tabellen: + +- `wp_instagram_gallery_posts` – Gespeicherte Bilder +- `wp_instagram_gallery_log` – AktivitĂ€tsprotokolle + +Bei Deinstallation werden alle Daten vollstĂ€ndig entfernt. + +--- + +## 📁 Dateistruktur + +``` +instagram-gallery-sync-pro/ +├── instagram-gallery-sync-pro.php # Hauptdatei +├── uninstall.php # Deinstallations-Routine +├── includes/ +│ ├── class-admin.php # Admin-Handler +│ ├── class-cron.php # Cron-Jobs +│ ├── class-database.php # Datenbank-Operationen +│ ├── class-gutenberg-block.php # Block-Editor +│ ├── class-image-handler.php # Bild-Download & -Verarbeitung +│ ├── class-logger.php # Logging-System +│ ├── class-scraper.php # Instagram-Scraper +│ └── class-shortcode.php # Shortcode-Handler +├── admin/ +│ ├── css/admin-style.css +│ ├── js/admin-script.js +│ ├── js/ajax-sync.js +│ └── views/ # Admin-Templates +├── public/ +│ ├── css/ # Frontend-Styles +│ └── js/ # Frontend-Scripts + Libs +└── languages/ # Übersetzungsdateien +``` + +--- + +## 🔐 Sicherheit + +- Prepared Statements fĂŒr alle DB-Queries +- Nonce-Verifizierung fĂŒr AJAX-Requests +- Capability-Checks (`manage_options`) +- Input-Sanitization fĂŒr alle Benutzereingaben +- Output-Escaping mit `esc_html()`, `esc_attr()`, etc. + +--- + +## 📝 Changelog + +### 1.0.0 (2026-01-22) +- Erste Veröffentlichung +- 5 Layout-Typen (Grid, Masonry, Justified, Slider, List) +- Gutenberg Block mit Live-Vorschau +- Shortcode mit allen Parametern +- Admin-Bereich mit 5 Tabs +- Multi-Methoden Scraping Engine +- Lokale Bildspeicherung mit Thumbnails +- Activity Logging System + +--- + +## 📄 Lizenz + +GPL-2.0-or-later © [Keil & Schick](https://keil-schick.de) + +--- + +## 🆘 Support + +FĂŒr Fragen und Support besuche: [https://keil-schick.de](https://keil-schick.de) + +--- + +*Entwickelt mit ❀ in Deutschland* diff --git a/admin/.DS_Store b/admin/.DS_Store new file mode 100644 index 0000000..fa9a617 Binary files /dev/null and b/admin/.DS_Store differ diff --git a/admin/css/admin-style.css b/admin/css/admin-style.css new file mode 100644 index 0000000..36ce707 --- /dev/null +++ b/admin/css/admin-style.css @@ -0,0 +1,649 @@ +/** + * Instagram Gallery Sync Pro - Admin Styles + * + * @package Instagram_Gallery_Sync_Pro + */ + +/* ============================================ + Variables + ============================================ */ +:root { + --igsp-primary: #e1306c; + --igsp-primary-hover: #c13584; + --igsp-success: #28a745; + --igsp-warning: #ffc107; + --igsp-error: #dc3545; + --igsp-info: #17a2b8; + --igsp-dark: #1e1e1e; + --igsp-gray: #6c757d; + --igsp-light: #f8f9fa; + --igsp-border: #e2e4e7; + --igsp-shadow: 0 2px 8px rgba(0,0,0,0.08); + --igsp-radius: 8px; +} + +/* ============================================ + Layout + ============================================ */ +.igsp-admin-wrap { + display: grid; + grid-template-columns: 1fr 300px; + gap: 20px; + margin: 20px 20px 20px 0; +} + +.igsp-admin-header { + grid-column: 1 / -1; + display: flex; + align-items: center; + gap: 15px; + padding: 20px; + background: linear-gradient(135deg, var(--igsp-primary), var(--igsp-primary-hover)); + color: #fff; + border-radius: var(--igsp-radius); + margin-bottom: 10px; +} + +.igsp-admin-header h1 { + margin: 0; + color: #fff; + display: flex; + align-items: center; + gap: 10px; + font-size: 24px; +} + +.igsp-admin-header .dashicons { + font-size: 30px; + width: 30px; + height: 30px; +} + +.igsp-version { + margin: 0; + opacity: 0.8; + font-size: 14px; +} + +.igsp-admin-content { + background: #fff; + border-radius: var(--igsp-radius); + box-shadow: var(--igsp-shadow); + overflow: hidden; +} + +/* ============================================ + Tabs Navigation + ============================================ */ +.igsp-tabs-nav { + display: flex; + background: var(--igsp-light); + border-bottom: 1px solid var(--igsp-border); + padding: 0 20px; + gap: 5px; +} + +.igsp-tab-link { + display: flex; + align-items: center; + gap: 8px; + padding: 15px 20px; + text-decoration: none; + color: var(--igsp-gray); + font-weight: 500; + border-bottom: 3px solid transparent; + margin-bottom: -1px; + transition: all 0.2s ease; +} + +.igsp-tab-link:hover { + color: var(--igsp-primary); +} + +.igsp-tab-link.active { + color: var(--igsp-primary); + border-bottom-color: var(--igsp-primary); + background: #fff; +} + +.igsp-tab-link .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +/* ============================================ + Tab Content + ============================================ */ +.igsp-tab-content { + padding: 30px; +} + +.igsp-section { + margin-bottom: 40px; +} + +.igsp-section:last-child { + margin-bottom: 0; +} + +.igsp-section h2 { + font-size: 18px; + font-weight: 600; + color: var(--igsp-dark); + margin: 0 0 20px 0; + padding-bottom: 10px; + border-bottom: 2px solid var(--igsp-border); +} + +/* ============================================ + Form Elements + ============================================ */ +.igsp-settings-form .form-table th { + padding: 20px 10px 20px 0; + font-weight: 500; +} + +.igsp-settings-form .form-table td { + padding: 15px 10px; +} + +.igsp-input-wrapper { + display: flex; + align-items: center; + max-width: 300px; +} + +.igsp-input-prefix { + background: var(--igsp-light); + border: 1px solid var(--igsp-border); + border-right: none; + padding: 6px 12px; + border-radius: var(--igsp-radius) 0 0 var(--igsp-radius); + color: var(--igsp-gray); +} + +.igsp-input-wrapper input { + border-radius: 0 var(--igsp-radius) var(--igsp-radius) 0 !important; +} + +/* Slider Wrapper */ +.igsp-slider-wrapper { + display: flex; + align-items: center; + gap: 15px; + max-width: 400px; +} + +.igsp-slider-wrapper input[type="range"] { + flex: 1; + -webkit-appearance: none; + height: 6px; + background: var(--igsp-border); + border-radius: 3px; + outline: none; +} + +.igsp-slider-wrapper input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--igsp-primary); + border-radius: 50%; + cursor: pointer; + transition: transform 0.2s; +} + +.igsp-slider-wrapper input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.1); +} + +.igsp-slider-value { + min-width: 50px; + padding: 5px 10px; + background: var(--igsp-light); + border-radius: var(--igsp-radius); + text-align: center; + font-weight: 500; + font-size: 13px; +} + +/* Inline Fields */ +.igsp-inline-fields { + display: flex; + gap: 10px; + align-items: center; +} + +/* ============================================ + Layout Selector + ============================================ */ +.igsp-layout-selector { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 15px; + max-width: 800px; +} + +.igsp-layout-option { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + border: 2px solid var(--igsp-border); + border-radius: var(--igsp-radius); + cursor: pointer; + transition: all 0.2s ease; + background: #fff; +} + +.igsp-layout-option:hover { + border-color: var(--igsp-primary); +} + +.igsp-layout-option.selected { + border-color: var(--igsp-primary); + background: rgba(225, 48, 108, 0.05); +} + +.igsp-layout-option input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.igsp-layout-preview { + width: 100px; + height: 70px; + display: grid; + gap: 3px; + margin-bottom: 10px; +} + +.igsp-layout-preview span { + background: var(--igsp-border); + border-radius: 2px; +} + +.igsp-preview-grid { + grid-template-columns: repeat(3, 1fr); +} + +.igsp-preview-masonry { + grid-template-columns: repeat(3, 1fr); +} + +.igsp-preview-justified { + display: flex; + flex-wrap: wrap; + gap: 3px; +} + +.igsp-preview-justified span { + height: 30px; +} + +.igsp-preview-slider { + display: flex; + align-items: center; + justify-content: space-between; + gap: 5px; +} + +.igsp-preview-slider .slide { + flex: 1; + height: 100%; +} + +.igsp-preview-slider .arrow-left, +.igsp-preview-slider .arrow-right { + background: none; + color: var(--igsp-gray); + font-size: 20px; +} + +.igsp-preview-list { + grid-template-columns: 1fr; +} + +.igsp-layout-name { + font-weight: 500; + color: var(--igsp-dark); + font-size: 13px; +} + +/* ============================================ + Sidebar + ============================================ */ +.igsp-admin-sidebar { + display: flex; + flex-direction: column; + gap: 20px; +} + +.igsp-sidebar-box { + background: #fff; + border-radius: var(--igsp-radius); + box-shadow: var(--igsp-shadow); + padding: 20px; +} + +.igsp-sidebar-box h3 { + margin: 0 0 15px 0; + font-size: 14px; + font-weight: 600; + color: var(--igsp-dark); +} + +/* Status Box */ +.igsp-status-row { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid var(--igsp-border); +} + +.igsp-status-row:last-child { + border-bottom: none; +} + +.igsp-status-label { + color: var(--igsp-gray); + font-size: 13px; +} + +.igsp-status-value { + font-weight: 600; + font-size: 13px; +} + +/* Shortcode Box */ +.igsp-shortcode-display { + display: block; + padding: 12px; + background: var(--igsp-dark); + color: #fff; + border-radius: var(--igsp-radius); + font-size: 13px; + margin-bottom: 10px; +} + +/* Help Box */ +.igsp-help-box ul { + margin: 0; + padding: 0; + list-style: none; +} + +.igsp-help-box li { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 0; +} + +.igsp-help-box li .dashicons { + color: var(--igsp-primary); +} + +.igsp-help-box a { + text-decoration: none; +} + +/* ============================================ + Progress Bar + ============================================ */ +.igsp-progress-wrapper { + margin-top: 15px; +} + +.igsp-progress-bar { + height: 8px; + background: var(--igsp-light); + border-radius: 4px; + overflow: hidden; + margin-bottom: 8px; +} + +.igsp-progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--igsp-primary), var(--igsp-primary-hover)); + width: 0%; + transition: width 0.3s ease; + animation: igsp-progress-pulse 1.5s ease-in-out infinite; +} + +@keyframes igsp-progress-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +.igsp-progress-text { + font-size: 13px; + color: var(--igsp-gray); +} + +/* Sync Result */ +.igsp-sync-result { + margin-top: 15px; + padding: 12px; + border-radius: var(--igsp-radius); + font-size: 13px; +} + +.igsp-sync-result.success { + background: rgba(40, 167, 69, 0.1); + color: var(--igsp-success); +} + +.igsp-sync-result.error { + background: rgba(220, 53, 69, 0.1); + color: var(--igsp-error); +} + +/* ============================================ + Tools Grid + ============================================ */ +.igsp-tools-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; +} + +.igsp-tool-card { + padding: 20px; + background: var(--igsp-light); + border-radius: var(--igsp-radius); +} + +.igsp-tool-card h3 { + margin: 0 0 10px 0; + font-size: 14px; +} + +.igsp-tool-card p { + margin: 0 0 15px 0; + font-size: 13px; + color: var(--igsp-gray); +} + +.igsp-tool-danger { + background: rgba(220, 53, 69, 0.05); +} + +.button-danger { + background: var(--igsp-error) !important; + border-color: var(--igsp-error) !important; + color: #fff !important; +} + +.button-danger:hover { + background: #c82333 !important; +} + +/* ============================================ + Logs + ============================================ */ +.igsp-logs-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.igsp-logs-header h2 { + margin: 0 !important; + padding: 0 !important; + border: none !important; +} + +.igsp-logs-table-wrapper { + max-height: 400px; + overflow-y: auto; + border: 1px solid var(--igsp-border); + border-radius: var(--igsp-radius); +} + +.igsp-logs-table { + margin: 0 !important; + border: none !important; +} + +.igsp-logs-table th { + background: var(--igsp-light); + position: sticky; + top: 0; +} + +.igsp-log-type { width: 100px; } +.igsp-log-date { width: 120px; } + +.igsp-log-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +.igsp-log-success { background: rgba(40, 167, 69, 0.1); color: var(--igsp-success); } +.igsp-log-error { background: rgba(220, 53, 69, 0.1); color: var(--igsp-error); } +.igsp-log-warning { background: rgba(255, 193, 7, 0.1); color: #856404; } +.igsp-log-info { background: rgba(23, 162, 184, 0.1); color: var(--igsp-info); } + +.igsp-no-logs { + padding: 40px; + text-align: center; + color: var(--igsp-gray); + background: var(--igsp-light); + border-radius: var(--igsp-radius); +} + +/* ============================================ + Style Preview + ============================================ */ +.igsp-style-preview { + padding: 30px; + background: var(--igsp-light); + border-radius: var(--igsp-radius); + display: flex; + justify-content: center; +} + +.igsp-preview-item { + text-align: center; +} + +.igsp-preview-image { + width: 200px; + height: 200px; + background: linear-gradient(135deg, #667eea, #764ba2); + border-radius: var(--igsp-radius); + position: relative; + overflow: hidden; + margin-bottom: 15px; +} + +.igsp-preview-overlay { + position: absolute; + inset: 0; + background: var(--hover); + opacity: 0; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.3s; +} + +.igsp-preview-image:hover .igsp-preview-overlay { + opacity: 0.8; +} + +.igsp-preview-caption { + color: var(--text); +} + +.igsp-preview-button { + background: var(--primary) !important; + color: var(--text) !important; + border: none !important; + padding: 10px 25px !important; + border-radius: var(--igsp-radius) !important; + cursor: pointer; + transition: background 0.2s; +} + +.igsp-preview-button:hover { + background: var(--hover) !important; +} + +/* ============================================ + Color Picker + ============================================ */ +.wp-picker-container { + display: flex; + align-items: center; + gap: 10px; +} + +/* ============================================ + Responsive + ============================================ */ +@media (max-width: 1200px) { + .igsp-admin-wrap { + grid-template-columns: 1fr; + } + + .igsp-admin-sidebar { + flex-direction: row; + flex-wrap: wrap; + } + + .igsp-sidebar-box { + flex: 1; + min-width: 250px; + } +} + +@media (max-width: 782px) { + .igsp-tabs-nav { + flex-wrap: wrap; + padding: 10px; + } + + .igsp-tab-link { + flex: 1; + justify-content: center; + padding: 10px; + font-size: 12px; + } + + .igsp-tab-link .dashicons { + display: none; + } + + .igsp-layout-selector { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..8142269 --- /dev/null +++ b/admin/index.php @@ -0,0 +1 @@ + 90) progress = 90; + $fill.css('width', progress + '%'); + }, 500); + + // Make AJAX request + $.ajax({ + url: igspAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'igsp_manual_sync', + nonce: igspAdmin.nonce + }, + success: function(response) { + clearInterval(progressInterval); + $fill.css('width', '100%'); + + setTimeout(function() { + $progress.hide(); + $result.removeClass('success error'); + + if (response.success) { + $result.addClass('success').html(response.data.message).show(); + IGSPAdmin.updateSyncStatus(); + } else { + $result.addClass('error').html(response.data.message || igspAdmin.strings.syncError).show(); + } + + $button.prop('disabled', false); + }, 500); + }, + error: function() { + clearInterval(progressInterval); + $progress.hide(); + $result.addClass('error').html(igspAdmin.strings.syncError).show(); + $button.prop('disabled', false); + } + }); + }); + }, + + /** + * Initialize tool buttons + */ + initToolButtons: function() { + // Clear cache + $('#igsp-clear-cache').on('click', function() { + const $button = $(this); + $button.prop('disabled', true).text(igspAdmin.strings.processing); + + $.post(igspAdmin.ajaxUrl, { + action: 'igsp_clear_cache', + nonce: igspAdmin.nonce + }, function(response) { + if (response.success) { + $button.text(igspAdmin.strings.done); + setTimeout(function() { + $button.prop('disabled', false).html(' Clear Cache'); + }, 2000); + } + }); + }); + + // Reset data + $('#igsp-reset-data').on('click', function() { + if (!confirm(igspAdmin.strings.confirmReset)) { + return; + } + + const $button = $(this); + $button.prop('disabled', true).text(igspAdmin.strings.processing); + + $.post(igspAdmin.ajaxUrl, { + action: 'igsp_reset_data', + nonce: igspAdmin.nonce + }, function(response) { + if (response.success) { + location.reload(); + } + }); + }); + + // Clear logs + $('#igsp-clear-logs').on('click', function() { + if (!confirm(igspAdmin.strings.confirmClearLogs)) { + return; + } + + const $button = $(this); + $button.prop('disabled', true); + + $.post(igspAdmin.ajaxUrl, { + action: 'igsp_clear_logs', + nonce: igspAdmin.nonce + }, function(response) { + if (response.success) { + location.reload(); + } + }); + }); + }, + + /** + * Update sync status + */ + updateSyncStatus: function() { + $.post(igspAdmin.ajaxUrl, { + action: 'igsp_get_sync_status', + nonce: igspAdmin.nonce + }, function(response) { + if (response.success) { + $('#igsp-total-posts').text(response.data.total_posts); + $('#igsp-last-sync').text(response.data.last_sync); + $('#igsp-next-sync').text(response.data.next_sync); + } + }); + }, + + /** + * Update live preview + */ + updatePreview: function() { + const primaryColor = $('#igsp_primary_color').val() || '#e1306c'; + const hoverColor = $('#igsp_hover_color').val() || '#c13584'; + const textColor = $('#igsp_text_color').val() || '#ffffff'; + const fontSize = $('#igsp_caption_font_size').val() || 14; + + $('.igsp-preview-item').css({ + '--primary': primaryColor, + '--hover': hoverColor, + '--text': textColor + }); + + $('.igsp-preview-caption').css('font-size', fontSize + 'px'); + }, + + /** + * Tab persistence + */ + initTabPersistence: function() { + // Store current tab in localStorage + $('.igsp-tab-link').on('click', function() { + const tab = $(this).attr('href').split('tab=')[1]; + if (tab) { + localStorage.setItem('igsp_active_tab', tab); + } + }); + } + }; + + // Initialize when document is ready + $(document).ready(function() { + IGSPAdmin.init(); + }); + +})(jQuery); diff --git a/admin/js/ajax-sync.js b/admin/js/ajax-sync.js new file mode 100644 index 0000000..9ce1e22 --- /dev/null +++ b/admin/js/ajax-sync.js @@ -0,0 +1,182 @@ +/** + * Instagram Gallery Sync Pro - AJAX Sync Handler + * + * Handles manual sync with progress feedback + * + * @package Instagram_Gallery_Sync_Pro + */ + +(function ($) { + 'use strict'; + + /** + * Sync Controller + */ + const IGSPSync = { + + isRunning: false, + + /** + * Start sync process + */ + start: function () { + if (this.isRunning) { + return; + } + + this.isRunning = true; + + const $button = $('#igsp-sync-now'); + const $progress = $('#igsp-sync-progress'); + const $progressFill = $progress.find('.igsp-progress-fill'); + const $progressText = $progress.find('.igsp-progress-text'); + const $result = $('#igsp-sync-result'); + + // Reset UI + $button.prop('disabled', true); + $progress.show(); + $result.hide().removeClass('success error'); + $progressFill.css('width', '0%'); + + // Start progress animation + this.animateProgress($progressFill, $progressText); + + // Execute sync + this.executeSync($button, $progress, $progressFill, $result); + }, + + /** + * Animate progress bar + */ + animateProgress: function ($fill, $text) { + const stages = [ + { progress: 20, text: 'Connecting to Instagram...' }, + { progress: 40, text: 'Fetching profile data...' }, + { progress: 60, text: 'Downloading images...' }, + { progress: 80, text: 'Saving to database...' }, + { progress: 90, text: 'Finalizing...' } + ]; + + let stageIndex = 0; + + this.progressInterval = setInterval(function () { + if (stageIndex < stages.length && IGSPSync.isRunning) { + $fill.css('width', stages[stageIndex].progress + '%'); + $text.text(stages[stageIndex].text); + stageIndex++; + } + }, 1500); + }, + + /** + * Execute sync via AJAX + */ + executeSync: function ($button, $progress, $fill, $result) { + const self = this; + + $.ajax({ + url: igspAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'igsp_manual_sync', + nonce: igspAdmin.nonce + }, + timeout: 120000, // 2 minute timeout + success: function (response) { + self.complete($button, $progress, $fill, $result, response); + }, + error: function (xhr, status, error) { + let message = igspAdmin.strings.syncError; + + if (status === 'timeout') { + message = 'Sync timed out. The process may still be running in the background.'; + } else if (xhr.responseJSON && xhr.responseJSON.data) { + message = xhr.responseJSON.data.message; + } + + self.complete($button, $progress, $fill, $result, { + success: false, + data: { message: message } + }); + } + }); + }, + + /** + * Complete sync process + */ + complete: function ($button, $progress, $fill, $result, response) { + clearInterval(this.progressInterval); + this.isRunning = false; + + // Complete progress bar + $fill.css('width', '100%'); + + setTimeout(function () { + $progress.hide(); + + if (response.success) { + $result + .addClass('success') + .html(IGSPSync.formatSuccessMessage(response.data)) + .show(); + + // Update sidebar status + IGSPSync.updateStatus(); + } else { + $result + .addClass('error') + .html(response.data.message || igspAdmin.strings.syncError) + .show(); + } + + $button.prop('disabled', false); + }, 500); + }, + + /** + * Format success message + */ + formatSuccessMessage: function (data) { + let html = '✓ ' + igspAdmin.strings.syncComplete + '
'; + + if (data.posts_added > 0) { + html += 'Added: ' + data.posts_added + ' new posts
'; + } + + if (data.posts_updated > 0) { + html += 'Updated: ' + data.posts_updated + ' existing posts
'; + } + + if (data.posts_skipped > 0) { + html += 'Skipped: ' + data.posts_skipped + ' posts'; + } + + if (data.errors && data.errors.length > 0) { + html += '
Warnings: ' + data.errors.length + ''; + } + + return html; + }, + + /** + * Update sync status in sidebar + */ + updateStatus: function () { + $.post(igspAdmin.ajaxUrl, { + action: 'igsp_get_sync_status', + nonce: igspAdmin.nonce + }, function (response) { + if (response.success) { + $('#igsp-total-posts').text(response.data.total_posts); + $('#igsp-last-sync').text(response.data.last_sync); + $('#igsp-next-sync').text(response.data.next_sync); + } + }); + } + }; + + // Make it available globally + window.IGSPSync = IGSPSync; + +})(jQuery); diff --git a/admin/views/settings-page.php b/admin/views/settings-page.php new file mode 100644 index 0000000..a5ba950 --- /dev/null +++ b/admin/views/settings-page.php @@ -0,0 +1,122 @@ +get_current_tab(); +$tabs = $admin->get_tabs(); +?> +
+
+

+ + +

+

+ +

+
+ +
+ + + + +
+ ' . esc_html__('Tab content not found.', 'instagram-gallery-sync-pro') . '

'; + } + ?> +
+
+ + +
+
+

+ +

+
+ get_sync_status(); + ?> +
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+
+
+ +
+

+ +

+ [instagram_gallery] +

+ +

+
+ +
+

+ +

+ +
+
+
\ No newline at end of file diff --git a/admin/views/tab-advanced.php b/admin/views/tab-advanced.php new file mode 100644 index 0000000..4d5449b --- /dev/null +++ b/admin/views/tab-advanced.php @@ -0,0 +1,264 @@ +get_formatted_logs(50); +$image_handler = new IGSP_Image_Handler(); +?> + +
+ + +
+

+ +

+ + + + + + +
+ + + +
+ + +
+

+ +

+
+
+ +
+

+ +

+ + + + + + + + + + + + + + + + +
+ + + + +

+ +

+
+ + +
+ + + s + +
+

+ +

+
+ + +
+ + + + +
+

+ +

+
+
+ +
+

+ +

+ + + + + + + + + + + +
+ + + +
+ + +
+ + +
+

+ +

+
+
+ + +
+ +
+

+ +

+ +
+
+

+ +

+

+ +

+ +
+ +
+

+ +

+

+ get_storage_used_formatted())); ?> +

+
+ +
+

+ +

+

+ +

+ +
+
+
+ +
+
+

+ +

+ +
+ + +

+ +

+ +
+ + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+
+ +
\ No newline at end of file diff --git a/admin/views/tab-display.php b/admin/views/tab-display.php new file mode 100644 index 0000000..59af56a --- /dev/null +++ b/admin/views/tab-display.php @@ -0,0 +1,233 @@ + + +
+ + +
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + +
+

+ +

+
+ + + + +
+ + + +
+ + +
+
+ + + + +
+ + + +
+ + +
+
+
+ +
+

+ +

+ + + + + + + + + + + +
+ + + + +
+ + + +
+ + +
+

+ +

+
+
+ +
+

+ +

+ + + + + + + + + + + +
+ + + +
+ + +
+
+ + + + +
+
+ + +
\ No newline at end of file diff --git a/admin/views/tab-instagram.php b/admin/views/tab-instagram.php new file mode 100644 index 0000000..d5b2af9 --- /dev/null +++ b/admin/views/tab-instagram.php @@ -0,0 +1,153 @@ +get_intervals(); +?> + +
+ + +
+

+ + + + + + + + + + + + + + + + + + + + + +
+ + +
+ @ + +
+

+
+ + + +

+
+
+ +
+ +
+
+ + + +
+
+ +
+

+ + + + + + + + + + + +
+ + + +

+
+ + + +
+
+ + +
diff --git a/admin/views/tab-layout.php b/admin/views/tab-layout.php new file mode 100644 index 0000000..1ff940e --- /dev/null +++ b/admin/views/tab-layout.php @@ -0,0 +1,279 @@ + + +
+ + +
+

+ +

+ + +
+ + + + + + + + + +
+
+ +
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + +
+

+ 1024px)', 'instagram-gallery-sync-pro'); ?> +

+
+ + +
+ + + + +
+

+ +

+
+ + +
+ + + + +
+

+ +

+
+ + +
+ + + px + +
+
+ + +
+ + + px + +
+
+
+ +
+

+ +

+ + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + +
+ + +
+ + + px + +
+
+ + + + +
+
+ + +
\ No newline at end of file diff --git a/admin/views/tab-styling.php b/admin/views/tab-styling.php new file mode 100644 index 0000000..39a8739 --- /dev/null +++ b/admin/views/tab-styling.php @@ -0,0 +1,158 @@ + + +
+ + +
+

+ +

+ + + + + + + + + + + + + + + + +
+ + + +

+ +

+
+ + + +

+ +

+
+ + + +

+ +

+
+
+ +
+

+ +

+ + + + + + +
+ + +
+ + + px + +
+
+
+ +
+

+ +

+ + + + + + + + + + + +
+ + + +

+ +

+
+ + + +

+ +

+
+
+ +
+

+ +

+
+
+
+
+ + + +
+
+ +
+
+
+ + +
\ No newline at end of file diff --git a/includes/class-admin.php b/includes/class-admin.php new file mode 100644 index 0000000..9884875 --- /dev/null +++ b/includes/class-admin.php @@ -0,0 +1,395 @@ +]*>(.*?)<\/script>/is', '', $css); + + // Remove javascript: URLs + $css = preg_replace('/javascript\s*:/i', '', $css); + + // Remove expression() + $css = preg_replace('/expression\s*\(/i', '', $css); + + // Remove behavior: (IE specific) + $css = preg_replace('/behavior\s*:/i', '', $css); + + return wp_strip_all_tags($css); + } + + /** + * Enqueue admin assets + * + * @param string $hook Current page hook + * @return void + */ + public function enqueue_assets($hook) + { + if (strpos($hook, self::PAGE_SLUG) === false) { + return; + } + + // Admin CSS + wp_enqueue_style( + 'igsp-admin-style', + IGSP_PLUGIN_URL . 'admin/css/admin-style.css', + array(), + IGSP_VERSION + ); + + // WordPress components + wp_enqueue_style('wp-color-picker'); + + // Admin JS + wp_enqueue_script( + 'igsp-admin-script', + IGSP_PLUGIN_URL . 'admin/js/admin-script.js', + array('jquery', 'wp-color-picker'), + IGSP_VERSION, + true + ); + + // AJAX Sync JS + wp_enqueue_script( + 'igsp-ajax-sync', + IGSP_PLUGIN_URL . 'admin/js/ajax-sync.js', + array('jquery'), + IGSP_VERSION, + true + ); + + // Localize script + wp_localize_script('igsp-admin-script', 'igspAdmin', array( + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('igsp_admin_nonce'), + 'strings' => array( + 'syncing' => __('Syncing...', 'instagram-gallery-sync-pro'), + 'syncComplete' => __('Sync complete!', 'instagram-gallery-sync-pro'), + 'syncError' => __('Sync failed. Check logs for details.', 'instagram-gallery-sync-pro'), + 'confirmReset' => __('Are you sure? This will delete ALL Instagram data including images. This action cannot be undone.', 'instagram-gallery-sync-pro'), + 'confirmClearLogs' => __('Are you sure you want to clear all logs?', 'instagram-gallery-sync-pro'), + 'processing' => __('Processing...', 'instagram-gallery-sync-pro'), + 'done' => __('Done!', 'instagram-gallery-sync-pro'), + ), + )); + } + + /** + * Render settings page + * + * @return void + */ + public function render_settings_page() + { + // Check permissions + if (!current_user_can('manage_options')) { + wp_die(__('You do not have sufficient permissions to access this page.', 'instagram-gallery-sync-pro')); + } + + include IGSP_PLUGIN_DIR . 'admin/views/settings-page.php'; + } + + /** + * Add settings link to plugins page + * + * @param array $links Existing links + * @return array + */ + public function add_settings_link($links) + { + $settings_link = sprintf( + '%s', + admin_url('admin.php?page=' . self::PAGE_SLUG), + __('Settings', 'instagram-gallery-sync-pro') + ); + + array_unshift($links, $settings_link); + + return $links; + } + + /** + * AJAX: Manual sync + * + * @return void + */ + public function ajax_manual_sync() + { + check_ajax_referer('igsp_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro'))); + } + + $cron = new IGSP_Cron(); + $result = $cron->manual_sync(); + + if ($result['success']) { + wp_send_json_success($result); + } else { + wp_send_json_error($result); + } + } + + /** + * AJAX: Clear cache + * + * @return void + */ + public function ajax_clear_cache() + { + check_ajax_referer('igsp_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro'))); + } + + igsp()->clear_transients(); + + wp_send_json_success(array('message' => __('Cache cleared successfully.', 'instagram-gallery-sync-pro'))); + } + + /** + * AJAX: Reset all data + * + * @return void + */ + public function ajax_reset_data() + { + check_ajax_referer('igsp_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro'))); + } + + // Delete all posts from database + $database = new IGSP_Database(); + $database->delete_all_posts(); + + // Clear all images + $image_handler = new IGSP_Image_Handler(); + $image_handler->clear_all_images(); + + // Clear logs + $logger = new IGSP_Logger(); + $logger->clear_logs(); + + // Clear transients + igsp()->clear_transients(); + + // Reset last sync + update_option('igsp_last_sync', ''); + + wp_send_json_success(array('message' => __('All data has been reset.', 'instagram-gallery-sync-pro'))); + } + + /** + * AJAX: Get sync status + * + * @return void + */ + public function ajax_get_sync_status() + { + check_ajax_referer('igsp_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro'))); + } + + $cron = new IGSP_Cron(); + $status = $cron->get_sync_status(); + + wp_send_json_success($status); + } + + /** + * AJAX: Clear logs + * + * @return void + */ + public function ajax_clear_logs() + { + check_ajax_referer('igsp_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro'))); + } + + $logger = new IGSP_Logger(); + $logger->clear_logs(); + + wp_send_json_success(array('message' => __('Logs cleared successfully.', 'instagram-gallery-sync-pro'))); + } + + /** + * Get current tab + * + * @return string + */ + public function get_current_tab() + { + return isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'instagram'; + } + + /** + * Get tabs configuration + * + * @return array + */ + public function get_tabs() + { + return array( + 'instagram' => array( + 'title' => __('Instagram', 'instagram-gallery-sync-pro'), + 'icon' => 'dashicons-instagram', + ), + 'layout' => array( + 'title' => __('Layout', 'instagram-gallery-sync-pro'), + 'icon' => 'dashicons-grid-view', + ), + 'display' => array( + 'title' => __('Display', 'instagram-gallery-sync-pro'), + 'icon' => 'dashicons-visibility', + ), + 'styling' => array( + 'title' => __('Styling', 'instagram-gallery-sync-pro'), + 'icon' => 'dashicons-art', + ), + 'advanced' => array( + 'title' => __('Advanced', 'instagram-gallery-sync-pro'), + 'icon' => 'dashicons-admin-generic', + ), + ); + } +} diff --git a/includes/class-cron.php b/includes/class-cron.php new file mode 100644 index 0000000..28f416c --- /dev/null +++ b/includes/class-cron.php @@ -0,0 +1,404 @@ +logger = new IGSP_Logger(); + } + + /** + * Initialize cron handlers + * + * @return void + */ + public function init() + { + // Register custom intervals + add_filter('cron_schedules', array($this, 'add_custom_intervals')); + + // Register cron hooks + add_action(self::SYNC_HOOK, array($this, 'run_sync')); + add_action(self::CLEANUP_HOOK, array($this, 'run_cleanup')); + } + + /** + * Add custom cron intervals + * + * @param array $schedules Existing schedules + * @return array + */ + public function add_custom_intervals($schedules) + { + $schedules['igsp_30min'] = array( + 'interval' => 30 * MINUTE_IN_SECONDS, + 'display' => __('Every 30 Minutes', 'instagram-gallery-sync-pro'), + ); + + $schedules['igsp_6hours'] = array( + 'interval' => 6 * HOUR_IN_SECONDS, + 'display' => __('Every 6 Hours', 'instagram-gallery-sync-pro'), + ); + + return $schedules; + } + + /** + * Get available sync intervals + * + * @return array + */ + public function get_intervals() + { + return array( + 'igsp_30min' => __('Every 30 Minutes', 'instagram-gallery-sync-pro'), + 'hourly' => __('Hourly', 'instagram-gallery-sync-pro'), + 'igsp_6hours' => __('Every 6 Hours', 'instagram-gallery-sync-pro'), + 'daily' => __('Daily', 'instagram-gallery-sync-pro'), + 'weekly' => __('Weekly', 'instagram-gallery-sync-pro'), + ); + } + + /** + * Schedule sync job + * + * @return void + */ + public function schedule_sync() + { + // Get interval setting + $interval = get_option('igsp_sync_interval', 'daily'); + + // Clear existing schedule + $this->unschedule_sync(); + + // Schedule new sync + if (!wp_next_scheduled(self::SYNC_HOOK)) { + wp_schedule_event(time(), $interval, self::SYNC_HOOK); + $this->logger->info(sprintf(__('Scheduled sync with interval: %s', 'instagram-gallery-sync-pro'), $interval)); + } + + // Schedule daily cleanup if not set + if (!wp_next_scheduled(self::CLEANUP_HOOK)) { + wp_schedule_event(time(), 'daily', self::CLEANUP_HOOK); + } + } + + /** + * Unschedule sync job + * + * @return void + */ + public function unschedule_sync() + { + $timestamp = wp_next_scheduled(self::SYNC_HOOK); + + if ($timestamp) { + wp_unschedule_event($timestamp, self::SYNC_HOOK); + } + + wp_clear_scheduled_hook(self::SYNC_HOOK); + } + + /** + * Check if sync is scheduled + * + * @return bool + */ + public function is_scheduled() + { + return (bool) wp_next_scheduled(self::SYNC_HOOK); + } + + /** + * Get next scheduled sync time + * + * @return int|false Timestamp or false + */ + public function get_next_sync() + { + return wp_next_scheduled(self::SYNC_HOOK); + } + + /** + * Run the sync process + * + * @param bool $manual Whether this is a manual sync + * @return array Result data + */ + public function run_sync($manual = false) + { + // Check if already running + if ($this->is_locked()) { + $this->logger->warning(__('Sync already in progress, skipping.', 'instagram-gallery-sync-pro')); + return array( + 'success' => false, + 'message' => __('Sync already in progress.', 'instagram-gallery-sync-pro'), + ); + } + + // Acquire lock + $this->acquire_lock(); + + $result = array( + 'success' => false, + 'message' => '', + 'posts_added' => 0, + 'posts_updated' => 0, + 'posts_skipped' => 0, + 'errors' => array(), + ); + + try { + // Get username + $username = get_option('igsp_username', ''); + + if (empty($username)) { + throw new Exception(__('No Instagram username configured.', 'instagram-gallery-sync-pro')); + } + + $this->logger->info(sprintf(__('Starting sync for @%s', 'instagram-gallery-sync-pro'), $username)); + + // Initialize components + $scraper = new IGSP_Scraper(); + $database = new IGSP_Database(); + $image_handler = new IGSP_Image_Handler(); + + // Fetch posts from Instagram + $posts = $scraper->fetch_profile_data($username); + + if (is_wp_error($posts)) { + throw new Exception($posts->get_error_message()); + } + + if (empty($posts)) { + throw new Exception(__('No posts found.', 'instagram-gallery-sync-pro')); + } + + // Get existing IDs + $existing_ids = $database->get_existing_instagram_ids(); + $save_locally = get_option('igsp_save_locally', 'yes') === 'yes'; + + // Process each post + foreach ($posts as $post) { + // Validate post data + if (!$scraper->validate_post_data($post)) { + $result['posts_skipped']++; + continue; + } + + $is_new = !in_array($post['instagram_id'], $existing_ids, true); + + // Prepare post data + $post_data = array( + 'instagram_id' => $post['instagram_id'], + 'username' => $post['username'], + 'post_url' => $post['post_url'], + 'caption' => $post['caption'], + 'likes_count' => $post['likes_count'], + 'comments_count' => $post['comments_count'], + 'posted_at' => $post['posted_at'], + 'image_width' => $post['image_width'], + 'image_height' => $post['image_height'], + ); + + // Download image if saving locally and is new post + if ($save_locally && $is_new && !empty($post['image_url'])) { + $image_result = $image_handler->download_image($post['image_url'], $post['instagram_id']); + + if (!is_wp_error($image_result)) { + $post_data['image_local_path'] = $image_result['local_path']; + $post_data['image_thumbnail_path'] = $image_result['thumbnail_path']; + $post_data['file_size'] = $image_result['file_size']; + $post_data['image_width'] = $image_result['width']; + $post_data['image_height'] = $image_result['height']; + } else { + $result['errors'][] = sprintf( + __('Failed to download image for post %s: %s', 'instagram-gallery-sync-pro'), + $post['instagram_id'], + $image_result->get_error_message() + ); + } + } + + // Save to database + $saved = $database->insert_post($post_data); + + if ($saved) { + if ($is_new) { + $result['posts_added']++; + } else { + $result['posts_updated']++; + } + } else { + $result['errors'][] = sprintf( + __('Failed to save post %s to database.', 'instagram-gallery-sync-pro'), + $post['instagram_id'] + ); + } + } + + // Update last sync time + update_option('igsp_last_sync', current_time('mysql')); + + // Delete old posts if auto-delete is enabled + $auto_delete_days = (int) get_option('igsp_auto_delete_days', 0); + if ($auto_delete_days > 0) { + $deleted = $database->delete_old_posts($auto_delete_days); + if ($deleted > 0) { + $this->logger->info(sprintf(__('Auto-deleted %d old posts.', 'instagram-gallery-sync-pro'), $deleted)); + } + } + + // Clear cache + igsp()->clear_transients(); + + $result['success'] = true; + $result['message'] = sprintf( + __('Sync completed. Added: %d, Updated: %d, Skipped: %d', 'instagram-gallery-sync-pro'), + $result['posts_added'], + $result['posts_updated'], + $result['posts_skipped'] + ); + + $this->logger->success($result['message'], array( + 'added' => $result['posts_added'], + 'updated' => $result['posts_updated'], + 'skipped' => $result['posts_skipped'], + )); + + } catch (Exception $e) { + $result['message'] = $e->getMessage(); + $result['errors'][] = $e->getMessage(); + $this->logger->error(__('Sync failed: ', 'instagram-gallery-sync-pro') . $e->getMessage()); + } + + // Release lock + $this->release_lock(); + + return $result; + } + + /** + * Run cleanup tasks + * + * @return void + */ + public function run_cleanup() + { + $logger = new IGSP_Logger(); + + // Cleanup old logs (keep last 7 days) + $logger->delete_logs_older_than(7); + + // Other cleanup tasks can be added here + } + + /** + * Check if sync is locked + * + * @return bool + */ + private function is_locked() + { + return (bool) get_transient(self::LOCK_KEY); + } + + /** + * Acquire sync lock + * + * @return void + */ + private function acquire_lock() + { + set_transient(self::LOCK_KEY, true, 10 * MINUTE_IN_SECONDS); + } + + /** + * Release sync lock + * + * @return void + */ + private function release_lock() + { + delete_transient(self::LOCK_KEY); + } + + /** + * Manually trigger sync + * + * @return array + */ + public function manual_sync() + { + return $this->run_sync(true); + } + + /** + * Get sync status for AJAX + * + * @return array + */ + public function get_sync_status() + { + $last_sync = get_option('igsp_last_sync', ''); + $next_sync = $this->get_next_sync(); + $is_running = $this->is_locked(); + + $database = new IGSP_Database(); + + return array( + 'is_running' => $is_running, + 'last_sync' => $last_sync ? mysql2date(get_option('date_format') . ' ' . get_option('time_format'), $last_sync) : __('Never', 'instagram-gallery-sync-pro'), + 'next_sync' => $next_sync ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_sync) : __('Not scheduled', 'instagram-gallery-sync-pro'), + 'total_posts' => $database->count_posts(), + ); + } +} diff --git a/includes/class-database.php b/includes/class-database.php new file mode 100644 index 0000000..cf13158 --- /dev/null +++ b/includes/class-database.php @@ -0,0 +1,459 @@ +get_charset_collate(); + + // Posts table + $posts_table = IGSP_TABLE_POSTS; + $sql_posts = "CREATE TABLE {$posts_table} ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + instagram_id VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL, + image_local_path VARCHAR(500) DEFAULT NULL, + image_thumbnail_path VARCHAR(500) DEFAULT NULL, + post_url VARCHAR(500) DEFAULT NULL, + caption TEXT DEFAULT NULL, + likes_count INT(11) DEFAULT NULL, + comments_count INT(11) DEFAULT NULL, + posted_at DATETIME DEFAULT NULL, + synced_at DATETIME DEFAULT CURRENT_TIMESTAMP, + file_size INT(11) DEFAULT 0, + image_width INT(11) DEFAULT 0, + image_height INT(11) DEFAULT 0, + is_active TINYINT(1) DEFAULT 1, + PRIMARY KEY (id), + UNIQUE KEY instagram_id (instagram_id), + KEY username (username), + KEY posted_at (posted_at), + KEY is_active (is_active) + ) {$charset_collate};"; + + // Log table + $log_table = IGSP_TABLE_LOG; + $sql_log = "CREATE TABLE {$log_table} ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + log_type VARCHAR(50) NOT NULL DEFAULT 'info', + message TEXT NOT NULL, + details LONGTEXT DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY log_type (log_type), + KEY created_at (created_at) + ) {$charset_collate};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + dbDelta($sql_posts); + dbDelta($sql_log); + + // Store database version + update_option('igsp_db_version', $this->db_version); + } + + /** + * Check and run database upgrades if needed + * + * @return void + */ + public function maybe_upgrade() + { + $installed_version = get_option('igsp_db_version', '0'); + + if (version_compare($installed_version, $this->db_version, '<')) { + $this->create_tables(); + } + } + + /** + * Insert or update a post + * + * @param array $data Post data + * @return int|false Insert ID or false on failure + */ + public function insert_post($data) + { + global $wpdb; + + // Check if post already exists + $existing = $this->get_post_by_instagram_id($data['instagram_id']); + + if ($existing) { + // Update existing post + return $this->update_post($existing->id, $data); + } + + // Sanitize data + $insert_data = array( + 'instagram_id' => sanitize_text_field($data['instagram_id']), + 'username' => sanitize_text_field($data['username']), + 'image_local_path' => isset($data['image_local_path']) ? sanitize_text_field($data['image_local_path']) : '', + 'image_thumbnail_path' => isset($data['image_thumbnail_path']) ? sanitize_text_field($data['image_thumbnail_path']) : '', + 'post_url' => isset($data['post_url']) ? esc_url_raw($data['post_url']) : '', + 'caption' => isset($data['caption']) ? wp_kses_post($data['caption']) : '', + 'likes_count' => isset($data['likes_count']) ? absint($data['likes_count']) : null, + 'comments_count' => isset($data['comments_count']) ? absint($data['comments_count']) : null, + 'posted_at' => isset($data['posted_at']) ? sanitize_text_field($data['posted_at']) : current_time('mysql'), + 'synced_at' => current_time('mysql'), + 'file_size' => isset($data['file_size']) ? absint($data['file_size']) : 0, + 'image_width' => isset($data['image_width']) ? absint($data['image_width']) : 0, + 'image_height' => isset($data['image_height']) ? absint($data['image_height']) : 0, + 'is_active' => isset($data['is_active']) ? absint($data['is_active']) : 1, + ); + + $result = $wpdb->insert( + IGSP_TABLE_POSTS, + $insert_data, + array( + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%d', + '%s', + '%s', + '%d', + '%d', + '%d', + '%d' + ) + ); + + return $result ? $wpdb->insert_id : false; + } + + /** + * Update a post + * + * @param int $id Post ID + * @param array $data Post data + * @return bool + */ + public function update_post($id, $data) + { + global $wpdb; + + $update_data = array(); + $formats = array(); + + // Map of fields and their formats + $field_map = array( + 'image_local_path' => '%s', + 'image_thumbnail_path' => '%s', + 'post_url' => '%s', + 'caption' => '%s', + 'likes_count' => '%d', + 'comments_count' => '%d', + 'posted_at' => '%s', + 'file_size' => '%d', + 'image_width' => '%d', + 'image_height' => '%d', + 'is_active' => '%d', + ); + + foreach ($field_map as $field => $format) { + if (isset($data[$field])) { + $update_data[$field] = $data[$field]; + $formats[] = $format; + } + } + + // Always update synced_at + $update_data['synced_at'] = current_time('mysql'); + $formats[] = '%s'; + + return $wpdb->update( + IGSP_TABLE_POSTS, + $update_data, + array('id' => $id), + $formats, + array('%d') + ) !== false; + } + + /** + * Get post by Instagram ID + * + * @param string $instagram_id Instagram post ID + * @return object|null + */ + public function get_post_by_instagram_id($instagram_id) + { + global $wpdb; + + return $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM " . IGSP_TABLE_POSTS . " WHERE instagram_id = %s", + $instagram_id + ) + ); + } + + /** + * Get all posts + * + * @param array $args Query arguments + * @return array + */ + public function get_posts($args = array()) + { + global $wpdb; + + $defaults = array( + 'limit' => 12, + 'offset' => 0, + 'order' => 'newest', + 'active' => true, + 'username' => '', + ); + + $args = wp_parse_args($args, $defaults); + + // Build query + $where = array('1=1'); + $values = array(); + + if ($args['active']) { + $where[] = 'is_active = 1'; + } + + if (!empty($args['username'])) { + $where[] = 'username = %s'; + $values[] = $args['username']; + } + + // Order + switch ($args['order']) { + case 'oldest': + $order = 'posted_at ASC'; + break; + case 'random': + $order = 'RAND()'; + break; + case 'newest': + default: + $order = 'posted_at DESC'; + break; + } + + $where_clause = implode(' AND ', $where); + + // Handle limit + $limit_clause = ''; + if ($args['limit'] > 0) { + $limit_clause = $wpdb->prepare('LIMIT %d OFFSET %d', $args['limit'], $args['offset']); + } + + $query = "SELECT * FROM " . IGSP_TABLE_POSTS . " WHERE {$where_clause} ORDER BY {$order} {$limit_clause}"; + + if (!empty($values)) { + $query = $wpdb->prepare($query, $values); + } + + return $wpdb->get_results($query); + } + + /** + * Count posts + * + * @param bool $active_only Count only active posts + * @return int + */ + public function count_posts($active_only = true) + { + global $wpdb; + + $where = $active_only ? 'WHERE is_active = 1' : ''; + + return (int) $wpdb->get_var( + "SELECT COUNT(*) FROM " . IGSP_TABLE_POSTS . " {$where}" + ); + } + + /** + * Delete post + * + * @param int $id Post ID + * @return bool + */ + public function delete_post($id) + { + global $wpdb; + + // Get post for file cleanup + $post = $this->get_post($id); + + if ($post) { + // Delete associated files + $this->delete_post_files($post); + } + + return $wpdb->delete( + IGSP_TABLE_POSTS, + array('id' => $id), + array('%d') + ) !== false; + } + + /** + * Get single post by ID + * + * @param int $id Post ID + * @return object|null + */ + public function get_post($id) + { + global $wpdb; + + return $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM " . IGSP_TABLE_POSTS . " WHERE id = %d", + $id + ) + ); + } + + /** + * Delete post image files + * + * @param object $post Post object + * @return void + */ + private function delete_post_files($post) + { + $upload_dir = wp_upload_dir(); + $base_path = $upload_dir['basedir']; + + // Delete main image + if (!empty($post->image_local_path)) { + $file_path = $base_path . '/' . $post->image_local_path; + if (file_exists($file_path)) { + unlink($file_path); + } + } + + // Delete thumbnail + if (!empty($post->image_thumbnail_path)) { + $thumb_path = $base_path . '/' . $post->image_thumbnail_path; + if (file_exists($thumb_path)) { + unlink($thumb_path); + } + } + } + + /** + * Delete old posts + * + * @param int $days Delete posts older than X days + * @return int Number of deleted posts + */ + public function delete_old_posts($days) + { + global $wpdb; + + if ($days <= 0) { + return 0; + } + + $date_threshold = date('Y-m-d H:i:s', strtotime("-{$days} days")); + + // Get posts to delete for file cleanup + $posts = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM " . IGSP_TABLE_POSTS . " WHERE synced_at < %s", + $date_threshold + ) + ); + + foreach ($posts as $post) { + $this->delete_post_files($post); + } + + // Delete from database + return $wpdb->query( + $wpdb->prepare( + "DELETE FROM " . IGSP_TABLE_POSTS . " WHERE synced_at < %s", + $date_threshold + ) + ); + } + + /** + * Delete all posts + * + * @return bool + */ + public function delete_all_posts() + { + global $wpdb; + + // Get all posts for file cleanup + $posts = $wpdb->get_results("SELECT * FROM " . IGSP_TABLE_POSTS); + + foreach ($posts as $post) { + $this->delete_post_files($post); + } + + // Truncate table + return $wpdb->query("TRUNCATE TABLE " . IGSP_TABLE_POSTS) !== false; + } + + /** + * Get existing Instagram IDs + * + * @return array + */ + public function get_existing_instagram_ids() + { + global $wpdb; + + return $wpdb->get_col( + "SELECT instagram_id FROM " . IGSP_TABLE_POSTS + ); + } +} diff --git a/includes/class-gutenberg-block.php b/includes/class-gutenberg-block.php new file mode 100644 index 0000000..577cfd7 --- /dev/null +++ b/includes/class-gutenberg-block.php @@ -0,0 +1,171 @@ + 'igsp-block-editor', + 'editor_style' => 'igsp-block-editor-style', + 'render_callback' => array($this, 'render_block'), + 'attributes' => $this->get_block_attributes(), + )); + } + + /** + * Get block attributes + * + * @return array + */ + private function get_block_attributes() + { + return array( + 'layout' => array( + 'type' => 'string', + 'default' => get_option('igsp_layout_type', 'grid'), + ), + 'columns' => array( + 'type' => 'number', + 'default' => get_option('igsp_columns_desktop', 3), + ), + 'spacing' => array( + 'type' => 'number', + 'default' => get_option('igsp_spacing', 10), + ), + 'limit' => array( + 'type' => 'number', + 'default' => get_option('igsp_display_limit', 12), + ), + 'order' => array( + 'type' => 'string', + 'default' => get_option('igsp_order', 'newest'), + ), + 'lightbox' => array( + 'type' => 'boolean', + 'default' => get_option('igsp_lightbox', 'yes') === 'yes', + ), + 'captions' => array( + 'type' => 'boolean', + 'default' => get_option('igsp_show_caption', 'no') === 'yes', + ), + 'className' => array( + 'type' => 'string', + 'default' => '', + ), + ); + } + + /** + * Enqueue editor assets + * + * @return void + */ + public function enqueue_editor_assets() + { + // Editor script + wp_enqueue_script( + 'igsp-block-editor', + IGSP_PLUGIN_URL . 'public/js/block-editor.js', + array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-server-side-render'), + IGSP_VERSION + ); + + // Editor style + wp_enqueue_style( + 'igsp-block-editor-style', + IGSP_PLUGIN_URL . 'public/css/block-editor.css', + array(), + IGSP_VERSION + ); + + // Localize + wp_localize_script('igsp-block-editor', 'igspBlock', array( + 'pluginUrl' => IGSP_PLUGIN_URL, + 'defaultLayout' => get_option('igsp_layout_type', 'grid'), + 'layouts' => array( + array('value' => 'grid', 'label' => __('Grid', 'instagram-gallery-sync-pro')), + array('value' => 'masonry', 'label' => __('Masonry', 'instagram-gallery-sync-pro')), + array('value' => 'slider', 'label' => __('Slider', 'instagram-gallery-sync-pro')), + array('value' => 'justified', 'label' => __('Justified', 'instagram-gallery-sync-pro')), + array('value' => 'list', 'label' => __('List', 'instagram-gallery-sync-pro')), + ), + 'orders' => array( + array('value' => 'newest', 'label' => __('Newest First', 'instagram-gallery-sync-pro')), + array('value' => 'oldest', 'label' => __('Oldest First', 'instagram-gallery-sync-pro')), + array('value' => 'random', 'label' => __('Random', 'instagram-gallery-sync-pro')), + ), + 'strings' => array( + 'title' => __('Instagram Gallery', 'instagram-gallery-sync-pro'), + 'description' => __('Display your Instagram photos in a beautiful gallery.', 'instagram-gallery-sync-pro'), + 'layout' => __('Layout', 'instagram-gallery-sync-pro'), + 'columns' => __('Columns', 'instagram-gallery-sync-pro'), + 'spacing' => __('Spacing', 'instagram-gallery-sync-pro'), + 'limit' => __('Number of Images', 'instagram-gallery-sync-pro'), + 'order' => __('Order', 'instagram-gallery-sync-pro'), + 'lightbox' => __('Enable Lightbox', 'instagram-gallery-sync-pro'), + 'captions' => __('Show Captions', 'instagram-gallery-sync-pro'), + ), + )); + } + + /** + * Render block + * + * @param array $attributes Block attributes + * @return string + */ + public function render_block($attributes) + { + // Convert block attributes to shortcode attributes + $atts = array( + 'layout' => $attributes['layout'] ?? 'grid', + 'columns' => $attributes['columns'] ?? 3, + 'spacing' => $attributes['spacing'] ?? 10, + 'limit' => $attributes['limit'] ?? 12, + 'order' => $attributes['order'] ?? 'newest', + 'lightbox' => $attributes['lightbox'] ? 'yes' : 'no', + 'captions' => $attributes['captions'] ? 'yes' : 'no', + 'class' => $attributes['className'] ?? '', + ); + + // Use shortcode handler for rendering + $shortcode = new IGSP_Shortcode(); + return $shortcode->render($atts); + } +} diff --git a/includes/class-image-handler.php b/includes/class-image-handler.php new file mode 100644 index 0000000..5fdcaae --- /dev/null +++ b/includes/class-image-handler.php @@ -0,0 +1,477 @@ + 150, + 'medium' => 320, + 'large' => 640, + ); + + /** + * Logger instance + * + * @var IGSP_Logger + */ + private $logger; + + /** + * Constructor + */ + public function __construct() + { + $this->setup_paths(); + $this->logger = new IGSP_Logger(); + } + + /** + * Setup upload paths + * + * @return void + */ + private function setup_paths() + { + $upload_dir = wp_upload_dir(); + $this->upload_path = $upload_dir['basedir'] . '/' . IGSP_UPLOAD_DIR; + $this->upload_url = $upload_dir['baseurl'] . '/' . IGSP_UPLOAD_DIR; + + // Ensure directory exists + if (!file_exists($this->upload_path)) { + wp_mkdir_p($this->upload_path); + } + } + + /** + * Download and save an image + * + * @param string $url Remote image URL + * @param string $instagram_id Instagram post ID for naming + * @return array|WP_Error Array with paths or WP_Error + */ + public function download_image($url, $instagram_id) + { + if (empty($url) || empty($instagram_id)) { + return new WP_Error('invalid_params', __('Invalid parameters for image download.', 'instagram-gallery-sync-pro')); + } + + // Sanitize ID for filename + $safe_id = sanitize_file_name($instagram_id); + + // Download the image + $temp_file = download_url($url, 30); + + if (is_wp_error($temp_file)) { + $this->logger->error( + sprintf(__('Failed to download image: %s', 'instagram-gallery-sync-pro'), $temp_file->get_error_message()), + array('url' => $url) + ); + return $temp_file; + } + + // Validate the downloaded file + $validation = $this->validate_image($temp_file); + + if (is_wp_error($validation)) { + @unlink($temp_file); + return $validation; + } + + // Get file info + $file_info = $validation; + $extension = $file_info['extension']; + + // Generate filenames + $filename = $safe_id . '.' . $extension; + $filepath = $this->upload_path . '/' . $filename; + + // Move file to upload directory + $moved = rename($temp_file, $filepath); + + if (!$moved) { + @unlink($temp_file); + return new WP_Error('move_failed', __('Failed to move downloaded file.', 'instagram-gallery-sync-pro')); + } + + // Set correct permissions + chmod($filepath, 0644); + + // Generate thumbnails + $thumbnail_path = $this->generate_thumbnail($filepath, $safe_id, $extension); + + // Get image dimensions + $dimensions = getimagesize($filepath); + + // Prepare result + $result = array( + 'local_path' => IGSP_UPLOAD_DIR . '/' . $filename, + 'thumbnail_path' => $thumbnail_path, + 'full_url' => $this->upload_url . '/' . $filename, + 'thumbnail_url' => !empty($thumbnail_path) ? $this->upload_url . '/' . str_replace(IGSP_UPLOAD_DIR . '/', '', $thumbnail_path) : '', + 'file_size' => filesize($filepath), + 'width' => $dimensions ? $dimensions[0] : 0, + 'height' => $dimensions ? $dimensions[1] : 0, + 'mime_type' => $file_info['mime_type'], + ); + + $this->logger->info( + sprintf(__('Image saved: %s', 'instagram-gallery-sync-pro'), $filename), + array('size' => $result['file_size'], 'dimensions' => $dimensions[0] . 'x' . $dimensions[1]) + ); + + return $result; + } + + /** + * Validate downloaded image + * + * @param string $filepath Path to temp file + * @return array|WP_Error File info or error + */ + private function validate_image($filepath) + { + if (!file_exists($filepath)) { + return new WP_Error('file_not_found', __('Downloaded file not found.', 'instagram-gallery-sync-pro')); + } + + // Check file size (max 20MB) + $max_size = 20 * 1024 * 1024; + if (filesize($filepath) > $max_size) { + return new WP_Error('file_too_large', __('File exceeds maximum size limit.', 'instagram-gallery-sync-pro')); + } + + // Verify it's actually an image + $file_info = wp_check_filetype_and_ext($filepath, basename($filepath)); + $image_info = @getimagesize($filepath); + + if (!$image_info) { + return new WP_Error('not_an_image', __('File is not a valid image.', 'instagram-gallery-sync-pro')); + } + + // Check allowed types + $allowed_types = array('image/jpeg', 'image/png', 'image/gif', 'image/webp'); + $mime_type = $image_info['mime']; + + if (!in_array($mime_type, $allowed_types, true)) { + return new WP_Error('invalid_type', __('Image type not allowed.', 'instagram-gallery-sync-pro')); + } + + // Map mime to extension + $ext_map = array( + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp', + ); + + return array( + 'mime_type' => $mime_type, + 'extension' => $ext_map[$mime_type] ?? 'jpg', + 'width' => $image_info[0], + 'height' => $image_info[1], + ); + } + + /** + * Generate thumbnail from image + * + * @param string $filepath Original image path + * @param string $safe_id Sanitized ID for naming + * @param string $extension File extension + * @return string|null Thumbnail path or null on failure + */ + public function generate_thumbnail($filepath, $safe_id, $extension) + { + // Use WordPress image editor + $editor = wp_get_image_editor($filepath); + + if (is_wp_error($editor)) { + $this->logger->warning( + sprintf(__('Could not create image editor: %s', 'instagram-gallery-sync-pro'), $editor->get_error_message()) + ); + return null; + } + + // Get current size + $size = $editor->get_size(); + + // Calculate thumbnail size (medium by default) + $thumb_size = $this->thumbnail_sizes['medium']; + + // Resize while maintaining aspect ratio + $editor->resize($thumb_size, $thumb_size, true); // true = crop + + // Optimize quality + $editor->set_quality(85); + + // Save thumbnail + $thumb_dir = $this->upload_path . '/thumbnails'; + + if (!file_exists($thumb_dir)) { + wp_mkdir_p($thumb_dir); + } + + $thumb_filename = $safe_id . '-thumb.' . $extension; + $thumb_path = $thumb_dir . '/' . $thumb_filename; + + $saved = $editor->save($thumb_path); + + if (is_wp_error($saved)) { + $this->logger->warning( + sprintf(__('Failed to save thumbnail: %s', 'instagram-gallery-sync-pro'), $saved->get_error_message()) + ); + return null; + } + + return IGSP_UPLOAD_DIR . '/thumbnails/' . $thumb_filename; + } + + /** + * Convert image to WebP (optional) + * + * @param string $filepath Original image path + * @return string|null WebP path or null + */ + public function convert_to_webp($filepath) + { + if (!function_exists('imagewebp')) { + return null; + } + + $image_info = getimagesize($filepath); + + if (!$image_info) { + return null; + } + + $mime = $image_info['mime']; + + // Create image resource based on type + switch ($mime) { + case 'image/jpeg': + $image = imagecreatefromjpeg($filepath); + break; + case 'image/png': + $image = imagecreatefrompng($filepath); + break; + case 'image/gif': + $image = imagecreatefromgif($filepath); + break; + default: + return null; + } + + if (!$image) { + return null; + } + + // Generate WebP path + $webp_path = preg_replace('/\.(jpe?g|png|gif)$/i', '.webp', $filepath); + + // Save as WebP + $success = imagewebp($image, $webp_path, 85); + imagedestroy($image); + + if (!$success) { + return null; + } + + return str_replace($this->upload_path, IGSP_UPLOAD_DIR, $webp_path); + } + + /** + * Strip EXIF data from image + * + * @param string $filepath Image path + * @return bool + */ + public function strip_exif($filepath) + { + $image_info = getimagesize($filepath); + + if (!$image_info || $image_info['mime'] !== 'image/jpeg') { + return false; + } + + $image = imagecreatefromjpeg($filepath); + + if (!$image) { + return false; + } + + // Re-save without EXIF + $result = imagejpeg($image, $filepath, 90); + imagedestroy($image); + + return $result; + } + + /** + * Delete image files for a post + * + * @param string $local_path Main image path + * @param string $thumbnail_path Thumbnail path + * @return bool + */ + public function delete_images($local_path, $thumbnail_path = '') + { + $upload_dir = wp_upload_dir(); + $base_path = $upload_dir['basedir']; + + $deleted = false; + + if (!empty($local_path)) { + $full_path = $base_path . '/' . $local_path; + if (file_exists($full_path) && strpos($full_path, IGSP_UPLOAD_DIR) !== false) { + $deleted = unlink($full_path); + } + } + + if (!empty($thumbnail_path)) { + $thumb_full_path = $base_path . '/' . $thumbnail_path; + if (file_exists($thumb_full_path) && strpos($thumb_full_path, IGSP_UPLOAD_DIR) !== false) { + unlink($thumb_full_path); + } + } + + return $deleted; + } + + /** + * Get image URL from local path + * + * @param string $local_path Local path + * @return string + */ + public function get_image_url($local_path) + { + if (empty($local_path)) { + return ''; + } + + $upload_dir = wp_upload_dir(); + return $upload_dir['baseurl'] . '/' . $local_path; + } + + /** + * Check if image exists locally + * + * @param string $local_path Local path + * @return bool + */ + public function image_exists($local_path) + { + if (empty($local_path)) { + return false; + } + + $upload_dir = wp_upload_dir(); + $full_path = $upload_dir['basedir'] . '/' . $local_path; + + return file_exists($full_path); + } + + /** + * Get total size of all stored images + * + * @return int Size in bytes + */ + public function get_total_storage_used() + { + $total = 0; + + if (!is_dir($this->upload_path)) { + return $total; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($this->upload_path, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + $total += $file->getSize(); + } + + return $total; + } + + /** + * Get human-readable storage size + * + * @return string + */ + public function get_storage_used_formatted() + { + $bytes = $this->get_total_storage_used(); + return size_format($bytes); + } + + /** + * Clear all stored images + * + * @return bool + */ + public function clear_all_images() + { + if (!is_dir($this->upload_path)) { + return true; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($this->upload_path, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $item) { + if ($item->isDir()) { + rmdir($item->getRealPath()); + } else { + // Don't delete index.php and .htaccess + $filename = $item->getFilename(); + if ($filename !== 'index.php' && $filename !== '.htaccess') { + unlink($item->getRealPath()); + } + } + } + + // Recreate thumbnails directory + wp_mkdir_p($this->upload_path . '/thumbnails'); + + return true; + } +} diff --git a/includes/class-logger.php b/includes/class-logger.php new file mode 100644 index 0000000..5ac87ee --- /dev/null +++ b/includes/class-logger.php @@ -0,0 +1,331 @@ +insert( + IGSP_TABLE_LOG, + array( + 'log_type' => $type, + 'message' => sanitize_text_field($message), + 'details' => !empty($details) ? wp_json_encode($details) : null, + 'created_at' => current_time('mysql'), + ), + array('%s', '%s', '%s', '%s') + ); + + // Also log to error_log if debug is enabled + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log(sprintf( + '[IGSP %s] %s %s', + strtoupper($type), + $message, + !empty($details) ? wp_json_encode($details) : '' + )); + } + + return $result ? $wpdb->insert_id : false; + } + + /** + * Log success + * + * @param string $message Message + * @param array $details Details + * @return int|false + */ + public function success($message, $details = array()) + { + return $this->log(self::TYPE_SUCCESS, $message, $details); + } + + /** + * Log error + * + * @param string $message Message + * @param array $details Details + * @return int|false + */ + public function error($message, $details = array()) + { + return $this->log(self::TYPE_ERROR, $message, $details); + } + + /** + * Log warning + * + * @param string $message Message + * @param array $details Details + * @return int|false + */ + public function warning($message, $details = array()) + { + return $this->log(self::TYPE_WARNING, $message, $details); + } + + /** + * Log info + * + * @param string $message Message + * @param array $details Details + * @return int|false + */ + public function info($message, $details = array()) + { + return $this->log(self::TYPE_INFO, $message, $details); + } + + /** + * Get log entries + * + * @param array $args Query arguments + * @return array + */ + public function get_logs($args = array()) + { + global $wpdb; + + $defaults = array( + 'limit' => 50, + 'offset' => 0, + 'type' => '', + 'search' => '', + ); + + $args = wp_parse_args($args, $defaults); + + $where = array('1=1'); + $values = array(); + + if (!empty($args['type'])) { + $where[] = 'log_type = %s'; + $values[] = $args['type']; + } + + if (!empty($args['search'])) { + $where[] = 'message LIKE %s'; + $values[] = '%' . $wpdb->esc_like($args['search']) . '%'; + } + + $where_clause = implode(' AND ', $where); + + $query = "SELECT * FROM " . IGSP_TABLE_LOG . " + WHERE {$where_clause} + ORDER BY created_at DESC + LIMIT %d OFFSET %d"; + + $values[] = $args['limit']; + $values[] = $args['offset']; + + return $wpdb->get_results( + $wpdb->prepare($query, $values) + ); + } + + /** + * Count log entries + * + * @param string $type Optional log type filter + * @return int + */ + public function count_logs($type = '') + { + global $wpdb; + + if (!empty($type)) { + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM " . IGSP_TABLE_LOG . " WHERE log_type = %s", + $type + ) + ); + } + + return (int) $wpdb->get_var( + "SELECT COUNT(*) FROM " . IGSP_TABLE_LOG + ); + } + + /** + * Clear all logs + * + * @return bool + */ + public function clear_logs() + { + global $wpdb; + + return $wpdb->query("TRUNCATE TABLE " . IGSP_TABLE_LOG) !== false; + } + + /** + * Cleanup old log entries + * + * @return int Number of deleted entries + */ + public function cleanup_old_logs() + { + global $wpdb; + + // Keep only the most recent entries + return $wpdb->query( + $wpdb->prepare( + "DELETE FROM " . IGSP_TABLE_LOG . " + WHERE id NOT IN ( + SELECT id FROM ( + SELECT id FROM " . IGSP_TABLE_LOG . " + ORDER BY created_at DESC + LIMIT %d + ) AS t + )", + $this->max_entries + ) + ); + } + + /** + * Delete logs older than X days + * + * @param int $days Days to keep + * @return int + */ + public function delete_logs_older_than($days) + { + global $wpdb; + + $date_threshold = date('Y-m-d H:i:s', strtotime("-{$days} days")); + + return $wpdb->query( + $wpdb->prepare( + "DELETE FROM " . IGSP_TABLE_LOG . " WHERE created_at < %s", + $date_threshold + ) + ); + } + + /** + * Get logs formatted for display + * + * @param int $limit Number of entries + * @return array + */ + public function get_formatted_logs($limit = 50) + { + $logs = $this->get_logs(array('limit' => $limit)); + $formatted = array(); + + foreach ($logs as $log) { + $formatted[] = array( + 'id' => $log->id, + 'type' => $log->log_type, + 'type_label' => $this->get_type_label($log->log_type), + 'type_class' => $this->get_type_class($log->log_type), + 'message' => $log->message, + 'details' => !empty($log->details) ? json_decode($log->details, true) : array(), + 'date' => mysql2date(get_option('date_format') . ' ' . get_option('time_format'), $log->created_at), + 'relative' => human_time_diff(strtotime($log->created_at), current_time('timestamp')) . ' ' . __('ago', 'instagram-gallery-sync-pro'), + ); + } + + return $formatted; + } + + /** + * Get type label + * + * @param string $type Log type + * @return string + */ + private function get_type_label($type) + { + $labels = array( + self::TYPE_SUCCESS => __('Success', 'instagram-gallery-sync-pro'), + self::TYPE_ERROR => __('Error', 'instagram-gallery-sync-pro'), + self::TYPE_WARNING => __('Warning', 'instagram-gallery-sync-pro'), + self::TYPE_INFO => __('Info', 'instagram-gallery-sync-pro'), + ); + + return isset($labels[$type]) ? $labels[$type] : $type; + } + + /** + * Get type CSS class + * + * @param string $type Log type + * @return string + */ + private function get_type_class($type) + { + $classes = array( + self::TYPE_SUCCESS => 'igsp-log-success', + self::TYPE_ERROR => 'igsp-log-error', + self::TYPE_WARNING => 'igsp-log-warning', + self::TYPE_INFO => 'igsp-log-info', + ); + + return isset($classes[$type]) ? $classes[$type] : 'igsp-log-info'; + } +} diff --git a/includes/class-scraper.php b/includes/class-scraper.php new file mode 100644 index 0000000..39f1d49 --- /dev/null +++ b/includes/class-scraper.php @@ -0,0 +1,938 @@ +timeout = (int) get_option('igsp_request_timeout', 30); + $this->logger = new IGSP_Logger(); + } + + /** + * Fetch Instagram profile data + * + * @param string $username Instagram username + * @return array|WP_Error Array of posts or WP_Error on failure + */ + public function fetch_profile_data($username) + { + $username = sanitize_text_field($username); + + if (empty($username)) { + return new WP_Error('no_username', __('Username not provided.', 'instagram-gallery-sync-pro')); + } + + // Clean username (remove @ if present) + $username = ltrim($username, '@'); + + $this->logger->info(sprintf(__('Starting fetch for username: %s', 'instagram-gallery-sync-pro'), $username)); + + // Try multiple methods in order + $methods = array( + 'fetch_via_web_profile_info', + 'fetch_via_embed_page', + 'fetch_via_profile_page', + ); + + $last_error = null; + + foreach ($methods as $method) { + $this->random_delay(); + + $result = $this->$method($username); + + if (!is_wp_error($result) && !empty($result)) { + $this->logger->success( + sprintf(__('Successfully fetched %d posts using %s', 'instagram-gallery-sync-pro'), count($result), $method), + array('method' => $method, 'count' => count($result)) + ); + return $result; + } + + if (is_wp_error($result)) { + $last_error = $result; + $this->logger->warning( + sprintf(__('Method %s failed: %s', 'instagram-gallery-sync-pro'), $method, $result->get_error_message()), + array('method' => $method) + ); + } + } + + // All methods failed + $error = $last_error ?? new WP_Error('fetch_failed', __('All scraping methods failed.', 'instagram-gallery-sync-pro')); + $this->logger->error(__('Failed to fetch Instagram data after trying all methods.', 'instagram-gallery-sync-pro')); + + return $error; + } + + /** + * Fetch via web profile info API + * This uses the internal API that Instagram's web app uses + * + * @param string $username Instagram username + * @return array|WP_Error + */ + private function fetch_via_web_profile_info($username) + { + // First, we need to get the app id and other required headers from the main page + $main_url = 'https://www.instagram.com/'; + $main_response = $this->make_request($main_url); + + if (is_wp_error($main_response)) { + return $main_response; + } + + $main_body = wp_remote_retrieve_body($main_response); + + // Extract the app ID + $app_id = $this->extract_app_id($main_body); + + $this->random_delay(); + + // Now fetch the profile using the web profile info endpoint + $url = 'https://www.instagram.com/api/v1/users/web_profile_info/?username=' . urlencode($username); + + $headers = array( + 'X-IG-App-ID' => $app_id ?: '936619743392459', + 'X-ASBD-ID' => '129477', + 'X-IG-WWW-Claim' => '0', + 'X-Requested-With' => 'XMLHttpRequest', + 'Referer' => 'https://www.instagram.com/' . $username . '/', + 'Accept' => '*/*', + ); + + $response = $this->make_request($url, array('headers' => $headers)); + + if (is_wp_error($response)) { + return $response; + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + return new WP_Error('json_error', __('Invalid JSON response from web profile info.', 'instagram-gallery-sync-pro')); + } + + // Check for user data + if (empty($data['data']['user'])) { + return new WP_Error('no_user', __('No user data found.', 'instagram-gallery-sync-pro')); + } + + $user = $data['data']['user']; + + // Check if profile is private + if (!empty($user['is_private'])) { + return new WP_Error('private_profile', __('This Instagram profile is private.', 'instagram-gallery-sync-pro')); + } + + // Get media + $edges = $user['edge_owner_to_timeline_media']['edges'] ?? array(); + + if (empty($edges)) { + return new WP_Error('no_media', __('No media found on this profile.', 'instagram-gallery-sync-pro')); + } + + return $this->parse_graphql_edges($edges, $username); + } + + /** + * Fetch via embed page (iframeable content) + * + * @param string $username Instagram username + * @return array|WP_Error + */ + private function fetch_via_embed_page($username) + { + // First get the profile to find some post shortcodes + $profile_url = 'https://www.instagram.com/' . $username . '/'; + $response = $this->make_request($profile_url); + + if (is_wp_error($response)) { + return $response; + } + + $body = wp_remote_retrieve_body($response); + + // Try to extract shortcodes from the HTML + $shortcodes = $this->extract_shortcodes($body); + + if (empty($shortcodes)) { + return new WP_Error('no_shortcodes', __('Could not find any post links.', 'instagram-gallery-sync-pro')); + } + + $posts = array(); + $max_images = min((int) get_option('igsp_max_images', 12), count($shortcodes)); + + // Fetch each post via embed + for ($i = 0; $i < $max_images; $i++) { + $this->random_delay(1, 2); + + $embed_url = 'https://www.instagram.com/p/' . $shortcodes[$i] . '/embed/'; + $embed_response = $this->make_request($embed_url); + + if (is_wp_error($embed_response)) { + continue; + } + + $embed_body = wp_remote_retrieve_body($embed_response); + $post_data = $this->parse_embed_page($embed_body, $shortcodes[$i], $username); + + if ($post_data) { + $posts[] = $post_data; + } + } + + if (empty($posts)) { + return new WP_Error('no_posts', __('Could not extract any posts from embeds.', 'instagram-gallery-sync-pro')); + } + + return $posts; + } + + /** + * Fetch via profile page HTML parsing (fallback) + * + * @param string $username Instagram username + * @return array|WP_Error + */ + private function fetch_via_profile_page($username) + { + $url = 'https://www.instagram.com/' . $username . '/'; + + $response = $this->make_request($url); + + if (is_wp_error($response)) { + return $response; + } + + $body = wp_remote_retrieve_body($response); + + if (empty($body)) { + return new WP_Error('empty_response', __('Empty response from Instagram.', 'instagram-gallery-sync-pro')); + } + + // Try multiple parsing strategies + $posts = $this->parse_require_js_data($body, $username); + + if (!empty($posts)) { + return $posts; + } + + $posts = $this->parse_shared_data($body, $username); + + if (!empty($posts)) { + return $posts; + } + + $posts = $this->parse_preloaded_data($body, $username); + + if (!empty($posts)) { + return $posts; + } + + $posts = $this->parse_og_images($body, $username); + + if (!empty($posts)) { + return $posts; + } + + return new WP_Error('parse_failed', __('Could not parse Instagram profile page.', 'instagram-gallery-sync-pro')); + } + + /** + * Extract app ID from Instagram main page + * + * @param string $html HTML content + * @return string|null + */ + private function extract_app_id($html) + { + // Look for the app ID in various places + $patterns = array( + '/"X-IG-App-ID":"(\d+)"/', + '/appId":"(\d+)"/', + '/APP_ID":"(\d+)"/', + '/{"APP_ID":"(\d+)"/', + ); + + foreach ($patterns as $pattern) { + if (preg_match($pattern, $html, $matches)) { + return $matches[1]; + } + } + + return null; + } + + /** + * Extract shortcodes from profile HTML + * + * @param string $html HTML content + * @return array + */ + private function extract_shortcodes($html) + { + $shortcodes = array(); + + // Pattern to find post links + $patterns = array( + '/\/p\/([A-Za-z0-9_-]+)\//', + '/\/reel\/([A-Za-z0-9_-]+)\//', + '/"shortcode":"([A-Za-z0-9_-]+)"/', + '/"code":"([A-Za-z0-9_-]+)"/', + ); + + foreach ($patterns as $pattern) { + if (preg_match_all($pattern, $html, $matches)) { + $shortcodes = array_merge($shortcodes, $matches[1]); + } + } + + // Remove duplicates and return + return array_unique($shortcodes); + } + + /** + * Parse embed page HTML + * + * @param string $html HTML content + * @param string $shortcode Post shortcode + * @param string $username Username + * @return array|null + */ + private function parse_embed_page($html, $shortcode, $username) + { + // Look for image URL in embed page + $image_url = null; + $caption = ''; + + // Try to find the main image + $img_patterns = array( + '/]+class="[^"]*EmbeddedMediaImage[^"]*"[^>]+src="([^"]+)"/', + '/]+src="([^"]+instagram[^"]+)"[^>]+class="[^"]*Image/', + '/property="og:image"[^>]+content="([^"]+)"/', + '/content="([^"]+)"[^>]+property="og:image"/', + '/]+srcset="([^"]+)"/', + ); + + foreach ($img_patterns as $pattern) { + if (preg_match($pattern, $html, $matches)) { + $image_url = $matches[1]; + // Handle srcset - get the first URL + if (strpos($image_url, ' ') !== false) { + $image_url = explode(' ', $image_url)[0]; + } + break; + } + } + + // Try to extract from JSON in the page + if (empty($image_url)) { + if (preg_match('/"display_url":"([^"]+)"/', $html, $matches)) { + $image_url = stripcslashes($matches[1]); + } + } + + if (empty($image_url)) { + return null; + } + + // Extract caption + if (preg_match('/
]*>.*?]*>(.+?)<\/span>/s', $html, $matches)) { + $caption = strip_tags($matches[1]); + } + + // Try to get timestamp + $timestamp = null; + if (preg_match('/datetime="([^"]+)"/', $html, $matches)) { + $timestamp = strtotime($matches[1]); + } + + return array( + 'instagram_id' => md5($shortcode . $username), + 'shortcode' => $shortcode, + 'username' => $username, + 'image_url' => $image_url, + 'thumbnail_url' => $image_url, + 'post_url' => 'https://www.instagram.com/p/' . $shortcode . '/', + 'caption' => $caption, + 'likes_count' => null, + 'comments_count' => null, + 'posted_at' => $timestamp ? date('Y-m-d H:i:s', $timestamp) : current_time('mysql'), + 'image_width' => 0, + 'image_height' => 0, + 'is_video' => false, + ); + } + + /** + * Parse require.js data from modern Instagram + * + * @param string $html HTML content + * @param string $username Username + * @return array + */ + private function parse_require_js_data($html, $username) + { + // Modern Instagram uses require.js with data in script tags + $pattern = '/requireLazy\(\["JSScheduler"[^}]+},\s*function\(\)[^{]*{\s*"use strict";\s*(.+?)\s*}\s*\)/s'; + + // Try to find JSON data in script tags + $json_patterns = array( + '/"xdt_api__v1__feed__user_timeline_graphql_connection":\s*({.+?})\s*,\s*"extensions"/', + '/"edge_owner_to_timeline_media":\s*({.+?})\s*,\s*"edge_/', + '/{"xdt_api__v1__users__web_profile_info".*?"user":\s*({.+?})\s*}\s*}/', + ); + + foreach ($json_patterns as $pattern) { + if (preg_match($pattern, $html, $matches)) { + $data = json_decode($matches[1], true); + if (json_last_error() === JSON_ERROR_NONE && !empty($data)) { + $edges = $data['edges'] ?? array(); + if (!empty($edges)) { + return $this->parse_graphql_edges($edges, $username); + } + } + } + } + + return array(); + } + + /** + * Parse preloaded data from script tags + * + * @param string $html HTML content + * @param string $username Username + * @return array + */ + private function parse_preloaded_data($html, $username) + { + // Look for preloaded data + $patterns = array( + '/window\.__additionalDataLoaded\s*\(\s*[\'"][^\'"]+[\'"]\s*,\s*({.+?})\s*\)\s*;/s', + '/window\._sharedData\s*=\s*({.+?});/s', + '/