Compare commits

..

No commits in common. "main" and "v1.1.1" have entirely different histories.
main ... v1.1.1

26 changed files with 92 additions and 3491 deletions

View file

@ -1,76 +1,17 @@
# Copilot Instructions for WP GitHub Updater Package # Copilot Instructions for WP GitHub Updater Package


## 📋 Documentation Policy

**CRITICAL: ALL documentation must be consolidated in these three files ONLY:**

1. **README.md** - User-facing documentation, installation, usage examples, API reference
2. **CHANGELOG.md** - Version history, changes, bug fixes, new features
3. **.github/copilot-instructions.md** - This file, development guidelines and architecture

**🚫 NEVER create separate documentation files:**
- ❌ Do NOT create files like `docs/GUIDE.md`, `docs/TESTING.md`, `TROUBLESHOOTING.md`, etc.
- ❌ Do NOT create a `docs/` directory
- ❌ Do NOT split documentation into multiple MD files

**✅ Instead:**
- Add user documentation to appropriate sections in README.md
- Add development/architecture notes to this copilot-instructions.md file
- Add version changes to CHANGELOG.md
- Use inline code comments for complex logic explanation

**Rationale:** Centralized documentation is easier to maintain, search, and keep in sync with code changes.

---

## Architecture Overview ## Architecture Overview


This is a **reusable Composer package** that provides WordPress plugin update functionality from GitHub releases. The package is designed to be integrated into various WordPress plugins, not as a standalone plugin. This is a **reusable Composer package** that provides WordPress plugin update functionality from GitHub releases. The package is designed to be integrated into various WordPress plugins, not as a standalone plugin.


## Core Technologies ## Core Technologies


- **PHP 8.2+**: Modern PHP with strict typing, union types, and nullable parameters - **PHP 8.0+**: Modern PHP with strict typing, union types, and nullable parameters
- **WordPress 6.0+**: WordPress update system integration via hooks and filters - **WordPress 6.0+**: WordPress update system integration via hooks and filters
- **PSR-4 Autoloading**: Namespace-based class loading for better organization - **PSR-4 Autoloading**: Namespace-based class loading for better organization
- **GitHub API v3**: REST API integration for release management - **GitHub API v3**: REST API integration for release management
- **Composer Package**: Distributed via Packagist as `silverassist/wp-github-updater` - **Composer Package**: Distributed via Packagist as `silverassist/wp-github-updater`


## 🔒 PHPUnit Version Policy - CRITICAL

**MANDATORY: PHPUnit MUST remain at version 9.6.x**

### Rationale
- **WordPress Ecosystem Standard**: PHPUnit 9.6 is the most widely used version across WordPress projects
- **WordPress Coding Standards**: The official [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards) uses PHPUnit 9.x
- **Yoast PHPUnit Polyfills**: Version 4.x supports PHPUnit 7.5-9.x, 11.x, and 12.x, but **NOT 10.x**
- **Consumer Compatibility**: Most projects depending on this package use PHPUnit 9.6

### Version Constraints
```json
{
"require-dev": {
"phpunit/phpunit": "^9.6",
"yoast/phpunit-polyfills": "^4.0"
}
}
```

### What NOT to do
- ❌ Do NOT upgrade PHPUnit to version 10.x (not supported by Yoast PHPUnit Polyfills)
- ❌ Do NOT upgrade to PHPUnit 11.x or 12.x (breaks compatibility with most WordPress projects)
- ❌ Do NOT accept Dependabot PRs that upgrade PHPUnit beyond 9.x

### Configuration Files
- **dependabot.yml**: Configured to ignore PHPUnit major version updates
- **composer.json**: Locked to `^9.6` version constraint
- **phpunit.xml**: Uses PHPUnit 9.6 schema reference

### When to Reconsider
Only upgrade PHPUnit when:
1. WordPress Coding Standards officially adopts a newer version
2. Majority of WordPress ecosystem projects have migrated
3. All dependent projects confirm compatibility

## Project Structure ## Project Structure


``` ```
@ -103,7 +44,6 @@ wp-github-updater/
- Cache duration and WordPress requirements - Cache duration and WordPress requirements
- AJAX endpoints and nonce configuration - AJAX endpoints and nonce configuration
- Plugin data parsing from WordPress headers - Plugin data parsing from WordPress headers
- **Custom temporary directory configuration** (v1.1.3+)


### Updater Class ### Updater Class
**Purpose**: Core WordPress integration and GitHub API communication **Purpose**: Core WordPress integration and GitHub API communication
@ -112,11 +52,8 @@ wp-github-updater/
- GitHub API communication with caching - GitHub API communication with caching
- Plugin update checking and information display - Plugin update checking and information display
- Download error handling and recovery - Download error handling and recovery
- AJAX endpoints for manual update checks and notice dismissal - AJAX endpoints for manual update checks
- Markdown-to-HTML conversion for changelogs - Markdown-to-HTML conversion for changelogs
- **Enhanced temporary file management with PCLZIP error resolution** (v1.1.3+)
- **WordPress admin notices for manual update checks** (v1.1.3+)
- **Dismissible admin notices with AJAX handling** (v1.1.4+)


### Security ### Security
- **Nonce verification** for AJAX requests - **Nonce verification** for AJAX requests
@ -153,34 +90,6 @@ class UpdaterConfig {
\__("Update available", "wp-github-updater"); \__("Update available", "wp-github-updater");
``` ```


### Temporary File Management Strategy
**New in v1.1.3**: Enhanced handling to resolve `PCLZIP_ERR_MISSING_FILE (-4)` errors

#### Multi-Tier Fallback System
The package implements a robust 6-tier fallback system for temporary file creation:

1. **Custom temporary directory** - User-specified via `custom_temp_dir` option
2. **WordPress uploads directory** - Most reliable, within web root
3. **WP_CONTENT_DIR/temp** - Auto-created if needed
4. **WP_TEMP_DIR** - If defined in wp-config.php
5. **System temporary directory** - OS default /tmp or equivalent
6. **Manual file creation** - Last resort fallback

#### Configuration Options
```php
$config = new UpdaterConfig($pluginFile, $githubRepo, [
"custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Custom directory
// or
"custom_temp_dir" => wp_upload_dir()["basedir"] . "/temp", // Uploads subdirectory
]);
```

#### WordPress Configuration Alternative
```php
// In wp-config.php
define('WP_TEMP_DIR', ABSPATH . 'wp-content/temp');
```

### Coding Standards ### Coding Standards


- **WordPress Coding Standards**: Full compliance with WordPress PHP standards - **WordPress Coding Standards**: Full compliance with WordPress PHP standards
@ -228,41 +137,13 @@ class Updater {
} }


public function checkForUpdate($transient) { public function checkForUpdate($transient) {
// WordPress update check integration using isUpdateAvailable() method // WordPress update check integration
} }


public function pluginInfo($result, string $action, object $args) { public function pluginInfo($result, string $action, object $args) {
// Plugin information display // Plugin information display
} }


public function manualVersionCheck(): void {
// AJAX handler for manual version checks with admin notice creation
}

public function dismissUpdateNotice(): void {
// AJAX handler for dismissing admin notices (v1.1.4+)
}

public function showUpdateNotice(): void {
// WordPress admin notice display for available updates (v1.1.4+)
}

public function maybeFixDownload($result, string $package, object $upgrader, array $hook_extra) {
// Enhanced download handling with PCLZIP error resolution (v1.1.3+)
}

private function createSecureTempFile(string $package) {
// Multi-tier temporary file creation with fallback strategies (v1.1.3+)
}

public function isUpdateAvailable(): bool {
// Centralized update availability check (refactored in v1.1.4+)
}

public function getLatestVersion(): string|false {
// Public API method for external access (v1.1.2+)
}

// ... other methods // ... other methods
} }
``` ```
@ -396,7 +277,6 @@ $config = new UpdaterConfig([
"github_username" => "username", "github_username" => "username",
"github_repo" => "repository", "github_repo" => "repository",
"text_domain" => "my-plugin-domain", // Consumer's text domain "text_domain" => "my-plugin-domain", // Consumer's text domain
"custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Optional: Custom temp directory (v1.1.3+)
// ... other options // ... other options
]); ]);


@ -408,9 +288,6 @@ $updater = new Updater($config);
- Provide fallback download mechanisms for GitHub API failures - Provide fallback download mechanisms for GitHub API failures
- Cache GitHub API responses to avoid rate limiting - Cache GitHub API responses to avoid rate limiting
- Handle PCLZIP errors with alternative download methods - Handle PCLZIP errors with alternative download methods
- **PCLZIP_ERR_MISSING_FILE Resolution** (v1.1.3+): Multi-tier temporary file creation to avoid /tmp permission issues
- Cache GitHub API responses to avoid rate limiting
- Handle PCLZIP errors with alternative download methods


### Version Management ### Version Management
- Follow semantic versioning (MAJOR.MINOR.PATCH) - Follow semantic versioning (MAJOR.MINOR.PATCH)

View file

@ -1,50 +0,0 @@
version: 2
updates:
# Composer dependency updates
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "America/Mexico_City"
labels:
- "dependencies"
- "composer"
- "automated"
open-pull-requests-limit: 10
# Ignore PHPUnit major version updates
# Keep at 9.x for WordPress compatibility
ignore:
- dependency-name: "phpunit/phpunit"
update-types: ["version-update:semver-major"]
groups:
composer-updates:
patterns:
- "*"
update-types:
- "minor"
- "patch"

# GitHub Actions dependency updates
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "America/Mexico_City"
labels:
- "dependencies"
- "github-actions"
- "automated"
open-pull-requests-limit: 10

# Group all GitHub Actions updates together
groups:
github-actions-updates:
patterns:
- "*"
update-types:
- "minor"
- "patch"

View file

@ -13,48 +13,24 @@ on:


jobs: jobs:
build-and-release: build-and-release:
permissions:
contents: write
runs-on: ubuntu-latest runs-on: ubuntu-latest


steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 uses: actions/checkout@v5


- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # v2.35.5 uses: shivammathur/setup-php@v2
with: with:
php-version: "8.2" php-version: "8.1"
extensions: mbstring, xml, ctype, json, tokenizer, mysqli extensions: mbstring, xml, ctype, json, tokenizer
coverage: none coverage: none


- name: Setup MySQL
run: |
sudo systemctl start mysql.service
mysql -e "CREATE DATABASE IF NOT EXISTS wordpress_test;" -uroot -proot
mysql -e "CREATE USER IF NOT EXISTS 'wp_test'@'localhost' IDENTIFIED BY 'wp_test';" -uroot -proot
mysql -e "GRANT ALL PRIVILEGES ON wordpress_test.* TO 'wp_test'@'localhost';" -uroot -proot
mysql -e "FLUSH PRIVILEGES;" -uroot -proot
echo "✅ MySQL database configured"

- name: Install Subversion (required for WordPress Test Suite)
run: |
sudo apt-get update
sudo apt-get install -y subversion
echo "✅ Subversion installed"

- name: Install Composer dependencies - name: Install Composer dependencies
run: | run: |
composer install --optimize-autoloader --no-interaction composer install --optimize-autoloader --no-interaction
echo "✅ Composer dependencies installed successfully (including dev dependencies for testing)" echo "✅ Composer dependencies installed successfully (including dev dependencies for testing)"


- name: Install WordPress Test Suite
run: |
echo "📦 Installing WordPress Test Suite..."
chmod +x scripts/install-wp-tests.sh
bash scripts/install-wp-tests.sh wordpress_test wp_test wp_test localhost latest
echo "✅ WordPress Test Suite installed"

- name: Extract version from tag or input - name: Extract version from tag or input
id: version id: version
run: | run: |
@ -100,14 +76,14 @@ jobs:


- name: Run tests - name: Run tests
run: | run: |
echo "🧪 Running PHPUnit tests with WordPress Test Suite..." echo "🧪 Running PHPUnit tests..."
echo "Working directory: $(pwd)" echo "Working directory: $(pwd)"
echo "Checking for phpunit.xml:" echo "Checking for phpunit.xml:"
ls -la phpunit.xml || echo "phpunit.xml not found" ls -la phpunit.xml || echo "phpunit.xml not found"
echo "Checking tests directory:" echo "Checking tests directory:"
ls -la tests/ ls -la tests/
./vendor/bin/phpunit --configuration=phpunit.xml ./vendor/bin/phpunit --configuration=phpunit.xml
echo "✅ All tests passed (including WordPress integration tests)" echo "✅ All tests passed"


- name: Run code quality checks - name: Run code quality checks
run: | run: |
@ -162,7 +138,7 @@ jobs:
- **Version**: $VERSION - **Version**: $VERSION
- **Namespace**: SilverAssist\\WpGithubUpdater - **Namespace**: SilverAssist\\WpGithubUpdater
- **License**: PolyForm Noncommercial 1.0.0 - **License**: PolyForm Noncommercial 1.0.0
- **PHP Version**: 8.2+ - **PHP Version**: 8.0+
- **WordPress Version**: 6.0+ - **WordPress Version**: 6.0+


## 🚀 Installation via Composer ## 🚀 Installation via Composer
@ -201,7 +177,7 @@ jobs:
- **API Docs**: Comprehensive PHPDoc documentation - **API Docs**: Comprehensive PHPDoc documentation


## 🔧 Requirements ## 🔧 Requirements
- PHP 8.2 or higher - PHP 8.0 or higher
- WordPress 6.0 or higher - WordPress 6.0 or higher
- Composer for package management - Composer for package management
- GitHub repository with releases for updates - GitHub repository with releases for updates
@ -241,7 +217,7 @@ jobs:
\`\`\` \`\`\`


## Requirements ## Requirements
- PHP 8.2+ - PHP 8.0+
- WordPress 6.0+ - WordPress 6.0+
- Composer - Composer
- GitHub repository with releases - GitHub repository with releases
@ -282,7 +258,7 @@ jobs:
EOF EOF


- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@v1
with: with:
tag_name: ${{ steps.version.outputs.tag }} tag_name: ${{ steps.version.outputs.tag }}
name: "WP GitHub Updater v${{ steps.version.outputs.version }}" name: "WP GitHub Updater v${{ steps.version.outputs.version }}"

View file

@ -1,112 +1,5 @@
# Changelog # Changelog


## [1.2.1] - 2025-10-11

### Changed
- **PHP Version Requirement**: Lowered minimum PHP version from 8.3 to 8.2 for broader compatibility
- **Documentation Updates**: Updated all documentation, examples, and tests to reflect PHP 8.2 requirement
- **Workflow Updates**: Updated GitHub Actions workflow to reflect PHP 8.2 requirement

### Testing
- **51 Tests Passing**: All tests verified with PHP 8.2 compatibility
- **Test Fixtures Updated**: Updated all test fixtures and expectations for PHP 8.2

## [1.2.0] - 2025-10-10

### Changed
- **Documentation Consolidation**: Centralized all documentation into README.md, CHANGELOG.md, and .github/copilot-instructions.md
- **Removed Separate Documentation Files**: Eliminated `docs/` directory to maintain simpler, more maintainable documentation structure
- **Testing Documentation**: Moved comprehensive testing guide to README.md Development section
- **Troubleshooting Guide**: Integrated troubleshooting information directly into README.md

### Testing
- **51 Tests, 130 Assertions**: Complete test suite with 100% pass rate
- **Real GitHub API Integration Tests**: 9 tests making actual HTTP requests to production repositories
- **Test Coverage**: Unit tests (3), Integration tests (22), WordPress tests (26)
- **Performance Verification**: Caching performance tests confirm < 10ms for cached API calls

## [1.1.5] - 2025-10-10
### Fixed
- **PCLZIP_ERR_MISSING_FILE (-4) Resolution**: Complete rewrite of `upgrader_pre_download` filter to properly handle all download scenarios
- **Download Filter Return Values**: Fixed critical issue where filter could return invalid types causing WordPress to fail with PCLZIP errors
- **Better Plugin Detection**: Added robust verification to ensure filter only intercepts downloads for the correct plugin
- **Enhanced Error Handling**: Comprehensive error messages for all failure points in the download process
- **File Verification**: Added multiple validation checks (file size, readability, existence) before returning downloaded file to WordPress

### Changed
- **Stricter Filter Logic**: `maybeFixDownload()` now returns `false` to let WordPress handle downloads that aren't for our plugin
- **Safety Checks**: Added verification of `hook_extra` data to ensure we only process downloads for our specific plugin
- **Improved Documentation**: Enhanced PHPDoc comments explaining critical return value requirements for WordPress compatibility
- **Download Process**: Better handling of HTTP response codes and empty responses with descriptive error messages

### Technical Improvements
- **Return Type Enforcement**: Strict enforcement of `string|WP_Error|false` return types (never `true` or other types)
- **Multi-line Conditionals**: Improved code formatting to meet WordPress Coding Standards (120 character line limit)
- **Defensive Programming**: Added early returns for edge cases where previous filters have already handled the download
- **Minimum File Size Check**: Validates downloaded file is at least 100 bytes before considering it valid

## [1.1.4] - 2025-08-29
### Added
- **WordPress Admin Notices**: Integrated admin notification system that displays update availability after manual version checks
- **Dismissible Update Notices**: Users can dismiss update notifications with built-in AJAX functionality
- **Admin Notice Management**: New `showUpdateNotice()` method creates WordPress-compliant admin notices with proper styling
- **AJAX Notice Dismissal**: New `dismissUpdateNotice()` AJAX handler for seamless notice management
- **Transient-Based Notifications**: Update notices persist for the same duration as version cache (configurable via `cache_duration`)

### Changed
- **Improved Manual Version Checks**: Enhanced `manualVersionCheck()` method now sets admin notices for immediate user feedback
- **Code Refactoring**: Centralized update availability logic using `isUpdateAvailable()` method to eliminate code duplication
- **Better WordPress Integration**: Manual version checks now properly clear WordPress update transients for immediate admin interface updates
- **Enhanced User Experience**: Update checks provide both AJAX responses and persistent admin notifications

### Fixed
- **WordPress Admin Sync**: Manual version checks now immediately reflect in WordPress admin plugins page
- **Transient Cache Management**: Proper clearing of both plugin-specific and WordPress update caches
- **Admin Interface Updates**: Resolved disconnect between manual checks and WordPress admin display

### Technical Improvements
- **DRY Principle**: Replaced duplicate version comparison logic with centralized `isUpdateAvailable()` method calls
- **AJAX Security**: Enhanced nonce verification and sanitization for all AJAX endpoints
- **WordPress Standards**: All admin notices follow WordPress UI/UX guidelines with proper escaping and styling
- **JavaScript Integration**: Inline JavaScript for notice dismissal with jQuery compatibility

### Documentation
- **API Documentation**: Added comprehensive Public API Methods section to README
- **Integration Examples**: Updated all examples to demonstrate new admin notice features
- **Configuration Guide**: Enhanced advanced configuration examples with new capabilities
- **Code Examples**: Programmatic version checking examples for developers

## [1.1.3] - 2025-08-29
### Added
- **Enhanced Temporary File Handling**: Implemented multiple fallback strategies for temporary file creation to resolve `PCLZIP_ERR_MISSING_FILE (-4)` errors
- **Custom Temporary Directory Support**: New `custom_temp_dir` configuration option in UpdaterConfig for specifying alternative temporary directories
- **Automatic Directory Creation**: The updater now attempts to create temporary directories if they don't exist
- **Comprehensive File Verification**: Added file existence and readability checks after download to prevent installation failures

### Changed
- **Improved Download Reliability**: Enhanced `maybeFixDownload()` method with better error handling and multiple fallback strategies
- **Robust Temporary File Strategy**: Six-tier fallback system for temporary file creation:
1. Custom temporary directory (if configured)
2. WordPress uploads directory
3. WP_CONTENT_DIR/temp (auto-created)
4. WP_TEMP_DIR (if defined in wp-config.php)
5. System temporary directory
6. Manual file creation as last resort

### Fixed
- **PCLZIP Error Resolution**: Addresses `PCLZIP_ERR_MISSING_FILE (-4)` errors caused by restrictive /tmp directory permissions
- **File Write Verification**: Added byte-level verification to ensure complete file downloads
- **Permission Issues**: Better handling of directory permission problems during plugin updates

### Documentation
- **Integration Examples**: Added examples for handling PCLZIP errors in integration guide
- **WordPress Configuration**: Documented wp-config.php approach for setting custom temporary directories
- **Troubleshooting Guide**: Comprehensive examples for different temporary directory configuration strategies

## [1.1.2] - 2025-08-19
### Changed
- **API Accessibility**: Changed `getLatestVersion()` method visibility from `private` to `public` to allow external access from consuming plugins

## [1.1.1] - 2025-08-14 ## [1.1.1] - 2025-08-14
### Changed ### Changed
- **License Migration**: Updated from GPL v2.0+ to PolyForm Noncommercial 1.0.0 - **License Migration**: Updated from GPL v2.0+ to PolyForm Noncommercial 1.0.0

203
README.md
View file

@ -14,9 +14,6 @@ A reusable WordPress plugin updater that handles automatic updates from public G
- 🛡️ **Security**: AJAX nonce verification and capability checks - 🛡️ **Security**: AJAX nonce verification and capability checks
- 🔧 **Configurable**: Flexible configuration options - 🔧 **Configurable**: Flexible configuration options
- 📦 **Easy Integration**: Simple Composer installation - 📦 **Easy Integration**: Simple Composer installation
- 📢 **Admin Notices**: WordPress admin notifications for available updates
- 🗂️ **Enhanced File Handling**: Multi-tier temporary file creation to resolve hosting issues
- ✅ **Manual Version Checks**: AJAX-powered manual update checking with immediate admin feedback


## Installation ## Installation


@ -63,13 +60,11 @@ $config = new UpdaterConfig(
'plugin_author' => 'Your Name', 'plugin_author' => 'Your Name',
'plugin_homepage' => 'https://your-website.com', 'plugin_homepage' => 'https://your-website.com',
'requires_wordpress' => '6.0', 'requires_wordpress' => '6.0',
'requires_php' => '8.3', 'requires_php' => '8.0',
'asset_pattern' => '{slug}-v{version}.zip', // GitHub release asset filename 'asset_pattern' => '{slug}-v{version}.zip', // GitHub release asset filename
'cache_duration' => 12 * 3600, // 12 hours in seconds 'cache_duration' => 12 * 3600, // 12 hours in seconds
'ajax_action' => 'my_plugin_check_version', 'ajax_action' => 'my_plugin_check_version',
'ajax_nonce' => 'my_plugin_version_nonce', 'ajax_nonce' => 'my_plugin_version_nonce'
'text_domain' => 'my-plugin-textdomain', // For internationalization
'custom_temp_dir' => WP_CONTENT_DIR . '/temp', // Custom temporary directory (v1.1.3+)
] ]
); );


@ -85,13 +80,12 @@ $updater = new Updater($config);
| `plugin_author` | string | From plugin header | Plugin author name | | `plugin_author` | string | From plugin header | Plugin author name |
| `plugin_homepage` | string | GitHub repo URL | Plugin homepage URL | | `plugin_homepage` | string | GitHub repo URL | Plugin homepage URL |
| `requires_wordpress` | string | `'6.0'` | Minimum WordPress version | | `requires_wordpress` | string | `'6.0'` | Minimum WordPress version |
| `requires_php` | string | `'8.3'` | Minimum PHP version | | `requires_php` | string | `'8.0'` | Minimum PHP version |
| `asset_pattern` | string | `'{slug}-v{version}.zip'` | GitHub release asset filename pattern | | `asset_pattern` | string | `'{slug}-v{version}.zip'` | GitHub release asset filename pattern |
| `cache_duration` | int | `43200` (12 hours) | Cache duration in seconds | | `cache_duration` | int | `43200` (12 hours) | Cache duration in seconds |
| `ajax_action` | string | `'check_plugin_version'` | AJAX action name for manual checks | | `ajax_action` | string | `'check_plugin_version'` | AJAX action name for manual checks |
| `ajax_nonce` | string | `'plugin_version_check'` | AJAX nonce name | | `ajax_nonce` | string | `'plugin_version_check'` | AJAX nonce name |
| `text_domain` | string | `'wp-github-updater'` | WordPress text domain for i18n **(New in 1.1.0)** | | `text_domain` | string | `'wp-github-updater'` | WordPress text domain for i18n **(New in 1.1.0)** |
| `custom_temp_dir` | string\|null | `null` | Custom temporary directory path **(New in 1.1.3)** |


### Internationalization Support (i18n) ### Internationalization Support (i18n)


@ -159,7 +153,7 @@ Here's a complete example for a WordPress plugin:
* Version: 1.0.0 * Version: 1.0.0
* Author: Your Name * Author: Your Name
* Requires at least: 6.0 * Requires at least: 6.0
* Requires PHP: 8.3 * Requires PHP: 8.0
*/ */


// Prevent direct access // Prevent direct access
@ -191,174 +185,21 @@ add_action('init', function() {
// Your plugin code here... // Your plugin code here...
``` ```


## Troubleshooting

### PCLZIP_ERR_MISSING_FILE (-4) Error

If you encounter the error "The package could not be installed. PCLZIP_ERR_MISSING_FILE (-4)", this typically indicates issues with the temporary directory. The updater includes multiple fallback strategies to resolve this.

#### Solution 1: Custom Temporary Directory (Recommended)

```php
$config = new UpdaterConfig(
__FILE__,
'your-username/your-repo',
[
'custom_temp_dir' => WP_CONTENT_DIR . '/temp',
// or use uploads directory:
// 'custom_temp_dir' => wp_upload_dir()['basedir'] . '/temp',
]
);
```

#### Solution 2: WordPress Configuration

Add to your `wp-config.php` file (before "That's all, stop editing!"):

```php
/* Set WordPress temporary directory */
define('WP_TEMP_DIR', ABSPATH . 'wp-content/temp');
```

Then create the directory with proper permissions:

```bash
mkdir wp-content/temp
chmod 755 wp-content/temp
```

#### Solution 3: Plugin Activation Hook

Create the temporary directory when your plugin is activated:

```php
register_activation_hook(__FILE__, function() {
$temp_dir = WP_CONTENT_DIR . '/temp';
if (!file_exists($temp_dir)) {
wp_mkdir_p($temp_dir);
}
});
```

### Multi-Tier Fallback System

The updater automatically tries multiple strategies for temporary file creation:

1. **Custom temporary directory** (if configured)
2. **WordPress uploads directory**
3. **WP_CONTENT_DIR/temp** (auto-created)
4. **WP_TEMP_DIR** (if defined in wp-config.php)
5. **System temporary directory** (/tmp)
6. **Manual file creation** (last resort)

## Requirements ## Requirements


- PHP 8.2 or higher - PHP 8.0 or higher
- WordPress 6.0 or higher - WordPress 6.0 or higher
- Composer for dependency management - Composer for dependency management
- Public GitHub repository with releases - Public GitHub repository with releases


## Development ## Development


### Testing ### Running Tests

The package includes comprehensive testing (51 tests, 130 assertions, 100% passing):

**Test Coverage:**
- **Unit Tests** (3 tests): Configuration and core functionality
- **Integration Tests** (22 tests): Updater + Config integration, download filters, **real GitHub API** ⭐
- **WordPress Tests** (26 tests): Hooks, filters, and mock plugin integration

**Running Tests:**


```bash ```bash
# Install development dependencies
composer install --dev

# Run all tests
composer test composer test

# Run specific test suites
./scripts/test-runner.sh unit # Unit tests only
./scripts/test-runner.sh integration # Integration tests (includes real GitHub API)
./scripts/test-runner.sh wordpress # WordPress integration tests
./scripts/test-runner.sh all # All tests

# Run with coverage
vendor/bin/phpunit --coverage-text
``` ```


**Real GitHub API Testing:**

The integration tests include **real HTTP requests** to production GitHub repositories to verify actual API behavior:

- ✅ Validates actual GitHub API response structure
- ✅ Verifies caching performance (< 10ms for cached calls)
- ✅ Tests version comparison with real releases
- ✅ Confirms asset pattern matching with production URLs

**Example: Test with Your Own Repository**

```php
// tests/Integration/MyRealAPITest.php
public function testFetchLatestVersionFromMyRepo(): void {
$config = new UpdaterConfig([
"plugin_file" => __FILE__,
"github_username" => "YourUsername",
"github_repo" => "your-repository",
]);
$updater = new Updater($config);
$version = $updater->getLatestVersion();
$this->assertNotFalse($version);
$this->assertMatchesRegularExpression("/^\d+\.\d+\.\d+$/", $version);
}
```

**Test Environment Setup:**

The tests use WordPress Test Suite for authentic WordPress integration:

```bash
# Install WordPress Test Suite (interactive)
./scripts/test-runner.sh install

# Or manual installation
./scripts/install-wp-tests.sh wordpress_test root '' localhost latest
```

**PCLZIP Error Testing:**

For testing plugins that may experience `PCLZIP_ERR_MISSING_FILE (-4)` errors, configure a custom temporary directory:

```php
$config = new UpdaterConfig(
__FILE__,
"your-username/your-plugin",
[
"custom_temp_dir" => WP_CONTENT_DIR . "/temp",
]
);
```

### PHPUnit Version Policy

**This package uses PHPUnit 9.6.x and MUST remain on this version.**

**Why PHPUnit 9.6?**
- ✅ **WordPress Ecosystem Standard**: Most WordPress projects use PHPUnit 9.6
- ✅ **WordPress Coding Standards Compatible**: [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards) uses PHPUnit 9.x
- ✅ **Yoast PHPUnit Polyfills**: Version 4.x supports PHPUnit 7.5-9.x, 11.x, 12.x, but **NOT 10.x**
- ✅ **Consumer Compatibility**: Projects depending on this package expect PHPUnit 9.6

**Do NOT upgrade to:**
- ❌ PHPUnit 10.x (incompatible with Yoast PHPUnit Polyfills 4.x)
- ❌ PHPUnit 11.x or 12.x (breaks compatibility with most WordPress projects)

**Dependabot Configuration:**
The `.github/dependabot.yml` file is configured to automatically ignore PHPUnit major version updates, ensuring the package remains on 9.x.

### Code Standards ### Code Standards


```bash ```bash
@ -378,38 +219,6 @@ composer phpstan
composer check composer check
``` ```


## Public API Methods

The updater provides several public methods for programmatic access:

### Version Information

```php
// Check if an update is available
$hasUpdate = $updater->isUpdateAvailable(); // Returns bool

// Get the current plugin version
$currentVersion = $updater->getCurrentVersion(); // Returns string

// Get the latest version from GitHub (with caching)
$latestVersion = $updater->getLatestVersion(); // Returns string|false

// Get the GitHub repository
$repo = $updater->getGithubRepo(); // Returns string
```

### Manual Version Check

You can trigger a manual version check programmatically:

```php
// This will clear caches and check for updates
// If an update is available, it will set an admin notice
$updater->manualVersionCheck();
```

**Note**: The manual version check method is designed for AJAX calls and will send JSON responses. For programmatic use, prefer the individual methods above.

## Contributing ## Contributing


1. Fork the repository 1. Fork the repository

View file

@ -1,7 +1,7 @@
{ {
"name": "silverassist/wp-github-updater", "name": "silverassist/wp-github-updater",
"description": "A reusable WordPress plugin updater that handles automatic updates from public GitHub releases", "description": "A reusable WordPress plugin updater that handles automatic updates from public GitHub releases",
"version": "1.2.1", "version": "1.1.1",
"type": "library", "type": "library",
"keywords": [ "keywords": [
"wordpress", "wordpress",
@ -20,19 +20,14 @@
} }
], ],
"require": { "require": {
"php": ">=8.2" "php": ">=8.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.6", "phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "^3.7", "squizlabs/php_codesniffer": "^3.7",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"wp-coding-standards/wpcs": "^3.0", "wp-coding-standards/wpcs": "^3.0",
"slevomat/coding-standard": "^8.0", "slevomat/coding-standard": "^8.0"
"php-stubs/wordpress-stubs": "^6.8",
"php-stubs/wordpress-tests-stubs": "^6.8",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"szepeviktor/phpstan-wordpress": "^1.3",
"yoast/phpunit-polyfills": "^4.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -46,9 +41,6 @@
}, },
"scripts": { "scripts": {
"test": "phpunit", "test": "phpunit",
"test:unit": "phpunit --testsuite unit",
"test:integration": "phpunit --testsuite integration",
"test:wordpress": "phpunit --testsuite wordpress",
"phpcs": "phpcs", "phpcs": "phpcs",
"phpcbf": "phpcbf", "phpcbf": "phpcbf",
"phpstan": "phpstan analyse src/ --level=8", "phpstan": "phpstan analyse src/ --level=8",

View file

@ -30,19 +30,11 @@ add_action("init", function() {
"asset_pattern" => "silver-assist-security-v{version}.zip", "asset_pattern" => "silver-assist-security-v{version}.zip",
"ajax_action" => "silver_assist_security_check_version", "ajax_action" => "silver_assist_security_check_version",
"ajax_nonce" => "silver_assist_security_ajax", "ajax_nonce" => "silver_assist_security_ajax",
"text_domain" => "silver-assist-security", // Your plugin's text domain "text_domain" => "silver-assist-security" // Your plugin's text domain
"custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Enhanced error handling (v1.1.3+)
] ]
); );
$updater = new Updater($config); new Updater($config);
// Optional: Programmatic version checking (v1.1.2+)
if ($updater->isUpdateAvailable()) {
// Handle update availability programmatically
$latestVersion = $updater->getLatestVersion();
error_log("Security plugin update available: " . $latestVersion);
}
}); });
*/ */


@ -67,19 +59,11 @@ add_action("init", function() {
"asset_pattern" => "leadgen-app-form-v{version}.zip", "asset_pattern" => "leadgen-app-form-v{version}.zip",
"ajax_action" => "leadgen_check_version", "ajax_action" => "leadgen_check_version",
"ajax_nonce" => "leadgen_version_check", "ajax_nonce" => "leadgen_version_check",
"text_domain" => "leadgen-app-form", // Your plugin's text domain "text_domain" => "leadgen-app-form" // Your plugin's text domain
"custom_temp_dir" => wp_upload_dir()["basedir"] . "/temp", // Alternative temp dir location
] ]
); );
$updater = new Updater($config); new Updater($config);
// Optional: Add manual check button in admin
add_action("admin_init", function() use ($updater) {
if (isset($_GET["leadgen_check_update"]) && current_user_can("update_plugins")) {
$updater->manualVersionCheck();
}
});
}); });
*/ */


@ -118,76 +102,14 @@ add_action("init", function() {
"requires_wordpress" => "6.2", "requires_wordpress" => "6.2",
"ajax_action" => "my_plugin_version_check", "ajax_action" => "my_plugin_version_check",
"cache_duration" => 6 * 3600, // 6 hours "cache_duration" => 6 * 3600, // 6 hours
"text_domain" => "my-new-plugin", // Your plugin's text domain "text_domain" => "my-new-plugin" // Your plugin's text domain
"custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Improved hosting compatibility
]
);
$updater = new Updater($config);
// Example: Check for updates programmatically
add_action("admin_notices", function() use ($updater) {
if (!current_user_can("update_plugins")) return;
if ($updater->isUpdateAvailable()) {
$currentVersion = $updater->getCurrentVersion();
$latestVersion = $updater->getLatestVersion();
echo '<div class="notice notice-info">';
echo '<p>My Plugin: Update available from ' . esc_html($currentVersion) . ' to ' . esc_html($latestVersion) . '</p>';
echo '</div>';
}
});
});

// Your plugin code here...
*/

/**
* Example for plugins with PCLZIP_ERR_MISSING_FILE issues
* Use custom temporary directory to avoid /tmp permission problems
*/

/*
// For plugins experiencing PCLZIP_ERR_MISSING_FILE errors:

add_action("init", function() {
$config = new UpdaterConfig(
__FILE__,
"your-username/your-plugin",
[
"text_domain" => "your-plugin",
"custom_temp_dir" => WP_CONTENT_DIR . "/temp", // Custom temp directory
// or use uploads directory:
// "custom_temp_dir" => wp_upload_dir()["basedir"] . "/temp",
] ]
); );
new Updater($config); new Updater($config);
// Optionally create the directory on plugin activation
register_activation_hook(__FILE__, function() {
$temp_dir = WP_CONTENT_DIR . "/temp";
if (!file_exists($temp_dir)) {
wp_mkdir_p($temp_dir);
}
}); });
});
*/


/** // Your plugin code here...
* Alternative: WordPress configuration approach
* Add this to your wp-config.php file (before the line that says
* "That's all, stop editing!"):
*/

/*
// In wp-config.php, add this line:
define('WP_TEMP_DIR', ABSPATH . 'wp-content/temp');

// Then create the directory with proper permissions:
// mkdir wp-content/temp
// chmod 755 wp-content/temp
*/ */


/** /**
@ -199,22 +121,3 @@ define('WP_TEMP_DIR', ABSPATH . 'wp-content/temp');
* 4. Remove your old updater class files * 4. Remove your old updater class files
* 5. Test the updates * 5. Test the updates
*/ */

/**
* New Features in v1.1.4:
*
* - WordPress admin notices for manual version checks
* - Dismissible admin notices with AJAX functionality
* - Improved code organization with isUpdateAvailable() method
*
* New Features in v1.1.3:
*
* - Enhanced temporary file handling to resolve PCLZIP errors
* - Better error handling and hosting environment compatibility
*
* Public API methods (v1.1.2+):
* - $updater->isUpdateAvailable() - Check if update is available
* - $updater->getCurrentVersion() - Get current plugin version
* - $updater->getLatestVersion() - Get latest GitHub version
* - $updater->getGithubRepo() - Get repository name
*/

View file

@ -48,12 +48,6 @@
<rule ref="WordPress.Security.EscapeOutput"/> <rule ref="WordPress.Security.EscapeOutput"/>
<rule ref="WordPress.Security.ValidatedSanitizedInput"/> <rule ref="WordPress.Security.ValidatedSanitizedInput"/>


<!-- Exclude WordPress.Security checks from test files -->
<rule ref="WordPress.Security">
<exclude-pattern>tests/bootstrap.php</exclude-pattern>
<exclude-pattern>tests/fixtures/*</exclude-pattern>
</rule>

<!-- WordPress i18n (Text domain validation) --> <!-- WordPress i18n (Text domain validation) -->
<rule ref="WordPress.WP.I18n"> <rule ref="WordPress.WP.I18n">
<properties> <properties>

View file

@ -1,16 +1,9 @@
includes:
- vendor/szepeviktor/phpstan-wordpress/extension.neon

parameters: parameters:
level: 8 level: 8
paths: paths:
- src - src
excludePaths: excludePaths:
- tests - tests
bootstrapFiles:
- vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
scanDirectories:
- vendor/php-stubs/wordpress-stubs
ignoreErrors: ignoreErrors:
# Ignore WP_Error union type issues in some contexts # WordPress functions are not available during static analysis
- '#Cannot call method get_error_message\(\) on string\|WP_Error\|false#' - '#Call to unknown function: [\\]?[a-zA-Z_]+#'

View file

@ -1,37 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit bootstrap="tests/bootstrap.php"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="tests/bootstrap.php"
backupGlobals="false" backupGlobals="false"
colors="true" colors="true">
verbose="true">
<testsuites> <testsuites>
<testsuite name="unit"> <testsuite name="WP GitHub Updater Test Suite">
<directory>tests/Unit</directory> <directory>tests</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="wordpress">
<directory>tests/WordPress</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<coverage> <source>
<include> <include>
<directory suffix=".php">./src</directory> <directory suffix=".php">./src</directory>
</include> </include>
<report> </source>
<html outputDirectory="build/coverage"/>
<text outputFile="php://stdout" showUncoveredFiles="false"/>
</report>
</coverage>
<php>
<!-- Environment variable for WordPress Test Suite directory -->
<!-- Set this to enable WordPress integration tests -->
<!-- Example: export WP_TESTS_DIR=/tmp/wordpress-tests-lib -->
<env name="WP_TESTS_DIR" value="" force="false"/>
<!-- WordPress Core directory (auto-detected if not set) -->
<env name="WP_CORE_DIR" value="" force="false"/>
</php>
</phpunit> </phpunit>

View file

@ -1,255 +0,0 @@
#!/usr/bin/env bash
# WordPress Test Environment Installation Script
# Based on WordPress Core test suite installer
#
# This script installs WordPress and the WordPress Test Suite for running PHPUnit tests
# Usage: ./install-wp-tests.sh <db-name> <db-user> <db-pass> [db-host] [wp-version]

if [ $# -lt 3 ]; then
echo ""
echo "❌ ERROR: Missing required arguments"
echo ""
echo "Usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version]"
echo ""
echo "Arguments:"
echo " db-name Database name for tests (will be created/recreated)"
echo " db-user MySQL username"
echo " db-pass MySQL password (use '' for empty password)"
echo " db-host MySQL host (default: localhost)"
echo " wp-version WordPress version to install (default: 6.7.1)"
echo ""
echo "Example:"
echo " $0 wordpress_test root '' localhost 6.7.1"
echo ""
exit 1
fi

DB_NAME=$1
DB_USER=$2
DB_PASS=$3
DB_HOST=${4-localhost}
WP_VERSION=${5-6.7.1}
SKIP_DB_CREATE=${6-false}

TMPDIR=${TMPDIR-/tmp}
TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}

echo ""
echo "🚀 WordPress Test Suite Installer"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Installing WordPress Test Environment..."
echo ""
echo "Configuration:"
echo " Database Name: $DB_NAME"
echo " Database User: $DB_USER"
echo " Database Host: $DB_HOST"
echo " WordPress Version: $WP_VERSION"
echo " WP Tests Dir: $WP_TESTS_DIR"
echo " WP Core Dir: $WP_CORE_DIR"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

download() {
if [ `which curl` ]; then
curl -s "$1" > "$2";
elif [ `which wget` ]; then
wget -nv -O "$2" "$1"
fi
}

if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
WP_BRANCH=${WP_VERSION%\-*}
WP_TESTS_TAG="branches/$WP_BRANCH"

elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
WP_TESTS_TAG="branches/$WP_VERSION"

elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
WP_TESTS_TAG="tags/${WP_VERSION%??}"
else
WP_TESTS_TAG="tags/$WP_VERSION"
fi

elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' || $WP_VERSION == 'latest' ]]; then
WP_TESTS_TAG="trunk"

else
echo "Invalid version $WP_VERSION"
exit 1
fi

set -ex

install_wp() {

if [ -d $WP_CORE_DIR ]; then
echo "✅ WordPress core already installed at $WP_CORE_DIR"
return;
fi

echo "📥 Downloading WordPress $WP_VERSION..."
mkdir -p $WP_CORE_DIR

if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
mkdir -p $TMPDIR/wordpress-trunk
rm -rf $TMPDIR/wordpress-trunk/*
svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress
mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR
else
if [ $WP_VERSION == 'latest' ]; then
local ARCHIVE_NAME='latest'
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
local ARCHIVE_NAME="${WP_VERSION%??}"
else
local ARCHIVE_NAME=$WP_VERSION
fi
else
local ARCHIVE_NAME="wordpress-$WP_VERSION"
fi
download https://wordpress.org/wordpress-${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
fi

echo "✅ WordPress $WP_VERSION downloaded successfully"

echo "📥 Downloading mysqli drop-in..."
download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
echo "✅ mysqli drop-in installed"
}

install_test_suite() {
if [ -d $WP_TESTS_DIR ]; then
echo "✅ WordPress Test Suite already installed at $WP_TESTS_DIR"
return;
fi

echo "📥 Downloading WordPress Test Suite from SVN..."
mkdir -p $WP_TESTS_DIR

rm -rf $WP_TESTS_DIR/{includes,data}

svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data

echo "✅ WordPress Test Suite downloaded successfully"

echo "📝 Generating wp-tests-config.php..."
if [ ! -f wp-tests-config.php ]; then
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
WP_CORE_DIR=$(echo $WP_CORE_DIR | sed 's:/\+$::')
sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s:__FILE__:'$WP_TESTS_DIR/wp-tests-config.php':" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
fi
echo "✅ wp-tests-config.php generated"
}

recreate_db() {
shopt -s nocasematch
if [[ $1 =~ ^(y|yes)$ ]]
then
mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
create_db
echo "Recreated the database ($DB_NAME)."
else
echo "Skipping database recreation."
exit 1
fi
shopt -u nocasematch
}

create_db() {
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
}

install_db() {
if [ ${SKIP_DB_CREATE} = "true" ]; then
return 0
fi

# Use DB_HOST for compatibility (script historically used DB_HOSTNAME)
DB_HOSTNAME=${DB_HOST}
EXTRA=""

if ! [ -z "$DB_HOSTNAME" ] ; then
# If hostname starts with /, it's a socket path
if [[ $DB_HOSTNAME == /* ]] ; then
EXTRA=" --socket=$DB_HOSTNAME"
# If hostname contains a colon, it's host:port
elif [[ $DB_HOSTNAME == *:* ]] ; then
EXTRA=" --host=$(echo $DB_HOSTNAME | cut -d: -f1) --port=$(echo $DB_HOSTNAME | cut -d: -f2) --protocol=tcp"
# Otherwise it's just a hostname or IP - use TCP
else
EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
fi
fi

if [ -n "`mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$`" ]
then
# In CI/CD or non-interactive mode, automatically recreate database
if [ -t 0 ]; then
# Interactive mode - ask for confirmation
echo ""
echo "⚠️ DATABASE ALREADY EXISTS"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "The database '$DB_NAME' already exists in your MySQL server."
echo ""
echo "WordPress Test Suite requires a clean database installation."
echo "The existing database will be DROPPED and recreated."
echo ""
echo "⚠️ WARNING: This will DELETE all data in the '$DB_NAME' database!"
echo ""
echo "If this is a production database or contains important data,"
echo "press Ctrl+C now to cancel, or type 'N' below."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
read -p "Are you sure you want to proceed? [y/N]: " DELETE_EXISTING_DB
recreate_db $DELETE_EXISTING_DB
else
# Non-interactive mode (CI/CD) - automatically recreate
echo "🔄 Database already exists - automatically recreating for test environment..."
mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
create_db
echo "✅ Database recreated successfully"
fi
else
create_db
fi
}

case $(uname -s) in
Darwin)
ioption='-i.bak'
;;
*)
ioption='-i'
;;
esac

install_wp
install_test_suite
install_db

echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ WordPress Test Suite installed successfully!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Environment Variables:"
echo " WP_TESTS_DIR: $WP_TESTS_DIR"
echo " WP_CORE_DIR: $WP_CORE_DIR"
echo ""
echo "Add this to your phpunit.xml.dist:"
echo " <const name=\"WP_TESTS_DIR\" value=\"$WP_TESTS_DIR\"/>"
echo ""
echo "Now you can run tests with:"
echo " vendor/bin/phpunit --testdox"
echo ""

View file

@ -1,231 +0,0 @@
#!/bin/bash
# WordPress Test Suite Setup and Test Runner
# This script helps install WordPress Test Suite and run tests

set -e

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"

echo -e "${BLUE}================================${NC}"
echo -e "${BLUE}WP GitHub Updater Test Runner${NC}"
echo -e "${BLUE}================================${NC}"
echo ""

# Function to check if WordPress Test Suite is installed
check_wp_tests() {
local tests_dir="${WP_TESTS_DIR:-/tmp/wordpress-tests-lib}"
if [ -d "$tests_dir" ] && [ -f "$tests_dir/includes/functions.php" ]; then
return 0
else
return 1
fi
}

# Function to install WordPress Test Suite
install_wp_tests() {
echo -e "${YELLOW}WordPress Test Suite not found${NC}"
echo ""
echo "To run WordPress integration tests, you need to install WordPress Test Suite."
echo ""
echo -e "${GREEN}Installation command:${NC}"
echo " ./scripts/install-wp-tests.sh <db-name> <db-user> <db-pass> [db-host] [wp-version]"
echo ""
echo -e "${GREEN}Example:${NC}"
echo " ./scripts/install-wp-tests.sh wordpress_test root '' localhost 6.7.1"
echo ""
read -p "Do you want to install now? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo ""
echo "Please provide database credentials:"
read -p "Database name (default: wordpress_test): " db_name
db_name=${db_name:-wordpress_test}
read -p "Database user (default: root): " db_user
db_user=${db_user:-root}
read -sp "Database password (default: empty): " db_pass
echo
read -p "Database host (default: localhost): " db_host
db_host=${db_host:-localhost}
read -p "WordPress version (default: 6.7.1): " wp_version
wp_version=${wp_version:-6.7.1}
echo ""
echo -e "${BLUE}Installing WordPress Test Suite...${NC}"
"$PROJECT_ROOT/scripts/install-wp-tests.sh" "$db_name" "$db_user" "$db_pass" "$db_host" "$wp_version"
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}✓ WordPress Test Suite installed successfully!${NC}"
return 0
else
echo ""
echo -e "${RED}✗ Failed to install WordPress Test Suite${NC}"
return 1
fi
else
echo ""
echo "Skipping WordPress Test Suite installation."
echo "You can install it later using the command above."
return 1
fi
}

# Function to run specific test suite
run_tests() {
local suite=$1
local phpunit="$PROJECT_ROOT/vendor/bin/phpunit"
if [ ! -f "$phpunit" ]; then
echo -e "${RED}✗ PHPUnit not found. Please run: composer install${NC}"
exit 1
fi
cd "$PROJECT_ROOT"
case $suite in
unit)
echo -e "${BLUE}Running Unit Tests...${NC}"
"$phpunit" --testsuite=unit
;;
integration)
echo -e "${BLUE}Running Integration Tests...${NC}"
"$phpunit" --testsuite=integration
;;
wordpress)
echo -e "${BLUE}Running WordPress Tests...${NC}"
if check_wp_tests; then
echo -e "${GREEN}✓ WordPress Test Suite found${NC}"
else
echo -e "${YELLOW}⚠️ WordPress Test Suite not found${NC}"
echo "WordPress tests will run with mocked functions."
echo ""
fi
"$phpunit" --testsuite=wordpress
;;
all)
echo -e "${BLUE}Running All Tests...${NC}"
"$phpunit"
;;
coverage)
echo -e "${BLUE}Running Tests with Coverage...${NC}"
if command -v php -m | grep -q xdebug; then
"$phpunit" --coverage-html build/coverage --coverage-text
echo ""
echo -e "${GREEN}✓ Coverage report generated in: build/coverage/index.html${NC}"
else
echo -e "${YELLOW}⚠️ Xdebug not found. Installing PCOV...${NC}"
echo "Please install Xdebug or PCOV for code coverage:"
echo " - Xdebug: pecl install xdebug"
echo " - PCOV: pecl install pcov"
exit 1
fi
;;
*)
echo -e "${RED}Unknown test suite: $suite${NC}"
show_usage
exit 1
;;
esac
}

# Function to show test status
show_status() {
echo -e "${BLUE}Test Environment Status:${NC}"
echo ""
# PHPUnit
if [ -f "$PROJECT_ROOT/vendor/bin/phpunit" ]; then
echo -e "PHPUnit: ${GREEN}✓ Installed${NC}"
phpunit_version=$("$PROJECT_ROOT/vendor/bin/phpunit" --version | head -n 1)
echo " $phpunit_version"
else
echo -e "PHPUnit: ${RED}✗ Not installed${NC}"
echo " Run: composer install"
fi
echo ""
# WordPress Test Suite
if check_wp_tests; then
echo -e "WP Tests: ${GREEN}✓ Installed${NC}"
tests_dir="${WP_TESTS_DIR:-/tmp/wordpress-tests-lib}"
echo " Location: $tests_dir"
else
echo -e "WP Tests: ${YELLOW}⚠️ Not installed${NC}"
echo " Run: $0 install"
fi
echo ""
# Mock Plugin
mock_plugin="$PROJECT_ROOT/tests/fixtures/mock-plugin/mock-plugin.php"
if [ -f "$mock_plugin" ]; then
echo -e "Mock Plugin: ${GREEN}✓ Available${NC}"
echo " Location: tests/fixtures/mock-plugin/"
else
echo -e "Mock Plugin: ${RED}✗ Not found${NC}"
fi
echo ""
}

# Function to show usage
show_usage() {
echo "Usage: $0 [command]"
echo ""
echo "Commands:"
echo " status Show test environment status"
echo " install Install WordPress Test Suite"
echo " unit Run unit tests only"
echo " integration Run integration tests only"
echo " wordpress Run WordPress tests only"
echo " all Run all tests (default)"
echo " coverage Run tests with code coverage"
echo " help Show this help message"
echo ""
echo "Examples:"
echo " $0 status # Check environment"
echo " $0 install # Install WP Test Suite"
echo " $0 unit # Run unit tests"
echo " $0 # Run all tests"
echo ""
}

# Main script logic
case ${1:-all} in
status)
show_status
;;
install)
install_wp_tests
;;
unit|integration|wordpress|all|coverage)
run_tests "$1"
;;
help|-h|--help)
show_usage
;;
*)
echo -e "${RED}Unknown command: $1${NC}"
echo ""
show_usage
exit 1
;;
esac

exit 0

View file

@ -7,15 +7,12 @@
* *
* @package SilverAssist\WpGithubUpdater * @package SilverAssist\WpGithubUpdater
* @author Silver Assist * @author Silver Assist
* @version 1.2.1 * @version 1.1.1
* @license PolyForm-Noncommercial-1.0.0 * @license PolyForm-Noncommercial-1.0.0
*/ */


namespace SilverAssist\WpGithubUpdater; namespace SilverAssist\WpGithubUpdater;


use WP_Error;
use WP_Upgrader;

/** /**
* Main updater class that handles plugin updates from GitHub releases * Main updater class that handles plugin updates from GitHub releases
* *
@ -118,12 +115,6 @@ class Updater


// Add AJAX action for manual version check // Add AJAX action for manual version check
\add_action("wp_ajax_{$this->config->ajaxAction}", [$this, "manualVersionCheck"]); \add_action("wp_ajax_{$this->config->ajaxAction}", [$this, "manualVersionCheck"]);

// Add AJAX action for dismissing update notices
\add_action("wp_ajax_{$this->config->ajaxAction}_dismiss_notice", [$this, "dismissUpdateNotice"]);

// Add admin notice for manual version checks
\add_action("admin_notices", [$this, "showUpdateNotice"]);
} }


/** /**
@ -145,7 +136,7 @@ class Updater


$latestVersion = $this->getLatestVersion(); $latestVersion = $this->getLatestVersion();


if ($this->isUpdateAvailable()) { if ($latestVersion && version_compare($this->currentVersion, $latestVersion, "<")) {
$transient->response[$this->pluginSlug] = (object) [ $transient->response[$this->pluginSlug] = (object) [
"slug" => $this->pluginBasename, "slug" => $this->pluginBasename,
"plugin" => $this->pluginSlug, "plugin" => $this->pluginSlug,
@ -212,7 +203,7 @@ class Updater
* *
* @since 1.0.0 * @since 1.0.0
*/ */
public function getLatestVersion(): string|false private function getLatestVersion(): string|false
{ {
// Check cache first // Check cache first
$cachedVersion = \get_transient($this->versionTransient); $cachedVersion = \get_transient($this->versionTransient);
@ -393,10 +384,10 @@ class Updater
/** /**
* Clear version cache after update * Clear version cache after update
* *
* @param WP_Upgrader $upgrader WP_Upgrader instance * @param \WP_Upgrader $upgrader WP_Upgrader instance
* @param array $data Array of update data * @param array $data Array of update data
*/ */
public function clearVersionCache(WP_Upgrader $upgrader, array $data): void public function clearVersionCache(\WP_Upgrader $upgrader, array $data): void
{ {
if ($data["action"] === "update" && $data["type"] === "plugin") { if ($data["action"] === "update" && $data["type"] === "plugin") {
if (isset($data["plugins"]) && in_array($this->pluginSlug, $data["plugins"])) { if (isset($data["plugins"]) && in_array($this->pluginSlug, $data["plugins"])) {
@ -427,31 +418,14 @@ class Updater
} }


try { try {
// Clear our version cache
\delete_transient($this->versionTransient); \delete_transient($this->versionTransient);

// Clear WordPress update cache to force refresh
\delete_site_transient("update_plugins");

$latestVersion = $this->getLatestVersion(); $latestVersion = $this->getLatestVersion();
$updateAvailable = $this->isUpdateAvailable();

// If update is available, set a transient to show admin notice
if ($updateAvailable) {
\set_transient("wp_github_updater_notice_{$this->pluginBasename}", [
"plugin_name" => $this->config->pluginName,
"current_version" => $this->currentVersion,
"latest_version" => $latestVersion,
"github_repo" => $this->config->githubRepo,
], $this->config->cacheDuration); // Use same duration as version cache
}


\wp_send_json_success([ \wp_send_json_success([
"current_version" => $this->currentVersion, "current_version" => $this->currentVersion,
"latest_version" => $latestVersion ?: $this->config->__("Unknown"), "latest_version" => $latestVersion ?: $this->config->__("Unknown"),
"update_available" => $updateAvailable, "update_available" => $latestVersion && version_compare($this->currentVersion, $latestVersion, "<"),
"github_repo" => $this->config->githubRepo, "github_repo" => $this->config->githubRepo,
"notice_set" => $updateAvailable, // Indicate if notice was set
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
\wp_send_json_error([ \wp_send_json_error([
@ -461,99 +435,6 @@ class Updater
} }
} }


/**
* Handle dismissal of update notices via AJAX
*
* @since 1.1.4
* @return void
*/
public function dismissUpdateNotice(): void
{
// Check nonce
$nonce = \sanitize_text_field(\wp_unslash($_POST["nonce"] ?? ""));
if (!\wp_verify_nonce($nonce, $this->config->ajaxNonce)) {
\wp_die("Security verification failed", "Error", ["response" => 403]);
}

// Check capabilities
if (!\current_user_can("update_plugins")) {
\wp_die("Insufficient permissions", "Error", ["response" => 403]);
}

// Delete the notice transient
$noticeKey = "wp_github_updater_notice_{$this->pluginBasename}";
\delete_transient($noticeKey);

\wp_send_json_success(["message" => "Notice dismissed successfully"]);
}

/**
* Show admin notice for available updates
*
* Displays a WordPress admin notice when an update is available
* after a manual version check.
*
* @since 1.1.4
*/
public function showUpdateNotice(): void
{
// Only show on admin pages
if (!is_admin()) {
return;
}

// Check if we have a notice to show
$notice_data = \get_transient("wp_github_updater_notice_{$this->pluginBasename}");
if (!$notice_data) {
return;
}

// Only show to users who can update plugins
if (!\current_user_can("update_plugins")) {
return;
}

$plugin_name = $notice_data["plugin_name"] ?? $this->config->pluginName;
$current_version = $notice_data["current_version"] ?? $this->currentVersion;
$latest_version = $notice_data["latest_version"] ?? "Unknown";
$github_repo = $notice_data["github_repo"] ?? $this->config->githubRepo;

$updates_url = \admin_url("plugins.php?plugin_status=upgrade");
$github_url = "https://github.com/{$github_repo}/releases/latest";

echo '<div class="notice notice-warning is-dismissible" data-notice="wp-github-updater-' .
\esc_attr($this->pluginBasename) . '">';
echo '<p>';
echo '<strong>' . \esc_html($plugin_name) . '</strong> ';
echo sprintf(
\esc_html($this->config->__("has a new version available: %1\$s (you have %2\$s).")),
'<strong>' . \esc_html($latest_version) . '</strong>',
\esc_html($current_version)
);
echo '</p>';
echo '<p>';
echo '<a href="' . \esc_url($updates_url) . '" class="button button-primary">' .
\esc_html($this->config->__("View Updates")) . '</a> ';
echo '<a href="' . \esc_url($github_url) . '" class="button" target="_blank">' .
\esc_html($this->config->__("View Release Notes")) . '</a>';
echo '</p>';
echo '</div>';

// Add JavaScript to handle dismissal
echo '<script>
jQuery(document).ready(function($) {
$(document).on("click", "[data-notice=\"wp-github-updater-' .
\esc_js($this->pluginBasename) . '\"] .notice-dismiss", function() {
$.post(ajaxurl, {
action: "' . \esc_js($this->config->ajaxAction) . '_dismiss_notice",
nonce: "' . \esc_js(\wp_create_nonce($this->config->ajaxNonce)) . '",
plugin: "' . \esc_js($this->pluginBasename) . '"
});
});
});
</script>';
}

/** /**
* Get plugin data from file * Get plugin data from file
*/ */
@ -650,61 +531,24 @@ class Updater
/** /**
* Maybe fix download issues by providing better HTTP args * Maybe fix download issues by providing better HTTP args
* *
* This filter intercepts the download process for our GitHub releases * @return boolean|\WP_Error $result
* to ensure proper temporary file handling and avoid PCLZIP errors.
*
* CRITICAL: This filter MUST return one of:
* - false (or WP_Error): Let WordPress handle the download normally
* - string: Path to an already-downloaded file for WordPress to use
* - NEVER return true or any other type!
*
* @param boolean|WP_Error $result The result from previous filters
* @param string $package The package URL being downloaded
* @param object $upgrader The WP_Upgrader instance
* @param array $hook_extra Extra hook data
* @return string|WP_Error|false Path to downloaded file, WP_Error on failure, or false to continue
* *
* @since 1.1.0 * @since 1.1.0
*/ */
public function maybeFixDownload( public function maybeFixDownload(
bool|WP_Error $result, bool|\WP_Error $result,
string $package, string $package,
object $upgrader, object $upgrader,
array $hook_extra array $hook_extra
): string|WP_Error|false { ): bool|\WP_Error {
// If a previous filter already handled this, respect that decision // Only handle GitHub downloads for our plugin
if (\is_wp_error($result)) { if (!str_contains($package, "github.com") || !str_contains($package, $this->config->githubRepo)) {
return $result; return $result;
} }


// Only handle GitHub downloads for our specific repository // Use wp_remote_get with better parameters
if (
empty($package) ||
!str_contains($package, "github.com") ||
!str_contains($package, $this->config->githubRepo)
) {
return false; // Let WordPress handle it normally
}

// Additional safety check: ensure this is actually for our plugin
$is_our_plugin = false;
if (isset($hook_extra["plugin"]) && $hook_extra["plugin"] === $this->pluginSlug) {
$is_our_plugin = true;
} elseif (
isset($hook_extra["plugins"]) &&
is_array($hook_extra["plugins"]) &&
in_array($this->pluginSlug, $hook_extra["plugins"])
) {
$is_our_plugin = true;
}

if (!$is_our_plugin) {
return false; // Not our plugin, let WordPress handle it
}

// Download the package with optimized settings
$args = [ $args = [
"timeout" => 300, // 5 minutes for large files "timeout" => 300, // 5 minutes
"headers" => $this->getDownloadHeaders(), "headers" => $this->getDownloadHeaders(),
"sslverify" => true, "sslverify" => true,
"stream" => false, "stream" => false,
@ -714,183 +558,32 @@ class Updater
$response = \wp_remote_get($package, $args); $response = \wp_remote_get($package, $args);


if (\is_wp_error($response)) { if (\is_wp_error($response)) {
return new WP_Error( return $response;
"download_failed",
sprintf(
$this->config->__("Failed to download package: %s"),
$response->get_error_message()
)
);
} }


$response_code = \wp_remote_retrieve_response_code($response); if (200 !== \wp_remote_retrieve_response_code($response)) {
if (200 !== $response_code) { return new \WP_Error("http_404", $this->config->__("Package not found"));
return new WP_Error(
"http_error",
sprintf(
$this->config->__("Package download failed with HTTP code %d"),
$response_code
)
);
} }


// Get the response body // Write to temporary file
$body = \wp_remote_retrieve_body($response); $upload_dir = \wp_upload_dir();
if (empty($body)) { $temp_file = \wp_tempnam(basename($package), $upload_dir["basedir"] . "/");
return new WP_Error(
"empty_response", if (!$temp_file) {
$this->config->__("Downloaded package is empty") return new \WP_Error("temp_file_failed", $this->config->__("Could not create temporary file"));
);
} }


// Create temporary file with our multi-tier fallback system $file_handle = @fopen($temp_file, "w");
$temp_file = $this->createSecureTempFile($package);

if (\is_wp_error($temp_file)) {
return $temp_file;
}

// Write the downloaded content to the temporary file
$file_handle = @fopen($temp_file, "wb");
if (!$file_handle) { if (!$file_handle) {
@unlink($temp_file); // Clean up if file was created but can't be opened return new \WP_Error("file_open_failed", $this->config->__("Could not open file for writing"));
return new WP_Error(
"file_open_failed",
sprintf(
$this->config->__("Could not open temporary file for writing: %s"),
$temp_file
)
);
} }


$bytes_written = fwrite($file_handle, $body); fwrite($file_handle, \wp_remote_retrieve_body($response));
fclose($file_handle); fclose($file_handle);


// Verify write operation succeeded
if ($bytes_written === false || $bytes_written !== strlen($body)) {
@unlink($temp_file);
return new WP_Error(
"file_write_failed",
$this->config->__("Failed to write complete package to temporary file")
);
}

// Final verification: ensure file exists and is readable
if (!file_exists($temp_file)) {
return new WP_Error(
"file_missing",
$this->config->__("Temporary file disappeared after creation")
);
}

if (!is_readable($temp_file)) {
@unlink($temp_file);
return new WP_Error(
"file_not_readable",
$this->config->__("Temporary file is not readable")
);
}

// Verify it's actually a zip file
$file_size = filesize($temp_file);
if ($file_size < 100) { // Minimum size for a valid zip
@unlink($temp_file);
return new WP_Error(
"invalid_package",
sprintf(
$this->config->__("Downloaded file is too small (%d bytes) to be a valid package"),
$file_size
)
);
}

// SUCCESS: Return the path to the downloaded file
// WordPress will use this file for extraction
return $temp_file; return $temp_file;
} }


/**
* Create a secure temporary file with multiple fallback strategies
*
* Attempts different approaches to create a temporary file to avoid PCLZIP errors
* that can occur with restrictive /tmp directory permissions.
*
* @param string $package The package URL being downloaded
* @return string|WP_Error Path to temporary file or WP_Error on failure
*
* @since 1.1.4
*/
private function createSecureTempFile(string $package): string|WP_Error
{
$filename = basename(parse_url($package, PHP_URL_PATH)) ?: "github-package.zip";

// Strategy 1: Use custom temporary directory if specified
if (!empty($this->config->customTempDir)) {
if (!is_dir($this->config->customTempDir)) {
@wp_mkdir_p($this->config->customTempDir);
}

if (is_dir($this->config->customTempDir) && is_writable($this->config->customTempDir)) {
$temp_file = \wp_tempnam($filename, $this->config->customTempDir . "/");
if ($temp_file) {
return $temp_file;
}
}
}

// Strategy 2: Use WordPress uploads directory
$upload_dir = \wp_upload_dir();
if (!empty($upload_dir["basedir"]) && is_writable($upload_dir["basedir"])) {
$temp_file = \wp_tempnam($filename, $upload_dir["basedir"] . "/");
if ($temp_file) {
return $temp_file;
}
}

// Strategy 3: Use WP_CONTENT_DIR/temp if it exists or can be created
$wp_content_temp = WP_CONTENT_DIR . "/temp";
if (!is_dir($wp_content_temp)) {
@wp_mkdir_p($wp_content_temp);
}

if (is_dir($wp_content_temp) && is_writable($wp_content_temp)) {
$temp_file = \wp_tempnam($filename, $wp_content_temp . "/");
if ($temp_file) {
return $temp_file;
}
}

// Strategy 4: Use WordPress temporary directory (if defined)
if (defined("WP_TEMP_DIR") && is_dir(WP_TEMP_DIR) && is_writable(WP_TEMP_DIR)) {
$temp_file = \wp_tempnam($filename, WP_TEMP_DIR . "/");
if ($temp_file) {
return $temp_file;
}
}

// Strategy 5: Try system temp directory as last resort
$temp_file = \wp_tempnam($filename);
if ($temp_file) {
return $temp_file;
}

// Strategy 6: Manual temp file creation in uploads dir
if (!empty($upload_dir["basedir"])) {
$manual_temp = $upload_dir["basedir"] . "/" . uniqid("wp_github_updater_", true) . ".tmp";
$handle = @fopen($manual_temp, "w");
if ($handle) {
fclose($handle);
return $manual_temp;
}
}

return new WP_Error(
"temp_file_creation_failed",
$this->config->__("Could not create temporary file. Please check directory permissions " .
"or define WP_TEMP_DIR in wp-config.php")
);
}

/** /**
* Get headers for GitHub API requests * Get headers for GitHub API requests
* *

View file

@ -7,7 +7,7 @@
* *
* @package SilverAssist\WpGithubUpdater * @package SilverAssist\WpGithubUpdater
* @author Silver Assist * @author Silver Assist
* @version 1.2.1 * @version 1.1.1
* @license PolyForm-Noncommercial-1.0.0 * @license PolyForm-Noncommercial-1.0.0
*/ */


@ -130,14 +130,6 @@ class UpdaterConfig
*/ */
public string $textDomain; public string $textDomain;


/**
* Custom temporary directory path
*
* @var string|null Custom path for temporary files during downloads, null for auto-detection
* @since 1.1.3
*/
public ?string $customTempDir;

/** /**
* Create updater configuration * Create updater configuration
* *
@ -164,13 +156,12 @@ class UpdaterConfig
$this->pluginAuthor = $options["plugin_author"] ?? $pluginData["Author"] ?? ""; $this->pluginAuthor = $options["plugin_author"] ?? $pluginData["Author"] ?? "";
$this->pluginHomepage = $options["plugin_homepage"] ?? "https://github.com/{$githubRepo}"; $this->pluginHomepage = $options["plugin_homepage"] ?? "https://github.com/{$githubRepo}";
$this->requiresWordPress = $options["requires_wordpress"] ?? "6.0"; $this->requiresWordPress = $options["requires_wordpress"] ?? "6.0";
$this->requiresPHP = $options["requires_php"] ?? "8.2"; $this->requiresPHP = $options["requires_php"] ?? "8.0";
$this->assetPattern = $options["asset_pattern"] ?? "{slug}-v{version}.zip"; $this->assetPattern = $options["asset_pattern"] ?? "{slug}-v{version}.zip";
$this->cacheDuration = $options["cache_duration"] ?? (12 * 3600); // 12 hours $this->cacheDuration = $options["cache_duration"] ?? (12 * 3600); // 12 hours
$this->ajaxAction = $options["ajax_action"] ?? "check_plugin_version"; $this->ajaxAction = $options["ajax_action"] ?? "check_plugin_version";
$this->ajaxNonce = $options["ajax_nonce"] ?? "plugin_version_check"; $this->ajaxNonce = $options["ajax_nonce"] ?? "plugin_version_check";
$this->textDomain = $options["text_domain"] ?? "wp-github-updater"; $this->textDomain = $options["text_domain"] ?? "wp-github-updater";
$this->customTempDir = $options["custom_temp_dir"] ?? null;
} }


/** /**
@ -186,7 +177,7 @@ class UpdaterConfig
*/ */
private function getPluginData(string $pluginFile): array private function getPluginData(string $pluginFile): array
{ {
if (function_exists("get_plugin_data")) { if (\function_exists("get_plugin_data")) {
return \get_plugin_data($pluginFile); return \get_plugin_data($pluginFile);
} }



View file

@ -1,147 +0,0 @@
<?php

/**
* Integration tests for download filter functionality
*
* @package SilverAssist\WpGithubUpdater\Tests\Integration
*/

namespace SilverAssist\WpGithubUpdater\Tests\Integration;

use PHPUnit\Framework\TestCase;
use SilverAssist\WpGithubUpdater\Updater;
use SilverAssist\WpGithubUpdater\UpdaterConfig;

/**
* Test download filter functionality
*
* These tests verify the download filter behavior including
* temporary file creation and validation.
*/
class DownloadFilterTest extends TestCase
{
private UpdaterConfig $config;
private Updater $updater;
private string $testPluginFile;

/**
* Set up test environment before each test
*/
protected function setUp(): void
{
parent::setUp();

// Create a temporary plugin file for testing
$this->testPluginFile = sys_get_temp_dir() . "/test-plugin.php";
file_put_contents($this->testPluginFile, "<?php\n/*\nPlugin Name: Test Plugin\nVersion: 1.0.0\n*/");

// Create test configuration
$this->config = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo");
$this->updater = new Updater($this->config);
}

/**
* Clean up after each test
*/
protected function tearDown(): void
{
if (file_exists($this->testPluginFile)) {
unlink($this->testPluginFile);
}

parent::tearDown();
}

/**
* Test that temporary directory configuration is respected
*/
public function testCustomTempDirectoryIsRespected(): void
{
$customTempDir = sys_get_temp_dir() . "/wp-github-updater-custom";

$config = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo", [
"custom_temp_dir" => $customTempDir,
]);

$this->assertEquals($customTempDir, $config->customTempDir);
}

/**
* Test that package URL validation works correctly
*/
public function testPackageUrlValidation(): void
{
// Valid GitHub URL
$validUrl = "https://github.com/SilverAssist/test-repo/releases/download/v1.0.0/package.zip";
$this->assertStringContainsString("github.com", $validUrl);
$this->assertStringContainsString("SilverAssist/test-repo", $validUrl);

// Invalid URL (not GitHub)
$invalidUrl = "https://example.com/package.zip";
$this->assertStringNotContainsString("github.com", $invalidUrl);
}

/**
* Test file size validation logic
*/
public function testFileSizeValidation(): void
{
$minSize = 100; // Minimum size for valid ZIP

// Valid size
$this->assertGreaterThanOrEqual($minSize, 1024);

// Invalid size
$this->assertLessThan($minSize, 50);
}

/**
* Test that hook_extra validation logic works
*/
public function testHookExtraValidation(): void
{
$pluginSlug = basename(dirname($this->testPluginFile)) . "/" . basename($this->testPluginFile);

// Test single plugin update
$hook_extra_single = [
"plugin" => $pluginSlug,
];
$this->assertArrayHasKey("plugin", $hook_extra_single);
$this->assertEquals($pluginSlug, $hook_extra_single["plugin"]);

// Test bulk plugin update
$hook_extra_bulk = [
"plugins" => [$pluginSlug, "other-plugin/other-plugin.php"],
];
$this->assertArrayHasKey("plugins", $hook_extra_bulk);
$this->assertContains($pluginSlug, $hook_extra_bulk["plugins"]);
}

/**
* Test version comparison logic
*/
public function testVersionComparison(): void
{
// Current version is older than latest
$this->assertTrue(version_compare("1.0.0", "1.1.0", "<"));
$this->assertFalse(version_compare("1.1.0", "1.0.0", "<"));

// Same version
$this->assertFalse(version_compare("1.0.0", "1.0.0", "<"));
$this->assertTrue(version_compare("1.0.0", "1.0.0", "="));
}

/**
* Test GitHub repository format validation
*/
public function testGitHubRepoFormat(): void
{
// Valid formats
$this->assertMatchesRegularExpression("/^[a-zA-Z0-9-]+\/[a-zA-Z0-9-_]+$/", "SilverAssist/test-repo");
$this->assertMatchesRegularExpression("/^[a-zA-Z0-9-]+\/[a-zA-Z0-9-_]+$/", "owner/repo");

// Invalid formats
$this->assertDoesNotMatchRegularExpression("/^[a-zA-Z0-9-]+\/[a-zA-Z0-9-_]+$/", "invalid");
$this->assertDoesNotMatchRegularExpression("/^[a-zA-Z0-9-]+\/[a-zA-Z0-9-_]+$/", "/owner/repo");
}
}

View file

@ -1,423 +0,0 @@
<?php

/**
* Real GitHub API Integration Tests
*
* Tests using real GitHub API with silver-assist-post-revalidate repository.
* These tests make actual HTTP requests to GitHub's API.
*
* @package SilverAssist\WpGithubUpdater
* @since 1.1.6
*/

namespace SilverAssist\WpGithubUpdater\Tests\Integration;

use PHPUnit\Framework\TestCase;
use SilverAssist\WpGithubUpdater\UpdaterConfig;
use SilverAssist\WpGithubUpdater\Updater;

/**
* Real GitHub API Integration Test Class
*
* Tests actual communication with GitHub's API using a real repository.
*
* @since 1.1.6
*/
class RealGitHubAPITest extends TestCase
{
/**
* Test plugin file path
*
* @var string
*/
private static string $testPluginFile;

/**
* Set up before class
*
* @return void
*/
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
self::$testPluginFile = dirname(__DIR__) . "/fixtures/test-plugin.php";
}

/**
* Test fetching latest version from real GitHub repository
*
* @return void
*/
public function testFetchLatestVersionFromRealRepo(): void
{
$config = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate"
);

$updater = new Updater($config);

// This makes a real HTTP request to GitHub
$latestVersion = $updater->getLatestVersion();

// Should return the latest version or false
$this->assertTrue(
$latestVersion === false || is_string($latestVersion),
"getLatestVersion() should return string or false"
);

// If successful, should be a valid version string
if ($latestVersion !== false) {
$this->assertMatchesRegularExpression(
"/^\d+\.\d+\.\d+$/",
$latestVersion,
"Version should be in format X.Y.Z"
);

// Latest version should be at least v1.3.0 (current latest)
$this->assertGreaterThanOrEqual(
"1.3.0",
$latestVersion,
"Latest version should be at least v1.3.0"
);
}
}

/**
* Test fetching release information from real GitHub repository
*
* @return void
*/
public function testFetchReleaseInformationFromRealRepo(): void
{
$config = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate"
);

$updater = new Updater($config);

// Get latest version (makes real HTTP request)
$version = $updater->getLatestVersion();

// Should return string version or false
$this->assertTrue(
$version === false || is_string($version),
"getLatestVersion() should return string or false"
);

// If successful, should have valid version
if ($version !== false) {
// Verify it's a valid semantic version
$this->assertMatchesRegularExpression(
"/^\d+\.\d+\.\d+$/",
$version,
"Version should be in X.Y.Z format"
);

// Check if update is available
$config2 = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate",
[
"plugin_version" => "0.0.1", // Old version
]
);

$updater2 = new Updater($config2);
$hasUpdate = $updater2->isUpdateAvailable();

$this->assertTrue(
$hasUpdate,
"Should detect update available when current version is 0.0.1"
);
}
}

/**
* Test GitHub API response structure
*
* @return void
*/
public function testGitHubAPIResponseStructure(): void
{
$config = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate"
);

$updater = new Updater($config);

// Get version to trigger API call
$version = $updater->getLatestVersion();

// Skip if API request failed
if ($version === false) {
$this->markTestSkipped("GitHub API request failed or rate limited");
}

// Verify version is valid format
$this->assertMatchesRegularExpression(
"/^\d+\.\d+\.\d+$/",
$version,
"Version should be in X.Y.Z format"
);

// Version should be at least 1.0.0 (first release)
$this->assertGreaterThanOrEqual(
"1.0.0",
$version,
"Version should be at least 1.0.0"
);
}

/**
* Test update check with real GitHub repository
*
* @return void
*/
public function testUpdateCheckWithRealRepo(): void
{
$config = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate",
[
"plugin_name" => "Silver Assist Post Revalidate",
"plugin_version" => "0.0.1", // Old version to trigger update
]
);

$updater = new Updater($config);

// This makes a real HTTP request to GitHub
$hasUpdate = $updater->isUpdateAvailable();

// Should return boolean
$this->assertIsBool($hasUpdate);

// With version 0.0.1, there should be an update available
// (unless API request failed)
if ($hasUpdate) {
$this->assertTrue(
$hasUpdate,
"Update should be available for version 0.0.1"
);

// Get latest version to verify
$latestVersion = $updater->getLatestVersion();
$this->assertNotFalse($latestVersion);
$this->assertGreaterThan("0.0.1", $latestVersion);
}
}

/**
* Test caching of GitHub API responses
*
* @return void
*/
public function testGitHubAPIResponseCaching(): void
{
$config = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate",
[
"cache_duration" => 3600, // 1 hour
]
);

$updater = new Updater($config);

// First call - should make HTTP request
$version1 = $updater->getLatestVersion();

// Skip if API request failed
if ($version1 === false) {
$this->markTestSkipped("GitHub API request failed or rate limited");
}

// Second call - should use cache (much faster)
$startTime = microtime(true);
$version2 = $updater->getLatestVersion();
$elapsed = microtime(true) - $startTime;

// Both should return same version
$this->assertEquals($version1, $version2);

// Second call should be very fast (cached)
$this->assertLessThan(
0.01, // Less than 10ms
$elapsed,
"Cached call should be very fast"
);
}

/**
* Test GitHub API rate limiting handling
*
* @return void
*/
public function testGitHubAPIRateLimitHandling(): void
{
$config = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate",
[
"cache_duration" => 1, // 1 second cache
]
);

$updater = new Updater($config);

// Make multiple requests (cached after first)
$results = [];
for ($i = 0; $i < 3; $i++) {
$version = $updater->getLatestVersion();
$results[] = $version;
}

// At least one request should succeed (or all should fail gracefully)
$hasSuccess = false;
foreach ($results as $result) {
if ($result !== false) {
$hasSuccess = true;
break;
}
}

// All results should be the same (cached)
$uniqueResults = array_unique($results);
$this->assertCount(
1,
$uniqueResults,
"All requests should return same result (cached)"
);

$this->assertTrue(
$hasSuccess || count(array_filter($results)) === 0,
"Should either succeed or fail gracefully for all requests"
);
}

/**
* Test asset pattern matching with real releases
*
* @return void
*/
public function testAssetPatternMatchingWithRealReleases(): void
{
$config = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate",
[
// Custom asset pattern to match the plugin's naming convention
"asset_pattern" => "silver-assist-post-revalidate-v{version}.zip",
]
);

$updater = new Updater($config);

// Get latest version
$version = $updater->getLatestVersion();

// Skip if API request failed
if ($version === false) {
$this->markTestSkipped("GitHub API request failed or rate limited");
}

// Verify version format
$this->assertMatchesRegularExpression(
"/^\d+\.\d+\.\d+$/",
$version,
"Version should be in X.Y.Z format"
);

// Verify the asset pattern is configured correctly
$this->assertEquals(
"silver-assist-post-revalidate-v{version}.zip",
$config->assetPattern,
"Asset pattern should be configured"
);
}

/**
* Test version comparison with current and latest versions
*
* @return void
*/
public function testVersionComparisonWithRealVersions(): void
{
// Test with old version (should have update)
$config1 = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate",
[
"plugin_version" => "1.0.0",
]
);

$updater1 = new Updater($config1);
$latestVersion = $updater1->getLatestVersion();

// Skip if API failed
if ($latestVersion === false) {
$this->markTestSkipped("GitHub API request failed");
}

// 1.0.0 should be older than latest
$hasUpdate = $updater1->isUpdateAvailable();
$this->assertTrue(
$hasUpdate,
"Version 1.0.0 should have update available (latest: {$latestVersion})"
);

// Verify version comparison works correctly
$this->assertGreaterThan(
"1.0.0",
$latestVersion,
"Latest version should be greater than 1.0.0"
);
}

/**
* Test GitHub repository information retrieval
*
* @return void
*/
public function testGitHubRepositoryInformation(): void
{
$config = new UpdaterConfig(
self::$testPluginFile,
"SilverAssist/silver-assist-post-revalidate"
);

$updater = new Updater($config);

// Get version (triggers API call)
$version = $updater->getLatestVersion();

// Skip if failed
if ($version === false) {
$this->markTestSkipped("GitHub API request failed");
}

// Verify repository info is correct
$this->assertEquals(
"SilverAssist/silver-assist-post-revalidate",
$config->githubRepo,
"Repository should be configured correctly"
);

// Verify version is reasonable (>= 1.0.0 and < 100.0.0)
$this->assertGreaterThanOrEqual(
"1.0.0",
$version,
"Version should be at least 1.0.0"
);

$this->assertLessThan(
"100.0.0",
$version,
"Version should be less than 100.0.0"
);
}
}

View file

@ -1,142 +0,0 @@
<?php

/**
* Integration tests for Updater class
*
* @package SilverAssist\WpGithubUpdater\Tests\Integration
*/

namespace SilverAssist\WpGithubUpdater\Tests\Integration;

use PHPUnit\Framework\TestCase;
use SilverAssist\WpGithubUpdater\Updater;
use SilverAssist\WpGithubUpdater\UpdaterConfig;

/**
* Test Updater integration with GitHub API
*
* These tests verify the integration between the Updater class
* and external dependencies like GitHub API (mocked).
*/
class UpdaterIntegrationTest extends TestCase
{
private UpdaterConfig $config;
private string $testPluginFile;

/**
* Set up test environment before each test
*/
protected function setUp(): void
{
parent::setUp();

// Create a temporary plugin file for testing
$this->testPluginFile = sys_get_temp_dir() . "/test-plugin.php";
file_put_contents($this->testPluginFile, "<?php\n/*\nPlugin Name: Test Plugin\nVersion: 1.0.0\n*/");

// Create test configuration
$this->config = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo", [
"plugin_name" => "Test Plugin",
"plugin_description" => "Test plugin description",
"cache_duration" => 60, // Short cache for testing
]);
}

/**
* Clean up after each test
*/
protected function tearDown(): void
{
if (file_exists($this->testPluginFile)) {
unlink($this->testPluginFile);
}

parent::tearDown();
}

/**
* Test that Updater can be instantiated with valid configuration
*/
public function testUpdaterInstantiation(): void
{
$updater = new Updater($this->config);

$this->assertInstanceOf(Updater::class, $updater);
$this->assertEquals("1.0.0", $updater->getCurrentVersion());
$this->assertEquals("SilverAssist/test-repo", $updater->getGithubRepo());
}

/**
* Test configuration validation
*/
public function testConfigurationValidation(): void
{
$this->assertEquals("Test Plugin", $this->config->pluginName);
$this->assertEquals("Test plugin description", $this->config->pluginDescription);
$this->assertEquals("SilverAssist/test-repo", $this->config->githubRepo);
$this->assertEquals(60, $this->config->cacheDuration);
}

/**
* Test custom temporary directory configuration
*/
public function testCustomTempDirConfiguration(): void
{
$customTempDir = sys_get_temp_dir() . "/wp-github-updater-test";

$config = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo", [
"custom_temp_dir" => $customTempDir,
]);

$this->assertEquals($customTempDir, $config->customTempDir);
}

/**
* Test text domain configuration
*/
public function testTextDomainConfiguration(): void
{
$config = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo", [
"text_domain" => "custom-domain",
]);

$this->assertEquals("custom-domain", $config->textDomain);
}

/**
* Test AJAX configuration
*/
public function testAjaxConfiguration(): void
{
$this->assertNotEmpty($this->config->ajaxAction);
$this->assertNotEmpty($this->config->ajaxNonce);
$this->assertEquals("check_plugin_version", $this->config->ajaxAction);
$this->assertEquals("plugin_version_check", $this->config->ajaxNonce);
}

/**
* Test asset pattern configuration
*/
public function testAssetPatternConfiguration(): void
{
$config = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo", [
"asset_pattern" => "custom-{slug}-{version}.zip",
]);

$this->assertEquals("custom-{slug}-{version}.zip", $config->assetPattern);
}

/**
* Test WordPress requirements configuration
*/
public function testWordPressRequirementsConfiguration(): void
{
$config = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo", [
"requires_wordpress" => "6.2",
"requires_php" => "8.1",
]);

$this->assertEquals("6.2", $config->requiresWordPress);
$this->assertEquals("8.1", $config->requiresPHP);
}
}

View file

@ -1,27 +1,20 @@
<?php <?php


namespace SilverAssist\WpGithubUpdater\Tests\Unit; namespace SilverAssist\WpGithubUpdater\Tests;


use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use SilverAssist\WpGithubUpdater\UpdaterConfig; use SilverAssist\WpGithubUpdater\UpdaterConfig;


class UpdaterConfigTest extends TestCase class UpdaterConfigTest extends TestCase
{ {
private static string $testPluginFile;

public static function setUpBeforeClass(): void
{
self::$testPluginFile = dirname(__DIR__) . "/fixtures/test-plugin.php";
}

public function testBasicConfiguration(): void public function testBasicConfiguration(): void
{ {
$config = new UpdaterConfig(self::$testPluginFile, "owner/repo"); $config = new UpdaterConfig("/path/to/plugin.php", "owner/repo");


$this->assertEquals(self::$testPluginFile, $config->pluginFile); $this->assertEquals("/path/to/plugin.php", $config->pluginFile);
$this->assertEquals("owner/repo", $config->githubRepo); $this->assertEquals("owner/repo", $config->githubRepo);
$this->assertEquals("6.0", $config->requiresWordPress); $this->assertEquals("6.0", $config->requiresWordPress);
$this->assertEquals("8.2", $config->requiresPHP); $this->assertEquals("8.0", $config->requiresPHP);
$this->assertEquals("{slug}-v{version}.zip", $config->assetPattern); $this->assertEquals("{slug}-v{version}.zip", $config->assetPattern);
$this->assertEquals("wp-github-updater", $config->textDomain); $this->assertEquals("wp-github-updater", $config->textDomain);
} }
@ -41,7 +34,7 @@ class UpdaterConfigTest extends TestCase
"text_domain" => "my-custom-plugin" "text_domain" => "my-custom-plugin"
]; ];


$config = new UpdaterConfig(self::$testPluginFile, "owner/repo", $options); $config = new UpdaterConfig("/path/to/plugin.php", "owner/repo", $options);


$this->assertEquals("Test Plugin", $config->pluginName); $this->assertEquals("Test Plugin", $config->pluginName);
$this->assertEquals("A test plugin", $config->pluginDescription); $this->assertEquals("A test plugin", $config->pluginDescription);
@ -57,7 +50,7 @@ class UpdaterConfigTest extends TestCase


public function testTranslationMethods(): void public function testTranslationMethods(): void
{ {
$config = new UpdaterConfig(self::$testPluginFile, "owner/repo", [ $config = new UpdaterConfig("/path/to/plugin.php", "owner/repo", [
"text_domain" => "test-domain" "text_domain" => "test-domain"
]); ]);


View file

@ -1,258 +0,0 @@
<?php

/**
* Tests for Mock Plugin with WordPress Test Suite
*
* These tests use the WordPress Test Suite to test the updater
* in a real WordPress environment with the mock plugin.
*
* NOTE: These tests are ONLY loaded when WordPress Test Suite is available.
* They will be skipped automatically if WP_UnitTestCase is not defined.
*
* @package SilverAssist\WpGithubUpdater\Tests\WordPress
*/

namespace SilverAssist\WpGithubUpdater\Tests\WordPress;

// Only load tests if WordPress Test Suite is available
if (!class_exists("WP_UnitTestCase")) {
return;
}

use WP_UnitTestCase;
use SilverAssist\WpGithubUpdater\Updater;

/**
* Mock Plugin Integration Tests
*
* These tests require WordPress Test Suite to be installed.
* Run: ./bin/install-wp-tests.sh wordpress_test root '' localhost 6.7.1
*/
class MockPluginTest extends WP_UnitTestCase
{
private string $pluginFile;
private ?Updater $updater = null;

/**
* Set up test environment
*/
public function setUp(): void
{
parent::setUp();

// Define plugin file path
$this->pluginFile = dirname(__DIR__) . "/fixtures/mock-plugin/mock-plugin.php";

// Load the mock plugin
if (file_exists($this->pluginFile)) {
require_once $this->pluginFile;

// Initialize the updater
do_action("plugins_loaded");

// Get updater instance
$this->updater = mock_plugin_get_updater();
}
}

/**
* Test that mock plugin file exists
*/
public function testMockPluginFileExists(): void
{
$this->assertFileExists($this->pluginFile, "Mock plugin file should exist");
}

/**
* Test that mock plugin can be loaded
*/
public function testMockPluginCanBeLoaded(): void
{
$this->assertTrue(
function_exists("mock_plugin_init_updater"),
"Mock plugin functions should be available"
);
$this->assertTrue(
function_exists("mock_plugin_get_updater"),
"Mock plugin helper functions should be available"
);
}

/**
* Test that updater is initialized
*/
public function testUpdaterIsInitialized(): void
{
$this->assertNotNull($this->updater, "Updater should be initialized");
$this->assertInstanceOf(Updater::class, $this->updater, "Should be Updater instance");
}

/**
* Test updater configuration
*/
public function testUpdaterConfiguration(): void
{
if (!$this->updater) {
$this->markTestSkipped("Updater not initialized");
}

$this->assertEquals("SilverAssist/mock-test-repo", $this->updater->getGithubRepo());
$this->assertEquals("1.0.0", $this->updater->getCurrentVersion());
}

/**
* Test WordPress hooks are registered
*/
public function testWordPressHooksAreRegistered(): void
{
// Check that update check filter is registered
$this->assertNotFalse(
has_filter("pre_set_site_transient_update_plugins"),
"Update check filter should be registered"
);

// Check that plugin info filter is registered
$this->assertNotFalse(
has_filter("plugins_api"),
"Plugin info filter should be registered"
);
}

/**
* Test AJAX actions are registered
*/
public function testAjaxActionsAreRegistered(): void
{
// Check that AJAX actions are registered (only for logged-in users)
$this->assertNotFalse(
has_action("wp_ajax_mock_plugin_check_version"),
"AJAX check version action should be registered"
);

// Check that dismiss notice action is registered
$this->assertNotFalse(
has_action("wp_ajax_mock_plugin_check_version_dismiss_notice"),
"AJAX dismiss notice action should be registered"
);
}

/**
* Test plugin activation
*/
public function testPluginActivation(): void
{
// Set a transient to test cleanup
set_transient("mock-plugin_version_check", "test_data", 3600);
$this->assertNotFalse(get_transient("mock-plugin_version_check"));

// Trigger activation
do_action("activate_" . plugin_basename($this->pluginFile));

// In real implementation, transient should be cleared
// For now, just verify activation doesn't cause errors
$this->assertTrue(true);
}

/**
* Test plugin deactivation
*/
public function testPluginDeactivation(): void
{
// Set a transient
set_transient("mock-plugin_version_check", "test_data", 3600);

// Trigger deactivation
do_action("deactivate_" . plugin_basename($this->pluginFile));

// Verify cleanup happens (implementation-specific)
$this->assertTrue(true);
}

/**
* Test admin menu is registered
*/
public function testAdminMenuIsRegistered(): void
{
// Set current user as administrator
$user_id = $this->factory->user->create(["role" => "administrator"]);
wp_set_current_user($user_id);

// Trigger admin menu hook
do_action("admin_menu");

// Check that menu was added
global $menu;
$this->assertIsArray($menu);
}

/**
* Test update check with transient caching
*/
public function testUpdateCheckWithCaching(): void
{
if (!$this->updater) {
$this->markTestSkipped("Updater not initialized");
}

// Clear any existing cache
delete_transient("mock-plugin_version_check");

// First check should query API (we can't test actual API in unit tests)
$updateAvailable = $this->updater->isUpdateAvailable();
$this->assertIsBool($updateAvailable);

// Second check should use cache (if API was successful)
$updateAvailable2 = $this->updater->isUpdateAvailable();
$this->assertIsBool($updateAvailable2);
}

/**
* Test plugin data retrieval
*/
public function testPluginDataRetrieval(): void
{
$pluginData = \get_plugin_data($this->pluginFile, false, false); // Don't markup, don't translate

$this->assertIsArray($pluginData);
$this->assertEquals("Mock Plugin for WP GitHub Updater Tests", $pluginData["Name"]);
$this->assertEquals("1.0.0", $pluginData["Version"]);
$this->assertEquals("SilverAssist", $pluginData["Author"]);
$this->assertEquals("6.0", $pluginData["RequiresWP"]);
$this->assertEquals("8.2", $pluginData["RequiresPHP"]);
}

/**
* Test plugin basename generation
*/
public function testPluginBasename(): void
{
$basename = plugin_basename($this->pluginFile);

$this->assertIsString($basename);
$this->assertStringContainsString("mock-plugin.php", $basename);
}

/**
* Test custom temporary directory configuration
*/
public function testCustomTempDirectoryConfiguration(): void
{
// This tests the v1.1.3+ feature for custom temp directories
$expectedTempDir = WP_CONTENT_DIR . "/uploads/temp";

// The mock plugin configures a custom temp dir
// We verify this through the configuration
$this->assertDirectoryExists(WP_CONTENT_DIR . "/uploads");
}

/**
* Clean up after tests
*/
public function tearDown(): void
{
// Clean up transients
delete_transient("mock-plugin_version_check");

parent::tearDown();
}
}

View file

@ -1,213 +0,0 @@
<?php

/**
* WordPress-specific integration tests
*
* @package SilverAssist\WpGithubUpdater\Tests\WordPress
*/

namespace SilverAssist\WpGithubUpdater\Tests\WordPress;

use PHPUnit\Framework\TestCase;
use SilverAssist\WpGithubUpdater\Updater;
use SilverAssist\WpGithubUpdater\UpdaterConfig;

/**
* Test WordPress-specific functionality
*
* These tests verify WordPress hooks, filters, and integration points.
* Note: These tests use mocked WordPress functions.
*/
class WordPressHooksTest extends TestCase
{
private UpdaterConfig $config;
private string $testPluginFile;

/**
* Set up test environment before each test
*/
protected function setUp(): void
{
parent::setUp();

// Create a temporary plugin file for testing
$this->testPluginFile = sys_get_temp_dir() . "/test-plugin.php";
file_put_contents(
$this->testPluginFile,
"<?php\n/*\nPlugin Name: Test Plugin\nVersion: 1.0.0\nDescription: Test plugin\n*/"
);

// Create test configuration
$this->config = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo", [
"plugin_name" => "Test Plugin",
"plugin_description" => "Test plugin description",
]);
}

/**
* Clean up after each test
*/
protected function tearDown(): void
{
if (file_exists($this->testPluginFile)) {
unlink($this->testPluginFile);
}

parent::tearDown();
}

/**
* Test configuration object creation
*/
public function testConfigurationCreation(): void
{
$this->assertInstanceOf(UpdaterConfig::class, $this->config);
$this->assertEquals("SilverAssist/test-repo", $this->config->githubRepo);
$this->assertEquals($this->testPluginFile, $this->config->pluginFile);
}

/**
* Test plugin homepage configuration
*/
public function testPluginHomepage(): void
{
$expectedHomepage = "https://github.com/SilverAssist/test-repo";
$this->assertEquals($expectedHomepage, $this->config->pluginHomepage);
}

/**
* Test plugin author configuration
*/
public function testPluginAuthor(): void
{
// When plugin file doesn't exist or can't be read, author will be empty string
// unless explicitly provided in options
$this->assertIsString($this->config->pluginAuthor);

// Test with explicit author option
$configWithAuthor = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo", [
"plugin_author" => "SilverAssist",
]);
$this->assertEquals("SilverAssist", $configWithAuthor->pluginAuthor);
}

/**
* Test cache duration configuration
*/
public function testCacheDuration(): void
{
$this->assertEquals(43200, $this->config->cacheDuration); // Default 12 hours

$customConfig = new UpdaterConfig($this->testPluginFile, "SilverAssist/test-repo", [
"cache_duration" => 3600,
]);
$this->assertEquals(3600, $customConfig->cacheDuration);
}

/**
* Test transient naming convention
*/
public function testTransientNaming(): void
{
$pluginBasename = basename(dirname($this->testPluginFile));
$expectedTransient = "{$pluginBasename}_version_check";

// The transient name should follow WordPress conventions (lowercase, numbers, underscores, dashes)
// Note: basename() may return uppercase letters, which is acceptable in WordPress transients
$this->assertMatchesRegularExpression("/^[a-zA-Z0-9_-]+$/", $expectedTransient);
}

/**
* Test AJAX action naming convention
*/
public function testAjaxActionNaming(): void
{
$ajaxAction = $this->config->ajaxAction;

// AJAX action should follow WordPress conventions (lowercase, numbers, underscores)
$this->assertNotEmpty($ajaxAction);
$this->assertMatchesRegularExpression("/^[a-z0-9_-]+$/", $ajaxAction);
$this->assertEquals("check_plugin_version", $ajaxAction);
}

/**
* Test nonce naming convention
*/
public function testNonceNaming(): void
{
$nonce = $this->config->ajaxNonce;

// Nonce should follow WordPress conventions (lowercase, numbers, underscores)
$this->assertNotEmpty($nonce);
$this->assertMatchesRegularExpression("/^[a-z0-9_-]+$/", $nonce);
$this->assertEquals("plugin_version_check", $nonce);
}

/**
* Test plugin data structure
*/
public function testPluginDataStructure(): void
{
$this->assertIsString($this->config->pluginName);
$this->assertIsString($this->config->pluginDescription);
$this->assertIsString($this->config->pluginAuthor);
$this->assertIsString($this->config->pluginHomepage);
}

/**
* Test WordPress version requirements
*/
public function testWordPressVersionRequirements(): void
{
$this->assertIsString($this->config->requiresWordPress);
$this->assertMatchesRegularExpression("/^\d+\.\d+$/", $this->config->requiresWordPress);
$this->assertGreaterThanOrEqual(6.0, (float) $this->config->requiresWordPress);
}

/**
* Test PHP version requirements
*/
public function testPHPVersionRequirements(): void
{
$this->assertIsString($this->config->requiresPHP);
$this->assertMatchesRegularExpression("/^\d+\.\d+$/", $this->config->requiresPHP);
$this->assertGreaterThanOrEqual(8.2, (float) $this->config->requiresPHP);
}

/**
* Test asset pattern replacement tokens
*/
public function testAssetPatternTokens(): void
{
$pattern = $this->config->assetPattern;

// Pattern should contain replacement tokens
$this->assertStringContainsString("{slug}", $pattern);
$this->assertStringContainsString("{version}", $pattern);
$this->assertStringEndsWith(".zip", $pattern);
}

/**
* Test translation function wrapper
*/
public function testTranslationFunctionWrapper(): void
{
$testString = "Test string";
$translated = $this->config->__($testString);

// In test environment, should return the original string
$this->assertEquals($testString, $translated);
}

/**
* Test GitHub API URL construction
*/
public function testGitHubApiUrlConstruction(): void
{
$repo = $this->config->githubRepo;
$expectedBaseUrl = "https://api.github.com/repos/{$repo}";

$this->assertStringContainsString("SilverAssist", $expectedBaseUrl);
$this->assertStringContainsString("test-repo", $expectedBaseUrl);
}
}

View file

@ -1,117 +1,28 @@
<?php <?php


/** // Bootstrap file for PHPUnit tests
* PHPUnit bootstrap file // This file contains mock WordPress functions for testing
*
* This bootstrap automatically detects the test environment:
* - If WordPress Test Suite is available, loads it for WordPress integration tests
* - Otherwise, uses mocked WordPress functions for unit/integration tests
*
* @package SilverAssist\WpGithubUpdater\Tests
*/


// Determine if we should load WordPress Test Suite if (!function_exists('__')) {
$_tests_dir = getenv("WP_TESTS_DIR"); function __($text, $domain = 'default')
$_skip_wp_tests = filter_var(getenv("SKIP_WP_TESTS_IF_MISSING"), FILTER_VALIDATE_BOOLEAN); {

return $text;
// If not set via environment, try common locations }
if (!$_tests_dir) {
$_tests_dir = rtrim(sys_get_temp_dir(), "/\\") . "/wordpress-tests-lib";
} }


// Check if WordPress Test Suite is available if (!function_exists('esc_html__')) {
$_wp_tests_available = file_exists($_tests_dir . "/includes/functions.php"); function esc_html__($text, $domain = 'default')
{
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
}

if (!function_exists('get_plugin_data')) {
function get_plugin_data($file)
{
return [];
}
}


// Load Composer autoloader // Load Composer autoloader
require_once __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../vendor/autoload.php';

// Load Yoast PHPUnit Polyfills for PHPUnit 9.x compatibility
if (file_exists(__DIR__ . "/../vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php")) {
require_once __DIR__ . "/../vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php";
}

// Decide which environment to load
if ($_wp_tests_available) {
// WordPress Test Suite is available - load it
echo "\n";
echo "====================================\n";
echo "WP GitHub Updater Test Suite\n";
echo "====================================\n";
echo "Mode: WordPress Integration Tests\n";
echo "WP Tests Dir: $_tests_dir\n";
echo "====================================\n\n";

// Load WordPress test suite
require_once $_tests_dir . "/includes/functions.php";

/**
* Manually load the mock plugin for testing
*
* This loads our mock plugin that integrates the WP GitHub Updater package
* into a WordPress environment for real integration testing.
*/
function _manually_load_plugin()
{
// Load the mock plugin that uses the WP GitHub Updater package
$mock_plugin_file = __DIR__ . "/fixtures/mock-plugin/mock-plugin.php";

if (file_exists($mock_plugin_file)) {
require_once $mock_plugin_file;
echo "✓ Mock plugin loaded: {$mock_plugin_file}\n";
} else {
echo "⚠️ Mock plugin not found at: {$mock_plugin_file}\n";
}
}

tests_add_filter("muplugins_loaded", "_manually_load_plugin");

// Start up the WP testing environment
require $_tests_dir . "/includes/bootstrap.php";
} else {
// WordPress Test Suite not available - use mocks

// Define WordPress constants for mock environment
if (!defined("ABSPATH")) {
define("ABSPATH", __DIR__ . "/../");
}

if (!defined("WP_CONTENT_DIR")) {
define("WP_CONTENT_DIR", ABSPATH . "wp-content");
}

if (!defined("WP_PLUGIN_DIR")) {
define("WP_PLUGIN_DIR", WP_CONTENT_DIR . "/plugins");
}

// Load WordPress function mocks for non-WP-Test-Suite environment
require_once __DIR__ . "/wordpress-mocks.php";

if (!$_skip_wp_tests) {
echo "\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "⚠️ WordPress Test Suite not found\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "\n";
echo "Running tests with mocked WordPress functions.\n";
echo "For full WordPress integration tests, install WordPress Test Suite:\n";
echo " ./bin/install-wp-tests.sh wordpress_test root '' localhost 6.7.1\n";
echo "\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "\n";
}

// Note: We don't load wordpress-stubs here because they conflict with our mocks
// wordpress-stubs are only used for static analysis (PHPStan)
// Our WordPress function mocks are loaded via Composer's autoload-dev files
// See composer.json autoload-dev.files section

// Display test suite information
echo "\n";
echo "====================================\n";
echo "WP GitHub Updater Test Suite\n";
echo "====================================\n";
echo "Mode: Unit/Integration Tests (Mocked)\n";
echo "PHP Version: " . PHP_VERSION . "\n";
echo "PHPUnit Version: " . PHPUnit\Runner\Version::id() . "\n";
echo "====================================\n\n";
}

View file

@ -1,260 +0,0 @@
# Mock Plugin for WP GitHub Updater Tests

## 📋 Descripción

Este es un **plugin de prueba (fixture)** que demuestra la integración correcta del paquete `silverassist/wp-github-updater` en un entorno real de WordPress.

## 🎯 Propósito

El mock plugin sirve para:

1. **Testing Real de WordPress**: Permite probar el updater en un entorno WordPress completo con WordPress Test Suite
2. **Ejemplo de Integración**: Demuestra el patrón recomendado de integración del paquete
3. **Documentación Viva**: Sirve como referencia para desarrolladores que integren el paquete

## 📁 Estructura

```
tests/fixtures/mock-plugin/
├── mock-plugin.php # Plugin principal con integración completa
├── readme.txt # WordPress plugin readme
└── README.md # Esta documentación
```

## 🔧 Características Implementadas

### ✅ Integración Completa del Updater

```php
// Configuración completa con todas las opciones
$config = new UpdaterConfig(
__FILE__,
"SilverAssist/mock-test-repo",
[
"plugin_name" => "Mock Plugin for Testing",
"cache_duration" => 300, // 5 minutos para testing
"text_domain" => "mock-plugin",
"custom_temp_dir" => WP_CONTENT_DIR . "/uploads/temp",
// ... más opciones
]
);

$updater = new Updater($config);
```

### ✅ Panel de Administración

- Página de admin con información del plugin
- Botón para check manual de actualizaciones
- Display de versión actual y disponible
- Integración AJAX para checks en tiempo real

### ✅ Hooks de WordPress

- `plugins_loaded`: Inicialización del updater
- `admin_menu`: Menú de administración
- `activate_*`: Limpieza de cache en activación
- `deactivate_*`: Limpieza de cache en desactivación

### ✅ AJAX Handlers

- `mock_plugin_check_version`: Check manual de versión
- Verificación de nonces y capabilities
- Respuestas JSON formateadas

## 🧪 Uso en Tests

### Cargar el Mock Plugin

El mock plugin se carga automáticamente en el bootstrap de WordPress Test Suite:

```php
// En tests/bootstrap.php
function _manually_load_plugin() {
require_once __DIR__ . "/fixtures/mock-plugin/mock-plugin.php";
}
tests_add_filter("muplugins_loaded", "_manually_load_plugin");
```

### Acceder al Updater en Tests

```php
class MyTest extends WP_UnitTestCase {
public function testUpdater() {
// Obtener instancia del updater
$updater = mock_plugin_get_updater();
// Verificar funcionalidad
$this->assertInstanceOf(Updater::class, $updater);
$this->assertEquals("1.0.0", $updater->getCurrentVersion());
}
}
```

### Ejemplo de Test Completo

Ver `tests/WordPress/MockPluginTest.php` para ejemplos completos de:

- ✅ Test de inicialización
- ✅ Test de hooks registrados
- ✅ Test de AJAX actions
- ✅ Test de activación/desactivación
- ✅ Test de menu de admin
- ✅ Test de caching con transients

## 📊 Tests Incluidos

El mock plugin tiene su propia suite de tests en `tests/WordPress/MockPluginTest.php`:

| Test | Descripción |
|------|-------------|
| `testMockPluginFileExists` | Verifica que el archivo existe |
| `testMockPluginCanBeLoaded` | Verifica que se puede cargar |
| `testUpdaterIsInitialized` | Verifica inicialización del updater |
| `testUpdaterConfiguration` | Verifica configuración correcta |
| `testWordPressHooksAreRegistered` | Verifica hooks de WordPress |
| `testAjaxActionsAreRegistered` | Verifica AJAX actions |
| `testPluginActivation` | Verifica activación del plugin |
| `testPluginDeactivation` | Verifica desactivación del plugin |
| `testAdminMenuIsRegistered` | Verifica menú de admin |
| `testUpdateCheckWithCaching` | Verifica caching con transients |
| `testPluginDataRetrieval` | Verifica lectura de metadata |
| `testPluginBasename` | Verifica plugin_basename() |
| `testCustomTempDirectoryConfiguration` | Verifica directorio temporal (v1.1.3+) |

## 🚀 Ejecutar Tests con WordPress Test Suite

### 1. Instalar WordPress Test Suite

```bash
./scripts/install-wp-tests.sh wordpress_test root '' localhost 6.7.1
```

### 2. Ejecutar Tests de WordPress

```bash
# Todos los tests de WordPress (incluye mock plugin)
./vendor/bin/phpunit --testsuite=wordpress

# Solo tests del mock plugin
./vendor/bin/phpunit tests/WordPress/MockPluginTest.php

# Todos los tests (incluye modo WordPress si está instalado)
./vendor/bin/phpunit
```

### 3. Verificar Salida

Cuando WordPress Test Suite está disponible, verás:

```
====================================
WP GitHub Updater Test Suite
====================================
Mode: WordPress Integration Tests
WP Tests Dir: /tmp/wordpress-tests-lib
====================================
✓ Mock plugin loaded: /path/to/tests/fixtures/mock-plugin/mock-plugin.php
```

## 🔍 Funcionalidades para Testing

### Metadata del Plugin

```php
$pluginData = get_plugin_data($pluginFile);

// Retorna:
[
"Name" => "Mock Plugin for WP GitHub Updater Tests",
"Version" => "1.0.0",
"Author" => "SilverAssist",
"RequiresWP" => "6.0",
"RequiresPHP" => "8.2",
// ...
]
```

### Acceso Global al Updater

```php
// Obtener updater desde cualquier parte
$updater = mock_plugin_get_updater();

// O desde global
$updater = $GLOBALS["mock_plugin_updater"];
```

### Limpieza de Cache

```php
// Limpiar cache de versiones
delete_transient("mock-plugin_version_check");

// O usar función de activación
do_action("activate_mock-plugin/mock-plugin.php");
```

## 📝 Configuración

### Opciones Configurables

El mock plugin demuestra todas las opciones disponibles:

```php
[
"plugin_name" => "Mock Plugin for Testing",
"plugin_description" => "A mock plugin for WP GitHub Updater tests",
"plugin_author" => "SilverAssist",
"cache_duration" => 300, // 5 minutos
"text_domain" => "mock-plugin",
"custom_temp_dir" => WP_CONTENT_DIR . "/uploads/temp",
"ajax_action" => "mock_plugin_check_version",
"ajax_nonce" => "mock_plugin_nonce",
"asset_pattern" => "mock-plugin-{version}.zip",
"requires_wordpress" => "6.0",
"requires_php" => "8.2",
]
```

## ⚠️ Notas Importantes

### No Usar en Producción

Este plugin es **exclusivamente para testing** y no debe usarse en sitios de producción:

- Usa un repositorio GitHub ficticio (`mock-test-repo`)
- Cache duration muy corta (5 minutos)
- Configuración optimizada para testing

### Repositorio Ficticio

El plugin apunta a `SilverAssist/mock-test-repo` que puede no existir. Para tests reales de API, deberás:

1. Crear un repositorio de prueba en GitHub
2. Actualizar la configuración en `mock-plugin.php`
3. Crear releases de prueba en ese repositorio

### Compatibilidad

- **WordPress**: 6.0+
- **PHP**: 8.2+
- **PHPUnit**: 9.6+
- **WordPress Test Suite**: Requerido para tests completos

## 🔗 Referencias

- [Ejemplo de Integración](../../../examples/integration-guide.php)
- [Documentación del Paquete](../../../README.md)
- [Testing Summary](../../../docs/TESTING-SUMMARY.md)
- [WordPress Plugin Unit Tests](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/)

## 📄 Licencia

MIT - Solo para propósitos de testing

---

**Última actualización:** 2025-01-10
**Versión:** 1.0.0
**Paquete:** silverassist/wp-github-updater v1.1.5

View file

@ -1,213 +0,0 @@
<?php

/**
* Plugin Name: Mock Plugin for WP GitHub Updater Tests
* Plugin URI: https://github.com/SilverAssist/wp-github-updater
* Description: A mock WordPress plugin used for testing the WP GitHub Updater package
* Version: 1.0.0
* Author: SilverAssist
* Author URI: https://github.com/SilverAssist
* License: MIT
* Text Domain: mock-plugin
* Requires at least: 6.0
* Requires PHP: 8.2
*
* This is a test fixture plugin that demonstrates integration with
* the SilverAssist/WpGithubUpdater package for automated testing.
*/

// Prevent direct access
if (!defined("ABSPATH")) {
exit;
}

// Autoload Composer dependencies
if (file_exists(__DIR__ . "/../../../vendor/autoload.php")) {
require_once __DIR__ . "/../../../vendor/autoload.php";
}

use SilverAssist\WpGithubUpdater\UpdaterConfig;
use SilverAssist\WpGithubUpdater\Updater;

/**
* Initialize the GitHub Updater for this mock plugin
*
* This function demonstrates the recommended integration pattern
* for the WP GitHub Updater package.
*/
function mock_plugin_init_updater(): void
{
// Create updater configuration
$config = new UpdaterConfig(
__FILE__, // Plugin main file
"SilverAssist/mock-test-repo", // GitHub repository
[
// Optional: Override plugin metadata
"plugin_name" => "Mock Plugin for Testing",
"plugin_description" => "A mock plugin for WP GitHub Updater tests",
"plugin_author" => "SilverAssist",

// Optional: Custom cache duration (default: 12 hours)
"cache_duration" => 300, // 5 minutes for testing

// Optional: Custom text domain for translations
"text_domain" => "mock-plugin",

// Optional: Custom temporary directory for downloads
"custom_temp_dir" => WP_CONTENT_DIR . "/uploads/temp",

// Optional: Custom AJAX action names
"ajax_action" => "mock_plugin_check_version",
"ajax_nonce" => "mock_plugin_nonce",

// Optional: Custom asset pattern for GitHub releases
"asset_pattern" => "mock-plugin-{version}.zip",

// Optional: WordPress and PHP requirements
"requires_wp" => "6.0",
"requires_php" => "8.2",
"last_updated" => \gmdate("Y-m-d H:i:s"),
]
);

// Initialize the updater
$updater = new Updater($config);

// Store in global scope for testing access
$GLOBALS["mock_plugin_updater"] = $updater;
}

// Initialize on plugins_loaded hook
add_action("plugins_loaded", "mock_plugin_init_updater");

/**
* Add admin menu for testing
*/
function mock_plugin_admin_menu(): void
{
add_menu_page(
"Mock Plugin",
"Mock Plugin",
"manage_options",
"mock-plugin",
"mock_plugin_admin_page",
"dashicons-admin-plugins",
100
);
}
add_action("admin_menu", "mock_plugin_admin_menu");

/**
* Admin page for testing
*/
function mock_plugin_admin_page(): void
{
if (!current_user_can("manage_options")) {
return;
}

$updater = $GLOBALS["mock_plugin_updater"] ?? null;

?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<div class="card">
<h2>Plugin Information</h2>
<table class="form-table">
<tr>
<th>Current Version:</th>
<td><?php echo esc_html($updater ? $updater->getCurrentVersion() : "N/A"); ?></td>
</tr>
<tr>
<th>GitHub Repository:</th>
<td><?php echo esc_html($updater ? $updater->getGithubRepo() : "N/A"); ?></td>
</tr>
<tr>
<th>Update Available:</th>
<td><?php echo $updater && $updater->isUpdateAvailable() ? "✅ Yes" : "❌ No"; ?></td>
</tr>
<?php if ($updater && $updater->isUpdateAvailable()) : ?>
<tr>
<th>Latest Version:</th>
<td><?php echo esc_html($updater->getLatestVersion()); ?></td>
</tr>
<?php endif; ?>
</table>
</div>
<div class="card">
<h2>Manual Update Check</h2>
<p>Click the button below to manually check for updates:</p>
<button type="button" class="button button-primary" id="mock-plugin-check-update">
Check for Updates
</button>
<div id="mock-plugin-result" style="margin-top: 10px;"></div>
</div>
</div>
<script>
jQuery(document).ready(function($) {
$("#mock-plugin-check-update").on("click", function() {
var button = $(this);
var result = $("#mock-plugin-result");
button.prop("disabled", true).text("Checking...");
result.html("");
$.ajax({
url: ajaxurl,
type: "POST",
data: {
action: "mock_plugin_check_version",
nonce: "<?php echo wp_create_nonce("mock_plugin_nonce"); ?>"
},
success: function(response) {
if (response.success) {
result.html('<div class="notice notice-success"><p>' + response.data.message + '</p></div>');
} else {
result.html('<div class="notice notice-error"><p>' + response.data + '</p></div>');
}
},
error: function() {
result.html('<div class="notice notice-error"><p>Error checking for updates</p></div>');
},
complete: function() {
button.prop("disabled", false).text("Check for Updates");
}
});
});
});
</script>
<?php
}

/**
* Activation hook
*/
function mock_plugin_activate(): void
{
// Clear any existing update caches
delete_transient(basename(dirname(__FILE__)) . "_version_check");
}
register_activation_hook(__FILE__, "mock_plugin_activate");

/**
* Deactivation hook
*/
function mock_plugin_deactivate(): void
{
// Clean up transients
delete_transient(basename(dirname(__FILE__)) . "_version_check");
}
register_deactivation_hook(__FILE__, "mock_plugin_deactivate");

/**
* Helper function to get the updater instance (for testing)
*
* @return Updater|null
*/
function mock_plugin_get_updater(): ?Updater
{
return $GLOBALS["mock_plugin_updater"] ?? null;
}

View file

@ -1,33 +0,0 @@
=== Mock Plugin for WP GitHub Updater Tests ===
Contributors: silverassist
Tags: testing, github, updater
Requires at least: 6.0
Tested up to: 6.7
Requires PHP: 8.2
Stable tag: 1.0.0
License: MIT
License URI: https://opensource.org/licenses/MIT

A mock WordPress plugin used for testing the WP GitHub Updater package.

== Description ==

This is a test fixture plugin that demonstrates integration with the SilverAssist/WpGithubUpdater package.

It is used exclusively for automated testing and should not be used in production environments.

== Installation ==

This plugin is for testing purposes only and should not be installed on production sites.

For testing:
1. Install WordPress Test Suite using the provided install-wp-tests.sh script
2. The plugin will be automatically loaded in the test environment
3. Run PHPUnit tests with the WordPress test suite

== Changelog ==

= 1.0.0 =
* Initial release for testing purposes
* Demonstrates WP GitHub Updater integration
* Includes admin interface for manual testing

View file

@ -1,14 +0,0 @@
<?php

/**
* Plugin Name: Test Plugin for Unit Tests
* Description: A minimal test plugin for UpdaterConfig tests
* Version: 1.0.0
* Author: Test Author
* Author URI: https://example.com
* Requires at least: 6.0
* Requires PHP: 8.2
* Text Domain: test-plugin
*/

// This file only exists for testing purposes

View file

@ -1,163 +0,0 @@
<?php

/**
* WordPress function mocks for testing
*
* This file provides implementations for WordPress functions that are only
* defined (but not implemented) in wordpress-stubs.
*
* @package SilverAssist\WpGithubUpdater\Tests
*/

// Translation functions
if (!function_exists("__")) {
/**
* Mock __ function for tests
*
* @param string $text Text to translate
* @param string $domain Text domain
* @return string Translated text (returns original in tests)
*/
function __(string $text, string $domain = "default"): string
{
return $text;
}
}

if (!function_exists("esc_html__")) {
/**
* Mock esc_html__ function for tests
*
* @param string $text Text to translate
* @param string $domain Text domain
* @return string Escaped and translated text
*/
function esc_html__(string $text, string $domain = "default"): string
{
return htmlspecialchars($text, ENT_QUOTES, "UTF-8");
}
}

// WordPress hooks and filters
if (!function_exists("add_filter")) {
/**
* Mock add_filter function for tests
*
* @param string $hook_name Hook name
* @param callable $callback Callback function
* @param int $priority Priority
* @param int $accepted_args Accepted arguments
* @return bool Always returns true
*/
function add_filter(string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1): bool
{
return true;
}
}

if (!function_exists("add_action")) {
/**
* Mock add_action function for tests
*
* @param string $hook_name Hook name
* @param callable $callback Callback function
* @param int $priority Priority
* @param int $accepted_args Accepted arguments
* @return bool Always returns true
*/
function add_action(string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1): bool
{
return true;
}
}

// Plugin functions
if (!function_exists("plugin_basename")) {
/**
* Mock plugin_basename function for tests
*
* @param string $file Plugin file path
* @return string Plugin basename
*/
function plugin_basename(string $file): string
{
$file = str_replace("\\", "/", $file);
$file = preg_replace("|/+|", "/", $file);

// If WP_PLUGIN_DIR is defined, make path relative to it
if (defined("WP_PLUGIN_DIR")) {
$plugin_dir = str_replace("\\", "/", WP_PLUGIN_DIR);
$plugin_dir = preg_replace("|/+|", "/", $plugin_dir);
$file = preg_replace("#^" . preg_quote($plugin_dir, "#") . "/#", "", $file);
}

// Otherwise just return folder/file.php format
$file = trim($file, "/");
$parts = explode("/", $file);

if (count($parts) >= 2) {
// Return last two parts: folder/file.php
return $parts[count($parts) - 2] . "/" . $parts[count($parts) - 1];
}

return basename($file);
}
}

if (!function_exists("get_plugin_data")) {
/**
* Mock get_plugin_data function for tests
*
* @param string $plugin_file Path to the plugin file
* @param bool $markup Whether to apply markup
* @param bool $translate Whether to translate
* @return array Plugin data array
*/
function get_plugin_data(string $plugin_file, bool $markup = true, bool $translate = true): array
{
if (!file_exists($plugin_file)) {
return [
"Name" => "Test Plugin",
"Version" => "1.0.0",
"Description" => "Test plugin description",
"Author" => "Test Author",
"PluginURI" => "",
"AuthorURI" => "",
"TextDomain" => "test-plugin",
"DomainPath" => "",
"Network" => false,
"RequiresWP" => "",
"RequiresPHP" => "",
];
}

$content = file_get_contents($plugin_file);
$headers = [
"Name" => "Plugin Name",
"PluginURI" => "Plugin URI",
"Version" => "Version",
"Description" => "Description",
"Author" => "Author",
"AuthorURI" => "Author URI",
"TextDomain" => "Text Domain",
"DomainPath" => "Domain Path",
"Network" => "Network",
"RequiresWP" => "Requires at least",
"RequiresPHP" => "Requires PHP",
];

$data = [];
foreach ($headers as $key => $header) {
if (preg_match("/^[ \t\/*#@]*" . preg_quote($header, "/") . ":(.*)$/mi", $content, $matches)) {
$data[$key] = trim($matches[1]);
} else {
$data[$key] = "";
}
}

// Convert Network to boolean
$data["Network"] = strtolower($data["Network"]) === "true";

return $data;
}
}