Compare commits
12
Commits
51ccabc24e
..
v0.1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f4d88f8dd | ||
|
|
bfac63799b | ||
|
|
9d3e67d16b | ||
|
|
843696d3a8 | ||
|
|
269626438e | ||
|
|
0e6f9639f4 | ||
|
|
0de0b2ef05 | ||
|
|
342eefd471 | ||
|
|
66758ac0f8 | ||
|
|
32697533ab | ||
|
|
4a7798a8c2 | ||
|
|
c7b590870c |
No files matched your search
@@ -0,0 +1,121 @@
|
|||||||
|
# AI Assistant Guide for WordPress Plugin Development
|
||||||
|
|
||||||
|
This guide helps AI assistants understand the project structure, workflows, and best practices for this repository.
|
||||||
|
|
||||||
|
## IMPORTANT: Repository Context
|
||||||
|
|
||||||
|
This workspace may contain multiple repository folders. Always focus ONLY on the current repository you're working in and avoid hallucinating functionality from other repositories in the workspace.
|
||||||
|
|
||||||
|
- **Current Repository**: wp-plugin-starter-template-for-ai-coding
|
||||||
|
- **Repository Purpose**: A comprehensive starter template for WordPress plugins with best practices for AI-assisted development
|
||||||
|
- **Repository Scope**: All code changes, documentation, and functionality discussions should be limited to THIS repository only
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
- **Plugin Name**: WordPress Plugin Starter Template
|
||||||
|
- **Plugin Slug**: wp-plugin-starter-template
|
||||||
|
- **Text Domain**: wp-plugin-starter-template
|
||||||
|
- **Namespace**: WPALLSTARS\PluginStarterTemplate
|
||||||
|
- **Version**: 0.1.1
|
||||||
|
- **Requires WordPress**: 5.0+
|
||||||
|
- **Requires PHP**: 7.0+
|
||||||
|
- **License**: GPL-2.0+
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
- **wp-plugin-starter-template.php**: Main plugin file with plugin headers
|
||||||
|
- **includes/**: Core plugin functionality
|
||||||
|
- **plugin.php**: Main plugin class that initializes everything
|
||||||
|
- **core.php**: Core functionality class
|
||||||
|
- **updater.php**: Update mechanism for multiple sources
|
||||||
|
- **admin/**: Admin-specific functionality
|
||||||
|
- **lib/**: Admin classes
|
||||||
|
- **css/**: Admin stylesheets
|
||||||
|
- **js/**: Admin JavaScript files
|
||||||
|
- **languages/**: Translation files
|
||||||
|
- **.github/workflows/**: GitHub Actions workflows
|
||||||
|
- **.ai-workflows/**: Documentation for AI assistants
|
||||||
|
- **.wiki/**: Wiki documentation templates
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
This project follows the [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/):
|
||||||
|
|
||||||
|
- Use tabs for indentation, not spaces
|
||||||
|
- Follow WordPress naming conventions:
|
||||||
|
- Class names: `Class_Name`
|
||||||
|
- Function names: `function_name`
|
||||||
|
- Variable names: `$variable_name`
|
||||||
|
- Use proper DocBlocks for all classes, methods, and functions
|
||||||
|
- Ensure all user-facing strings are translatable
|
||||||
|
- Validate and sanitize all inputs
|
||||||
|
- Escape all outputs
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
For detailed instructions on common tasks like creating releases, adding features, fixing bugs, and testing previous versions, see **@.ai-workflows/release-process.md**.
|
||||||
|
|
||||||
|
## Avoiding Cross-Repository Confusion
|
||||||
|
|
||||||
|
When working in a multi-repository workspace, follow these guidelines to avoid confusion:
|
||||||
|
|
||||||
|
1. **Verify Repository Context**: Always check which repository you're currently working in before making any changes or recommendations.
|
||||||
|
|
||||||
|
2. **Limit Code Search Scope**: When searching for code or functionality, explicitly limit your search to the current repository.
|
||||||
|
|
||||||
|
3. **Don't Assume Features**: Never assume that features present in one repository should be implemented in another. Each repository has its own specific purpose and feature set.
|
||||||
|
|
||||||
|
4. **Repository-Specific Documentation**: Documentation should only reflect the actual features and functionality of the current repository.
|
||||||
|
|
||||||
|
5. **Cross-Repository Inspiration**: If you want to implement a feature inspired by another repository, explicitly mention that it's a new feature being added, not an existing one.
|
||||||
|
|
||||||
|
6. **Verify Before Implementation**: Before implementing or documenting a feature, verify that it actually exists in the current repository by checking the codebase.
|
||||||
|
|
||||||
|
## Internationalization (i18n)
|
||||||
|
|
||||||
|
All user-facing strings should be translatable:
|
||||||
|
|
||||||
|
- Use `__()` for simple strings
|
||||||
|
- Use `_e()` for echoed strings
|
||||||
|
- Use `esc_html__()` for escaped strings
|
||||||
|
- Use `esc_html_e()` for escaped and echoed strings
|
||||||
|
- Always use the plugin's text domain: `wp-plugin-starter-template`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```php
|
||||||
|
echo esc_html__('This is a translatable string', 'wp-plugin-starter-template');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
- Validate and sanitize all inputs
|
||||||
|
- Escape all outputs
|
||||||
|
- Use nonces for form submissions
|
||||||
|
- Use capability checks for user actions
|
||||||
|
- Follow the principle of least privilege
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- Keep code comments up-to-date
|
||||||
|
- Update README.md and readme.txt when adding new features
|
||||||
|
- Update wiki documentation in the `.wiki/` directory
|
||||||
|
- Update changelog in both CHANGELOG.md and readme.txt
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
- Create feature branches from `main`
|
||||||
|
- Use descriptive branch names (e.g., `feature/add-settings-page`)
|
||||||
|
- Make atomic commits with clear messages
|
||||||
|
- Create pull requests for review
|
||||||
|
- Tag releases with version numbers (e.g., `v1.0.0`)
|
||||||
|
|
||||||
|
## Developer Preferences
|
||||||
|
|
||||||
|
When working with this repository, remember these preferences:
|
||||||
|
|
||||||
|
1. Follow WordPress coding standards
|
||||||
|
2. Use OOP approach with namespaced classes
|
||||||
|
3. Keep code modular and maintainable
|
||||||
|
3. Reference these preferences in future interactions
|
||||||
|
|
||||||
|
This ensures consistency across coding sessions and reduces the need for developers to repeatedly explain their preferences.
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
# Bug Fixing Guide for AI Assistants
|
||||||
|
|
||||||
|
This document provides guidance for AI assistants to help with bug fixing for the Fix Plugin Does Not Exist Notices plugin.
|
||||||
|
|
||||||
|
## Bug Fixing Workflow
|
||||||
|
|
||||||
|
### 1. Create a Bug Fix Branch
|
||||||
|
|
||||||
|
Always start by creating a bug fix branch from the latest main branch pulled from origin (this step is mandatory):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git pull origin main # Critical step - never skip this
|
||||||
|
git checkout -b fix/bug-description
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a descriptive name that clearly indicates what bug is being fixed. If there's an issue number, include it in the branch name (e.g., `fix/123-plugin-activation-error`).
|
||||||
|
|
||||||
|
For more detailed git workflow guidelines, see **@.ai-workflows/git-workflow.md**.
|
||||||
|
|
||||||
|
### 2. Understand the Bug
|
||||||
|
|
||||||
|
Before fixing a bug, make sure you understand:
|
||||||
|
|
||||||
|
- What is the expected behavior?
|
||||||
|
- What is the actual behavior?
|
||||||
|
- What are the steps to reproduce the bug?
|
||||||
|
- What is the impact of the bug?
|
||||||
|
- What is the root cause of the bug?
|
||||||
|
|
||||||
|
### 3. Fix the Bug
|
||||||
|
|
||||||
|
When fixing a bug:
|
||||||
|
|
||||||
|
- Make minimal changes necessary to fix the bug
|
||||||
|
- Avoid introducing new features while fixing bugs
|
||||||
|
- Maintain backward compatibility
|
||||||
|
- Add appropriate comments explaining the fix
|
||||||
|
- Consider adding tests to prevent regression
|
||||||
|
|
||||||
|
### 4. Update Documentation
|
||||||
|
|
||||||
|
Update relevant documentation to reflect the bug fix:
|
||||||
|
|
||||||
|
- Add a description to CHANGELOG.md under an "Unreleased" section
|
||||||
|
- Update readme.txt if the bug fix affects user-facing functionality
|
||||||
|
|
||||||
|
### 5. Testing
|
||||||
|
|
||||||
|
Test the bug fix thoroughly:
|
||||||
|
|
||||||
|
- Verify that the bug is fixed
|
||||||
|
- Ensure no regression in related functionality
|
||||||
|
- Test with the latest WordPress version
|
||||||
|
- Test with the minimum supported WordPress version (5.0)
|
||||||
|
- Test with PHP 7.0+ (minimum supported version)
|
||||||
|
|
||||||
|
### 6. Commit Changes
|
||||||
|
|
||||||
|
Make atomic commits with clear messages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Fix #123: Brief description of the bug fix"
|
||||||
|
```
|
||||||
|
|
||||||
|
If there's an issue number, reference it in the commit message.
|
||||||
|
|
||||||
|
### 7. Prepare for Release
|
||||||
|
|
||||||
|
When the bug fix is ready to be released:
|
||||||
|
|
||||||
|
1. Create a version branch for the release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Merge your bug fix branch into the version branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git merge fix/bug-description --no-ff
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update version numbers and changelog entries
|
||||||
|
|
||||||
|
4. Follow the standard release process from this point
|
||||||
|
|
||||||
|
### 8. Push to Remote (Optional for Collaboration)
|
||||||
|
|
||||||
|
If you need to collaborate with others on the bug fix before it's ready for release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push github HEAD:fix/bug-description
|
||||||
|
git push gitea HEAD:fix/bug-description
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Create Pull Request (Optional)
|
||||||
|
|
||||||
|
If the repository uses pull requests for code review, create a pull request from the bug fix branch to the version branch.
|
||||||
|
|
||||||
|
## Determining Version Increment
|
||||||
|
|
||||||
|
After fixing a bug and confirming it works, determine the appropriate version increment:
|
||||||
|
|
||||||
|
- **PATCH** (e.g., 1.6.0 → 1.6.1): For most bug fixes that don't change functionality
|
||||||
|
- **MINOR** (e.g., 1.6.0 → 1.7.0): For bug fixes that introduce new features or significant changes
|
||||||
|
- **MAJOR** (e.g., 1.6.0 → 2.0.0): For bug fixes that introduce breaking changes
|
||||||
|
|
||||||
|
**IMPORTANT**: Don't update version numbers during initial development and testing. Only create a version branch (e.g., `v2.2.3`) and update version numbers when the fix is confirmed working.
|
||||||
|
|
||||||
|
This approach is more time-efficient as it allows you to focus on fixing the bug without worrying about version updates until the fix is confirmed working.
|
||||||
|
|
||||||
|
For detailed guidelines on time-efficient development and testing, see **@.ai-workflows/incremental-development.md**.
|
||||||
|
|
||||||
|
## Testing Previous Versions
|
||||||
|
|
||||||
|
To test a previous version of the plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Checkout a specific tag for testing
|
||||||
|
git checkout v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
|
||||||
|
# Or create a test branch from a specific tag
|
||||||
|
git checkout v{MAJOR}.{MINOR}.{PATCH} -b test/some-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hotfix Process
|
||||||
|
|
||||||
|
For critical bugs that need immediate fixing in a released version:
|
||||||
|
|
||||||
|
### 1. Create a Hotfix Branch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
git checkout -b hotfix/v{MAJOR}.{MINOR}.{PATCH+1}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Fix the Issues
|
||||||
|
|
||||||
|
Apply the minimal fix necessary to address the critical issue.
|
||||||
|
|
||||||
|
### 3. Update Version Numbers
|
||||||
|
|
||||||
|
Increment the PATCH version and update all version numbers:
|
||||||
|
|
||||||
|
- Main plugin file (fix-plugin-does-not-exist-notices.php)
|
||||||
|
- FPDEN_VERSION constant
|
||||||
|
- CHANGELOG.md
|
||||||
|
- readme.txt
|
||||||
|
- README.md
|
||||||
|
- languages/fix-plugin-does-not-exist-notices.pot (Project-Id-Version)
|
||||||
|
|
||||||
|
### 4. Commit and Push
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Hotfix: Brief description of the critical bug fix"
|
||||||
|
git push github HEAD:hotfix/v{MAJOR}.{MINOR}.{PATCH+1}
|
||||||
|
git push gitea HEAD:hotfix/v{MAJOR}.{MINOR}.{PATCH+1}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Create and Push Tag
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v{MAJOR}.{MINOR}.{PATCH+1} -m "Hotfix release version {MAJOR}.{MINOR}.{PATCH+1}"
|
||||||
|
git push github refs/tags/v{MAJOR}.{MINOR}.{PATCH+1}
|
||||||
|
git push gitea refs/tags/v{MAJOR}.{MINOR}.{PATCH+1}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Bug Types and Fixing Strategies
|
||||||
|
|
||||||
|
### WordPress Compatibility Issues
|
||||||
|
|
||||||
|
- Test with the specific WordPress version where the issue occurs
|
||||||
|
- Check for deprecated functions or hooks
|
||||||
|
- Review WordPress changelog for relevant changes
|
||||||
|
|
||||||
|
### PHP Compatibility Issues
|
||||||
|
|
||||||
|
- Test with the specific PHP version where the issue occurs
|
||||||
|
- Check for deprecated PHP functions or features
|
||||||
|
- Use appropriate polyfills if necessary
|
||||||
|
|
||||||
|
### JavaScript Issues
|
||||||
|
|
||||||
|
- Test in different browsers
|
||||||
|
- Check for browser console errors
|
||||||
|
- Consider browser-specific workarounds if necessary
|
||||||
|
|
||||||
|
### CSS Issues
|
||||||
|
|
||||||
|
- Test in different browsers and screen sizes
|
||||||
|
- Use browser developer tools to inspect elements
|
||||||
|
- Consider browser-specific workarounds if necessary
|
||||||
|
|
||||||
|
### Database Issues
|
||||||
|
|
||||||
|
- Use proper database prefixing
|
||||||
|
- Sanitize database inputs
|
||||||
|
- Use prepared statements for queries
|
||||||
|
- Consider database version differences
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# Code Review Guide for AI Assistants
|
||||||
|
|
||||||
|
This document provides guidance for AI assistants to help with code review for the Fix Plugin Does Not Exist Notices plugin.
|
||||||
|
|
||||||
|
## Code Review Checklist
|
||||||
|
|
||||||
|
When reviewing code, check for the following:
|
||||||
|
|
||||||
|
### Functionality
|
||||||
|
|
||||||
|
- [ ] Does the code work as expected?
|
||||||
|
- [ ] Does it handle edge cases appropriately?
|
||||||
|
- [ ] Are there any logical errors?
|
||||||
|
- [ ] Is error handling implemented properly?
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- [ ] Does the code follow WordPress coding standards?
|
||||||
|
- [ ] Is the code well-organized and easy to understand?
|
||||||
|
- [ ] Are there any code smells (duplicate code, overly complex functions, etc.)?
|
||||||
|
- [ ] Are functions and variables named appropriately?
|
||||||
|
- [ ] Are there appropriate comments and documentation?
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- [ ] Is user input properly validated and sanitized?
|
||||||
|
- [ ] Is output properly escaped?
|
||||||
|
- [ ] Are capability checks used for user actions?
|
||||||
|
- [ ] Are nonces used for form submissions?
|
||||||
|
- [ ] Are there any potential SQL injection vulnerabilities?
|
||||||
|
- [ ] Are there any potential XSS vulnerabilities?
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- [ ] Are there any performance bottlenecks?
|
||||||
|
- [ ] Are database queries optimized?
|
||||||
|
- [ ] Is caching used appropriately?
|
||||||
|
- [ ] Are assets (CSS, JS) properly enqueued?
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
|
||||||
|
- [ ] Is the code compatible with the minimum supported WordPress version (5.0)?
|
||||||
|
- [ ] Is the code compatible with the minimum supported PHP version (7.0)?
|
||||||
|
- [ ] Are there any browser compatibility issues?
|
||||||
|
- [ ] Are there any conflicts with other plugins?
|
||||||
|
|
||||||
|
### Internationalization
|
||||||
|
|
||||||
|
- [ ] Are all user-facing strings translatable?
|
||||||
|
- [ ] Is the correct text domain used?
|
||||||
|
- [ ] Are translation functions used correctly?
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- [ ] Does the code follow accessibility best practices?
|
||||||
|
- [ ] Are ARIA attributes used appropriately?
|
||||||
|
- [ ] Is keyboard navigation supported?
|
||||||
|
- [ ] Is screen reader support implemented?
|
||||||
|
|
||||||
|
## Code Review Process
|
||||||
|
|
||||||
|
### 1. Understand the Context
|
||||||
|
|
||||||
|
Before reviewing code, understand:
|
||||||
|
- What problem is the code trying to solve?
|
||||||
|
- What are the requirements?
|
||||||
|
- What are the constraints?
|
||||||
|
|
||||||
|
### 2. Review the Code
|
||||||
|
|
||||||
|
Review the code with the checklist above in mind.
|
||||||
|
|
||||||
|
### 3. Provide Feedback
|
||||||
|
|
||||||
|
When providing feedback:
|
||||||
|
- Be specific and clear
|
||||||
|
- Explain why a change is needed
|
||||||
|
- Provide examples or suggestions when possible
|
||||||
|
- Prioritize feedback (critical issues vs. minor improvements)
|
||||||
|
- Be constructive and respectful
|
||||||
|
|
||||||
|
### 4. Follow Up
|
||||||
|
|
||||||
|
After the code has been updated:
|
||||||
|
- Review the changes
|
||||||
|
- Verify that issues have been addressed
|
||||||
|
- Provide additional feedback if necessary
|
||||||
|
|
||||||
|
## Common Issues to Look For
|
||||||
|
|
||||||
|
### PHP Issues
|
||||||
|
|
||||||
|
- Undefined variables or functions
|
||||||
|
- Incorrect function parameters
|
||||||
|
- Missing return statements
|
||||||
|
- Improper error handling
|
||||||
|
- Inefficient loops or conditionals
|
||||||
|
- Hardcoded values that should be configurable
|
||||||
|
|
||||||
|
### WordPress-Specific Issues
|
||||||
|
|
||||||
|
- Incorrect hook usage
|
||||||
|
- Missing or incorrect nonces
|
||||||
|
- Missing capability checks
|
||||||
|
- Direct database queries instead of using WordPress functions
|
||||||
|
- Improper enqueuing of scripts and styles
|
||||||
|
- Not using WordPress functions for common tasks
|
||||||
|
|
||||||
|
### JavaScript Issues
|
||||||
|
|
||||||
|
- Undefined variables or functions
|
||||||
|
- Event listener memory leaks
|
||||||
|
- jQuery conflicts
|
||||||
|
- Browser compatibility issues
|
||||||
|
- Missing error handling
|
||||||
|
|
||||||
|
### CSS Issues
|
||||||
|
|
||||||
|
- Browser compatibility issues
|
||||||
|
- Specificity issues
|
||||||
|
- Unused styles
|
||||||
|
- Overriding WordPress admin styles inappropriately
|
||||||
|
|
||||||
|
## Example Feedback
|
||||||
|
|
||||||
|
### Good Feedback Example
|
||||||
|
|
||||||
|
```
|
||||||
|
In function `handle_remove_reference()`:
|
||||||
|
|
||||||
|
1. The nonce check is missing, which could lead to CSRF vulnerabilities.
|
||||||
|
Consider adding:
|
||||||
|
```php
|
||||||
|
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'fpden_remove_reference')) {
|
||||||
|
wp_die(__('Security check failed.', 'fix-plugin-does-not-exist-notices'));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The user capability check should be more specific. Instead of:
|
||||||
|
```php
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
```
|
||||||
|
Consider using:
|
||||||
|
```php
|
||||||
|
if (!current_user_can('activate_plugins')) {
|
||||||
|
```
|
||||||
|
This is more appropriate for the action being performed.
|
||||||
|
|
||||||
|
3. The success message should be translatable:
|
||||||
|
```php
|
||||||
|
// Change this:
|
||||||
|
add_settings_error('fpden', 'fpden_removed', 'Plugin reference removed successfully.', 'updated');
|
||||||
|
|
||||||
|
// To this:
|
||||||
|
add_settings_error('fpden', 'fpden_removed', __('Plugin reference removed successfully.', 'fix-plugin-does-not-exist-notices'), 'updated');
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
### Poor Feedback Example
|
||||||
|
|
||||||
|
```
|
||||||
|
This code has security issues and doesn't follow best practices. Fix it.
|
||||||
|
```
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Developer Preferences Memory
|
||||||
|
|
||||||
|
This document serves as a persistent memory for developer preferences established during coding sessions. AI assistants should refer to this document to understand the developer's preferences and update it as new preferences are established.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
- Maintain a consistent record of developer preferences across coding sessions
|
||||||
|
- Ensure AI assistants can provide assistance that aligns with the developer's preferred coding style and practices
|
||||||
|
- Reduce the need for developers to repeatedly explain their preferences
|
||||||
|
|
||||||
|
## How to Use This Document
|
||||||
|
|
||||||
|
- **AI Assistants**: Review this document before providing assistance. Update it when new preferences are established through user feedback.
|
||||||
|
- **Developers**: Reference this document to see what preferences have been recorded. Feel free to edit it directly to add or modify preferences.
|
||||||
|
|
||||||
|
## Recorded Preferences
|
||||||
|
|
||||||
|
### File and Directory Structure
|
||||||
|
|
||||||
|
- Prefer lowercase filenames for consistency across the codebase
|
||||||
|
- Use unique folder names following best practices
|
||||||
|
- Folder references should be easily identifiable when using @mentions in AI-assisted coding
|
||||||
|
- Admin-specific functionality should be in the `admin/lib/` directory
|
||||||
|
- Core plugin functionality should be in the `includes/` directory
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- Follow WordPress coding standards
|
||||||
|
- Use OOP best practices for WordPress plugins
|
||||||
|
- Create modular, maintainable, and efficient code structure
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Prefer token-efficient documentation in `.ai-assistant.md` that references `.ai-workflows/` files
|
||||||
|
- Document the release workflow in `.ai-assistant.md` and `.ai-workflows/release-process.md`
|
||||||
|
- Store environment variable documentation in `.ai-workflows/local-env-vars.md`
|
||||||
|
- Maintain consistent documentation across readme.txt, README.md, and CHANGELOG.md
|
||||||
|
|
||||||
|
### Asset Organization
|
||||||
|
|
||||||
|
- Store banner, icon, and screenshot images in `.wordpress-org/assets/`
|
||||||
|
- Store WORDPRESS_ORG files within `/wordpress-org`
|
||||||
|
- Organize files in `/assets` into relevant `/admin` folders
|
||||||
|
|
||||||
|
### Version Control
|
||||||
|
|
||||||
|
- Use standard Git practices for version control and code management
|
||||||
|
- When updating plugin versions, create a GitHub tag and trigger GitHub actions
|
||||||
|
- Follow a specific release process with proper tagging and GitHub releases
|
||||||
|
- Ensure commits are merged to the main branch as Git Updater pulls data from the readme.txt file in the primary branch
|
||||||
|
|
||||||
|
### Plugin Development
|
||||||
|
|
||||||
|
- Prefer simpler solutions over complex ones for plugins
|
||||||
|
- Use a specific formatting style for the CHANGELOG.md file, using #### for section headings
|
||||||
|
- When updating plugin versions, remember to update language files (POT/PO)
|
||||||
|
- Comment out redundant code during testing
|
||||||
|
|
||||||
|
### Potential AI Assised IDE Issues
|
||||||
|
|
||||||
|
- Check for non-standard local terminal commandline customisations that might not be understood by the AI IDE in its terminal useage and cause errors in execution or confusion in not seeing expected results, and advise on how to resolve
|
||||||
|
- Check for non-standard or multiple python and node.js versions, including homebrew versions, that might not be understood by the AI IDE in its terminal useage and cause errors in execution or confusion in not seeing expected results, and advise on how to resolve
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
# Feature Development Guide for AI Assistants
|
||||||
|
|
||||||
|
This document provides guidance for AI assistants to help with feature development for the Fix Plugin Does Not Exist Notices plugin.
|
||||||
|
|
||||||
|
## Feature Development Workflow
|
||||||
|
|
||||||
|
### 1. Create a Feature Branch
|
||||||
|
|
||||||
|
Always start by creating a feature branch from the latest main branch pulled from origin (this step is mandatory):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git pull origin main # Critical step - never skip this
|
||||||
|
git checkout -b feature/descriptive-name
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a descriptive name that clearly indicates what the feature is about. If there's an issue number, include it in the branch name (e.g., `feature/123-update-source-selector`).
|
||||||
|
|
||||||
|
For more detailed git workflow guidelines, see **@.ai-workflows/git-workflow.md**.
|
||||||
|
|
||||||
|
### 2. Implement the Feature
|
||||||
|
|
||||||
|
When implementing a new feature:
|
||||||
|
|
||||||
|
- Follow WordPress coding standards
|
||||||
|
- Ensure all strings are translatable
|
||||||
|
- Add appropriate comments
|
||||||
|
- Consider performance implications
|
||||||
|
- Maintain backward compatibility
|
||||||
|
- Review reference plugins in the `reference-plugins/` directory for inspiration and best practices
|
||||||
|
|
||||||
|
### 3. Update Documentation
|
||||||
|
|
||||||
|
Update relevant documentation to reflect the new feature:
|
||||||
|
|
||||||
|
- Add a description to CHANGELOG.md under an "Unreleased" section
|
||||||
|
- Update readme.txt if the feature affects user-facing functionality
|
||||||
|
- Update README.md with the new feature description
|
||||||
|
- Update inline documentation/comments
|
||||||
|
- Update wiki documentation in the `.wiki` directory:
|
||||||
|
- Create or update feature-specific pages
|
||||||
|
- Update the Home.md page if necessary
|
||||||
|
- Add the feature to any relevant existing pages
|
||||||
|
- Add screenshots or examples if applicable
|
||||||
|
- Remember that any feature addition will require a version increment in all relevant files
|
||||||
|
|
||||||
|
For detailed guidelines on maintaining wiki documentation, see **@.ai-workflows/wiki-documentation.md**.
|
||||||
|
|
||||||
|
### 4. Testing
|
||||||
|
|
||||||
|
Test the feature thoroughly:
|
||||||
|
|
||||||
|
- Test with the latest WordPress version
|
||||||
|
- Test with the minimum supported WordPress version (5.0)
|
||||||
|
- Test with PHP 7.0+ (minimum supported version)
|
||||||
|
- Test in different environments (if possible)
|
||||||
|
|
||||||
|
### 5. Commit Changes
|
||||||
|
|
||||||
|
Make atomic commits with clear messages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Add feature: descriptive name"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Prepare for Release
|
||||||
|
|
||||||
|
When the feature is ready to be released:
|
||||||
|
|
||||||
|
1. Create a version branch with the appropriate version number (typically increment the minor version for features):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: from 2.2.0 to 2.3.0
|
||||||
|
git checkout -b v{MAJOR}.{MINOR+1}.0
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Now update version numbers in all required files:
|
||||||
|
|
||||||
|
- Main plugin file (wp-fix-plugin-does-not-exist-notices.php)
|
||||||
|
- CHANGELOG.md (add a new version section)
|
||||||
|
- readme.txt
|
||||||
|
- README.md
|
||||||
|
- languages/wp-fix-plugin-does-not-exist-notices.pot (Project-Id-Version)
|
||||||
|
|
||||||
|
3. Commit the version updates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Version {MAJOR}.{MINOR+1}.0 - [brief description]"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Tag the version as stable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v{MAJOR}.{MINOR+1}.0-stable -m "Stable version {MAJOR}.{MINOR+1}.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Follow the standard release process from this point
|
||||||
|
|
||||||
|
**IMPORTANT**: Don't update version numbers during initial development and testing. Only create a version branch and update version numbers when the feature is confirmed working.
|
||||||
|
|
||||||
|
For detailed guidelines on time-efficient development and testing, see **@.ai-workflows/incremental-development.md**.
|
||||||
|
|
||||||
|
### 7. Push to Remote (Optional for Collaboration)
|
||||||
|
|
||||||
|
If you need to collaborate with others on the feature before it's ready for release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push github HEAD:feature/descriptive-name
|
||||||
|
git push gitea HEAD:feature/descriptive-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Create Pull Request (Optional)
|
||||||
|
|
||||||
|
If the repository uses pull requests for code review, create a pull request from the feature branch to the version branch.
|
||||||
|
|
||||||
|
## Code Standards and Best Practices
|
||||||
|
|
||||||
|
### PHP Coding Standards
|
||||||
|
|
||||||
|
- Follow [WordPress PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/)
|
||||||
|
- Use tabs for indentation, not spaces
|
||||||
|
- Use proper naming conventions:
|
||||||
|
- Class names: `Class_Name`
|
||||||
|
- Function names: `function_name`
|
||||||
|
- Variable names: `$variable_name`
|
||||||
|
|
||||||
|
### JavaScript Coding Standards
|
||||||
|
|
||||||
|
- Follow [WordPress JavaScript Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/)
|
||||||
|
- Use tabs for indentation, not spaces
|
||||||
|
- Use proper naming conventions:
|
||||||
|
- Function names: `functionName`
|
||||||
|
- Variable names: `variableName`
|
||||||
|
|
||||||
|
### Internationalization (i18n)
|
||||||
|
|
||||||
|
- Wrap all user-facing strings in appropriate translation functions:
|
||||||
|
- `__()` for simple strings
|
||||||
|
- `_e()` for echoed strings
|
||||||
|
- `esc_html__()` for escaped strings
|
||||||
|
- `esc_html_e()` for escaped and echoed strings
|
||||||
|
- Always use the plugin's text domain: `fix-plugin-does-not-exist-notices`
|
||||||
|
|
||||||
|
### Security Best Practices
|
||||||
|
|
||||||
|
- Validate and sanitize all input
|
||||||
|
- Escape all output
|
||||||
|
- Use nonces for form submissions
|
||||||
|
- Use capability checks for user actions
|
||||||
|
|
||||||
|
## Working in Multi-Repository Workspaces
|
||||||
|
|
||||||
|
When developing features in a workspace with multiple repositories:
|
||||||
|
|
||||||
|
1. **Verify Repository Context**:
|
||||||
|
- Confirm you're working in the correct repository before suggesting or implementing features
|
||||||
|
- Use `pwd` and `git remote -v` to verify the current repository
|
||||||
|
|
||||||
|
2. **Feature Verification**:
|
||||||
|
- Before implementing a feature, verify it doesn't already exist in the current repository
|
||||||
|
- Don't assume features from other repositories should be implemented in this one
|
||||||
|
- Use `codebase-retrieval` to search for existing functionality
|
||||||
|
|
||||||
|
3. **Repository-Specific Implementation**:
|
||||||
|
- Implement features appropriate for this specific plugin's purpose
|
||||||
|
- Maintain consistency with the current repository's architecture and coding style
|
||||||
|
- Don't copy code directly from other repositories without adaptation
|
||||||
|
|
||||||
|
4. **Cross-Repository Inspiration**:
|
||||||
|
- If implementing a feature inspired by another repository, explicitly note that it's a new feature
|
||||||
|
- Adapt the feature to fit the current repository's needs and architecture
|
||||||
|
- Document the inspiration source in code comments
|
||||||
|
|
||||||
|
For detailed guidelines on working in multi-repository workspaces, see **@.ai-workflows/multi-repo-workspace.md**.
|
||||||
|
|
||||||
|
## Feature Types and Implementation Guidelines
|
||||||
|
|
||||||
|
### Admin Interface Features
|
||||||
|
|
||||||
|
When adding features to the admin interface:
|
||||||
|
|
||||||
|
- Use WordPress admin UI components for consistency
|
||||||
|
- Follow WordPress admin UI patterns
|
||||||
|
- Ensure accessibility compliance
|
||||||
|
- Add appropriate help text
|
||||||
|
|
||||||
|
### Plugin Functionality Features
|
||||||
|
|
||||||
|
When adding core functionality:
|
||||||
|
|
||||||
|
- Ensure compatibility with WordPress hooks system
|
||||||
|
- Consider performance impact
|
||||||
|
- Maintain backward compatibility
|
||||||
|
- Add appropriate error handling
|
||||||
|
|
||||||
|
### Integration Features
|
||||||
|
|
||||||
|
When adding integration with other plugins or services:
|
||||||
|
|
||||||
|
- Make integrations optional when possible
|
||||||
|
- Check if the integrated plugin/service is available before using it
|
||||||
|
- Provide fallback functionality when the integration is not available
|
||||||
|
- Document the integration requirements
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Folder Structure
|
||||||
|
|
||||||
|
This document outlines the folder structure of the plugin and explains the purpose of each directory.
|
||||||
|
|
||||||
|
## Root Directories
|
||||||
|
|
||||||
|
- **admin/** - Contains admin-specific functionality and assets
|
||||||
|
- **includes/** - Contains core plugin functionality and classes
|
||||||
|
- **languages/** - Contains translation files
|
||||||
|
- **scripts/** - Contains build and deployment scripts
|
||||||
|
- **.ai-workflows/** - Contains documentation for AI assistants
|
||||||
|
- **.github/** - Contains GitHub-specific files like workflows
|
||||||
|
- **.wordpress-org/** - Contains WordPress.org assets like banners and screenshots
|
||||||
|
|
||||||
|
## Admin Directory Structure
|
||||||
|
|
||||||
|
- **admin/css/** - Admin-specific CSS files
|
||||||
|
- **admin/js/** - Admin-specific JavaScript files
|
||||||
|
- **admin/images/** - Admin-specific images
|
||||||
|
- **admin/partials/** - Admin-specific template partials
|
||||||
|
- **admin/settings/** - Admin settings functionality
|
||||||
|
- **admin/tools/** - Admin tools functionality
|
||||||
|
- **admin/lib/** - Admin-specific library files and helper functions
|
||||||
|
- **admin/lib/admin.php** - Admin class for handling admin-specific functionality
|
||||||
|
- **admin/lib/modal.php** - Modal class for handling the update source selector modal
|
||||||
|
|
||||||
|
## Includes Directory
|
||||||
|
|
||||||
|
The `includes/` directory contains the core plugin functionality:
|
||||||
|
|
||||||
|
- **includes/core.php** - Core class for handling the main plugin functionality
|
||||||
|
- **includes/plugin.php** - Main plugin class that initializes all components
|
||||||
|
- **includes/updater.php** - Updater class for handling plugin updates
|
||||||
|
|
||||||
|
## File Naming Conventions
|
||||||
|
|
||||||
|
- All PHP files in the `includes/` directory use lowercase filenames
|
||||||
|
- All directories use lowercase names
|
||||||
|
- JavaScript and CSS files use kebab-case (e.g., `update-source-selector.js`)
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Unique Directory Names**: Each directory should have a unique name to avoid confusion
|
||||||
|
2. **Logical Organization**: Files should be organized logically by function
|
||||||
|
3. **Consistent Naming**: File and directory names should follow consistent naming conventions
|
||||||
|
4. **Clear Separation**: Admin functionality should be separate from core functionality
|
||||||
|
5. **Minimal Dependencies**: Files should have minimal dependencies on other files
|
||||||
|
|
||||||
|
## @mentions for AI Assistants
|
||||||
|
|
||||||
|
When referring to files or directories in AI conversations, use the following format:
|
||||||
|
|
||||||
|
- **@includes/plugin.php** - Main plugin class
|
||||||
|
- **@includes/core.php** - Core functionality
|
||||||
|
- **@admin/lib/admin.php** - Admin functionality
|
||||||
|
- **@admin/lib/modal.php** - Modal functionality
|
||||||
|
- **@includes/updater.php** - Updater functionality
|
||||||
|
- **@admin/js/update-source-selector.js** - Update source selector JavaScript
|
||||||
|
- **@admin/css/update-source-selector.css** - Update source selector CSS
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
# Git Workflow Guide for AI Assistants
|
||||||
|
|
||||||
|
This document provides guidance for AI assistants to help with git workflow management for the Fix Plugin Does Not Exist Notices plugin.
|
||||||
|
|
||||||
|
## Core Git Workflow Principles
|
||||||
|
|
||||||
|
### 1. Always Start from Latest Main Branch
|
||||||
|
|
||||||
|
Before creating any new branch, always ensure you're working with the latest code from the main branch by pulling from the origin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
This critical step ensures that your new branch includes all the latest changes from the remote repository and reduces the chance of merge conflicts later. Never skip this step, as working from an outdated main branch can lead to integration problems.
|
||||||
|
|
||||||
|
### 2. One Issue Per Branch
|
||||||
|
|
||||||
|
Create a separate branch for each issue or feature you're working on:
|
||||||
|
|
||||||
|
- For bug fixes: `fix/issue-description` or `fix/issue-number-description`
|
||||||
|
- For features: `feature/descriptive-name`
|
||||||
|
- For small improvements: `patch/descriptive-name`
|
||||||
|
- For code restructuring: `refactor/descriptive-name`
|
||||||
|
|
||||||
|
**Important**: Use descriptive names without version numbers for development branches. This allows focusing on the changes without worrying about version updates until the changes are confirmed working.
|
||||||
|
|
||||||
|
Only create version branches (e.g., `v2.2.3`) when changes are ready for release, and only then update version numbers in files.
|
||||||
|
|
||||||
|
This approach keeps changes focused, makes code review easier, and provides clear rollback points if needed.
|
||||||
|
|
||||||
|
### 3. Pull Request for Each Issue
|
||||||
|
|
||||||
|
Create a separate pull request for each issue or feature. This ensures:
|
||||||
|
|
||||||
|
- Each change can be reviewed independently
|
||||||
|
- Issues can be merged as soon as they're ready
|
||||||
|
- Changes can be reverted individually if needed
|
||||||
|
- CI/CD checks can run on focused changes
|
||||||
|
|
||||||
|
## Detailed Workflow
|
||||||
|
|
||||||
|
### Starting a New Task
|
||||||
|
|
||||||
|
1. **Update Main Branch from Origin**
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
This step is mandatory before creating any new branch to ensure you're working with the latest code.
|
||||||
|
|
||||||
|
2. **Create a New Branch**
|
||||||
|
```bash
|
||||||
|
git checkout -b [branch-type]/[description]
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```bash
|
||||||
|
git checkout -b fix/123-plugin-activation-error
|
||||||
|
git checkout -b feature/update-source-selector
|
||||||
|
git checkout -b patch/2.2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Make Your Changes**
|
||||||
|
- Make focused changes related only to the specific issue
|
||||||
|
- Commit regularly with clear, descriptive messages
|
||||||
|
- Reference issue numbers in commit messages when applicable
|
||||||
|
|
||||||
|
4. **Testing Approach**
|
||||||
|
|
||||||
|
For efficient development:
|
||||||
|
- **Local Testing (Default)**: Test without updating version numbers
|
||||||
|
```bash
|
||||||
|
# Get current version from plugin file
|
||||||
|
CURRENT_VERSION=$(grep -o "Version: [0-9.]*" wp-fix-plugin-does-not-exist-notices.php | cut -d' ' -f2)
|
||||||
|
|
||||||
|
# Build and deploy with current version
|
||||||
|
./build.sh $CURRENT_VERSION
|
||||||
|
```
|
||||||
|
- **Remote Testing (When Requested)**: Push development branch to remote
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "[Brief description] for remote testing"
|
||||||
|
git push origin [branch-name]
|
||||||
|
```
|
||||||
|
- **Version Creation**: Only when changes are confirmed working
|
||||||
|
```bash
|
||||||
|
# Create version branch
|
||||||
|
git checkout -b v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
|
||||||
|
# Update version numbers in all required files
|
||||||
|
# Commit version updates
|
||||||
|
git add .
|
||||||
|
git commit -m "Version {MAJOR}.{MINOR}.{PATCH} - Brief description"
|
||||||
|
|
||||||
|
# Tag as stable
|
||||||
|
git tag -a v{MAJOR}.{MINOR}.{PATCH}-stable -m "Stable version {MAJOR}.{MINOR}.{PATCH}"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Push Branch to Remote (When Needed)**
|
||||||
|
```bash
|
||||||
|
git push origin [branch-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating a Pull Request
|
||||||
|
|
||||||
|
1. **Ensure Tests Pass Locally**
|
||||||
|
- Run any available tests to ensure your changes work as expected
|
||||||
|
- Fix any issues before creating a pull request
|
||||||
|
|
||||||
|
2. **Create Pull Request**
|
||||||
|
- Create a pull request from your branch to the main branch
|
||||||
|
- Include a clear description of the changes
|
||||||
|
- Reference any related issues
|
||||||
|
- Assign reviewers if appropriate
|
||||||
|
|
||||||
|
3. **Address Review Feedback**
|
||||||
|
- Make requested changes
|
||||||
|
- Push additional commits to the same branch
|
||||||
|
- Respond to comments
|
||||||
|
|
||||||
|
### CI/CD Integration
|
||||||
|
|
||||||
|
Each pull request should pass through CI/CD checks before being merged. This ensures that all changes are compatible with the existing codebase and meet quality standards.
|
||||||
|
|
||||||
|
1. **Automated Tests**
|
||||||
|
- Unit tests
|
||||||
|
- Integration tests
|
||||||
|
- Code style checks
|
||||||
|
- Compatibility checks
|
||||||
|
|
||||||
|
2. **Manual Review**
|
||||||
|
- Code review by team members
|
||||||
|
- Functional testing in test environment
|
||||||
|
- Verification of feature requirements
|
||||||
|
|
||||||
|
3. **Approval Process**
|
||||||
|
- Required approvals before merging
|
||||||
|
- Final checks for conflicts with other pending PRs
|
||||||
|
- Verification that all CI/CD checks have passed
|
||||||
|
|
||||||
|
4. **Compatibility with Unmerged PRs**
|
||||||
|
- When multiple PRs are in progress simultaneously, ensure each PR is compatible with the main branch
|
||||||
|
- For related changes, consider using feature flags to allow independent merging
|
||||||
|
- Document dependencies between PRs in the PR description
|
||||||
|
|
||||||
|
### Handling Concurrent Development
|
||||||
|
|
||||||
|
When working on multiple issues simultaneously:
|
||||||
|
|
||||||
|
1. **Keep Branches Independent**
|
||||||
|
- Always create new branches from the latest main branch pulled from origin, not from other feature branches
|
||||||
|
- This ensures each PR can be merged independently and contains all the latest changes
|
||||||
|
|
||||||
|
2. **Handle Conflicts Proactively**
|
||||||
|
- If main has been updated with other changes while you're working:
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
git checkout your-branch
|
||||||
|
git merge main
|
||||||
|
```
|
||||||
|
- Resolve any conflicts locally before pushing
|
||||||
|
|
||||||
|
3. **Coordinate on Dependent Changes**
|
||||||
|
- If changes depend on each other, note this in the PR description
|
||||||
|
- Consider using the "Depends on #PR-number" notation in PR descriptions
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
When preparing for a release:
|
||||||
|
|
||||||
|
1. **Ensure All Required PRs are Merged**
|
||||||
|
- All features and fixes planned for the release should be merged to main
|
||||||
|
|
||||||
|
2. **Create a Release Branch**
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
git checkout -b v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Follow Standard Release Process**
|
||||||
|
- Update version numbers
|
||||||
|
- Update changelogs
|
||||||
|
- Create tag
|
||||||
|
- See **@.ai-workflows/release-process.md** for complete details
|
||||||
|
|
||||||
|
## Contributing to External Repositories
|
||||||
|
|
||||||
|
When working on issues for external repositories (pull/merge requests):
|
||||||
|
|
||||||
|
### 1. Clearly Indicate Testing Status
|
||||||
|
|
||||||
|
In the PR description and comments, clearly indicate the testing status:
|
||||||
|
|
||||||
|
- **Not tested**: "This PR addresses [issue] but has not been tested locally or remotely. It's ready for community/maintainer testing."
|
||||||
|
- **Locally tested**: "This PR has been tested in a local WordPress environment and [describe results]."
|
||||||
|
- **Remotely tested**: "This PR has been tested with a remote build and [describe results]."
|
||||||
|
|
||||||
|
### 2. Provide Testing Instructions
|
||||||
|
|
||||||
|
Include clear instructions for maintainers on how to test the changes:
|
||||||
|
|
||||||
|
- Steps to reproduce the original issue (if applicable)
|
||||||
|
- Steps to verify the fix or feature
|
||||||
|
- Any specific environments or configurations needed for testing
|
||||||
|
|
||||||
|
### 3. Be Responsive to Feedback
|
||||||
|
|
||||||
|
Monitor the PR for feedback from maintainers and be prepared to make additional changes if requested.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
|
||||||
|
- Use present tense ("Add feature" not "Added feature")
|
||||||
|
- Start with a verb
|
||||||
|
- Keep the first line under 50 characters
|
||||||
|
- Reference issues when relevant: "Fix #123: Resolve plugin detection issue"
|
||||||
|
- For more complex changes, add a detailed description after the first line
|
||||||
|
|
||||||
|
### Branch Management
|
||||||
|
|
||||||
|
- Delete branches after they've been merged
|
||||||
|
- Keep branch names descriptive but concise
|
||||||
|
- Use consistent naming conventions
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
- Review code thoroughly before approving
|
||||||
|
- Test changes locally when possible
|
||||||
|
- Provide constructive feedback
|
||||||
|
- See **@.ai-workflows/code-review.md** for detailed code review guidelines
|
||||||
|
|
||||||
|
### Suggested Improvements
|
||||||
|
|
||||||
|
If you identify potential improvements outside the scope of the current issue:
|
||||||
|
|
||||||
|
1. **Document the Suggestion**
|
||||||
|
- Note the suggestion in the PR comments
|
||||||
|
- Create a new issue for the suggestion
|
||||||
|
- Be specific about the benefits and implementation details
|
||||||
|
|
||||||
|
2. **Create a Separate Branch**
|
||||||
|
- Don't include unrelated improvements in the current PR
|
||||||
|
- Create a new branch from the latest main branch for the suggested improvement
|
||||||
|
- Submit a separate PR for the suggestion
|
||||||
|
|
||||||
|
3. **Ensure Compatibility**
|
||||||
|
- Make sure the suggested improvement is compatible with any unmerged PRs
|
||||||
|
- If the improvement depends on changes in another PR, note this dependency
|
||||||
|
- Consider how the improvement will interact with other pending changes
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
# Incremental Development and Testing Guide
|
||||||
|
|
||||||
|
This document provides guidance for AI assistants to help with incremental development and testing for the Fix Plugin Does Not Exist Notices plugin.
|
||||||
|
|
||||||
|
## Time-Efficient Development Principles
|
||||||
|
|
||||||
|
### Branch Naming for Development
|
||||||
|
|
||||||
|
1. **Initial Development Branches**
|
||||||
|
- Use descriptive names without version numbers:
|
||||||
|
- `fix/issue-description` - For bug fixes
|
||||||
|
- `feature/descriptive-name` - For new features
|
||||||
|
- `patch/descriptive-name` - For small improvements
|
||||||
|
- `refactor/descriptive-name` - For code restructuring
|
||||||
|
- **Don't update version numbers** during this phase
|
||||||
|
- Focus on implementing and testing the changes
|
||||||
|
|
||||||
|
2. **Version Branches**
|
||||||
|
- Only create after changes are confirmed working:
|
||||||
|
- `v{MAJOR}.{MINOR}.{PATCH}` (e.g., `v2.2.3`)
|
||||||
|
- Only update version numbers at this point
|
||||||
|
- This minimizes unnecessary version updates
|
||||||
|
|
||||||
|
### Version Numbering Guidelines
|
||||||
|
|
||||||
|
1. **Patch Versions (X.Y.Z → X.Y.Z+1)**
|
||||||
|
- Use for bug fixes and small improvements
|
||||||
|
- Example: `v2.2.3`
|
||||||
|
|
||||||
|
2. **Minor Versions (X.Y.Z → X.Y+1.0)**
|
||||||
|
- Use for new features or significant improvements
|
||||||
|
- Example: `v2.3.0`
|
||||||
|
|
||||||
|
3. **Major Versions (X.Y.Z → X+1.0.0)**
|
||||||
|
- Only increment when numerous features and fixes are tested and confirmed stable
|
||||||
|
- Reserved for breaking changes or significant overhauls
|
||||||
|
- Example: `v3.0.0`
|
||||||
|
|
||||||
|
### Marking Stable Versions
|
||||||
|
|
||||||
|
When the user confirms that changes are working correctly:
|
||||||
|
1. Create a version branch and update version numbers
|
||||||
|
2. Tag the version branch as stable
|
||||||
|
```bash
|
||||||
|
git tag -a v{MAJOR}.{MINOR}.{PATCH}-stable -m "Stable version {MAJOR}.{MINOR}.{PATCH}"
|
||||||
|
```
|
||||||
|
3. Document in the PR or issue that this version has been confirmed stable
|
||||||
|
|
||||||
|
## Local Testing Workflow
|
||||||
|
|
||||||
|
### 1. Create a Descriptive Branch for Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure you have the latest main branch
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Create a descriptive branch (without version numbers)
|
||||||
|
git checkout -b fix/plugin-activation-error
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Make Changes Without Updating Version Numbers
|
||||||
|
|
||||||
|
During the development and testing phase:
|
||||||
|
- Implement the necessary changes
|
||||||
|
- **Don't update version numbers** in any files yet
|
||||||
|
- Focus on the functionality
|
||||||
|
|
||||||
|
### 3. Build and Deploy Locally
|
||||||
|
|
||||||
|
For local testing, use the current version number from the main plugin file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get the current version from the plugin file
|
||||||
|
CURRENT_VERSION=$(grep -o "Version: [0-9.]*" wp-fix-plugin-does-not-exist-notices.php | cut -d' ' -f2)
|
||||||
|
|
||||||
|
# Build and deploy with current version
|
||||||
|
./build.sh $CURRENT_VERSION
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Create a build directory
|
||||||
|
2. Copy required files to the build directory
|
||||||
|
3. Deploy the plugin to your local WordPress testing environment
|
||||||
|
|
||||||
|
**Note**: For local testing iterations, you do not need to commit changes, push to remote repositories, or create tags unless specifically requested.
|
||||||
|
|
||||||
|
### 4. Test and Evaluate
|
||||||
|
|
||||||
|
Test the changes thoroughly in the local environment:
|
||||||
|
- Verify that the specific issue is fixed or feature works as expected
|
||||||
|
- Check for any regressions or new issues
|
||||||
|
- Document the results
|
||||||
|
|
||||||
|
### 5. Based on Testing Results
|
||||||
|
|
||||||
|
- **If changes need further refinement**: Continue working in the same branch
|
||||||
|
- **If changes work as expected**: Proceed to version branch creation
|
||||||
|
|
||||||
|
### 6. Creating a Version Branch
|
||||||
|
|
||||||
|
When changes are confirmed working and ready for release:
|
||||||
|
|
||||||
|
1. Create a version branch with the appropriate version number:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Determine the appropriate version increment (patch, minor, or major)
|
||||||
|
# based on the nature of the changes
|
||||||
|
git checkout -b v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Now update version numbers in all required files:
|
||||||
|
|
||||||
|
- Main plugin file (wp-fix-plugin-does-not-exist-notices.php)
|
||||||
|
- CHANGELOG.md (add a new version section)
|
||||||
|
- readme.txt
|
||||||
|
- README.md
|
||||||
|
- languages/wp-fix-plugin-does-not-exist-notices.pot (Project-Id-Version)
|
||||||
|
|
||||||
|
3. Commit the version updates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Version {MAJOR}.{MINOR}.{PATCH} - [brief description]"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Tag the version as stable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v{MAJOR}.{MINOR}.{PATCH}-stable -m "Stable version {MAJOR}.{MINOR}.{PATCH}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Remote Testing Workflow
|
||||||
|
|
||||||
|
When the user specifically requests remote testing:
|
||||||
|
|
||||||
|
### 1. Commit Changes to Remote Repository
|
||||||
|
|
||||||
|
If testing a development branch (without version updates):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "[brief description] for remote testing"
|
||||||
|
git push origin [branch-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
If testing a version branch (with version updates):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Version {MAJOR}.{MINOR}.{PATCH} - [brief description]"
|
||||||
|
git push origin v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create and Push Tag (For Version Branches Only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v{MAJOR}.{MINOR}.{PATCH} -m "Version {MAJOR}.{MINOR}.{PATCH} for remote testing"
|
||||||
|
git push origin v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will trigger GitHub Actions to build the installable ZIP file.
|
||||||
|
|
||||||
|
### 3. Verify Remote Build
|
||||||
|
|
||||||
|
Check that the GitHub Actions workflow completed successfully and the ZIP file is available for download.
|
||||||
|
|
||||||
|
### 4. Test and Evaluate
|
||||||
|
|
||||||
|
Test the remotely built version and document the results.
|
||||||
|
|
||||||
|
## Contributing to External Repositories
|
||||||
|
|
||||||
|
When working on issues for external repositories (pull/merge requests):
|
||||||
|
|
||||||
|
### 1. Clearly Indicate Testing Status
|
||||||
|
|
||||||
|
In the PR description and comments, clearly indicate the testing status:
|
||||||
|
|
||||||
|
- **Not tested**: "This PR addresses [issue] but has not been tested locally or remotely. It's ready for community/maintainer testing."
|
||||||
|
- **Locally tested**: "This PR has been tested in a local WordPress environment and [describe results]."
|
||||||
|
- **Remotely tested**: "This PR has been tested with a remote build and [describe results]."
|
||||||
|
|
||||||
|
### 2. Provide Testing Instructions
|
||||||
|
|
||||||
|
Include clear instructions for maintainers on how to test the changes:
|
||||||
|
|
||||||
|
- Steps to reproduce the original issue (if applicable)
|
||||||
|
- Steps to verify the fix or feature
|
||||||
|
- Any specific environments or configurations needed for testing
|
||||||
|
|
||||||
|
### 3. Be Responsive to Feedback
|
||||||
|
|
||||||
|
Monitor the PR for feedback from maintainers and be prepared to make additional changes if requested.
|
||||||
|
|
||||||
|
## Rollback Procedure
|
||||||
|
|
||||||
|
If a change causes issues after release:
|
||||||
|
|
||||||
|
### 1. Identify the Last Stable Version
|
||||||
|
|
||||||
|
Find the last version that was marked as stable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -l "*-stable"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create a New Branch from the Stable Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout v{MAJOR}.{MINOR}.{PATCH}-stable
|
||||||
|
git checkout -b fix/rollback-based-fix
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Make Necessary Changes
|
||||||
|
|
||||||
|
Implement the fix based on the stable version. Don't update version numbers yet.
|
||||||
|
|
||||||
|
### 4. Test the Changes
|
||||||
|
|
||||||
|
Test thoroughly to ensure the fix resolves the issues.
|
||||||
|
|
||||||
|
### 5. When Confirmed Working
|
||||||
|
|
||||||
|
Create a version branch with an incremented patch version and update all version numbers as described in the "Creating a Version Branch" section.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Local Development Environment Variables
|
||||||
|
|
||||||
|
This file contains important paths and URLs for local development.
|
||||||
|
|
||||||
|
## Repository Paths
|
||||||
|
- Local development repository: ~/Git/wp-fix-plugin-does-not-exist-notices
|
||||||
|
- LocalWP plugin testing site storage: ~/Local/plugin-testing/app/wp-fix-plugin-does-not-exist-notices
|
||||||
|
- LocalWP plugin testing site configuration: ~/Local/plugin-testing/conf/
|
||||||
|
|
||||||
|
## URLs
|
||||||
|
- LocalWP plugin testing URL: http://plugin-testing.local/
|
||||||
|
- PHP details: http://plugin-testing.local/local-phpinfo.php
|
||||||
|
- XDebug info: http://plugin-testing.local/local-xdebuginfo.php
|
||||||
|
- Adminer Evo: http://localhost:10010/?username=root&db=local
|
||||||
|
- Mailpit: http://localhost:10000/
|
||||||
|
|
||||||
|
## Build and Deploy Scripts
|
||||||
|
- Build script: ~/Git/wp-fix-plugin-does-not-exist-notices/build.sh
|
||||||
|
- Local deploy script: ~/Git/wp-fix-plugin-does-not-exist-notices/deploy-local.sh
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Working in Multi-Repository Workspaces
|
||||||
|
|
||||||
|
This document provides guidelines for AI assistants working in VSCode/VSCodium workspaces that contain multiple repository folders.
|
||||||
|
|
||||||
|
## Understanding Multi-Repository Workspaces
|
||||||
|
|
||||||
|
In VSCode/VSCodium, developers often create workspaces that include multiple repository folders. This allows them to work on related projects simultaneously or reference code from one project while working on another.
|
||||||
|
|
||||||
|
### Common Workspace Configurations
|
||||||
|
|
||||||
|
1. **Multiple WordPress Plugins**: A workspace containing several WordPress plugin repositories
|
||||||
|
2. **Plugin and Theme Combinations**: Repositories for both plugins and themes that work together
|
||||||
|
3. **Reference Repositories**: Including repositories purely for reference or inspiration
|
||||||
|
4. **Shared Libraries**: Repositories containing shared code used across multiple projects
|
||||||
|
|
||||||
|
## Potential Issues in Multi-Repository Workspaces
|
||||||
|
|
||||||
|
### 1. Feature Hallucination
|
||||||
|
|
||||||
|
The most common issue is "feature hallucination" - assuming that features present in one repository should be implemented in another, or documenting non-existent features based on code seen in other repositories.
|
||||||
|
|
||||||
|
### 2. Cross-Repository Code References
|
||||||
|
|
||||||
|
Referencing or suggesting code patterns from one repository when working on another can lead to inconsistent coding styles and approaches.
|
||||||
|
|
||||||
|
### 3. Documentation Confusion
|
||||||
|
|
||||||
|
Creating documentation that includes features or functionality from other repositories in the workspace.
|
||||||
|
|
||||||
|
### 4. Scope Creep
|
||||||
|
|
||||||
|
Suggesting changes or improvements based on other repositories, leading to scope creep and feature bloat.
|
||||||
|
|
||||||
|
## Best Practices for AI Assistants
|
||||||
|
|
||||||
|
### 1. Repository Verification
|
||||||
|
|
||||||
|
**ALWAYS** verify which repository you're currently working in before:
|
||||||
|
- Making code suggestions
|
||||||
|
- Creating or updating documentation
|
||||||
|
- Discussing features or functionality
|
||||||
|
- Implementing new features
|
||||||
|
|
||||||
|
### 2. Explicit Code Search Scoping
|
||||||
|
|
||||||
|
When searching for code or functionality:
|
||||||
|
- Explicitly limit searches to the current repository
|
||||||
|
- Use repository-specific paths in search queries
|
||||||
|
- Verify search results are from the current repository before using them
|
||||||
|
|
||||||
|
### 3. Feature Verification Process
|
||||||
|
|
||||||
|
Before documenting or implementing a feature:
|
||||||
|
|
||||||
|
1. **Check the codebase**: Use the codebase-retrieval tool to search for relevant code in the current repository
|
||||||
|
2. **Verify functionality**: Look for actual implementation, not just references or comments
|
||||||
|
3. **Check documentation**: Review existing documentation to understand intended functionality
|
||||||
|
4. **Ask for clarification**: If uncertain, ask the developer to confirm the feature's existence or scope
|
||||||
|
|
||||||
|
### 4. Documentation Guidelines
|
||||||
|
|
||||||
|
When creating or updating documentation:
|
||||||
|
|
||||||
|
1. **Repository-specific content**: Only document features and functionality that exist in the current repository
|
||||||
|
2. **Verify before documenting**: Check the codebase to confirm features actually exist
|
||||||
|
3. **Clear boundaries**: Make it clear which repository the documentation applies to
|
||||||
|
4. **Accurate feature descriptions**: Describe features as they are implemented, not as they might be in other repositories
|
||||||
|
|
||||||
|
### 5. Cross-Repository Inspiration
|
||||||
|
|
||||||
|
When implementing features inspired by other repositories:
|
||||||
|
|
||||||
|
1. **Explicit attribution**: Clearly state that the feature is inspired by another repository
|
||||||
|
2. **New implementation**: Treat it as a new feature being added, not an existing one
|
||||||
|
3. **Repository-appropriate adaptation**: Adapt the feature to fit the current repository's architecture and style
|
||||||
|
4. **Developer confirmation**: Confirm with the developer that adding the feature is appropriate
|
||||||
|
|
||||||
|
## Repository Context Verification Checklist
|
||||||
|
|
||||||
|
Before making significant changes or recommendations, run through this checklist:
|
||||||
|
|
||||||
|
- [ ] Verified current working directory/repository
|
||||||
|
- [ ] Confirmed repository name and purpose
|
||||||
|
- [ ] Checked that code searches are limited to current repository
|
||||||
|
- [ ] Verified features exist in current repository before documenting them
|
||||||
|
- [ ] Ensured documentation reflects only the current repository's functionality
|
||||||
|
- [ ] Confirmed that any cross-repository inspiration is clearly marked as new functionality
|
||||||
|
|
||||||
|
## Example Verification Workflow
|
||||||
|
|
||||||
|
1. **Check current repository**:
|
||||||
|
```
|
||||||
|
pwd
|
||||||
|
git remote -v
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify feature existence**:
|
||||||
|
```
|
||||||
|
# Use codebase-retrieval to search for the feature
|
||||||
|
# Look for actual implementation, not just references
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Document with clear repository context**:
|
||||||
|
```
|
||||||
|
# Always include repository name in documentation
|
||||||
|
# Be specific about which features are included
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **When suggesting new features**:
|
||||||
|
```
|
||||||
|
# Clearly state if inspired by another repository
|
||||||
|
# Explain why it's appropriate for the current repository
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handling Repository Switching
|
||||||
|
|
||||||
|
When the developer switches between repositories in the workspace:
|
||||||
|
|
||||||
|
1. **Acknowledge the switch**: Confirm the new repository context
|
||||||
|
2. **Reset context**: Don't carry over assumptions from the previous repository
|
||||||
|
3. **Verify new environment**: Check the structure and features of the new repository
|
||||||
|
4. **Update documentation references**: Ensure you're referencing documentation specific to the new repository
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AI Workflows
|
||||||
|
|
||||||
|
This directory contains workflow documentation for AI assistants working with this repository.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- **bug-fixing.md**: Guidelines for identifying and fixing bugs in the codebase
|
||||||
|
- **code-review.md**: Standards and procedures for reviewing code changes
|
||||||
|
- **dev-prefs-memory.md**: Persistent memory of developer preferences
|
||||||
|
- **feature-development.md**: Process for developing new features
|
||||||
|
- **folder-structure.md**: Documentation of the plugin's folder structure and naming conventions
|
||||||
|
- **git-workflow.md**: Detailed git workflow and branch management guidelines
|
||||||
|
- **incremental-development.md**: Time-efficient approach for incremental development and testing
|
||||||
|
- **local-env-vars.md**: Local development environment paths and URLs
|
||||||
|
- **multi-repo-workspace.md**: Guidelines for working in workspaces with multiple repositories
|
||||||
|
- **release-process.md**: Steps for preparing and publishing new releases
|
||||||
|
- **wiki-documentation.md**: Guidelines for maintaining wiki documentation
|
||||||
|
|
||||||
|
These documents help ensure consistent quality and approach when using AI tools to assist with development tasks.
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Release Process Guide for AI Assistants
|
||||||
|
|
||||||
|
This document outlines the process for preparing and publishing new releases of the WordPress Plugin Starter Template.
|
||||||
|
|
||||||
|
## Release Workflow Overview
|
||||||
|
|
||||||
|
1. Create a version branch
|
||||||
|
2. Update version numbers in all required files
|
||||||
|
3. Build and test the plugin
|
||||||
|
4. Commit version changes
|
||||||
|
5. Create version tags
|
||||||
|
6. Push to remote repositories
|
||||||
|
7. Merge into main branch
|
||||||
|
|
||||||
|
## Detailed Steps
|
||||||
|
|
||||||
|
### 1. Create a Version Branch
|
||||||
|
|
||||||
|
Always start by creating a version branch from the latest main branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git pull origin main # Critical step - never skip this
|
||||||
|
git checkout -b v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: `git checkout -b v1.0.0`
|
||||||
|
|
||||||
|
### 2. Update Version Numbers
|
||||||
|
|
||||||
|
Update version numbers in all required files:
|
||||||
|
|
||||||
|
1. **Main plugin file** (wp-plugin-starter-template.php):
|
||||||
|
- Update the `Version:` header
|
||||||
|
- Update the version parameter in the Plugin class instantiation
|
||||||
|
|
||||||
|
2. **readme.txt**:
|
||||||
|
- Update the `Stable tag:` value
|
||||||
|
- Add a new section to the changelog
|
||||||
|
|
||||||
|
3. **README.md**:
|
||||||
|
- Update version references
|
||||||
|
- Add new section to the changelog
|
||||||
|
|
||||||
|
4. **CHANGELOG.md**:
|
||||||
|
- Add a new version section at the top
|
||||||
|
|
||||||
|
5. **languages/wp-plugin-starter-template.pot**:
|
||||||
|
- Update the `Project-Id-Version` header
|
||||||
|
|
||||||
|
### 3. Build and Test the Plugin
|
||||||
|
|
||||||
|
Build the plugin with the new version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh {MAJOR}.{MINOR}.{PATCH}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Create a clean build in the `build/` directory
|
||||||
|
- Generate a ZIP file for distribution
|
||||||
|
- Deploy to a local WordPress installation if configured
|
||||||
|
|
||||||
|
Test the plugin thoroughly:
|
||||||
|
- Test with the latest WordPress version
|
||||||
|
- Test with the minimum supported WordPress version (5.0)
|
||||||
|
- Test with PHP 7.0+ (minimum supported version)
|
||||||
|
- Test all features and functionality
|
||||||
|
|
||||||
|
### 4. Commit Version Changes
|
||||||
|
|
||||||
|
Commit all version changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add wp-plugin-starter-template.php readme.txt README.md CHANGELOG.md languages/wp-plugin-starter-template.pot
|
||||||
|
git commit -m "Version {MAJOR}.{MINOR}.{PATCH} - [brief description]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Create Version Tags
|
||||||
|
|
||||||
|
Create version tags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v{MAJOR}.{MINOR}.{PATCH} -m "Version {MAJOR}.{MINOR}.{PATCH}"
|
||||||
|
git tag -a v{MAJOR}.{MINOR}.{PATCH}-stable -m "Stable version {MAJOR}.{MINOR}.{PATCH}"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-stable` tag is used by Git Updater to identify the stable version.
|
||||||
|
|
||||||
|
### 6. Push to Remote Repositories
|
||||||
|
|
||||||
|
Push the version branch and tags to remote repositories:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Push to GitHub
|
||||||
|
git push github refs/heads/v{MAJOR}.{MINOR}.{PATCH}:refs/heads/v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
git push github refs/tags/v{MAJOR}.{MINOR}.{PATCH}:refs/tags/v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
git push github refs/tags/v{MAJOR}.{MINOR}.{PATCH}-stable:refs/tags/v{MAJOR}.{MINOR}.{PATCH}-stable
|
||||||
|
|
||||||
|
# Push to Gitea
|
||||||
|
git push gitea refs/heads/v{MAJOR}.{MINOR}.{PATCH}:refs/heads/v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
git push gitea refs/tags/v{MAJOR}.{MINOR}.{PATCH}:refs/tags/v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
git push gitea refs/tags/v{MAJOR}.{MINOR}.{PATCH}-stable:refs/tags/v{MAJOR}.{MINOR}.{PATCH}-stable
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Merge into Main Branch
|
||||||
|
|
||||||
|
Merge the version branch into the main branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git merge v{MAJOR}.{MINOR}.{PATCH} --no-ff -m "Merge v{MAJOR}.{MINOR}.{PATCH} into main"
|
||||||
|
git push github main
|
||||||
|
git push gitea main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Numbering Guidelines
|
||||||
|
|
||||||
|
Follow semantic versioning (MAJOR.MINOR.PATCH):
|
||||||
|
|
||||||
|
- **MAJOR**: Incompatible API changes
|
||||||
|
- **MINOR**: Add functionality in a backward-compatible manner
|
||||||
|
- **PATCH**: Backward-compatible bug fixes
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- Bug fix: 1.0.0 → 1.0.1
|
||||||
|
- New feature: 1.0.0 → 1.1.0
|
||||||
|
- Breaking change: 1.0.0 → 2.0.0
|
||||||
|
|
||||||
|
## GitHub Actions Workflow
|
||||||
|
|
||||||
|
When you push a tag, the GitHub Actions workflow will:
|
||||||
|
|
||||||
|
1. Build the plugin
|
||||||
|
2. Create a ZIP file
|
||||||
|
3. Create a GitHub release
|
||||||
|
4. Attach the ZIP file to the release
|
||||||
|
|
||||||
|
You can monitor the workflow in the "Actions" tab of the GitHub repository.
|
||||||
|
|
||||||
|
## WordPress.org Deployment (If Applicable)
|
||||||
|
|
||||||
|
If the plugin is hosted on WordPress.org:
|
||||||
|
|
||||||
|
1. Ensure the `readme.txt` file is properly formatted
|
||||||
|
2. Ensure the stable tag matches the new version
|
||||||
|
3. Use the WordPress.org SVN repository to deploy the new version
|
||||||
|
|
||||||
|
## Post-Release Tasks
|
||||||
|
|
||||||
|
After releasing a new version:
|
||||||
|
|
||||||
|
1. Update the wiki documentation if needed
|
||||||
|
2. Announce the release in relevant channels
|
||||||
|
3. Monitor for any issues or feedback
|
||||||
|
4. Start planning the next release
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# Wiki Documentation Guide for AI Assistants
|
||||||
|
|
||||||
|
This document provides guidelines for maintaining and updating the wiki documentation for the WordPress Plugin Starter Template.
|
||||||
|
|
||||||
|
## Wiki Structure
|
||||||
|
|
||||||
|
The wiki documentation is organized into the following sections:
|
||||||
|
|
||||||
|
1. **User Documentation**:
|
||||||
|
- Home
|
||||||
|
- Installation Guide
|
||||||
|
- Usage Instructions
|
||||||
|
- Frequently Asked Questions
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
|
2. **Developer Documentation**:
|
||||||
|
- Architecture Overview
|
||||||
|
- Customization Guide
|
||||||
|
- Extending the Plugin
|
||||||
|
- Coding Standards
|
||||||
|
- Release Process
|
||||||
|
|
||||||
|
3. **AI Documentation**:
|
||||||
|
- AI Workflow Documentation
|
||||||
|
|
||||||
|
4. **Additional Resources**:
|
||||||
|
- Changelog
|
||||||
|
- Contributing
|
||||||
|
|
||||||
|
## Wiki Files Location
|
||||||
|
|
||||||
|
The wiki documentation is stored in the `.wiki/` directory in the repository. This allows:
|
||||||
|
|
||||||
|
1. Version control of wiki content alongside the code
|
||||||
|
2. Automated synchronization with the GitHub wiki using GitHub Actions
|
||||||
|
3. Easy contribution to documentation through pull requests
|
||||||
|
|
||||||
|
## Synchronization Process
|
||||||
|
|
||||||
|
When changes are pushed to the `.wiki/` directory in the `main` branch, the GitHub Actions workflow `.github/workflows/sync-wiki.yml` automatically synchronizes these changes with the GitHub wiki.
|
||||||
|
|
||||||
|
## Documentation Standards
|
||||||
|
|
||||||
|
### General Guidelines
|
||||||
|
|
||||||
|
- Use clear, concise language
|
||||||
|
- Follow Markdown formatting conventions
|
||||||
|
- Include code examples where appropriate
|
||||||
|
- Use screenshots or diagrams for complex concepts
|
||||||
|
- Keep documentation up-to-date with code changes
|
||||||
|
|
||||||
|
### Markdown Formatting
|
||||||
|
|
||||||
|
- Use `#` for main headings
|
||||||
|
- Use `##` for section headings
|
||||||
|
- Use `###` for subsection headings
|
||||||
|
- Use backticks for inline code: `code`
|
||||||
|
- Use triple backticks for code blocks:
|
||||||
|
```php
|
||||||
|
// Code example
|
||||||
|
function example() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Use bullet points for lists
|
||||||
|
- Use numbered lists for sequential steps
|
||||||
|
- Use blockquotes for important notes: > Note
|
||||||
|
|
||||||
|
### Code Examples
|
||||||
|
|
||||||
|
- Include complete, working code examples
|
||||||
|
- Use proper syntax highlighting
|
||||||
|
- Include comments to explain complex parts
|
||||||
|
- Follow WordPress coding standards in examples
|
||||||
|
|
||||||
|
## Updating Documentation
|
||||||
|
|
||||||
|
### When to Update
|
||||||
|
|
||||||
|
Documentation should be updated:
|
||||||
|
|
||||||
|
1. When adding new features
|
||||||
|
2. When changing existing functionality
|
||||||
|
3. When fixing bugs that affect user experience
|
||||||
|
4. When improving or clarifying existing documentation
|
||||||
|
|
||||||
|
### How to Update
|
||||||
|
|
||||||
|
1. Identify the relevant wiki file(s) in the `.wiki/` directory
|
||||||
|
2. Make the necessary changes
|
||||||
|
3. Commit and push the changes to the `main` branch
|
||||||
|
4. The GitHub Actions workflow will automatically sync the changes with the GitHub wiki
|
||||||
|
|
||||||
|
### Creating New Pages
|
||||||
|
|
||||||
|
To create a new wiki page:
|
||||||
|
|
||||||
|
1. Create a new Markdown file in the `.wiki/` directory
|
||||||
|
2. Name the file according to the page title (e.g., `New-Feature.md`)
|
||||||
|
3. Add a link to the new page in the appropriate section of `Home.md`
|
||||||
|
4. Add a link to the new page in `_Sidebar.md`
|
||||||
|
|
||||||
|
## Repository-Specific Documentation
|
||||||
|
|
||||||
|
When working in a multi-repository workspace, it's critical to ensure that wiki documentation accurately reflects the features and functionality of the **current repository only**.
|
||||||
|
|
||||||
|
### Avoiding Cross-Repository Confusion
|
||||||
|
|
||||||
|
1. **Verify Features Before Documenting**:
|
||||||
|
- Always verify that a feature exists in the current repository before documenting it
|
||||||
|
- Use `codebase-retrieval` to search for feature implementations
|
||||||
|
- Check the actual code, not just comments or references
|
||||||
|
|
||||||
|
2. **Repository-Specific Content**:
|
||||||
|
- Document only features that exist in the current repository
|
||||||
|
- Don't assume features from other repositories are present in this one
|
||||||
|
- Be explicit about which repository the documentation applies to
|
||||||
|
|
||||||
|
3. **Feature Inspiration vs. Existing Features**:
|
||||||
|
- If documenting a feature inspired by another repository but not yet implemented, clearly mark it as a proposed feature
|
||||||
|
- Don't document features as if they exist when they're only planned or inspired by other repositories
|
||||||
|
|
||||||
|
4. **Cross-Repository References**:
|
||||||
|
- If referencing functionality from another repository, clearly indicate that it's external
|
||||||
|
- Use phrases like "unlike Repository X, this plugin does not include..."
|
||||||
|
|
||||||
|
For detailed guidelines on working in multi-repository workspaces, see **@.ai-workflows/multi-repo-workspace.md**.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Content Guidelines
|
||||||
|
|
||||||
|
- Use clear, concise language
|
||||||
|
- Include step-by-step instructions for complex tasks
|
||||||
|
- Use screenshots or diagrams to illustrate concepts
|
||||||
|
- Provide code examples for developers
|
||||||
|
- Keep the documentation organized and easy to navigate
|
||||||
|
|
||||||
|
### Formatting Guidelines
|
||||||
|
|
||||||
|
- Use consistent Markdown formatting
|
||||||
|
- Use headings to create a clear hierarchy
|
||||||
|
- Use lists for steps or related items
|
||||||
|
- Use tables for structured data
|
||||||
|
- Use code blocks for code examples
|
||||||
|
|
||||||
|
### Maintenance Guidelines
|
||||||
|
|
||||||
|
- Review documentation regularly for accuracy
|
||||||
|
- Update documentation when code changes
|
||||||
|
- Remove outdated information
|
||||||
|
- Add new documentation for new features
|
||||||
|
- Respond to user feedback about documentation
|
||||||
|
|
||||||
|
## User-Focused Documentation
|
||||||
|
|
||||||
|
When writing documentation for users:
|
||||||
|
|
||||||
|
1. Assume minimal technical knowledge
|
||||||
|
2. Provide clear, step-by-step instructions
|
||||||
|
3. Include screenshots for visual guidance
|
||||||
|
4. Explain technical terms when necessary
|
||||||
|
5. Focus on what users want to accomplish
|
||||||
|
|
||||||
|
## Developer-Focused Documentation
|
||||||
|
|
||||||
|
When writing documentation for developers:
|
||||||
|
|
||||||
|
1. Assume familiarity with WordPress development
|
||||||
|
2. Provide technical details and explanations
|
||||||
|
3. Include code examples and API references
|
||||||
|
4. Explain architectural decisions
|
||||||
|
5. Focus on how to extend or customize the plugin
|
||||||
|
|
||||||
|
## Responding to User Feedback
|
||||||
|
|
||||||
|
Improve documentation based on user feedback:
|
||||||
|
|
||||||
|
1. Monitor GitHub issues and WordPress.org support forums for common questions
|
||||||
|
2. Update the FAQ and troubleshooting sections based on user feedback
|
||||||
|
3. Add new examples or clarifications based on user questions
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"root_files": {
|
|
||||||
"allow": [
|
|
||||||
"readme.txt",
|
|
||||||
"changelog.txt",
|
|
||||||
"STANDARDS.md",
|
|
||||||
"DEVELOPMENT.md",
|
|
||||||
"RELEASING.md",
|
|
||||||
"LAUNCH.md",
|
|
||||||
"TESTING.md",
|
|
||||||
"STABILITY.md"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
# Files excluded from the release builds (scripts/build-release.sh, wp dist-archive).
|
|
||||||
# The WordPress.org build also leaves out the files in .distignore-wporg.
|
|
||||||
.git
|
|
||||||
.gitattributes
|
|
||||||
.gitignore
|
|
||||||
.distignore
|
|
||||||
.distignore-wporg
|
|
||||||
# WordPress.org SVN assets (banners) and their source; admin/images/banner.svg ships instead.
|
|
||||||
.wordpress-org
|
|
||||||
.woodpecker.yml
|
|
||||||
# Development tools and CI (composer.json lists development tools only).
|
|
||||||
.github
|
|
||||||
.editorconfig
|
|
||||||
.gitleaks.toml
|
|
||||||
.aidevops.json
|
|
||||||
composer.json
|
|
||||||
composer.lock
|
|
||||||
phpcs.xml.dist
|
|
||||||
phpcs.xml
|
|
||||||
phpstan.neon.dist
|
|
||||||
phpstan.neon
|
|
||||||
phpstan-baseline.neon
|
|
||||||
phpstan-plugin.neon
|
|
||||||
/dist
|
|
||||||
.DS_Store
|
|
||||||
.aider*
|
|
||||||
# Local AI tooling state (aidevops), never part of the plugin.
|
|
||||||
.agents
|
|
||||||
.security-analysis
|
|
||||||
*.bak
|
|
||||||
.metadata_never_index
|
|
||||||
.syncing
|
|
||||||
AGENTS.md
|
|
||||||
CONTRIBUTING.md
|
|
||||||
DEVELOPMENT.md
|
|
||||||
LAUNCH.md
|
|
||||||
SECURITY.md
|
|
||||||
STANDARDS.md
|
|
||||||
RELEASING.md
|
|
||||||
ROADMAP.md
|
|
||||||
STABILITY.md
|
|
||||||
TESTING.md
|
|
||||||
/scripts
|
|
||||||
reference-plugins
|
|
||||||
project-documents
|
|
||||||
node_modules
|
|
||||||
/vendor
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Files left out of the WordPress.org build, on top of .distignore
|
|
||||||
# (rsync exclude patterns; a leading / anchors to the plugin folder).
|
|
||||||
# Plugins hosted on WordPress.org may not install or update code from
|
|
||||||
# anywhere else, so updates from GitHub stay in GitHub releases only.
|
|
||||||
# scripts/build-release.sh also removes the Git Updater header lines
|
|
||||||
# (GitHub Plugin URI, Primary Branch, Release Asset) from wp-plugin-starter-template.php.
|
|
||||||
/includes/github-updater/
|
|
||||||
# Keep README.md: the admin Read Me tab shows it (admin/data/readme.php).
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Opt out of AI model training
|
|
||||||
* ai-training=false
|
|
||||||
|
|
||||||
# Keep development files out of release archives (git archive).
|
|
||||||
/.distignore export-ignore
|
|
||||||
/.distignore-wporg export-ignore
|
|
||||||
/.gitattributes export-ignore
|
|
||||||
/.gitignore export-ignore
|
|
||||||
/.woodpecker.yml export-ignore
|
|
||||||
/DEVELOPMENT.md export-ignore
|
|
||||||
/RELEASING.md export-ignore
|
|
||||||
/ROADMAP.md export-ignore
|
|
||||||
/STABILITY.md export-ignore
|
|
||||||
/TESTING.md export-ignore
|
|
||||||
/scripts export-ignore
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
name: Bug report
|
|
||||||
description: Something in WP Plugin Starter does not work as it should.
|
|
||||||
labels: ["bug"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Security problems: please do not report them here. See SECURITY.md.
|
|
||||||
- type: textarea
|
|
||||||
id: what
|
|
||||||
attributes:
|
|
||||||
label: What happened
|
|
||||||
description: What you saw, and what you expected instead.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: steps
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce
|
|
||||||
description: The feature involved, its settings, and what you clicked.
|
|
||||||
placeholder: |
|
|
||||||
1. Turn on …
|
|
||||||
2. Open …
|
|
||||||
3. See …
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: WP Plugin Starter version
|
|
||||||
description: Shown on the Plugins screen.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: source
|
|
||||||
attributes:
|
|
||||||
label: Installed from
|
|
||||||
options:
|
|
||||||
- GitHub release
|
|
||||||
- WordPress.org
|
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: environment
|
|
||||||
attributes:
|
|
||||||
label: WordPress and PHP versions
|
|
||||||
description: Tools → Site Health → Info shows both.
|
|
||||||
placeholder: WordPress 6.8, PHP 8.3
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: log
|
|
||||||
attributes:
|
|
||||||
label: Messages in debug.log
|
|
||||||
description: Lines from wp-content/debug.log that mention wpstarter, if any. Remove anything private first.
|
|
||||||
render: text
|
|
||||||
- type: textarea
|
|
||||||
id: more
|
|
||||||
attributes:
|
|
||||||
label: Anything else
|
|
||||||
description: Theme, other plugins involved, multisite, screenshots.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: Security problem
|
|
||||||
url: https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/blob/main/SECURITY.md
|
|
||||||
about: Report security problems privately, never in a public issue.
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: Feature request
|
|
||||||
description: Suggest a feature or an improvement.
|
|
||||||
labels: ["enhancement"]
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: problem
|
|
||||||
attributes:
|
|
||||||
label: The problem
|
|
||||||
description: What is slow, missing or annoying today, and for whom.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: idea
|
|
||||||
attributes:
|
|
||||||
label: What WP Plugin Starter could do
|
|
||||||
description: What it would do on its own, without new settings where possible.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: replaces
|
|
||||||
attributes:
|
|
||||||
label: Plugin it would replace
|
|
||||||
description: If another plugin does this now, its name and WordPress.org page.
|
|
||||||
- type: textarea
|
|
||||||
id: more
|
|
||||||
attributes:
|
|
||||||
label: Anything else
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Weekly updates for the pinned GitHub Actions and the Composer development
|
|
||||||
# tools (composer.json; the plugin itself has no Composer dependencies).
|
|
||||||
# Each ecosystem's updates come grouped in one pull request.
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
groups:
|
|
||||||
actions:
|
|
||||||
patterns: ["*"]
|
|
||||||
|
|
||||||
- package-ecosystem: composer
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
versioning-strategy: lockfile-only
|
|
||||||
groups:
|
|
||||||
dev-tools:
|
|
||||||
patterns: ["*"]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
## What and why
|
|
||||||
|
|
||||||
<!-- What changed, and the problem it solves. Link the issue: Resolves #123. -->
|
|
||||||
|
|
||||||
## How it was tested
|
|
||||||
|
|
||||||
<!-- The steps you took on a real WordPress site, and what you saw. -->
|
|
||||||
|
|
||||||
- [ ] `scripts/lint.sh` passes.
|
|
||||||
- [ ] Tested the changed feature on a WordPress site, with no new messages
|
|
||||||
mentioning `wpstarter` in `debug.log`.
|
|
||||||
- [ ] WordPress 6.2 with PHP 7.4, if this uses a core function. <!-- or n/a -->
|
|
||||||
- [ ] Kadence light and dark mode, if this changes front-end styles. <!-- or n/a -->
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] Changelog lines in `README.md`, `changelog.txt` and `readme.txt`.
|
|
||||||
- [ ] New options, post meta, transients or cron hooks are removed in `uninstall.php`.
|
|
||||||
- [ ] New features are off by default.
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
# Checks for every pull request and every push to main. Run the same checks
|
|
||||||
# locally with scripts/lint.sh, scripts/preflight-release.sh,
|
|
||||||
# scripts/plugin-check.sh and scripts/smoke-test.sh (DEVELOPMENT.md).
|
|
||||||
#
|
|
||||||
# Lint runs on every push. The release and smoke-test jobs take longer, so
|
|
||||||
# draft pull requests skip them until they are marked ready for review.
|
|
||||||
# Actions are pinned to commit SHAs; Dependabot keeps them up to date.
|
|
||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Lint (PHP 7.4)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
|
|
||||||
with:
|
|
||||||
php-version: "7.4"
|
|
||||||
coverage: none
|
|
||||||
tools: composer:v2
|
|
||||||
|
|
||||||
- name: Composer cache folder
|
|
||||||
id: composer-cache
|
|
||||||
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
|
||||||
key: composer-${{ hashFiles('composer.lock') }}
|
|
||||||
restore-keys: composer-
|
|
||||||
|
|
||||||
- name: Install development tools
|
|
||||||
run: composer install --no-interaction --no-progress --prefer-dist
|
|
||||||
|
|
||||||
# PHPStan's result cache: the latest run's is restored, so only files
|
|
||||||
# changed since then are analysed again.
|
|
||||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: .cache/phpstan
|
|
||||||
key: phpstan-${{ github.run_id }}
|
|
||||||
restore-keys: phpstan-
|
|
||||||
|
|
||||||
- name: PHP syntax (7.4), JavaScript syntax, ShellCheck, PHPCS, PHPStan
|
|
||||||
run: scripts/lint.sh php js shell phpcs phpstan
|
|
||||||
|
|
||||||
- name: actionlint
|
|
||||||
run: docker run --rm -v "$PWD:/repo" --workdir /repo rhysd/actionlint:1.7.12 -color
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Release build, preflight and Plugin Check
|
|
||||||
needs: lint
|
|
||||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 20
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
|
|
||||||
with:
|
|
||||||
php-version: "7.4"
|
|
||||||
coverage: none
|
|
||||||
tools: none
|
|
||||||
|
|
||||||
- name: Preflight
|
|
||||||
run: scripts/preflight-release.sh --offline --no-docker
|
|
||||||
|
|
||||||
- name: Build both zips
|
|
||||||
run: scripts/build-release.sh --out dist
|
|
||||||
|
|
||||||
- name: Plugin Check
|
|
||||||
run: |
|
|
||||||
args=()
|
|
||||||
for zip in dist/*.zip; do
|
|
||||||
args+=(--zip "$zip")
|
|
||||||
done
|
|
||||||
scripts/plugin-check.sh "${args[@]}" --keep-output dist/plugin-check
|
|
||||||
|
|
||||||
- name: Keep the zips and reports for testing
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: ${{ github.event.repository.name }}-build-${{ github.sha }}
|
|
||||||
path: dist/
|
|
||||||
retention-days: 7
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
smoke:
|
|
||||||
name: Smoke test (WordPress ${{ matrix.wp }}, PHP ${{ matrix.php }})
|
|
||||||
needs: lint
|
|
||||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 20
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
# Minimums (readme.txt, plugin header).
|
|
||||||
- wp: "6.2"
|
|
||||||
php: "7.4"
|
|
||||||
- wp: latest
|
|
||||||
php: "8.3"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Smoke test
|
|
||||||
run: scripts/smoke-test.sh --wp "${{ matrix.wp }}" --php "${{ matrix.php }}" --keep-log debug.log
|
|
||||||
|
|
||||||
- name: Keep debug.log
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: debug-log-wp${{ matrix.wp }}-php${{ matrix.php }}
|
|
||||||
path: debug.log
|
|
||||||
retention-days: 7
|
|
||||||
if-no-files-found: ignore
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
- 'v*-stable'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '7.4'
|
||||||
|
extensions: mbstring, intl, zip
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Get tag name
|
||||||
|
id: get_tag
|
||||||
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
VERSION=$(echo ${{ env.TAG }} | sed 's/^v//' | sed 's/-stable$//')
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
- name: Create build directory
|
||||||
|
run: |
|
||||||
|
mkdir -p build/wp-plugin-starter-template-for-ai-coding
|
||||||
|
cp -R *.php README.md LICENSE CHANGELOG.md readme.txt composer.json build/wp-plugin-starter-template-for-ai-coding/
|
||||||
|
cp -R admin includes languages vendor build/wp-plugin-starter-template-for-ai-coding/
|
||||||
|
mkdir -p build/wp-plugin-starter-template-for-ai-coding/assets/banner build/wp-plugin-starter-template-for-ai-coding/assets/icon build/wp-plugin-starter-template-for-ai-coding/assets/screenshots
|
||||||
|
if [ -d "assets/banner" ]; then cp -R assets/banner/* build/wp-plugin-starter-template-for-ai-coding/assets/banner/; fi
|
||||||
|
if [ -d "assets/icon" ]; then cp -R assets/icon/* build/wp-plugin-starter-template-for-ai-coding/assets/icon/; fi
|
||||||
|
if [ -d "assets/screenshots" ]; then cp -R assets/screenshots/* build/wp-plugin-starter-template-for-ai-coding/assets/screenshots/; fi
|
||||||
|
|
||||||
|
- name: Create ZIP file
|
||||||
|
run: |
|
||||||
|
cd build
|
||||||
|
zip -r ../wp-plugin-starter-template-for-ai-coding-${{ env.VERSION }}.zip wp-plugin-starter-template-for-ai-coding -x "*.DS_Store" -x "*.git*"
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: wp-plugin-starter-template-for-ai-coding-${{ env.VERSION }}.zip
|
||||||
|
name: Version ${{ env.VERSION }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
body: |
|
||||||
|
Release of version ${{ env.VERSION }}
|
||||||
|
|
||||||
|
See [CHANGELOG.md](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/blob/main/CHANGELOG.md) for details.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Sync Wiki
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '.wiki/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-wiki:
|
||||||
|
name: Sync Wiki to GitHub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "GitHub Actions"
|
||||||
|
git config --global user.email "actions@github.com"
|
||||||
|
|
||||||
|
- name: Clone wiki repository
|
||||||
|
run: |
|
||||||
|
git clone https://github.com/${{ github.repository }}.wiki.git wiki
|
||||||
|
|
||||||
|
- name: Sync wiki content
|
||||||
|
run: |
|
||||||
|
# Remove all files from wiki repository except .git
|
||||||
|
find wiki -mindepth 1 -maxdepth 1 -not -name '.git' -exec rm -rf {} \;
|
||||||
|
|
||||||
|
# Copy .wiki content to wiki repository
|
||||||
|
cp -r .wiki/* wiki/
|
||||||
|
|
||||||
|
# Go to wiki repository
|
||||||
|
cd wiki
|
||||||
|
|
||||||
|
# Add all changes
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Check if there are changes to commit
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
git commit -m "Sync wiki from source repository"
|
||||||
|
|
||||||
|
# Push changes
|
||||||
|
git push https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: PHP ${{ matrix.php-versions }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php-versions: ['7.0', '7.4', '8.0']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php-versions }}
|
||||||
|
extensions: mbstring, intl, zip
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --prefer-dist --no-progress
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: ./vendor/bin/phpunit
|
||||||
|
|
||||||
|
code-style:
|
||||||
|
name: Code Style
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '7.4'
|
||||||
|
extensions: mbstring, intl, zip
|
||||||
|
tools: composer:v2, phpcs
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --prefer-dist --no-progress
|
||||||
|
|
||||||
|
- name: Run PHPCS
|
||||||
|
run: ./vendor/bin/phpcs --standard=WordPress
|
||||||
+45
-54
@@ -1,51 +1,35 @@
|
|||||||
# macOS
|
# Operating System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
Icon
|
|
||||||
._*
|
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.Spotlight-V100
|
|
||||||
.TemporaryItems
|
|
||||||
.Trashes
|
|
||||||
.VolumeIcon.icns
|
|
||||||
.com.apple.timemachine.donotpresent
|
|
||||||
.AppleDB
|
|
||||||
.AppleDesktop
|
|
||||||
Network Trash Folder
|
|
||||||
Temporary Items
|
|
||||||
.apdisk
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
Thumbs.db:encryptable
|
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
ehthumbs_vista.db
|
Desktop.ini
|
||||||
*.stackdump
|
|
||||||
[Dd]esktop.ini
|
|
||||||
$RECYCLE.BIN/
|
$RECYCLE.BIN/
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
*.lnk
|
|
||||||
|
|
||||||
# Development & Local Environment
|
# IDE Files
|
||||||
.aider*
|
.idea/
|
||||||
.git_status_output.txt
|
.vscode/
|
||||||
.git_status_temp.txt
|
*.sublime-project
|
||||||
project-documents/
|
*.sublime-workspace
|
||||||
reference-plugins/
|
*.komodoproject
|
||||||
*.bak*
|
.komodotools/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Dependency Directories
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
|
||||||
.cache/
|
|
||||||
vendor/
|
vendor/
|
||||||
|
bower_components/
|
||||||
|
|
||||||
# WordPress specific
|
# Build Files
|
||||||
*.log
|
build/
|
||||||
|
dist/
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Composer
|
||||||
|
composer.phar
|
||||||
|
|
||||||
|
# WordPress
|
||||||
wp-config.php
|
wp-config.php
|
||||||
wp-content/advanced-cache.php
|
wp-content/advanced-cache.php
|
||||||
wp-content/backup-db/
|
wp-content/backup-db/
|
||||||
@@ -54,20 +38,27 @@ wp-content/blogs.dir/
|
|||||||
wp-content/cache/
|
wp-content/cache/
|
||||||
wp-content/upgrade/
|
wp-content/upgrade/
|
||||||
wp-content/uploads/
|
wp-content/uploads/
|
||||||
|
wp-content/wp-cache-config.php
|
||||||
|
wp-content/plugins/hello.php
|
||||||
|
|
||||||
# IDE & Editors
|
# Log Files
|
||||||
.vscode/
|
*.log
|
||||||
.idea/
|
logs/
|
||||||
*.sublime-project
|
error_log
|
||||||
*.sublime-workspace
|
|
||||||
|
# Environment Files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Temporary Files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*~
|
||||||
|
|
||||||
# Keep .woodpecker.yml if needed for CI/CD
|
# Test Coverage
|
||||||
# .woodpecker.yml
|
coverage/
|
||||||
|
.phpunit.result.cache
|
||||||
# aidevops runtime state
|
|
||||||
.agents/loop-state/
|
|
||||||
|
|
||||||
# Release builds (scripts/build-release.sh)
|
|
||||||
/dist/
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# gitleaks: secret scan of the Git history (DEVELOPMENT.md → Secrets in history).
|
|
||||||
# Keep gitleaks' own rules; list only checked false positives here, each with
|
|
||||||
# its reason and as narrow as possible.
|
|
||||||
|
|
||||||
[extend]
|
|
||||||
useDefault = true
|
|
||||||
|
|
||||||
[[allowlists]]
|
|
||||||
description = "Fluent Forms field keys (el_ and a uniqid()) in starter forms, not secrets"
|
|
||||||
condition = "AND"
|
|
||||||
paths = ['''^starters/fluentform\.json$''']
|
|
||||||
regexTarget = "line"
|
|
||||||
regexes = ['''"uniqElKey": "el_[0-9.]+"''']
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
This page documents all notable changes to the WordPress Plugin Starter Template.
|
||||||
|
|
||||||
|
## Version 0.1.0 (2025-04-17)
|
||||||
|
- Initial release with basic template structure
|
||||||
|
- Added core plugin architecture with OOP approach
|
||||||
|
- Added admin interface components and styling
|
||||||
|
- Added update mechanism with multiple source options
|
||||||
|
- Added documentation templates for users and developers
|
||||||
|
- Added AI workflow documentation for AI-assisted development
|
||||||
|
- Added GitHub Actions workflows for automated tasks
|
||||||
|
- Added wiki documentation templates
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
This page answers common questions about the WordPress Plugin Starter Template.
|
||||||
|
|
||||||
|
## General Questions
|
||||||
|
|
||||||
|
### What is the WordPress Plugin Starter Template?
|
||||||
|
|
||||||
|
The WordPress Plugin Starter Template is a comprehensive starting point for developing WordPress plugins. It provides a well-structured codebase, documentation templates, and best practices to help you create high-quality WordPress plugins efficiently.
|
||||||
|
|
||||||
|
### Who is this template for?
|
||||||
|
|
||||||
|
This template is designed for:
|
||||||
|
- WordPress plugin developers looking for a solid foundation
|
||||||
|
- Developers who want to leverage AI assistance in their development workflow
|
||||||
|
- Anyone who wants to create a WordPress plugin following best practices
|
||||||
|
|
||||||
|
### Is this template suitable for all types of plugins?
|
||||||
|
|
||||||
|
Yes, this template provides a solid foundation for most WordPress plugins. It's designed to be flexible and can be adapted for various types of plugins, from simple utilities to complex applications.
|
||||||
|
|
||||||
|
## Usage Questions
|
||||||
|
|
||||||
|
### How do I use this template?
|
||||||
|
|
||||||
|
This template is meant to be a starting point for your own plugin development. You should:
|
||||||
|
|
||||||
|
1. Copy the template files to your new plugin directory
|
||||||
|
2. Rename files and update namespaces to match your plugin name
|
||||||
|
3. Update plugin headers in the main PHP file
|
||||||
|
4. Customize the functionality to meet your specific needs
|
||||||
|
5. Update documentation to reflect your plugin's features
|
||||||
|
|
||||||
|
For detailed instructions, see the [Usage Instructions](Usage-Instructions) page.
|
||||||
|
|
||||||
|
### Do I need to keep the original credits?
|
||||||
|
|
||||||
|
While not strictly required, we appreciate if you keep a reference to the original template in your plugin's documentation. This helps others discover the template and contributes to the open-source community.
|
||||||
|
|
||||||
|
### Can I use this template for commercial plugins?
|
||||||
|
|
||||||
|
Yes, you can use this template for both free and commercial plugins. The template is licensed under GPL-2.0+, which allows for commercial use.
|
||||||
|
|
||||||
|
## Technical Questions
|
||||||
|
|
||||||
|
### What PHP version is required?
|
||||||
|
|
||||||
|
The template requires PHP 7.0 or higher, which is the minimum recommended version for WordPress plugins today.
|
||||||
|
|
||||||
|
### Does this template support Gutenberg blocks?
|
||||||
|
|
||||||
|
The template doesn't include Gutenberg blocks by default, but it provides a structure that makes it easy to add them. You can extend the template to include block registration and block-specific assets.
|
||||||
|
|
||||||
|
### How do I add custom post types or taxonomies?
|
||||||
|
|
||||||
|
To add custom post types or taxonomies:
|
||||||
|
|
||||||
|
1. Create a new class in `includes/` for your post type or taxonomy
|
||||||
|
2. Register the post type or taxonomy in the class constructor
|
||||||
|
3. Initialize the class in `includes/plugin.php`
|
||||||
|
|
||||||
|
### How do I handle plugin updates?
|
||||||
|
|
||||||
|
The template includes an update mechanism that supports multiple sources:
|
||||||
|
|
||||||
|
- WordPress.org
|
||||||
|
- GitHub
|
||||||
|
- Gitea
|
||||||
|
|
||||||
|
To configure the update mechanism:
|
||||||
|
|
||||||
|
1. Update the plugin headers in the main PHP file
|
||||||
|
2. Customize the `updater.php` file if needed
|
||||||
|
3. Ensure your repository follows the required structure for updates
|
||||||
|
|
||||||
|
## AI-Assisted Development Questions
|
||||||
|
|
||||||
|
### How does this template support AI-assisted development?
|
||||||
|
|
||||||
|
The template includes:
|
||||||
|
|
||||||
|
1. Detailed documentation in the `.ai-assistant.md` file
|
||||||
|
2. Workflow guidelines in the `.ai-workflows/` directory
|
||||||
|
3. Clear code structure and comments that help AI understand the codebase
|
||||||
|
4. Best practices for AI-friendly code organization
|
||||||
|
|
||||||
|
### What AI tools work best with this template?
|
||||||
|
|
||||||
|
This template is designed to work well with various AI coding assistants, including:
|
||||||
|
|
||||||
|
- GitHub Copilot
|
||||||
|
- Claude
|
||||||
|
- ChatGPT
|
||||||
|
- Other AI coding assistants
|
||||||
|
|
||||||
|
### How do I use the AI workflow documentation?
|
||||||
|
|
||||||
|
The AI workflow documentation in the `.ai-workflows/` directory provides guidelines for AI assistants working with this template. These files help ensure consistent, high-quality code and documentation when using AI tools for development.
|
||||||
|
|
||||||
|
## Contributing Questions
|
||||||
|
|
||||||
|
### How can I contribute to this template?
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to:
|
||||||
|
|
||||||
|
1. Report bugs or issues
|
||||||
|
2. Suggest new features or improvements
|
||||||
|
3. Submit pull requests with bug fixes or enhancements
|
||||||
|
|
||||||
|
For more information, see the [Contributing](Contributing) page.
|
||||||
|
|
||||||
|
### Where can I report issues?
|
||||||
|
|
||||||
|
You can report issues on the [GitHub repository](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/issues).
|
||||||
|
|
||||||
|
### Is there a roadmap for future development?
|
||||||
|
|
||||||
|
Yes, we maintain a roadmap of planned features and improvements. You can find it in the [GitHub repository](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/projects).
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# WordPress Plugin Starter Template
|
||||||
|
|
||||||
|
Welcome to the documentation wiki for the WordPress Plugin Starter Template.
|
||||||
|
|
||||||
|
This template provides a solid foundation for developing WordPress plugins with best practices for AI-assisted development.
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- [Starter Prompt](Starter-Prompt)
|
||||||
|
- [Installation Guide](Installation-Guide)
|
||||||
|
- [Usage Instructions](Usage-Instructions)
|
||||||
|
- [Frequently Asked Questions](Frequently-Asked-Questions)
|
||||||
|
- [Contributing](Contributing)
|
||||||
|
- [Changelog](Changelog)
|
||||||
|
|
||||||
|
## About This Template
|
||||||
|
|
||||||
|
The WordPress Plugin Starter Template is designed to help developers quickly create new WordPress plugins with a solid foundation of best practices. It incorporates modern coding standards, comprehensive documentation, and AI-assisted development workflows.
|
||||||
|
|
||||||
|
This template is based on the experience gained from developing the "Fix 'Plugin file does not exist' Notices" plugin and other successful WordPress plugins.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Object-Oriented Architecture**: Well-structured, maintainable code using OOP principles
|
||||||
|
- **Namespace Support**: Modern PHP namespacing for better organization and avoiding conflicts
|
||||||
|
- **Comprehensive Documentation**: Detailed documentation for both users and developers
|
||||||
|
- **Testing Framework**: PHPUnit setup for unit testing
|
||||||
|
- **Internationalization Ready**: Full support for translation and localization
|
||||||
|
- **Update Source Selection**: Choose between WordPress.org, GitHub, or Gitea for plugin updates
|
||||||
|
- **AI Workflow Documentation**: Detailed guides for AI-assisted development
|
||||||
|
- **Wiki Documentation**: Ready-to-use wiki structure for comprehensive documentation
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
To get started with this template, check out the [Starter Prompt](Starter-Prompt) for a comprehensive guide on customizing the template for your specific plugin needs.
|
||||||
|
|
||||||
|
**Important**: For the best AI assistance, add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context. In most AI IDEs, you can pin these files to ensure they're considered in each message.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you encounter any issues or have questions about the template, please check the [Frequently Asked Questions](Frequently-Asked-Questions) section. If you still need help, you can [open an issue](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/issues) on GitHub.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Installation Guide
|
||||||
|
|
||||||
|
This guide provides instructions for installing and setting up the WordPress Plugin Starter Template.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before installing the plugin, ensure you have:
|
||||||
|
|
||||||
|
- WordPress 5.0 or higher
|
||||||
|
- PHP 7.0 or higher
|
||||||
|
- A WordPress site with administrator access
|
||||||
|
|
||||||
|
## Installation Methods
|
||||||
|
|
||||||
|
### Method 1: From WordPress.org (For Released Plugins)
|
||||||
|
|
||||||
|
1. Log in to your WordPress admin dashboard.
|
||||||
|
2. Navigate to **Plugins > Add New**.
|
||||||
|
3. Search for "WordPress Plugin Starter Template".
|
||||||
|
4. Click **Install Now** next to the plugin.
|
||||||
|
5. After installation, click **Activate**.
|
||||||
|
|
||||||
|
### Method 2: Manual Installation
|
||||||
|
|
||||||
|
1. Download the latest release from the [GitHub repository](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/releases).
|
||||||
|
2. Log in to your WordPress admin dashboard.
|
||||||
|
3. Navigate to **Plugins > Add New**.
|
||||||
|
4. Click the **Upload Plugin** button at the top of the page.
|
||||||
|
5. Click **Choose File** and select the downloaded ZIP file.
|
||||||
|
6. Click **Install Now**.
|
||||||
|
7. After installation, click **Activate**.
|
||||||
|
|
||||||
|
### Method 3: Using as a Template for Your Plugin
|
||||||
|
|
||||||
|
1. Clone or download the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding.git your-plugin-name
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Navigate to your plugin directory:
|
||||||
|
```bash
|
||||||
|
cd your-plugin-name
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Rename files and update namespaces:
|
||||||
|
- Rename `wp-plugin-starter-template.php` to your plugin name (e.g., `your-plugin-name.php`)
|
||||||
|
- Update the namespace from `WPALLSTARS\PluginStarterTemplate` to your own
|
||||||
|
- Update text domain from `wp-plugin-starter-template` to your own
|
||||||
|
|
||||||
|
4. Update plugin headers in the main PHP file.
|
||||||
|
|
||||||
|
5. Install dependencies:
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Upload the plugin to your WordPress site or use the local development script:
|
||||||
|
```bash
|
||||||
|
./scripts/deploy-local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Installation
|
||||||
|
|
||||||
|
After installing and activating the plugin, you should:
|
||||||
|
|
||||||
|
1. Review the plugin settings (if applicable).
|
||||||
|
2. Customize the plugin for your specific needs.
|
||||||
|
3. Update documentation to reflect your plugin's features.
|
||||||
|
|
||||||
|
## Troubleshooting Installation Issues
|
||||||
|
|
||||||
|
If you encounter any issues during installation, please check the following:
|
||||||
|
|
||||||
|
1. **Plugin Conflicts**: Deactivate other plugins to check for conflicts.
|
||||||
|
2. **Server Requirements**: Ensure your server meets the minimum requirements.
|
||||||
|
3. **File Permissions**: Check that your WordPress installation has the correct file permissions.
|
||||||
|
4. **Memory Limit**: Increase PHP memory limit if you encounter memory-related errors.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After installation, refer to the [Usage Instructions](Usage-Instructions) to learn how to use and customize the plugin.
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
# WordPress Plugin Starter Template - AI Assistant Prompt
|
||||||
|
|
||||||
|
This document provides a comprehensive prompt to help you get started with creating your own WordPress plugin using this starter template with the assistance of AI tools like GitHub Copilot, Claude, or ChatGPT.
|
||||||
|
|
||||||
|
## Important: Optimize AI Context
|
||||||
|
|
||||||
|
**Before starting, add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context.** In most AI IDEs, you can pin these files to ensure they're considered in each message. This will help the AI understand the project structure and follow the established best practices.
|
||||||
|
|
||||||
|
## Initial Setup Prompt
|
||||||
|
|
||||||
|
Use the following prompt to guide the AI assistant in helping you set up your new plugin based on this template:
|
||||||
|
|
||||||
|
```
|
||||||
|
I'm creating a new WordPress plugin based on the wp-plugin-starter-template-for-ai-coding template. Please help me customize this template for my specific plugin needs.
|
||||||
|
|
||||||
|
Here are the details for my new plugin:
|
||||||
|
|
||||||
|
- Plugin Name: [YOUR PLUGIN NAME]
|
||||||
|
- Plugin Slug: [YOUR-PLUGIN-SLUG]
|
||||||
|
- Text Domain: [your-plugin-text-domain]
|
||||||
|
- Namespace: [VENDOR]\[PluginName]
|
||||||
|
- Description: [BRIEF DESCRIPTION OF YOUR PLUGIN]
|
||||||
|
- Version: 0.1.0 (starting version)
|
||||||
|
- Author: [YOUR NAME/ORGANIZATION]
|
||||||
|
- Author URI: [YOUR WEBSITE]
|
||||||
|
- License: GPL-2.0+ (or specify another compatible license)
|
||||||
|
- Requires WordPress: [MINIMUM WP VERSION, e.g., 5.0]
|
||||||
|
- Requires PHP: [MINIMUM PHP VERSION, e.g., 7.0]
|
||||||
|
- GitHub Repository: [YOUR GITHUB REPO URL]
|
||||||
|
- Gitea Repository (if applicable): [YOUR GITEA REPO URL]
|
||||||
|
|
||||||
|
I need help with the following tasks:
|
||||||
|
|
||||||
|
1. Updating all template placeholders with my plugin information
|
||||||
|
2. Customizing the plugin structure for my specific needs
|
||||||
|
3. Setting up the initial functionality for my plugin
|
||||||
|
|
||||||
|
I've added the .ai-assistant.md and .ai-workflows/ directory to the chat context to ensure you have all the necessary information about the project structure and best practices.
|
||||||
|
|
||||||
|
Please guide me through this process step by step, starting with identifying all files that need to be updated with my plugin information.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files That Need Updating
|
||||||
|
|
||||||
|
The AI will help you identify and update the following files with your plugin information:
|
||||||
|
|
||||||
|
1. **Main Plugin File**: Rename `wp-plugin-starter-template.php` to `your-plugin-slug.php` and update all plugin header information
|
||||||
|
2. **README.md**: Update with your plugin details, features, and installation instructions
|
||||||
|
3. **readme.txt**: Update WordPress.org plugin repository information
|
||||||
|
4. **CHANGELOG.md**: Initialize with your starting version
|
||||||
|
5. **composer.json**: Update package name and description
|
||||||
|
6. **languages/pot file**: Rename and update the POT file
|
||||||
|
7. **.github/workflows/**: Update GitHub Actions workflows with your repository information
|
||||||
|
8. **.wiki/**: Update wiki documentation with your plugin information
|
||||||
|
9. **.ai-assistant.md**: Update AI assistant guidance for your specific plugin
|
||||||
|
10. **includes/plugin.php**: Update namespace and class references
|
||||||
|
11. **includes/core.php**: Update namespace and customize core functionality
|
||||||
|
12. **admin/lib/admin.php**: Update namespace and customize admin functionality
|
||||||
|
|
||||||
|
## Customization Process
|
||||||
|
|
||||||
|
After providing the initial information, follow this process with your AI assistant:
|
||||||
|
|
||||||
|
### 1. File Renaming
|
||||||
|
|
||||||
|
Ask the AI to help you identify all files that need to be renamed:
|
||||||
|
|
||||||
|
```
|
||||||
|
Please list all files that need to be renamed to match my plugin slug, and provide the commands to rename them.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Namespace Updates
|
||||||
|
|
||||||
|
Ask the AI to help you update all namespace references:
|
||||||
|
|
||||||
|
```
|
||||||
|
Please help me update all namespace references from WPALLSTARS\PluginStarterTemplate to [VENDOR]\[PluginName] throughout the codebase.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Text Domain Updates
|
||||||
|
|
||||||
|
Ask the AI to help you update all text domain references:
|
||||||
|
|
||||||
|
```
|
||||||
|
Please help me update all text domain references from 'wp-plugin-starter-template' to '[your-plugin-text-domain]' throughout the codebase.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Function Prefix Updates
|
||||||
|
|
||||||
|
Ask the AI to help you update any function prefixes:
|
||||||
|
|
||||||
|
```
|
||||||
|
Please help me update any function prefixes from 'wpst_' to '[your_prefix]_' throughout the codebase.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Customizing Core Functionality
|
||||||
|
|
||||||
|
Ask the AI to help you customize the core functionality for your specific plugin needs:
|
||||||
|
|
||||||
|
```
|
||||||
|
Now I'd like to customize the core functionality for my plugin. Here's what my plugin should do:
|
||||||
|
|
||||||
|
[DESCRIBE YOUR PLUGIN'S CORE FUNCTIONALITY]
|
||||||
|
|
||||||
|
Please help me modify the includes/core.php file to implement this functionality.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Customizing Admin Interface
|
||||||
|
|
||||||
|
Ask the AI to help you customize the admin interface for your specific plugin needs:
|
||||||
|
|
||||||
|
```
|
||||||
|
I'd like to customize the admin interface for my plugin. Here's what I need in the admin area:
|
||||||
|
|
||||||
|
[DESCRIBE YOUR PLUGIN'S ADMIN INTERFACE NEEDS]
|
||||||
|
|
||||||
|
Please help me modify the admin/lib/admin.php file and any other necessary files to implement this.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Testing Setup
|
||||||
|
|
||||||
|
Ask the AI to help you set up testing for your plugin:
|
||||||
|
|
||||||
|
```
|
||||||
|
Please help me update the testing setup to match my plugin's namespace and functionality. I want to ensure I have proper test coverage for the core features.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Customization Areas
|
||||||
|
|
||||||
|
Depending on your plugin's needs, you might want to ask the AI for help with:
|
||||||
|
|
||||||
|
1. **Custom Post Types**: Setting up custom post types and taxonomies
|
||||||
|
2. **Settings API**: Implementing WordPress Settings API for your plugin options
|
||||||
|
3. **Shortcodes**: Creating shortcodes for front-end functionality
|
||||||
|
4. **Widgets**: Developing WordPress widgets
|
||||||
|
5. **REST API**: Adding custom REST API endpoints
|
||||||
|
6. **Blocks**: Creating Gutenberg blocks
|
||||||
|
7. **Internationalization**: Ensuring proper i18n setup
|
||||||
|
8. **Database Tables**: Creating custom database tables if needed
|
||||||
|
9. **Cron Jobs**: Setting up WordPress cron jobs
|
||||||
|
10. **User Roles and Capabilities**: Managing custom user roles and capabilities
|
||||||
|
|
||||||
|
## Final Review
|
||||||
|
|
||||||
|
Once you've completed the customization, ask the AI to help you review everything:
|
||||||
|
|
||||||
|
```
|
||||||
|
Please help me review all the changes we've made to ensure:
|
||||||
|
|
||||||
|
1. All template placeholders have been replaced with my plugin information
|
||||||
|
2. All namespaces, text domains, and function prefixes have been updated consistently
|
||||||
|
3. The plugin structure matches my specific needs
|
||||||
|
4. The initial functionality is properly implemented
|
||||||
|
5. All documentation (README.md, readme.txt, wiki) is updated and consistent
|
||||||
|
|
||||||
|
Are there any issues or inconsistencies that need to be addressed?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and Testing
|
||||||
|
|
||||||
|
Finally, ask the AI to guide you through building and testing your plugin:
|
||||||
|
|
||||||
|
```
|
||||||
|
Please guide me through the process of building and testing my plugin:
|
||||||
|
|
||||||
|
1. How do I use the build script to create a deployable version?
|
||||||
|
2. What tests should I run to ensure everything is working correctly?
|
||||||
|
3. How do I set up a local testing environment?
|
||||||
|
4. What should I check before releasing the first version?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimizing AI Assistance
|
||||||
|
|
||||||
|
To ensure the AI assistant has all the necessary context about your plugin's structure and best practices:
|
||||||
|
|
||||||
|
```
|
||||||
|
Please add the .ai-assistant.md and .ai-workflows/ directory to your AI IDE chat context. In most AI IDEs, you can pin these files to ensure they're considered in each message. This will help the AI understand the project structure and follow the established best practices.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Remember
|
||||||
|
|
||||||
|
- This template is designed to be a starting point. Feel free to add, remove, or modify components as needed for your specific plugin.
|
||||||
|
- The AI assistant can help you understand the existing code and make appropriate modifications, but you should review all changes to ensure they meet your requirements.
|
||||||
|
- Always test your plugin thoroughly before releasing it.
|
||||||
|
- Keep documentation updated as you develop your plugin.
|
||||||
|
- Pin the .ai-assistant.md and .ai-workflows/ files in your AI IDE chat to ensure the AI has the necessary context for each interaction.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
This plugin is based on the [WordPress Plugin Starter Template for AI Coding](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding) by WPALLSTARS.
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Usage Instructions
|
||||||
|
|
||||||
|
This guide provides instructions for using and customizing the WordPress Plugin Starter Template for your own plugin development.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
The WordPress Plugin Starter Template is designed to be a starting point for your WordPress plugin development. It provides a well-structured codebase that you can customize to create your own plugin.
|
||||||
|
|
||||||
|
### Template Structure
|
||||||
|
|
||||||
|
The template follows a modular structure:
|
||||||
|
|
||||||
|
- `wp-plugin-starter-template.php`: Main plugin file with plugin headers
|
||||||
|
- `includes/`: Core plugin functionality
|
||||||
|
- `plugin.php`: Main plugin class that initializes everything
|
||||||
|
- `core.php`: Core functionality class
|
||||||
|
- `updater.php`: Update mechanism for multiple sources
|
||||||
|
- `admin/`: Admin-specific functionality
|
||||||
|
- `lib/`: Admin classes
|
||||||
|
- `css/`: Admin stylesheets
|
||||||
|
- `js/`: Admin JavaScript files
|
||||||
|
- `languages/`: Translation files
|
||||||
|
- `.github/workflows/`: GitHub Actions workflows
|
||||||
|
- `.ai-workflows/`: Documentation for AI assistants
|
||||||
|
- `.wiki/`: Wiki documentation templates
|
||||||
|
|
||||||
|
### Customizing for Your Plugin
|
||||||
|
|
||||||
|
1. **Rename Files and Update Namespaces**:
|
||||||
|
- Rename `wp-plugin-starter-template.php` to your plugin name
|
||||||
|
- Update the namespace from `WPALLSTARS\PluginStarterTemplate` to your own
|
||||||
|
- Update text domain from `wp-plugin-starter-template` to your own
|
||||||
|
|
||||||
|
2. **Update Plugin Headers**:
|
||||||
|
- Edit the plugin headers in the main PHP file
|
||||||
|
- Update GitHub/Gitea repository URLs
|
||||||
|
|
||||||
|
3. **Customize Functionality**:
|
||||||
|
- Modify the core functionality in `includes/core.php`
|
||||||
|
- Add your own classes as needed
|
||||||
|
- Customize admin interfaces in the `admin/` directory
|
||||||
|
|
||||||
|
4. **Update Documentation**:
|
||||||
|
- Update README.md and readme.txt with your plugin information
|
||||||
|
- Customize wiki documentation in the `.wiki/` directory
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Adding Admin Pages
|
||||||
|
|
||||||
|
The template includes a structure for adding admin pages to the WordPress dashboard. To add an admin page:
|
||||||
|
|
||||||
|
1. Uncomment the `add_admin_menu` method in `admin/lib/admin.php`
|
||||||
|
2. Customize the menu parameters to match your plugin
|
||||||
|
3. Create the corresponding render method for your admin page
|
||||||
|
4. Create template files in an `admin/templates/` directory
|
||||||
|
|
||||||
|
### Adding Settings
|
||||||
|
|
||||||
|
To add settings to your plugin:
|
||||||
|
|
||||||
|
1. Create a settings class in `includes/settings.php`
|
||||||
|
2. Register settings using the WordPress Settings API
|
||||||
|
3. Create form fields for your settings
|
||||||
|
4. Handle settings validation and sanitization
|
||||||
|
|
||||||
|
### Adding Custom Post Types or Taxonomies
|
||||||
|
|
||||||
|
To add custom post types or taxonomies:
|
||||||
|
|
||||||
|
1. Create a new class in `includes/` for your post type or taxonomy
|
||||||
|
2. Register the post type or taxonomy in the class constructor
|
||||||
|
3. Initialize the class in `includes/plugin.php`
|
||||||
|
|
||||||
|
### Internationalization
|
||||||
|
|
||||||
|
The template is ready for internationalization. To make your plugin translatable:
|
||||||
|
|
||||||
|
1. Use translation functions for all user-facing strings:
|
||||||
|
- `__()` for simple strings
|
||||||
|
- `_e()` for echoed strings
|
||||||
|
- `esc_html__()` for escaped strings
|
||||||
|
2. Update the text domain in all translation functions
|
||||||
|
3. Generate a POT file for translations
|
||||||
|
|
||||||
|
### Update Mechanism
|
||||||
|
|
||||||
|
The template includes an update mechanism that supports multiple sources:
|
||||||
|
|
||||||
|
- WordPress.org
|
||||||
|
- GitHub
|
||||||
|
- Gitea
|
||||||
|
|
||||||
|
To configure the update mechanism:
|
||||||
|
|
||||||
|
1. Update the plugin headers in the main PHP file
|
||||||
|
2. Customize the `updater.php` file if needed
|
||||||
|
3. Ensure your repository follows the required structure for updates
|
||||||
|
|
||||||
|
## Building and Releasing
|
||||||
|
|
||||||
|
The template includes scripts for building and releasing your plugin:
|
||||||
|
|
||||||
|
1. **Building the Plugin**:
|
||||||
|
```bash
|
||||||
|
./build.sh <version>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploying to a Local WordPress Installation**:
|
||||||
|
```bash
|
||||||
|
./scripts/deploy-local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Creating a Release**:
|
||||||
|
- Tag a new version in Git
|
||||||
|
- Push the tag to GitHub
|
||||||
|
- The GitHub Actions workflow will create a release
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After customizing the template for your needs, refer to the [Architecture Overview](Architecture-Overview) to understand the plugin's structure in more detail.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
## User Documentation
|
||||||
|
* [Home](Home)
|
||||||
|
* [Installation Guide](Installation-Guide)
|
||||||
|
* [Usage Instructions](Usage-Instructions)
|
||||||
|
* [Frequently Asked Questions](Frequently-Asked-Questions)
|
||||||
|
* [Troubleshooting](Troubleshooting)
|
||||||
|
|
||||||
|
## Developer Documentation
|
||||||
|
* [Architecture Overview](Architecture-Overview)
|
||||||
|
* [Customization Guide](Customization-Guide)
|
||||||
|
* [Extending the Plugin](Extending-the-Plugin)
|
||||||
|
* [Coding Standards](Coding-Standards)
|
||||||
|
* [Release Process](Release-Process)
|
||||||
|
|
||||||
|
## AI Documentation
|
||||||
|
* [AI Workflow Documentation](AI-Workflow-Documentation)
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
* [Changelog](Changelog)
|
||||||
|
* [Contributing](Contributing)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# WordPress.org Plugin Assets
|
||||||
|
|
||||||
|
This directory contains assets for the WordPress.org plugin repository. These assets are not included in the plugin itself but are used for the plugin's WordPress.org page.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
- `assets/`: Contains images and other assets for the WordPress.org plugin page
|
||||||
|
- `banner-772x250.png`: Banner image for the plugin page (772x250 pixels)
|
||||||
|
- `banner-1544x500.png`: Retina banner image (1544x500 pixels)
|
||||||
|
- `icon-128x128.png`: Icon for the plugin (128x128 pixels)
|
||||||
|
- `icon-256x256.png`: Retina icon (256x256 pixels)
|
||||||
|
- `screenshot-1.png`: First screenshot
|
||||||
|
- `screenshot-2.png`: Second screenshot (if applicable)
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
## Asset Requirements
|
||||||
|
|
||||||
|
### Banner
|
||||||
|
|
||||||
|
- Regular: 772x250 pixels
|
||||||
|
- Retina: 1544x500 pixels
|
||||||
|
- PNG or JPG format
|
||||||
|
- The banner should be visually appealing and represent the plugin's purpose
|
||||||
|
|
||||||
|
### Icon
|
||||||
|
|
||||||
|
- Regular: 128x128 pixels
|
||||||
|
- Retina: 256x256 pixels
|
||||||
|
- PNG format with transparency
|
||||||
|
- The icon should be simple and recognizable
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
- Should be at least 1200 pixels wide
|
||||||
|
- PNG or JPG format
|
||||||
|
- Should demonstrate the plugin's functionality
|
||||||
|
- Should be clear and easy to understand
|
||||||
|
|
||||||
|
## WordPress.org Plugin Submission
|
||||||
|
|
||||||
|
When submitting a plugin to WordPress.org, you need to:
|
||||||
|
|
||||||
|
1. Create a plugin ZIP file without the `.wordpress-org` directory
|
||||||
|
2. Upload the ZIP file to WordPress.org
|
||||||
|
3. Upload the assets separately through the WordPress.org plugin repository interface
|
||||||
|
|
||||||
|
For more information, see [WORDPRESS_ORG_SUBMISSION.md](WORDPRESS_ORG_SUBMISSION.md).
|
||||||
|
|
||||||
|
## Asset Design Guidelines
|
||||||
|
|
||||||
|
- Use consistent branding (colors, fonts, etc.)
|
||||||
|
- Keep the design simple and focused
|
||||||
|
- Ensure text is readable
|
||||||
|
- Use high-quality images
|
||||||
|
- Test how assets look on different devices and screen sizes
|
||||||
|
|
||||||
|
## Updating Assets
|
||||||
|
|
||||||
|
When updating assets:
|
||||||
|
|
||||||
|
1. Replace the files in this directory
|
||||||
|
2. Upload the new assets to WordPress.org through the plugin repository interface
|
||||||
|
3. It may take some time for the changes to appear on WordPress.org
|
||||||
|
|
||||||
|
For more information about WordPress.org plugin assets, see [WORDPRESS_ORG_ASSETS.md](WORDPRESS_ORG_ASSETS.md).
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# WordPress.org Plugin Assets Guide
|
||||||
|
|
||||||
|
This document provides detailed information about the assets required for the WordPress.org plugin repository.
|
||||||
|
|
||||||
|
## Required Assets
|
||||||
|
|
||||||
|
### Banner
|
||||||
|
|
||||||
|
The banner is displayed at the top of your plugin's page on WordPress.org.
|
||||||
|
|
||||||
|
- **Regular Banner**: 772x250 pixels
|
||||||
|
- **Retina Banner**: 1544x500 pixels
|
||||||
|
- **Format**: PNG or JPG
|
||||||
|
- **File Names**: `banner-772x250.{png|jpg}` and `banner-1544x500.{png|jpg}`
|
||||||
|
|
||||||
|
### Icon
|
||||||
|
|
||||||
|
The icon is displayed next to your plugin's name in search results and on your plugin's page.
|
||||||
|
|
||||||
|
- **Regular Icon**: 128x128 pixels
|
||||||
|
- **Retina Icon**: 256x256 pixels
|
||||||
|
- **Format**: PNG with transparency
|
||||||
|
- **File Names**: `icon-128x128.png` and `icon-256x256.png`
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
Screenshots are displayed in the "Screenshots" tab on your plugin's page.
|
||||||
|
|
||||||
|
- **Size**: At least 1200 pixels wide
|
||||||
|
- **Format**: PNG or JPG
|
||||||
|
- **File Names**: `screenshot-1.{png|jpg}`, `screenshot-2.{png|jpg}`, etc.
|
||||||
|
- **Description**: Screenshot descriptions are added in the `readme.txt` file under the "Screenshots" section
|
||||||
|
|
||||||
|
## Asset Design Best Practices
|
||||||
|
|
||||||
|
### Banner Design
|
||||||
|
|
||||||
|
- **Keep it Simple**: Don't overcrowd the banner with too much information
|
||||||
|
- **Consistent Branding**: Use colors and fonts that match your plugin's branding
|
||||||
|
- **Readable Text**: Ensure any text is large enough to read
|
||||||
|
- **Purpose**: Clearly communicate what your plugin does
|
||||||
|
- **Avoid Small Details**: Small details may not be visible at the displayed size
|
||||||
|
|
||||||
|
### Icon Design
|
||||||
|
|
||||||
|
- **Simple and Recognizable**: The icon should be simple and easily recognizable
|
||||||
|
- **Consistent with Banner**: Use similar colors and style as your banner
|
||||||
|
- **Works at Small Sizes**: Ensure the icon is recognizable even at small sizes
|
||||||
|
- **Transparent Background**: Use a transparent background for the PNG
|
||||||
|
|
||||||
|
### Screenshot Best Practices
|
||||||
|
|
||||||
|
- **Show Key Features**: Showcase the most important features of your plugin
|
||||||
|
- **Logical Order**: Arrange screenshots in a logical order that tells a story
|
||||||
|
- **Clear and Focused**: Each screenshot should focus on a specific feature
|
||||||
|
- **Annotations**: Consider adding annotations to highlight important elements
|
||||||
|
- **Real-World Examples**: Show the plugin in action with real-world examples
|
||||||
|
|
||||||
|
## Uploading Assets
|
||||||
|
|
||||||
|
Assets are uploaded separately from your plugin code:
|
||||||
|
|
||||||
|
1. Log in to your WordPress.org account
|
||||||
|
2. Navigate to your plugin's page
|
||||||
|
3. Click on the "Assets" tab
|
||||||
|
4. Upload your assets using the provided interface
|
||||||
|
|
||||||
|
## Asset Updates
|
||||||
|
|
||||||
|
When updating assets:
|
||||||
|
|
||||||
|
1. Prepare the new assets following the guidelines above
|
||||||
|
2. Log in to your WordPress.org account
|
||||||
|
3. Navigate to your plugin's page
|
||||||
|
4. Click on the "Assets" tab
|
||||||
|
5. Upload the new assets
|
||||||
|
6. It may take some time for the changes to appear on WordPress.org
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [WordPress Plugin Developer Handbook](https://developer.wordpress.org/plugins/)
|
||||||
|
- [WordPress Plugin Directory Guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)
|
||||||
|
- [WordPress Plugin Assets](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/)
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# WordPress.org Plugin Submission Guide
|
||||||
|
|
||||||
|
This document provides a step-by-step guide for submitting your plugin to the WordPress.org plugin repository.
|
||||||
|
|
||||||
|
## Before Submission
|
||||||
|
|
||||||
|
### 1. Prepare Your Plugin
|
||||||
|
|
||||||
|
- Ensure your plugin follows the [WordPress Plugin Guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)
|
||||||
|
- Check that your plugin meets the [Plugin Directory Requirements](https://developer.wordpress.org/plugins/wordpress-org/plugin-developer-faq/)
|
||||||
|
- Verify that your plugin follows WordPress coding standards
|
||||||
|
- Test your plugin thoroughly on different WordPress versions
|
||||||
|
|
||||||
|
### 2. Prepare Required Files
|
||||||
|
|
||||||
|
- **readme.txt**: Create a readme.txt file following the [WordPress readme.txt standard](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/)
|
||||||
|
- **Plugin Header**: Ensure your main plugin file includes the required plugin header
|
||||||
|
- **License**: Include the GPL v2 or later license
|
||||||
|
- **Assets**: Prepare banner, icon, and screenshots (see [WORDPRESS_ORG_ASSETS.md](WORDPRESS_ORG_ASSETS.md))
|
||||||
|
|
||||||
|
### 3. Create a Clean Build
|
||||||
|
|
||||||
|
- Remove any development files (e.g., .git, node_modules, etc.)
|
||||||
|
- Remove any unnecessary files (e.g., .DS_Store, Thumbs.db, etc.)
|
||||||
|
- Create a ZIP file of your plugin
|
||||||
|
|
||||||
|
## Submission Process
|
||||||
|
|
||||||
|
### 1. Create a WordPress.org Account
|
||||||
|
|
||||||
|
If you don't already have one, create an account on [WordPress.org](https://wordpress.org/).
|
||||||
|
|
||||||
|
### 2. Submit Your Plugin
|
||||||
|
|
||||||
|
1. Go to the [Add Your Plugin](https://wordpress.org/plugins/developers/add/) page
|
||||||
|
2. Fill out the submission form:
|
||||||
|
- Plugin Name
|
||||||
|
- Plugin URL (optional)
|
||||||
|
- Description
|
||||||
|
- Upload your plugin ZIP file
|
||||||
|
3. Agree to the terms and submit your plugin
|
||||||
|
|
||||||
|
### 3. Wait for Review
|
||||||
|
|
||||||
|
- The WordPress.org team will review your plugin
|
||||||
|
- This process can take several weeks
|
||||||
|
- You'll receive an email when your plugin is approved or if there are issues to address
|
||||||
|
|
||||||
|
### 4. Address Review Feedback
|
||||||
|
|
||||||
|
If the WordPress.org team provides feedback:
|
||||||
|
|
||||||
|
1. Make the requested changes
|
||||||
|
2. Create a new ZIP file
|
||||||
|
3. Reply to the review email with the changes you've made
|
||||||
|
4. Attach the updated ZIP file or provide a link to download it
|
||||||
|
|
||||||
|
### 5. Upload Assets
|
||||||
|
|
||||||
|
Once your plugin is approved:
|
||||||
|
|
||||||
|
1. Log in to your WordPress.org account
|
||||||
|
2. Navigate to your plugin's page
|
||||||
|
3. Click on the "Assets" tab
|
||||||
|
4. Upload your banner, icon, and screenshots
|
||||||
|
|
||||||
|
## After Approval
|
||||||
|
|
||||||
|
### 1. Set Up SVN Access
|
||||||
|
|
||||||
|
WordPress.org uses Subversion (SVN) for plugin management:
|
||||||
|
|
||||||
|
1. You'll receive SVN access details via email
|
||||||
|
2. Set up SVN on your local machine
|
||||||
|
3. Check out your plugin repository
|
||||||
|
|
||||||
|
### 2. Manage Your Plugin with SVN
|
||||||
|
|
||||||
|
Basic SVN commands for managing your plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check out your plugin repository
|
||||||
|
svn checkout https://plugins.svn.wordpress.org/your-plugin-name/
|
||||||
|
|
||||||
|
# Add new files
|
||||||
|
svn add new-file.php
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
svn commit -m "Description of changes"
|
||||||
|
|
||||||
|
# Update your local copy
|
||||||
|
svn update
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Release Updates
|
||||||
|
|
||||||
|
To release a new version:
|
||||||
|
|
||||||
|
1. Update the version number in your plugin file and readme.txt
|
||||||
|
2. Update the changelog in readme.txt
|
||||||
|
3. Test the new version thoroughly
|
||||||
|
4. Commit the changes to the `trunk` directory
|
||||||
|
5. Tag the new version:
|
||||||
|
```bash
|
||||||
|
svn copy https://plugins.svn.wordpress.org/your-plugin-name/trunk https://plugins.svn.wordpress.org/your-plugin-name/tags/1.0.1 -m "Tagging version 1.0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Respond Promptly**: Respond to review feedback promptly
|
||||||
|
- **Be Patient**: The review process can take time
|
||||||
|
- **Be Thorough**: Test your plugin thoroughly before submission
|
||||||
|
- **Follow Guidelines**: Adhere to all WordPress.org guidelines
|
||||||
|
- **Maintain Your Plugin**: Regularly update your plugin to fix bugs and add features
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [WordPress Plugin Developer Handbook](https://developer.wordpress.org/plugins/)
|
||||||
|
- [WordPress Plugin Directory Guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/)
|
||||||
|
- [WordPress Plugin Developer FAQ](https://developer.wordpress.org/plugins/wordpress-org/plugin-developer-faq/)
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 635 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 185 KiB |
@@ -1,129 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!--
|
|
||||||
WP Plugin Starter banner (WordPress.org banners and the Read Me tab).
|
|
||||||
Source: .wordpress-org/banner.svg. Edit that, then run scripts/build-banner.sh.
|
|
||||||
Branding follows wpallstars.com: navy, red and cream, Zilla Slab italic
|
|
||||||
headings, the wpallstars plugin stack in red only, the logo's arc of five stars and its red double stripes (here with cream between).
|
|
||||||
-->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1544" height="500" viewBox="0 0 1544 500">
|
|
||||||
<title>WPALLSTARS WP Plugin Starter: Built with AI</title>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#000c1f"/>
|
|
||||||
<stop offset="0.55" stop-color="#001537"/>
|
|
||||||
<stop offset="1" stop-color="#0d264d"/>
|
|
||||||
</linearGradient>
|
|
||||||
<radialGradient id="glow-blue" cx="1214" cy="270" r="440" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#bb0000" stop-opacity="0.45"/>
|
|
||||||
<stop offset="1" stop-color="#bb0000" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="glow-red" cx="200" cy="580" r="520" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#bb0000" stop-opacity="0.2"/>
|
|
||||||
<stop offset="1" stop-color="#bb0000" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="glow-gold" cx="1214" cy="76" r="120" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#ffc94d" stop-opacity="0.45"/>
|
|
||||||
<stop offset="1" stop-color="#ffc94d" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="dots-fade" cx="1214" cy="260" r="560" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity="1"/>
|
|
||||||
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
<pattern id="dots" width="28" height="28" patternUnits="userSpaceOnUse">
|
|
||||||
<circle cx="14" cy="14" r="1.5" fill="#ffffea" fill-opacity="0.12"/>
|
|
||||||
</pattern>
|
|
||||||
<mask id="dots-mask">
|
|
||||||
<rect width="1544" height="500" fill="url(#dots-fade)"/>
|
|
||||||
</mask>
|
|
||||||
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#ffe08a"/>
|
|
||||||
<stop offset="1" stop-color="#f5a524"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="gold-text" x1="96" y1="280" x2="700" y2="340" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#ffd970"/>
|
|
||||||
<stop offset="1" stop-color="#f5a524"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="top-red" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#ef4444"/>
|
|
||||||
<stop offset="1" stop-color="#bb0000"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="top-cream" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#ff8a8a"/>
|
|
||||||
<stop offset="1" stop-color="#e03131"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="top-blue" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#c81e1e"/>
|
|
||||||
<stop offset="1" stop-color="#7a0000"/>
|
|
||||||
</linearGradient>
|
|
||||||
<!-- A five-pointed star, radius 1, centred on 0,0 -->
|
|
||||||
<polygon id="star" points="0,-1 0.2245,-0.309 0.9511,-0.309 0.3633,0.118 0.5878,0.809 0,0.382 -0.5878,0.809 -0.3633,0.118 -0.9511,-0.309 -0.2245,-0.309"/>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Background -->
|
|
||||||
<rect width="1544" height="500" fill="url(#bg)"/>
|
|
||||||
<rect width="1544" height="500" fill="url(#glow-red)"/>
|
|
||||||
<rect width="1544" height="500" fill="url(#glow-blue)"/>
|
|
||||||
<rect width="1544" height="500" fill="url(#dots)" mask="url(#dots-mask)"/>
|
|
||||||
|
|
||||||
<!-- wpallstars.com's red double lines, with a cream line between them. -->
|
|
||||||
<g>
|
|
||||||
<rect y="0" width="1544" height="6" fill="#bb0000"/>
|
|
||||||
<rect y="6" width="1544" height="5" fill="#ffffea"/>
|
|
||||||
<rect y="11" width="1544" height="6" fill="#bb0000"/>
|
|
||||||
<rect y="483" width="1544" height="6" fill="#bb0000"/>
|
|
||||||
<rect y="489" width="1544" height="5" fill="#ffffea"/>
|
|
||||||
<rect y="494" width="1544" height="6" fill="#bb0000"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- The picture: scaled down a little so it keeps clear of the stripes -->
|
|
||||||
<g transform="translate(1214 248) scale(0.86) translate(-1214 -248)">
|
|
||||||
<!-- The stack: red, cream and blue layers, each with a top face and two sides -->
|
|
||||||
<g stroke-linejoin="round">
|
|
||||||
<g transform="translate(0 350)">
|
|
||||||
<polygon points="1064,0 1214,87 1214,117 1064,30" fill="#5c0000"/>
|
|
||||||
<polygon points="1214,87 1364,0 1364,30 1214,117" fill="#420000"/>
|
|
||||||
<polygon points="1214,-87 1364,0 1214,87 1064,0" fill="url(#top-blue)"/>
|
|
||||||
<polygon points="1214,-61 1319,0 1214,61 1109,0" fill="none" stroke="#ffffff" stroke-opacity="0.3" stroke-width="2"/>
|
|
||||||
</g>
|
|
||||||
<g transform="translate(0 294)">
|
|
||||||
<polygon points="1064,0 1214,87 1214,117 1064,30" fill="#c22424"/>
|
|
||||||
<polygon points="1214,87 1364,0 1364,30 1214,117" fill="#9e1414"/>
|
|
||||||
<polygon points="1214,-87 1364,0 1214,87 1064,0" fill="url(#top-cream)"/>
|
|
||||||
<polygon points="1214,-61 1319,0 1214,61 1109,0" fill="none" stroke="#001537" stroke-opacity="0.18" stroke-width="2"/>
|
|
||||||
</g>
|
|
||||||
<g transform="translate(0 238)">
|
|
||||||
<polygon points="1064,0 1214,87 1214,117 1064,30" fill="#940000"/>
|
|
||||||
<polygon points="1214,87 1364,0 1364,30 1214,117" fill="#700000"/>
|
|
||||||
<polygon points="1214,-87 1364,0 1214,87 1064,0" fill="url(#top-red)"/>
|
|
||||||
<!-- The WordPress Plugins icon (Dashicons admin-plugins, GPLv2 or later),
|
|
||||||
laid flat on the top face: icon x runs along the top-right edge and
|
|
||||||
icon y along the top-left edge, each 20-unit side 66% of the face.
|
|
||||||
Turned 45 degrees so the plug points along the back-left edge. -->
|
|
||||||
<g transform="matrix(4.95 2.871 -4.95 2.871 1214 -57.42)">
|
|
||||||
<path fill="#5c0000" fill-opacity="0.55" transform="translate(0.3 0.3) rotate(-45 10 10)" d="M13.11 4.36L9.87 7.6 8 5.73l3.24-3.24c.35-.34 1.05-.2 1.56.32.52.51.66 1.21.31 1.55zm-8 1.77l.91-1.12 9.01 9.01-1.19.84c-.71.71-2.63 1.16-3.82 1.16H6.14L4.9 17.26c-.59.59-1.54.59-2.12 0-.59-.58-.59-1.53 0-2.12l1.24-1.24v-3.88c0-1.13.4-3.19 1.09-3.89zm7.26 3.97l3.24-3.24c.34-.35 1.04-.21 1.55.31.52.51.66 1.21.31 1.55l-3.24 3.25z"/>
|
|
||||||
<path fill="#ffffea" transform="rotate(-45 10 10)" d="M13.11 4.36L9.87 7.6 8 5.73l3.24-3.24c.35-.34 1.05-.2 1.56.32.52.51.66 1.21.31 1.55zm-8 1.77l.91-1.12 9.01 9.01-1.19.84c-.71.71-2.63 1.16-3.82 1.16H6.14L4.9 17.26c-.59.59-1.54.59-2.12 0-.59-.58-.59-1.53 0-2.12l1.24-1.24v-3.88c0-1.13.4-3.19 1.09-3.89zm7.26 3.97l3.24-3.24c.34-.35 1.04-.21 1.55.31.52.51.66 1.21.31 1.55l-3.24 3.25z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- The logo's arc of five stars: gold in the middle, floating above the stack -->
|
|
||||||
<circle cx="1214" cy="76" r="120" fill="url(#glow-gold)"/>
|
|
||||||
<g stroke-linejoin="round">
|
|
||||||
<g fill="#bb0000" stroke="#ffffea" stroke-width="5">
|
|
||||||
<use xlink:href="#star" transform="translate(1043 150) scale(17)" stroke-width="0.3"/>
|
|
||||||
<use xlink:href="#star" transform="translate(1110 100) scale(26)" stroke-width="0.2"/>
|
|
||||||
<use xlink:href="#star" transform="translate(1318 100) scale(26)" stroke-width="0.2"/>
|
|
||||||
<use xlink:href="#star" transform="translate(1385 150) scale(17)" stroke-width="0.3"/>
|
|
||||||
</g>
|
|
||||||
<use xlink:href="#star" transform="translate(1214 78) scale(54)" fill="url(#gold)" stroke="#ffffea" stroke-width="0.11"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Words -->
|
|
||||||
<g font-family="Zilla Slab" text-rendering="geometricPrecision">
|
|
||||||
<text x="98" y="128" font-size="27" font-weight="600" letter-spacing="5" fill="#ff999b">WPALLSTARS</text>
|
|
||||||
<text x="92" y="240" font-size="104" font-weight="700" font-style="italic" letter-spacing="-1" fill="#ffffea">WP Plugin Starter</text>
|
|
||||||
<text x="96" y="326" font-size="72" font-weight="700" font-style="italic" fill="url(#gold-text)">Built with AI</text>
|
|
||||||
<text x="98" y="396" font-size="33" font-weight="500" fill="#c2cad6">A clean start for WordPress plugins, made with aidevops.</text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 7.7 KiB |
@@ -1,51 +0,0 @@
|
|||||||
# WP Plugin Starter — agent guide
|
|
||||||
|
|
||||||
The wpallstars starter plugin: what every wpallstars plugin is made from.
|
|
||||||
It has no features of its own, only the parts every plugin needs.
|
|
||||||
|
|
||||||
**Read `STANDARDS.md` before any change.** It holds the rules every plugin
|
|
||||||
made from the starter shares: structure and core files, code rules, Updates
|
|
||||||
from GitHub, releases, front-end styling and dark mode, and testing. This
|
|
||||||
repository holds the master copy of it and of every core file
|
|
||||||
(`scripts/core-files.txt`). This file holds only what is the starter's own.
|
|
||||||
|
|
||||||
| Placeholder in `STANDARDS.md` | WP Plugin Starter |
|
|
||||||
|---|---|
|
|
||||||
| `{slug}` | `wp-plugin-starter-template` (main file `wp-plugin-starter-template.php`) |
|
|
||||||
| `{prefix}` | `wpstarter` |
|
|
||||||
| `{Prefix}` | `WPStarter` |
|
|
||||||
| `{PREFIX}` | `WPSTARTER` |
|
|
||||||
| `{Name}` | WP Plugin Starter |
|
|
||||||
| `{css}` | `wps` |
|
|
||||||
|
|
||||||
User docs: `README.md` (developers, and the Read Me tab) and `readme.txt`.
|
|
||||||
Development: `DEVELOPMENT.md`. Releases: `RELEASING.md`; launch state:
|
|
||||||
`LAUNCH.md`.
|
|
||||||
|
|
||||||
## What belongs here
|
|
||||||
|
|
||||||
- Only what every plugin needs. A part one plugin needs stays in that
|
|
||||||
plugin (its `{Prefix}_Setup`, a feature or its own files). When a lesson
|
|
||||||
from a plugin applies to all of them, change the core file here first,
|
|
||||||
then run `scripts/sync-core.sh` in each plugin (`--check` lists what
|
|
||||||
differs). Keep core files free of any one plugin's names, examples and
|
|
||||||
paths: this repository is public.
|
|
||||||
- `includes/class-wpstarter-setup.php` stays empty of features, so the
|
|
||||||
settings screen shows the empty General tab and the Read Me tab. Test a
|
|
||||||
core change in a plugin that uses it, then here.
|
|
||||||
- `scripts/rename-plugin.sh` must keep working on a fresh copy: after a
|
|
||||||
change to names in core files, try it on a throwaway clone (rename, lint,
|
|
||||||
smoke test).
|
|
||||||
- The banner source is `.wordpress-org/banner.svg`: the wpallstars plugin
|
|
||||||
stack picture in red only, on wpallstars branding. Rebuild it with
|
|
||||||
`scripts/build-banner.sh`.
|
|
||||||
- This repository is public. Never name private repositories, their
|
|
||||||
issues, or local paths in it (commits, docs, comments or examples).
|
|
||||||
- Every plugin made from the starter keeps the **Built with AI** credit to
|
|
||||||
aidevops (https://aidevops.sh) in `README.md` and `readme.txt`.
|
|
||||||
|
|
||||||
## Test sites
|
|
||||||
|
|
||||||
Install the GitHub build on a local test site with
|
|
||||||
`scripts/preview-site.sh` (see `DEVELOPMENT.md`), or install the GitHub
|
|
||||||
zip from `scripts/build-release.sh` on any test site.
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
All notable changes to this project should be documented both here and in the main Readme files.
|
||||||
|
|
||||||
|
#### [0.1.5] - 2025-04-19
|
||||||
|
#### Fixed
|
||||||
|
- Release workflow to use correct plugin directory name
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
- Testing setup with wp-env and Cypress
|
||||||
|
- Multisite compatibility
|
||||||
|
- npm scripts for development and testing
|
||||||
|
|
||||||
|
#### [0.1.3] - 2025-04-19
|
||||||
|
#### Added
|
||||||
|
- Improved AI IDE context recommendations in documentation
|
||||||
|
- Enhanced Starter Prompt with guidance on pinning .ai-assistant.md and .ai-workflows/
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- Updated README.md and readme.txt with AI IDE context recommendations
|
||||||
|
- Improved documentation for AI-assisted development
|
||||||
|
- Moved Starter Prompt to the wiki for better organization
|
||||||
|
|
||||||
|
#### [0.1.2] - 2025-04-18
|
||||||
|
#### Added
|
||||||
|
- STARTER-PROMPT.md with comprehensive guide for customizing the template
|
||||||
|
- Additional AI workflow files for better development guidance
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- Updated documentation files with improved instructions
|
||||||
|
|
||||||
|
#### [0.1.1] - 2025-04-18
|
||||||
|
#### Changed
|
||||||
|
- Updated LICENSE file with correct GPL-2.0 text
|
||||||
|
|
||||||
|
#### [0.1.0] - 2025-04-17
|
||||||
|
#### Added
|
||||||
|
- Initial release with basic template structure
|
||||||
|
- Core plugin architecture with OOP approach
|
||||||
|
- Admin interface components and styling
|
||||||
|
- Update mechanism with multiple source options
|
||||||
|
- Documentation templates for users and developers
|
||||||
|
- AI workflow documentation for AI-assisted development
|
||||||
|
- GitHub Actions workflows for automated tasks
|
||||||
|
- Wiki documentation templates
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Contributing to WP Plugin Starter
|
|
||||||
|
|
||||||
Thank you for helping. Bug reports, fixes and ideas are all welcome.
|
|
||||||
|
|
||||||
## Reporting a problem
|
|
||||||
|
|
||||||
Open an issue with the **Bug report** form. Include the versions, the steps
|
|
||||||
and any messages from `wp-content/debug.log`. Security problems go through
|
|
||||||
`SECURITY.md`, never a public issue.
|
|
||||||
|
|
||||||
## Suggesting a feature
|
|
||||||
|
|
||||||
Open an issue with the **Feature request** form before writing code. Each
|
|
||||||
setting makes a choice for the plugin's users, so a new one has to earn its
|
|
||||||
place: say what the feature does on its own and what problem it solves.
|
|
||||||
|
|
||||||
## Pull requests
|
|
||||||
|
|
||||||
1. Read `STANDARDS.md` (how features, settings, migrations and front-end
|
|
||||||
styles work, the code rules and testing), `AGENTS.md` (what is this
|
|
||||||
plugin's own) and `DEVELOPMENT.md` (set-up and checks).
|
|
||||||
2. Keep each pull request to one change. Features are off by default.
|
|
||||||
3. Run `scripts/lint.sh` and fix what it finds in the code.
|
|
||||||
4. Test on a real WordPress site, including WordPress 6.2 with PHP 7.4 if
|
|
||||||
you use a core function, and Kadence light and dark mode if you change
|
|
||||||
front-end styles.
|
|
||||||
5. Add a line to the changelogs: `README.md` (details), `changelog.txt`
|
|
||||||
(for users) and `readme.txt` (one short line).
|
|
||||||
|
|
||||||
CI runs the checks on every pull request. Write the pull request description
|
|
||||||
so a reviewer knows what changed, why, and how you tested it.
|
|
||||||
|
|
||||||
## Licence
|
|
||||||
|
|
||||||
WP Plugin Starter is GPL-2.0-or-later. By contributing, you agree your work is
|
|
||||||
released under the same licence.
|
|
||||||
-206
@@ -1,206 +0,0 @@
|
|||||||
# Development
|
|
||||||
|
|
||||||
How changes are made and checked in every plugin made from the wpallstars
|
|
||||||
starter plugin. This file is the same in each of them (names as
|
|
||||||
placeholders: `STANDARDS.md` lists them, and the plugin's `AGENTS.md` gives
|
|
||||||
its values). Rules for features, code, styling and testing: `STANDARDS.md`;
|
|
||||||
the plugin's own rules: `AGENTS.md`. Releases: `RELEASING.md`.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Open or pick an issue that says what should change and how to check it.
|
|
||||||
2. Work on a branch in its own worktree, never on `main`. Branch names:
|
|
||||||
`feature/…`, `bugfix/…`, `chore/…`.
|
|
||||||
3. Commit small, working steps. Run `scripts/lint.sh` before pushing.
|
|
||||||
4. Push and open a pull request (`Resolves #N`). Open it as a draft while
|
|
||||||
the work is in progress: CI lints every push, and the longer release and
|
|
||||||
smoke-test jobs start when the pull request is marked ready for review.
|
|
||||||
5. Check the change on the shared preview site (`scripts/preview-site.sh`,
|
|
||||||
`STANDARDS.md` → Testing), in light and dark mode for front-end styles.
|
|
||||||
6. Merge once CI passes. Releases are separate: `RELEASING.md`.
|
|
||||||
|
|
||||||
## Set up
|
|
||||||
|
|
||||||
Needs PHP 7.4 or later, Composer 2, Node.js (syntax checks only),
|
|
||||||
ShellCheck and Docker (release checks and smoke test). actionlint is
|
|
||||||
optional locally; CI runs it.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
composer install
|
|
||||||
```
|
|
||||||
|
|
||||||
`composer.json` lists development tools only. The plugin has no Composer
|
|
||||||
dependencies, and `vendor/`, `composer.*`, the tool configuration and
|
|
||||||
`.github/` are left out of release zips (`.distignore`; the preflight fails
|
|
||||||
if one gets in).
|
|
||||||
|
|
||||||
## Checks
|
|
||||||
|
|
||||||
Every pull request and every push to `main` runs these in GitHub Actions
|
|
||||||
(`.github/workflows/ci.yml`). Each one runs the same way locally.
|
|
||||||
|
|
||||||
| Check | Command | What it finds |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| PHP syntax | `scripts/lint.sh php` | Syntax errors; CI uses PHP 7.4, the minimum. |
|
|
||||||
| JavaScript syntax | `scripts/lint.sh js` | Syntax errors (`node --check`). |
|
|
||||||
| Shell scripts | `scripts/lint.sh shell` | ShellCheck findings in `scripts/`. |
|
|
||||||
| Workflows | `scripts/lint.sh workflows` | actionlint findings in `.github/workflows/`. |
|
|
||||||
| Coding standards | `scripts/lint.sh phpcs` | WordPress Coding Standards: escaping, sanitising, nonces, prepared SQL, i18n, PHP 7.4 and WordPress 6.2 compatibility (`phpcs.xml.dist`). |
|
|
||||||
| Static analysis | `scripts/lint.sh phpstan` | Unknown functions, classes and methods, wrong argument counts and types, dead code (PHPStan level 5, `phpstan.neon.dist`). |
|
|
||||||
| Release build | `scripts/preflight-release.sh --offline` | Versions, headers, `readme.txt`, presets (where the plugin has them) and the contents of both zips. |
|
|
||||||
| Plugin Check | `scripts/plugin-check.sh` | The WordPress.org review tool, on both zips. |
|
|
||||||
| Smoke test | `scripts/smoke-test.sh --wp 6.2 --php 7.4` and `scripts/smoke-test.sh` | Installs the GitHub zip, loads the site and admin screens with default settings and with every feature on, runs cron, uninstalls. Fails on any PHP message, a failed page or leftover options. |
|
|
||||||
|
|
||||||
`scripts/lint.sh` with no arguments runs the first six.
|
|
||||||
|
|
||||||
The scripts work out which plugin they are in from its main file
|
|
||||||
(`scripts/lib/plugin.sh`): the PHP file at the top of the repository with a
|
|
||||||
`Plugin Name:` header gives the slug (its file name), the name, the class
|
|
||||||
prefix (`@package`) and the constant prefix (`define('<PREFIX>_VERSION', …)`).
|
|
||||||
Environment overrides use that prefix, for example
|
|
||||||
`{PREFIX}_PREVIEW_SITE` or `{PREFIX}_DB_IMAGE`. Checks for parts only some
|
|
||||||
plugins have (presets and starter data, the replaced plugins count) run
|
|
||||||
when their files exist, so the same scripts serve every plugin made from the
|
|
||||||
starter.
|
|
||||||
|
|
||||||
### Coding standards
|
|
||||||
|
|
||||||
`phpcs.xml.dist` uses the WordPress ruleset. The plugin's own style
|
|
||||||
differs from it in a few places (spacing, `array()`, file names, Yoda
|
|
||||||
conditions), and those sniffs are off; the comments in the file say why.
|
|
||||||
Security, database and compatibility sniffs stay on. Fix findings in the
|
|
||||||
code. Where a finding is intended, add an inline
|
|
||||||
`// phpcs:ignore Sniff.Name -- reason` on that line only.
|
|
||||||
|
|
||||||
`vendor/bin/phpcbf` fixes what it can automatically.
|
|
||||||
|
|
||||||
### Static analysis
|
|
||||||
|
|
||||||
PHPStan reads the code with WordPress's stubs and PHP 7.4's functions.
|
|
||||||
`scripts/phpstan-bootstrap.php` defines the constants WordPress and the
|
|
||||||
plugin set while loading. Classes and functions of other plugins the code
|
|
||||||
uses (WP-CLI, WooCommerce and the like) are ignored in `phpstan-plugin.neon`,
|
|
||||||
because the code uses them only after checking they are loaded.
|
|
||||||
|
|
||||||
`phpstan-baseline.neon` is empty: every finding fails the check. Fix the
|
|
||||||
code. Where PHPStan or the stubs are wrong (a custom `wp_hash()` scheme, a
|
|
||||||
check for a method newer WordPress versions have, variables a closure
|
|
||||||
changes by reference), add an entry under `ignoreErrors` with the
|
|
||||||
identifier, the file and the reason: in `phpstan.neon.dist` for the files
|
|
||||||
every plugin made from the starter shares, in `phpstan-plugin.neon` for the
|
|
||||||
plugin's own files and extra paths. Never put findings in the baseline to
|
|
||||||
get a change through.
|
|
||||||
|
|
||||||
### Secrets in history
|
|
||||||
|
|
||||||
Scan the whole Git history before the repository goes public, and after
|
|
||||||
importing code from elsewhere:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm -v "$PWD:/repo:ro" ghcr.io/gitleaks/gitleaks:latest git /repo --redact --no-banner
|
|
||||||
```
|
|
||||||
|
|
||||||
In a linked worktree, also mount the main repository's `.git` folder at
|
|
||||||
the same path. `.gitleaks.toml` lists the false positives with reasons. A
|
|
||||||
real secret is rotated first, then removed from history.
|
|
||||||
|
|
||||||
### Smoke test
|
|
||||||
|
|
||||||
`scripts/smoke-test.sh` starts a throwaway WordPress in Docker (MariaDB,
|
|
||||||
Apache and WP-CLI images for the chosen PHP version), so nothing touches
|
|
||||||
the shared preview site. CI runs it on WordPress 6.2 with PHP 7.4 and on
|
|
||||||
the latest WordPress with PHP 8.3. Every feature with an on/off setting is
|
|
||||||
switched on at once, except one with the key `maintenance` (a maintenance
|
|
||||||
mode would answer every visitor page with its notice). `--keep-log FILE` saves `debug.log`; CI keeps it as an artifact
|
|
||||||
when the test fails.
|
|
||||||
|
|
||||||
## Test site resources
|
|
||||||
|
|
||||||
Test sites run the plugin alongside many others (a plugin that recommends
|
|
||||||
plugins is tested with all of them active). PHP's defaults are too small for that: OPcache fills
|
|
||||||
and restarts, admin requests queue behind two workers, and the slowdowns
|
|
||||||
look like bugs. Size every test site, shared or throwaway, with room to
|
|
||||||
spare:
|
|
||||||
|
|
||||||
| Setting | Where (LocalWP, nginx) | Value | PHP or Local default |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `opcache.memory_consumption` | `conf/php/php.ini.hbs` | `1024` | 128 |
|
|
||||||
| `opcache.interned_strings_buffer` | `conf/php/php.ini.hbs` | `64` | 8 |
|
|
||||||
| `opcache.max_accelerated_files` | `conf/php/php.ini.hbs` | `50000` | 10000 |
|
|
||||||
| `memory_limit` | `conf/php/php.ini.hbs` | `768M` | 256M |
|
|
||||||
| `pm.max_children` | `conf/php/php-fpm.d/www.conf.hbs` | `5` | 2 |
|
|
||||||
|
|
||||||
Measured on a shared test site with 72 plugins installed: OPcache used
|
|
||||||
509 MB of 512 MB and restarted, then 315 MB of 1 GB with 31,723 PHP files
|
|
||||||
cached; pages peaked at 352 MB of memory; the busiest hour needed 5 workers.
|
|
||||||
|
|
||||||
- LocalWP keeps these per site in `<site>/conf/`. Edit the `.hbs`
|
|
||||||
templates, not the generated files under Local's `run/` folder, then
|
|
||||||
stop and start the site in Local: it rebuilds the configuration only on
|
|
||||||
start. Add a comment with the date and reason next to each change.
|
|
||||||
- The OPcache lines sit inside `{{#unless apache}}`: on an Apache site,
|
|
||||||
move them out or set them in Apache's own configuration.
|
|
||||||
- Check after the restart: Site Health → Info → Server shows the new values.
|
|
||||||
- Raise them again when a site needs more, and update this table in the
|
|
||||||
starter.
|
|
||||||
- To test low-resource warnings, lower a value on a throwaway site, never
|
|
||||||
the shared one.
|
|
||||||
- Docker sites (`scripts/smoke-test.sh`, step 4 in `STANDARDS.md` →
|
|
||||||
Testing) take the same values in a `.ini` file mounted into
|
|
||||||
`/usr/local/etc/php/conf.d/`.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
Dependabot (`.github/dependabot.yml`) opens one pull request a week for
|
|
||||||
the GitHub Actions and one for the Composer tools. Actions are pinned to
|
|
||||||
commit SHAs with the version in a comment; keep it that way when editing
|
|
||||||
workflows. Do not add third-party actions that only save a few lines of
|
|
||||||
shell.
|
|
||||||
|
|
||||||
## While private: checks are advisory
|
|
||||||
|
|
||||||
The repository is private until it is released, so Actions minutes cost
|
|
||||||
money and most code-review services are paid. Until then, work moves fast:
|
|
||||||
|
|
||||||
- No branch protection or required checks on `main`. A failing check does
|
|
||||||
not block a merge, so a merge never waits on an unrelated failure.
|
|
||||||
- Fix any failure your change causes before merging. For a failure your
|
|
||||||
change did not cause, open an issue with the run link and the first error,
|
|
||||||
and merge anyway.
|
|
||||||
- Draft pull requests run only the lint job. A new push cancels the run for
|
|
||||||
the previous one.
|
|
||||||
- The build zips and Plugin Check reports are kept for seven days on each
|
|
||||||
run (artifact `<repository>-build-…`) for testing a branch on a site.
|
|
||||||
- Review apps that are already installed (CodeRabbit, qlty, Socket) give
|
|
||||||
advice only. A rate-limited or missing review never holds up a merge.
|
|
||||||
|
|
||||||
## At public launch: full sweep
|
|
||||||
|
|
||||||
Making the repository public needs the owner's say. Do this sweep in the
|
|
||||||
same step, while the code-review services are free for
|
|
||||||
public repositories. It goes through the whole codebase once, then keeps
|
|
||||||
it at that standard:
|
|
||||||
|
|
||||||
1. Turn on the free reviewers for the whole codebase, not just new
|
|
||||||
changes: CodeRabbit full review, Codacy, SonarCloud (SonarQube Cloud)
|
|
||||||
and qlty, plus GitHub's CodeQL (PHP and JavaScript), Dependabot
|
|
||||||
security alerts, secret scanning with push protection, and OpenSSF
|
|
||||||
Scorecard. Socket keeps checking dependencies.
|
|
||||||
2. Fix what they find in the code, in small pull requests by area
|
|
||||||
(security first). Each finding is either fixed, explained in an inline
|
|
||||||
comment, or marked as a false positive in that service with the reason.
|
|
||||||
3. Raise the PHPStan level one step at a time (6, then higher if the
|
|
||||||
findings are real bugs and not noise). The baseline is already empty.
|
|
||||||
4. Require the CI checks on `main` (Lint, Release build, both Smoke
|
|
||||||
tests) with a branch ruleset, without "branch must be up to date": the
|
|
||||||
checks are fast, and changelog lines conflict on every merge.
|
|
||||||
5. Turn on private vulnerability reporting (Settings → Security), which
|
|
||||||
`SECURITY.md` asks reporters to use, and add the CI badge to
|
|
||||||
`README.md`. `SECURITY.md`, `CONTRIBUTING.md` and the issue and pull
|
|
||||||
request templates are already in place.
|
|
||||||
6. Run `workflows/public-launch-checklist.md` from the AI DevOps framework
|
|
||||||
for anything public: no private paths, site names or secrets in the code,
|
|
||||||
history, issues or docs. Run the history scan above again for commits
|
|
||||||
made since the last one.
|
|
||||||
|
|
||||||
What a plugin has already done ahead of launch is in its `LAUNCH.md`.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# WP Plugin Starter — launch state
|
|
||||||
|
|
||||||
The starter is released on GitHub only. It is not meant for WordPress.org:
|
|
||||||
it has no features, and plugins made from it are submitted under their own
|
|
||||||
names. `RELEASING.md` → WordPress.org applies to those plugins, each with its
|
|
||||||
own `LAUNCH.md`.
|
|
||||||
|
|
||||||
`scripts/preflight-release.sh` and `scripts/plugin-check.sh` still run on
|
|
||||||
every release, so the core files stay ready for a plugin's submission.
|
|
||||||
@@ -1,108 +1,320 @@
|
|||||||

|
# WordPress Plugin Starter Template for AI Coding
|
||||||
|
|
||||||
# WP Plugin Starter
|
[](https://www.gnu.org/licenses/gpl-2.0.html)
|
||||||
|
|
||||||
A clean start for a WordPress plugin: a settings screen, a Read Me tab, updates from GitHub and release scripts, ready for your features.
|
A comprehensive starter template for WordPress plugins with best practices for AI-assisted development.
|
||||||
|
|
||||||
WP Plugin Starter is what wpallstars plugins are made from. It has no features of its own: it holds the parts every plugin needs, built and tested in real plugins, so a new plugin starts with them working and spends its time on what makes it different.
|
## Description
|
||||||
|
|
||||||
If this saves you time, headaches and costs, feel free to [buy me a coffee](https://buymeacoffee.com/marcusquinn), or whatever you like, to invest in making more things open-source.
|
The WordPress Plugin Starter Template provides a solid foundation for developing WordPress plugins. It incorporates best practices, modern coding standards, and a comprehensive structure that makes it easy to get started with plugin development.
|
||||||
|
|
||||||
Version: {WPSTARTER_VERSION}
|
This template is based on the experience gained from developing the "Fix 'Plugin file does not exist' Notices" plugin and other successful WordPress plugins.
|
||||||
|
|
||||||
## What you get
|
### Key Features
|
||||||
|
|
||||||
- **A settings screen** (Settings → WP Plugin Starter) that features fill by declaring their settings: switches, numbers, text, lists, choices and Media Library pictures, saved instantly with no Save button, searchable, in tabs. With no features yet it shows one empty tab.
|
* **Object-Oriented Architecture**: Well-structured, maintainable code using OOP principles
|
||||||
- **A Read Me tab** that shows this file, banner included, so users read the same guide inside WordPress as on GitHub.
|
* **Namespace Support**: Modern PHP namespacing for better organization and avoiding conflicts
|
||||||
- **Features as classes**: one file per feature, off by default, with settings, hooks, one-off imports from the plugins it replaces and clean uninstall.
|
* **Comprehensive Documentation**: Detailed documentation for both users and developers
|
||||||
- **Replaced plugins**: a feature that does another plugin's job imports its settings once, waits while that plugin is active, and the Plugins screen suggests deactivating and deleting it.
|
* **Testing Framework**: PHPUnit setup for unit testing and Cypress for e2e testing
|
||||||
- **Updates from GitHub**: the shared wpallstars updater (`includes/github-updater/`). Sites get each GitHub release as a normal WordPress update. Every wpallstars plugin carries a copy and only the newest copy on a site runs, so they are all checked together, once.
|
* **Internationalization Ready**: Full support for translation and localization
|
||||||
- **Two builds of each version**: the GitHub release, and a WordPress.org build without the updater, as WordPress.org requires.
|
* **Update Source Selection**: Choose between WordPress.org, GitHub, or Gitea for plugin updates
|
||||||
- **Scripts and CI**: lint (PHP 7.4, WordPress coding and security rules, PHPStan), a smoke test on a real WordPress, the release build, a preflight check of both zips, Plugin Check, a preview site, the banner build, and `scripts/sync-core.sh` to keep each plugin's shared parts the same as the starter's.
|
* **AI Workflow Documentation**: Detailed guides for AI-assisted development
|
||||||
- **Shared rules for people and AI**: `STANDARDS.md` (structure, code rules, releases, styling, testing), `DEVELOPMENT.md` (set-up and checks) and `RELEASING.md`, the same in every plugin made from the starter.
|
* **Wiki Documentation**: Ready-to-use wiki structure for comprehensive documentation
|
||||||
|
* **Multisite Compatible**: Fully tested and compatible with WordPress multisite installations
|
||||||
|
|
||||||
## Start a plugin
|
### How to Use This Template
|
||||||
|
|
||||||
1. On GitHub, choose **Use this template** to make your repository, and clone it.
|
1. Clone or download this repository
|
||||||
2. Give it its names: `scripts/rename-plugin.sh --slug my-plugin --name "My Plugin" --prefix MyPlugin`. Add `--css mp` for a short CSS prefix and `--repo owner/repo` if it is not under wpallstars. Review with `git diff`, then commit.
|
2. **Important**: Begin by reading the [Starter Prompt](.wiki/Starter-Prompt.md) file for detailed instructions
|
||||||
3. Replace this README, `readme.txt`, `changelog.txt` and `AGENTS.md` with your plugin's own, and its banner (`.wordpress-org/banner.svg`, then `scripts/build-banner.sh`).
|
3. Add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context (pin them if possible)
|
||||||
4. Add features: a class in `includes/features/` listed in `MyPlugin_Setup::FEATURES` (see Developers below and `STANDARDS.md`).
|
4. Use the prompt in the Starter Prompt file to guide the AI in customizing the template for your plugin
|
||||||
5. Keep the shared parts up to date: change them in the starter first, then run `scripts/sync-core.sh` in each plugin (`--check` lists what differs).
|
5. Rename files and update namespaces to match your plugin
|
||||||
|
6. Customize the functionality for your specific needs
|
||||||
|
7. Update documentation to reflect your plugin's features
|
||||||
|
8. Build and test your plugin
|
||||||
|
|
||||||
The easiest way to do all of this is with [aidevops](https://aidevops.sh): open the repository with it and describe the plugin you want. It reads `AGENTS.md` and `STANDARDS.md`, builds the features, tests them on a real site and runs the release checks.
|
### AI-Assisted Development
|
||||||
|
|
||||||
## Where to find it
|
This template includes comprehensive documentation for AI-assisted development:
|
||||||
|
|
||||||
Go to **Settings → WP Plugin Starter**. The screen has two groups of tabs:
|
* **.ai-assistant.md**: Guide for AI assistants to understand the project structure
|
||||||
|
* **.ai-workflows/**: Detailed workflow documentation for common development tasks
|
||||||
|
* **Starter Prompt**: Comprehensive prompt for AI tools to help customize the template (available in the [wiki](.wiki/Starter-Prompt.md))
|
||||||
|
|
||||||
- **Settings**: General, empty until features add settings. Changes save instantly; there is no Save button. **Search features** (next to the plugin name) finds settings on every tab.
|
**Important**: For the best AI assistance, add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context. In most AI IDEs, you can pin these files to ensure they're considered in each message.
|
||||||
- **About**: this Read Me.
|
|
||||||
|
|
||||||
**Report a problem**, at the top right of the screen, opens the plugin’s [GitHub issues](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/issues) in a new tab. Say what you did, what you expected and what happened, with the versions of WordPress, PHP and WP Plugin Starter. Leave out passwords, licence keys and personal data, since issues are public. **Buy me a coffee**, next to it, opens the maker’s [Buy Me a Coffee](https://buymeacoffee.com/marcusquinn) page in a new tab.
|
## Installation
|
||||||
|
|
||||||
## Requirements
|
1. Clone or download this repository
|
||||||
|
2. Read the [Starter Prompt](.wiki/Starter-Prompt.md) file for detailed instructions
|
||||||
|
3. Add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context
|
||||||
|
4. Use the prompt in the Starter Prompt file to guide the AI in customizing the template
|
||||||
|
5. Rename files and update namespaces to match your plugin
|
||||||
|
6. Customize the functionality for your specific needs
|
||||||
|
7. Update documentation to reflect your plugin's features
|
||||||
|
8. Build and test your plugin
|
||||||
|
|
||||||
- WordPress 6.2 or later
|
## Usage
|
||||||
- PHP 7.4 or later
|
|
||||||
|
|
||||||
## Updates and releases
|
### Getting Started
|
||||||
|
|
||||||
There are two builds of each version:
|
To get started with this template, follow these steps:
|
||||||
|
|
||||||
- **GitHub release** (`wp-plugin-starter-template-X.Y.Z.zip` on the repository’s Releases page): everything, including the shared updater in `includes/github-updater/`, so sites get each release as a normal update.
|
1. In your terminal, navigate to the folder you keep you Git repositories (eg: `~/Git/`), then clone this repository to your local machine:
|
||||||
- **WordPress.org**: the same files without the updater (listed in `.distignore-wporg`) and without the `GitHub Plugin URI`, `Primary Branch` and `Release Asset` header lines, because plugins hosted there may not install or update code from elsewhere.
|
```bash
|
||||||
|
git clone https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding.git
|
||||||
|
```
|
||||||
|
|
||||||
The updater waits while Git Updater is active, so the two never both update a plugin.
|
2. Open the [Starter Prompt](.wiki/Starter-Prompt.md) file and follow the instructions to customize the template for your plugin.
|
||||||
|
|
||||||
Releasing on GitHub:
|
3. Add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context.
|
||||||
|
|
||||||
1. Merge the version change (`Version:` and `WPSTARTER_VERSION` in `wp-plugin-starter-template.php`, `Stable tag:` in `readme.txt`) to `main`.
|
4. Use an AI assistant like GitHub Copilot, Claude, or ChatGPT to help you customize the template by providing the prompt from the Starter Prompt file.
|
||||||
2. Straight away, tag that commit `vX.Y.Z` and publish a GitHub release with `wp-plugin-starter-template-X.Y.Z.zip` attached. `scripts/build-release.sh --ref vX.Y.Z` builds it (and the WordPress.org zip) from the tag with `.distignore` applied, everything inside a `wp-plugin-starter-template/` folder; `scripts/preflight-release.sh` and `scripts/plugin-check.sh` check them first. Sites pick the latest release whose tag is a plain version number and the asset whose name starts with `wp-plugin-starter-template`, so never attach the WordPress.org zip. Full steps: `RELEASING.md`.
|
|
||||||
3. Sites offer the update when they next check (within 12 hours, or at once with **Check again** on the Updates screen).
|
|
||||||
|
|
||||||
Mark test builds as pre-releases on GitHub (or tag them with letters, such as `v1.2.0-rc1`): sites never offer those. Do not add an `Update URI` header: WordPress.org rejects it, and Plugin Check reports it as an updater.
|
### Development Environment
|
||||||
|
|
||||||
|
This template includes configuration for WordPress Environment (wp-env) to make local development easier:
|
||||||
|
|
||||||
|
1. Install Node.js dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the WordPress environment:
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
3. For multisite testing:
|
||||||
|
```bash
|
||||||
|
npm run multisite
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Access your local WordPress site at http://localhost:8888 (admin credentials: admin/password)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
The template includes both PHP unit tests and end-to-end tests:
|
||||||
|
|
||||||
|
#### PHP Unit Tests
|
||||||
|
|
||||||
|
1. Install Composer dependencies:
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run PHP unit tests:
|
||||||
|
```bash
|
||||||
|
npm run test:php
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Check PHP coding standards:
|
||||||
|
```bash
|
||||||
|
npm run lint:php
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Fix PHP coding standards issues:
|
||||||
|
```bash
|
||||||
|
npm run fix:php
|
||||||
|
```
|
||||||
|
|
||||||
|
#### End-to-End Tests
|
||||||
|
|
||||||
|
1. Start the WordPress environment:
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run Cypress tests in interactive mode:
|
||||||
|
```bash
|
||||||
|
npm run test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run Cypress tests in headless mode:
|
||||||
|
```bash
|
||||||
|
npm run test:e2e:headless
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
Use the included build script to create a deployable version of your plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Or directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh {VERSION}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a ZIP file that you can install in WordPress.
|
||||||
|
|
||||||
|
### Using with Git Updater
|
||||||
|
|
||||||
|
If you've installed this plugin from GitHub or Gitea, you'll need Git Updater to receive updates:
|
||||||
|
|
||||||
|
1. Install the Git Updater plugin from [git-updater.com/git-updater/](https://git-updater.com/git-updater/)
|
||||||
|
2. Go to Settings > Git Updater > Remote Management
|
||||||
|
3. Click the "Refresh Cache" button to ensure Git Updater recognizes the latest version
|
||||||
|
4. Updates will now appear in your WordPress dashboard when available
|
||||||
|
|
||||||
|
### Choosing Your Update Source
|
||||||
|
|
||||||
|
This template includes functionality that allows users to choose where they want to receive updates from:
|
||||||
|
|
||||||
|
1. In the Plugins list, find your plugin
|
||||||
|
2. Click the "Update Source" link next to the plugin
|
||||||
|
3. Select your preferred update source:
|
||||||
|
- **WordPress.org**: Updates from the official WordPress.org repository
|
||||||
|
- **GitHub**: Updates directly from the GitHub repo
|
||||||
|
- **Gitea**: Updates directly from the Gitea repo
|
||||||
|
4. Click "Save" to apply your preference
|
||||||
|
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
### How do I customize this template for my plugin?
|
||||||
|
|
||||||
|
See the [Starter Prompt](.wiki/Starter-Prompt.md) file for detailed instructions on customizing this template for your specific plugin needs. Make sure to add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context for the best results.
|
||||||
|
|
||||||
|
### What files do I need to update with my plugin information?
|
||||||
|
|
||||||
|
The main files you need to update include:
|
||||||
|
1. Main plugin file (rename and update header)
|
||||||
|
2. README.md
|
||||||
|
3. readme.txt
|
||||||
|
4. CHANGELOG.md
|
||||||
|
5. composer.json
|
||||||
|
6. package.json
|
||||||
|
7. languages/pot file
|
||||||
|
8. .github/workflows/
|
||||||
|
9. .wiki/
|
||||||
|
10. .ai-assistant.md
|
||||||
|
11. includes/plugin.php
|
||||||
|
12. includes/core.php
|
||||||
|
13. admin/lib/admin.php
|
||||||
|
|
||||||
|
### How do I build and test my plugin?
|
||||||
|
|
||||||
|
Use the included build.sh script to create a deployable version of your plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh {VERSION}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a ZIP file that you can install in WordPress.
|
||||||
|
|
||||||
|
### How do I add custom functionality to my plugin?
|
||||||
|
|
||||||
|
Customize the includes/core.php file to implement your core functionality and the admin/lib/admin.php file for admin-specific functionality.
|
||||||
|
|
||||||
|
### Is this template compatible with WordPress multisite?
|
||||||
|
|
||||||
|
Yes, this template is fully compatible with WordPress multisite installations. You can test multisite compatibility by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run multisite
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support & Feedback
|
||||||
|
|
||||||
|
If you need help with this template, there are several ways to get support:
|
||||||
|
|
||||||
|
* [GitHub Issues](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/issues)
|
||||||
|
* [Gitea Issues](https://gitea.wpallstars.com/wpallstars/wp-plugin-starter-template-for-ai-coding/issues)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
1. Fork the repository on [GitHub](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/) or [Gitea](https://gitea.wpallstars.com/wpallstars/wp-plugin-starter-template-for-ai-coding/)
|
||||||
|
2. Create your feature branch: `git checkout -b feature/amazing-feature`
|
||||||
|
3. Commit your changes: `git commit -m 'Add some amazing feature'`
|
||||||
|
4. Push to the branch: `git push origin feature/amazing-feature`
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
## Developers
|
## Developers
|
||||||
|
|
||||||
A feature is a class in `includes/features/class-wpstarter-{name}.php` that extends `WPStarter_Feature`, listed in `WPStarter_Setup::FEATURES`. It declares its settings in `settings()`, adds its hooks in `boot()` (returning early unless `self::enabled()`), and can import another plugin’s settings once in `migrate()` with `self::import_setting()`. Anything only this plugin needs goes in `WPStarter_Setup` (`includes/class-wpstarter-setup.php`): features, settings tabs, header links, settings version and its own helpers. The other files in `includes/` and `admin/` are the starter’s core files: `STANDARDS.md` → Structure.
|
### AI-Powered Development
|
||||||
|
|
||||||
Filters:
|
This repository is configured to work with various AI-powered development tools. You can use any of the following AI IDEs to contribute to this project:
|
||||||
|
|
||||||
- `wpstarter_features`: register a feature class that extends `WPStarter_Feature`.
|
- [Augment Code](https://www.augmentcode.com/) - AI-powered coding assistant
|
||||||
- `wpstarter_settings_schema`: add or change settings. Each entry sets `type` (bool, int, text, url, lines, domains, select, multi, times or media, a picture from the Media Library stored as its attachment ID), `default`, `label`, `description` and either `tab` or `parent`; select and multi also take `options` (an array or a callable), and multi takes `open` to keep saved values that are not currently registered. A child setting can take `hidden` (true): it is not shown or searched, for wiring that code sets. `reload` (true) makes the saved message ask to reload the page, for changes that show only after a page load. `replaces` (slug => name) shows which plugin a feature replaces. Settings render and save automatically.
|
- [Cursor](https://cursor.com/) - AI-first code editor
|
||||||
- `wpstarter_admin_tabs`: add or reorder admin tabs. Each tab sets `label`, `group` (settings, discover or about), a `render` callback and an optional `capability`; tabs the current user lacks the capability for are hidden.
|
- [v0](https://v0.dev/) - AI-powered design and development tool
|
||||||
- `wpstarter_admin_script_deps` and `wpstarter_admin_script_data`: the settings screen script’s dependencies and the data it reads as `wpstarterAdmin` (both with the active tab).
|
- [Windsurf](https://www.windsurf.com/) - AI coding assistant
|
||||||
- `wpstarter_can_change_settings`: return false to stop the current user changing WP Plugin Starter’s settings (on top of `manage_options`).
|
- [Cline](https://cline.bot/) - AI terminal assistant
|
||||||
- `wpstarter_replaced_plugin_extras`: what a replaced plugin does on this site that WP Plugin Starter does not (plain names, plugin folder). The Plugins screen names them instead of saying the plugin can go.
|
- [Roo Code](https://roocode.com/) - AI pair programmer
|
||||||
- `wpstarter_stored_active_plugins` and `wpstarter_plugins_skipped`: for code that skips plugins on some requests, the active plugin files as stored and whether some are skipped on this request, so features still count those plugins as active.
|
- [Loveable](https://lovable.dev/) - AI development environment
|
||||||
- `wpallstars_github_plugins` (GitHub builds, shared updater): change which plugins update from GitHub releases (plugin file => `repo` as owner/repo, `asset_only`, `version`, `name`).
|
- [Bolt](https://www.bolt.new/) - AI-powered code editor
|
||||||
- `wpallstars_github_token` (GitHub builds, shared updater): GitHub token for a repository (token, owner/repo), for private repositories; defaults to the `WPALLSTARS_GITHUB_TOKEN` constant.
|
- [Cody](https://sourcegraph.com/cody) - Sourcegraph's AI coding assistant
|
||||||
- `wpallstars_github_updater_enabled` and `wpallstars_github_updater_early` (GitHub builds, shared updater): whether it runs (false while Git Updater is active) and whether plugins also on WordPress.org take GitHub releases first.
|
- [Continue](https://continue.dev/) - Open-source AI coding assistant
|
||||||
|
|
||||||
Actions:
|
The repository includes configuration files for all these tools to ensure a consistent development experience.
|
||||||
|
|
||||||
- `wpstarter_admin_enqueue`: enqueue the plugin’s own admin styles and scripts on its settings screen (active tab), depending on `wpstarter-admin`. Its script can use `wpstarterAdmin.api` (`post`, `speak`, `errorMessage`).
|
### Git Updater Integration
|
||||||
- `wpstarter_setting_saved`: a setting was saved from the admin screen.
|
|
||||||
- `wpstarter_setting_panel`: print status at the top of a setting’s options panel (setting key, schema entry); wrap it in `<div class="wps-panel-note">`.
|
|
||||||
- `wpstarter_settings_tab_after`: print a section after a settings tab’s cards (tab slug).
|
|
||||||
|
|
||||||
Read a setting with `WPStarter_Settings::get( 'key' )`.
|
This template is designed to work seamlessly with the Git Updater plugin for updates from GitHub and Gitea. To ensure proper integration:
|
||||||
|
|
||||||
## Uninstall
|
1. **Required Headers**: The plugin includes specific headers in the main plugin file that Git Updater uses to determine update sources and branches:
|
||||||
|
```php
|
||||||
|
* GitHub Plugin URI: wpallstars/wp-plugin-starter-template-for-ai-coding
|
||||||
|
* GitHub Branch: main
|
||||||
|
* Primary Branch: main
|
||||||
|
* Release Branch: main
|
||||||
|
* Release Asset: true
|
||||||
|
* Gitea Plugin URI: https://gitea.wpallstars.com/wpallstars/wp-plugin-starter-template-for-ai-coding
|
||||||
|
* Gitea Branch: main
|
||||||
|
```
|
||||||
|
|
||||||
Deleting the plugin removes its settings, its cached data, who hid lines of the Plugins screen notice about replaced plugins, and the cached GitHub releases.
|
2. **Tagging Releases**: When creating a new release, always tag it with the 'v' prefix (e.g., `v0.1.2`) to ensure GitHub Actions can create the proper release assets.
|
||||||
|
|
||||||
|
3. **GitHub Actions**: The repository includes a GitHub Actions workflow that automatically builds the plugin and creates a release with the .zip file when a new tag is pushed.
|
||||||
|
|
||||||
|
4. **Update Source Selection**: The template includes a feature that allows users to choose their preferred update source (WordPress.org, GitHub, or Gitea).
|
||||||
|
|
||||||
|
For more information on Git Updater integration, see the [Git Updater Required Headers documentation](https://git-updater.com/knowledge-base/required-headers/).
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
### 1.0.0
|
### 0.1.5
|
||||||
|
* Fixed: Release workflow to use correct plugin directory name
|
||||||
|
* Added: Testing setup with wp-env and Cypress
|
||||||
|
* Added: Multisite compatibility
|
||||||
|
* Added: npm scripts for development and testing
|
||||||
|
|
||||||
- A fresh start, made from the parts every wpallstars plugin needs, taken from plugins in daily use: the settings screen with search, the Read Me tab, features as classes, replaced plugins, the shared GitHub updater, the release and check scripts, CI and the shared rules (`STANDARDS.md`, `DEVELOPMENT.md`, `RELEASING.md`). Earlier versions of this starter are retired; start again from this one.
|
### 0.1.3
|
||||||
- New: `scripts/rename-plugin.sh` gives a copy of the starter its own names, and `scripts/sync-core.sh` keeps a plugin's core files the same as the starter's (`--check` lists any that differ).
|
* Added: Improved AI IDE context recommendations in documentation
|
||||||
|
* Enhanced: Starter Prompt with guidance on pinning .ai-assistant.md and .ai-workflows/
|
||||||
|
* Moved: Starter Prompt to the wiki for better organization
|
||||||
|
* Updated: README.md and readme.txt with AI IDE context recommendations
|
||||||
|
* Improved: Documentation for AI-assisted development
|
||||||
|
|
||||||
## Built with AI
|
### 0.1.2
|
||||||
|
* Added: STARTER-PROMPT.md with comprehensive guide for customizing the template
|
||||||
|
* Updated: Documentation files with improved instructions
|
||||||
|
* Added: Additional AI workflow files for better development guidance
|
||||||
|
|
||||||
WP Plugin Starter is built and maintained with [aidevops](https://aidevops.sh), the same developer's open-source AI harness for creating and managing anything online with AI, plugins like this one included. It is free on [GitHub](https://github.com/marcusquinn/aidevops).
|
### 0.1.1
|
||||||
|
* Updated: LICENSE file with correct GPL-2.0 text
|
||||||
|
|
||||||
|
### 0.1.0
|
||||||
|
* Initial release with basic template structure
|
||||||
|
* Added: Core plugin architecture with OOP approach
|
||||||
|
* Added: Admin interface components and styling
|
||||||
|
* Added: Update mechanism with multiple source options
|
||||||
|
* Added: Documentation templates for users and developers
|
||||||
|
* Added: AI workflow documentation for AI-assisted development
|
||||||
|
* Added: GitHub Actions workflows for automated tasks
|
||||||
|
* Added: Wiki documentation templates
|
||||||
|
|
||||||
|
[View full changelog](CHANGELOG.md)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL-2.0-or-later.
|
This project is licensed under the GPL-2.0+ License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
This template is based on the experience gained from developing the ["Fix 'Plugin file does not exist' Notices"](https://github.com/wpallstars/wp-fix-plugin-does-not-exist-notices) plugin by WPALLSTARS.
|
||||||
-130
@@ -1,130 +0,0 @@
|
|||||||
# Releasing
|
|
||||||
|
|
||||||
How every plugin made from the wpallstars starter plugin is released. This
|
|
||||||
file is the same in each of them (names as placeholders: `STANDARDS.md`
|
|
||||||
lists them, and the plugin's `AGENTS.md` gives its values). The plugin's own
|
|
||||||
submission state and guideline review: `LAUNCH.md`.
|
|
||||||
|
|
||||||
Releases on GitHub and the WordPress.org submission need the owner's say.
|
|
||||||
None of the scripts below tags, publishes, uploads or commits anything.
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
|
|
||||||
| Script | What it does |
|
|
||||||
|--------|--------------|
|
|
||||||
| `scripts/build-release.sh [--ref REF] [--out DIR]` | Builds both zips from a Git ref (default `HEAD`) into `dist/` (gitignored), with `SHA256SUMS`. Files come from Git, never the working tree. |
|
|
||||||
| `scripts/preflight-release.sh [--ref REF] [--strict] [--offline]` | Checks versions, headers, readme, both zips (layout, development files, PHP 7.4 and JS syntax, updater code), remote assets, presets and starter data where the plugin has them, and the Git tag. Errors stop a release; `--strict` also fails on warnings, for a WordPress.org submission. |
|
|
||||||
| `scripts/plugin-check.sh [--ref REF] [--zip FILE]` | Runs Plugin Check on both zips in a disposable WordPress in Docker, then removes it. |
|
|
||||||
|
|
||||||
The two builds of each version:
|
|
||||||
|
|
||||||
| Zip | Contents | Goes to |
|
|
||||||
|-----|----------|---------|
|
|
||||||
| `{slug}-X.Y.Z.zip` | Files in Git, less `.distignore` | GitHub release asset |
|
|
||||||
| `wordpress-org-{slug}-X.Y.Z.zip` | The same, less `.distignore-wporg` (the GitHub updater) and the `GitHub Plugin URI`, `Primary Branch` and `Release Asset` header lines | WordPress.org only |
|
|
||||||
|
|
||||||
Sites install the release asset whose name starts with `{slug}` (the shared
|
|
||||||
GitHub updater, and Git Updater where it is still active), so the
|
|
||||||
WordPress.org zip is named differently and must never be attached to a
|
|
||||||
GitHub release.
|
|
||||||
|
|
||||||
Plugin Check reports the GitHub updater as an updater
|
|
||||||
(`plugin_updater_detected`, `update_modification_detected`, and
|
|
||||||
`OffloadedContent` for its raw.githubusercontent.com address) and its shared
|
|
||||||
`wpallstars_` names as unprefixed, in the GitHub zip; `scripts/plugin-check.sh`
|
|
||||||
lists those as expected there and fails on the updater findings in the
|
|
||||||
WordPress.org zip.
|
|
||||||
|
|
||||||
## GitHub release
|
|
||||||
|
|
||||||
1. In a pull request, set the version in `{slug}.php` (`Version:` and
|
|
||||||
`{PREFIX}_VERSION`) and `readme.txt` (`Stable tag:`), rename the
|
|
||||||
changelog's Unreleased section to the version in `readme.txt`,
|
|
||||||
`changelog.txt` and `README.md`, and add an upgrade notice if people need
|
|
||||||
to act. `readme.txt` keeps only the newest version, in short, and must stay
|
|
||||||
under 10 KB; `changelog.txt` keeps every version in full.
|
|
||||||
Numbers only (`1.2.3`): sites still on Git Updater offer a `Version:` on
|
|
||||||
`main` to every site, and the shared updater skips tags with letters.
|
|
||||||
2. On the pull request's branch: `scripts/preflight-release.sh` (no errors) and
|
|
||||||
`scripts/plugin-check.sh` (no errors).
|
|
||||||
3. Merge, then straight away:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git fetch origin
|
|
||||||
scripts/preflight-release.sh --ref origin/main # no errors before tagging
|
|
||||||
git tag -a vX.Y.Z origin/main -m "{Name} X.Y.Z"
|
|
||||||
scripts/build-release.sh --ref vX.Y.Z
|
|
||||||
git push origin vX.Y.Z
|
|
||||||
gh release create vX.Y.Z dist/{slug}-X.Y.Z.zip --title "{Name} X.Y.Z" --notes-file <notes>
|
|
||||||
```
|
|
||||||
|
|
||||||
Sites with the shared updater see the release when they next check.
|
|
||||||
Sites still on Git Updater are offered the `Version:` on `main` before the
|
|
||||||
release exists, so do this in the same sitting as the merge.
|
|
||||||
4. Check the release has exactly one asset, `{slug}-X.Y.Z.zip`, and on a
|
|
||||||
site with the previous version that **Check again** on the Updates screen
|
|
||||||
shows the update and that it installs.
|
|
||||||
|
|
||||||
Sites read the repository without signing in, so it must be public for sites
|
|
||||||
to get updates. While it is private, a test site can use a read-only token
|
|
||||||
in `WPALLSTARS_GITHUB_TOKEN` (`wp-config.php`; it serves every plugin with
|
|
||||||
the shared updater). Making it public needs the owner's say; do the quality
|
|
||||||
sweep in `DEVELOPMENT.md` → At public launch in the same step.
|
|
||||||
|
|
||||||
## WordPress.org
|
|
||||||
|
|
||||||
Guidelines: [Detailed Plugin Guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/),
|
|
||||||
[Planning, submitting and maintaining](https://developer.wordpress.org/plugins/wordpress-org/planning-submitting-and-maintaining-plugins/),
|
|
||||||
[Plugin readmes](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/),
|
|
||||||
[Plugin assets](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/),
|
|
||||||
[Using Subversion](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/).
|
|
||||||
|
|
||||||
Before submitting, work through the plugin's `LAUNCH.md`: what
|
|
||||||
`scripts/preflight-release.sh --strict` still reports, and a review of the
|
|
||||||
code against each guideline (no code from elsewhere, external services,
|
|
||||||
scripts and styles shipped with the plugin, links, admin notices, defaults,
|
|
||||||
files outside the plugin folder, uninstall, GPL, name and trademarks, a
|
|
||||||
complete plugin).
|
|
||||||
|
|
||||||
### Submitting
|
|
||||||
|
|
||||||
1. `scripts/build-release.sh --ref vX.Y.Z` (a version already released on
|
|
||||||
GitHub), then `scripts/preflight-release.sh --ref vX.Y.Z --strict` passes,
|
|
||||||
or every warning is accepted.
|
|
||||||
2. `scripts/plugin-check.sh --ref vX.Y.Z` reports no errors; read every
|
|
||||||
warning.
|
|
||||||
3. Test the WordPress.org zip on a clean site (latest WordPress and 6.2 with
|
|
||||||
PHP 7.4), with `WP_DEBUG` on: activate, turn each feature on and off,
|
|
||||||
deactivate, delete. `debug.log` stays empty.
|
|
||||||
4. Signed in as the owner's WordPress.org account, upload
|
|
||||||
`dist/wordpress-org-{slug}-X.Y.Z.zip` at
|
|
||||||
[Add your plugin](https://wordpress.org/plugins/developers/add/), and ask
|
|
||||||
for the `{slug}` slug in the notes.
|
|
||||||
5. Review takes about 1 to 10 business days. Reply to the reviewer's email
|
|
||||||
from the same account; fix issues in the repository, not in a copy.
|
|
||||||
|
|
||||||
### After approval (Subversion)
|
|
||||||
|
|
||||||
The SVN repository is `https://plugins.svn.wordpress.org/{slug}/` (or the
|
|
||||||
slug given). It is for releases only, not development. The SVN password is
|
|
||||||
separate from the account password and is set on the WordPress.org profile
|
|
||||||
(see Using Subversion). Install Subversion first (`brew install subversion`).
|
|
||||||
|
|
||||||
1. Check out the empty repository: `svn co https://plugins.svn.wordpress.org/{slug}/ svn-{slug}`.
|
|
||||||
2. Unzip the WordPress.org build's `{slug}/` folder into `trunk/` (no zip
|
|
||||||
files in SVN), `svn add` new files, `svn rm` removed ones.
|
|
||||||
3. Copy trunk to the tag: `svn cp trunk tags/X.Y.Z`. `Stable tag:` in
|
|
||||||
`trunk/readme.txt` and `tags/X.Y.Z/readme.txt` must be `X.Y.Z`; never
|
|
||||||
`trunk`.
|
|
||||||
4. Put the banners, icons and screenshots in `assets/`.
|
|
||||||
5. `svn ci -m "Release X.Y.Z"`, then check the plugin page and download.
|
|
||||||
6. Consider release confirmation emails (Plugin Handbook → Release
|
|
||||||
Confirmation Emails), so a release goes out only after it is confirmed.
|
|
||||||
|
|
||||||
Each later WordPress.org release: build from the tag that is already on GitHub,
|
|
||||||
`--strict` preflight, Plugin Check, then steps 2 to 5. Readme-only changes
|
|
||||||
(such as raising Tested up to) go to trunk and the current tag.
|
|
||||||
|
|
||||||
Once listed, sites with the GitHub build update from WordPress.org, unless
|
|
||||||
the plugin returns true from `wpallstars_github_updater_early` (a setting
|
|
||||||
such as Early updates from GitHub), which keeps them on GitHub releases.
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
# Security policy
|
|
||||||
|
|
||||||
## Supported versions
|
|
||||||
|
|
||||||
Security fixes go into the latest release only. Update to it before
|
|
||||||
reporting, and check that the problem is still there.
|
|
||||||
|
|
||||||
## Reporting a vulnerability
|
|
||||||
|
|
||||||
Please do not open a public issue, pull request or discussion for a
|
|
||||||
security problem.
|
|
||||||
|
|
||||||
Report it privately on GitHub: open the repository's **Security** tab and
|
|
||||||
choose **Report a vulnerability**. Include:
|
|
||||||
|
|
||||||
- the WP Plugin Starter version, and whether it came from GitHub or
|
|
||||||
WordPress.org;
|
|
||||||
- the WordPress and PHP versions;
|
|
||||||
- the feature involved and the settings needed;
|
|
||||||
- steps to reproduce it, and what an attacker gains (for example, which
|
|
||||||
role they need and what they can then read or change).
|
|
||||||
|
|
||||||
We reply within 30 days, agree a fix and a date with you, and credit you in
|
|
||||||
the changelog unless you would rather not be named. If it is urgent (for
|
|
||||||
example, it is already being used against sites), report it on GitHub and
|
|
||||||
also email security@evergreen.je with a link to the report. Follow-up
|
|
||||||
emails are welcome.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
In scope: the code in this repository, as installed from a GitHub release
|
|
||||||
or WordPress.org.
|
|
||||||
|
|
||||||
Out of scope: other plugins, themes and services that WP Plugin Starter
|
|
||||||
recommends, links to or works with (report those to their makers), and
|
|
||||||
problems that need an administrator account, since administrators can
|
|
||||||
already run code on their site.
|
|
||||||
-278
@@ -1,278 +0,0 @@
|
|||||||
# Plugin standards
|
|
||||||
|
|
||||||
Rules every plugin made from the wpallstars starter plugin follows. This file
|
|
||||||
is the same in each of them: the starter holds the master copy, so a lesson
|
|
||||||
learned in one plugin goes into the starter's copy and then to every plugin
|
|
||||||
(`scripts/sync-core.sh` shows the differences). Never put rules for one
|
|
||||||
plugin here; they go in its `AGENTS.md`.
|
|
||||||
|
|
||||||
Names below are placeholders. Each plugin's `AGENTS.md` gives its values:
|
|
||||||
|
|
||||||
| Placeholder | Meaning | Example |
|
|
||||||
|---|---|---|
|
|
||||||
| `{slug}` | Folder, main file and text domain | `my-plugin` |
|
|
||||||
| `{prefix}` | Option, hook, function and file prefix: `{PREFIX}` in lower case | `myplugin` |
|
|
||||||
| `{Prefix}` | Class prefix (`@package`) | `MyPlugin` |
|
|
||||||
| `{PREFIX}` | Constant prefix | `MYPLUGIN` |
|
|
||||||
| `{Name}` | Plugin name | My Plugin |
|
|
||||||
| `{css}` | CSS class and data attribute prefix (`{css}-card`, `data-{css}-setting`) | `mp` |
|
|
||||||
|
|
||||||
Minimums: **WordPress 6.2, PHP 7.4** (`readme.txt`, plugin header).
|
|
||||||
How changes are made and checked: `DEVELOPMENT.md`. Releases: `RELEASING.md`.
|
|
||||||
|
|
||||||
## Structure
|
|
||||||
|
|
||||||
- Core files are listed in `scripts/core-files.txt`: the feature registry
|
|
||||||
(`includes/class-{prefix}.php`), the base feature
|
|
||||||
(`includes/class-{prefix}-feature.php`), the settings store
|
|
||||||
(`includes/class-{prefix}-settings.php`), the admin screen and Read Me tab
|
|
||||||
(`admin/`), the shared GitHub updater (`includes/github-updater/`), the
|
|
||||||
scripts, the CI workflow and tool configuration, and the shared docs (this
|
|
||||||
file, `DEVELOPMENT.md`, `RELEASING.md`, `CONTRIBUTING.md`, `SECURITY.md`).
|
|
||||||
A plugin's copy differs from the starter's only in the names above. They
|
|
||||||
hold no code for one plugin: they read `{Prefix}_Setup`
|
|
||||||
(`includes/class-{prefix}-setup.php`: features, settings tabs, header
|
|
||||||
links, settings version and history, the plugin's own helpers and admin
|
|
||||||
parts) or use hooks (`{prefix}_admin_tabs` for tabs,
|
|
||||||
`{prefix}_admin_enqueue` for the plugin's own admin CSS and JS,
|
|
||||||
`scripts/preflight-plugin.sh` for its own release checks). Anything only
|
|
||||||
one plugin needs goes there, in a feature, or in its own file loaded from
|
|
||||||
there. To change a core file, change the starter first, then run
|
|
||||||
`scripts/sync-core.sh` in each plugin; `scripts/sync-core.sh --check` lists
|
|
||||||
core files that differ. A new plugin starts as a copy of the starter
|
|
||||||
renamed with `scripts/rename-plugin.sh`.
|
|
||||||
- One class per feature in `includes/features/`, extending `{Prefix}_Feature`,
|
|
||||||
registered in `{Prefix}_Setup::FEATURES`. Features some builds leave out
|
|
||||||
go in `{Prefix}_Setup::OPTIONAL_FEATURES` and load only when present.
|
|
||||||
- `settings()` declares the schema; the admin screen renders, searches and
|
|
||||||
saves it with no extra code. Field types and keys: `README.md` → Developers.
|
|
||||||
- `boot()` returns early unless `self::enabled()`. Features are **off by
|
|
||||||
default**; the plugin's `AGENTS.md` lists any the owner asked to be on.
|
|
||||||
Turning another feature on by default needs the owner's say.
|
|
||||||
- A feature that replaces another plugin sets `'replaces' => array(slug => name)`
|
|
||||||
and imports that plugin's settings in `migrate()` with
|
|
||||||
`self::import_setting()` (fills only unset keys). It never writes or
|
|
||||||
deletes the other plugin's options. While that plugin is active the
|
|
||||||
feature waits, and the Plugins screen suggests deactivating and deleting
|
|
||||||
it (`admin/includes/class-replaced-plugins.php`) with no extra code.
|
|
||||||
- Migrations run once per `{Prefix}_Setup::DB_VERSION`. After a release, a
|
|
||||||
new or changed import needs a version bump and a line in its docblock.
|
|
||||||
- New options, post meta, user meta, transients, cron hooks and files must be
|
|
||||||
removed in `uninstall.php`.
|
|
||||||
- `README.md` is also the plugin's Read Me tab
|
|
||||||
(`admin/includes/class-readme-manager.php`), which renders headings, lists,
|
|
||||||
tables, bold, italic, inline code, links (http(s) and `#heading` links,
|
|
||||||
with GitHub-style heading IDs) and images from the plugin folder on a line
|
|
||||||
of their own (``). Use only that Markdown,
|
|
||||||
or extend the renderer in the same change.
|
|
||||||
- Update `README.md` (feature section, hooks, changelog), `changelog.txt`
|
|
||||||
(the user-facing changelog entry) and `readme.txt` in the same change.
|
|
||||||
`readme.txt` must stay under 10 KB for WordPress.org: one short line per
|
|
||||||
feature, every service the plugin contacts under External services, and
|
|
||||||
only the newest version's changelog, in short. Details go in `README.md`.
|
|
||||||
- `.distignore` lists files kept out of the release zip. Add new
|
|
||||||
development-only files there (the preflight fails when a known one gets in),
|
|
||||||
then check the build with Plugin Check.
|
|
||||||
|
|
||||||
## Code rules
|
|
||||||
|
|
||||||
- PHP 7.4 syntax and WordPress 6.2 APIs. Guard newer core APIs with
|
|
||||||
`function_exists()` or `method_exists()`.
|
|
||||||
- Capability and nonce checks on every admin action and AJAX handler; escape on
|
|
||||||
output; sanitise through the schema.
|
|
||||||
- Prefix everything global with `{prefix}_`, `{Prefix}_` or `{PREFIX}_`. The
|
|
||||||
shared GitHub updater is the one exception: its `wpallstars_` names are the
|
|
||||||
same in every plugin, so that one copy can stand in for the others.
|
|
||||||
- Admin copy: short, plain words, sentence case, no jargon.
|
|
||||||
- Decide for the user. Within a feature, the plugin makes the choices
|
|
||||||
(which plugins, screens or items it applies to) from what it can detect.
|
|
||||||
Settings are there to bypass something that causes a problem, not choices
|
|
||||||
people need to understand first. A new setting must earn its place;
|
|
||||||
prefer detecting the right behaviour plus a short bypass list.
|
|
||||||
- Site owner in control, performance first: the owner decides what their site
|
|
||||||
sends, contacts and shows. Calls to outside services are opt-in where they
|
|
||||||
are not the point of the feature, made only as often and for as long as
|
|
||||||
needed (cache answers, never on every page load), and never block a
|
|
||||||
visitor's page when they can run later. Features that rein in other
|
|
||||||
plugins hand the choice to the owner instead of deciding for them.
|
|
||||||
WordPress update checks and downloads are the exception: leave them alone
|
|
||||||
(next rule).
|
|
||||||
- Do not change WordPress update behaviour (update transients, `auto_update_*`
|
|
||||||
filters, update checks) outside the shared GitHub updater. Plugin Check
|
|
||||||
reports `plugin_updater_detected` as an error, and WordPress.org asks plugins
|
|
||||||
not to interfere with the updater.
|
|
||||||
- Leave no PHP errors, warnings, notices or deprecations behind. Fix any that
|
|
||||||
the plugin causes as you find them, in the same change when it is small,
|
|
||||||
or as a tracked issue. That includes ones in other plugins that only happen
|
|
||||||
because of this one. Messages that other plugins cause on their own are
|
|
||||||
theirs: mention them, do not hide them.
|
|
||||||
|
|
||||||
## Updates from GitHub
|
|
||||||
|
|
||||||
The one exception to the update rule, at the owner's request (for speed,
|
|
||||||
reliability and site owners' control, and because the plugins are released
|
|
||||||
on GitHub first): the shared GitHub updater in `includes/github-updater/`.
|
|
||||||
It replaces Git Updater.
|
|
||||||
|
|
||||||
- Every plugin made from the starter carries a copy. Each copy registers its
|
|
||||||
version from `load.php` when its plugin loads; on `plugins_loaded` only the
|
|
||||||
newest copy loads, once, and serves every installed plugin with a
|
|
||||||
`GitHub Plugin URI` header. One update check covers them all.
|
|
||||||
- It adds GitHub releases of those plugins to core's own update check and
|
|
||||||
`plugins_api`, and leaves the download, install, auto-updates and rollback
|
|
||||||
to core. It only adds entries for those plugins; it never removes or blocks
|
|
||||||
other updates.
|
|
||||||
- It is the same in every plugin apart from its text domain and `@package`.
|
|
||||||
Change it in the starter, raise the version in its `load.php`, and copy it
|
|
||||||
to each plugin. Plugins change what it does only through its filters
|
|
||||||
(`wpallstars_github_updater_enabled`, `wpallstars_github_updater_early`,
|
|
||||||
`wpallstars_github_plugins`, `wpallstars_github_token`), never by calling
|
|
||||||
its class: another plugin's copy may be the one that runs.
|
|
||||||
- Keep anything that installs or updates code from outside WordPress.org in
|
|
||||||
that folder (and in a feature listed in `.distignore-wporg`, when a plugin
|
|
||||||
has a setting for it), because the WordPress.org build leaves them out.
|
|
||||||
- Never add an `Update URI` header. Tokens for private repositories come only
|
|
||||||
from `wp-config.php` (`WPALLSTARS_GITHUB_TOKEN`) or the filter, go only to
|
|
||||||
api.github.com and are never stored.
|
|
||||||
|
|
||||||
## Releases
|
|
||||||
|
|
||||||
GitHub releases are the early channel; WordPress.org gets settled versions.
|
|
||||||
Sites install the latest GitHub release whose tag is a plain version and the
|
|
||||||
asset whose name starts with the plugin folder (the shared updater; Git
|
|
||||||
Updater, where still active, reads `Version:` on `main` instead), so:
|
|
||||||
|
|
||||||
- Publish the GitHub release (tag `vX.Y.Z`, asset `{slug}-X.Y.Z.zip` with a
|
|
||||||
`{slug}/` folder, built with `.distignore`) straight after the version
|
|
||||||
change reaches `main`.
|
|
||||||
- Never put a pre-release version (`-beta1`, `-rc1`) in `Version:` on `main`;
|
|
||||||
mark test releases as pre-releases on GitHub.
|
|
||||||
- The WordPress.org build is the release build without the files in
|
|
||||||
`.distignore-wporg` (the GitHub updater) and the `GitHub Plugin URI`,
|
|
||||||
`Primary Branch` and `Release Asset` header lines. Its zip is named
|
|
||||||
`wordpress-org-{slug}-X.Y.Z.zip` so no updater picks it; never attach it
|
|
||||||
to a GitHub release.
|
|
||||||
- Any plugin released on GitHub (made from the starter or not) follows the
|
|
||||||
same pattern: a `GitHub Plugin URI: owner/repo` header (and
|
|
||||||
`Release Asset: true`), plain version tags, and a `{folder}-X.Y.Z.zip`
|
|
||||||
asset with a `{folder}/` inside.
|
|
||||||
- Build both zips with `scripts/build-release.sh`, check them with
|
|
||||||
`scripts/preflight-release.sh` and `scripts/plugin-check.sh`. None of them
|
|
||||||
tags, publishes or uploads anything.
|
|
||||||
- Releasing and submitting to WordPress.org need the owner's say. A private
|
|
||||||
repository cannot be read by sites without a token
|
|
||||||
(`WPALLSTARS_GITHUB_TOKEN`).
|
|
||||||
|
|
||||||
Details: `RELEASING.md`; the plugin's own submission state: `LAUNCH.md`.
|
|
||||||
|
|
||||||
## Front-end styling and dark mode
|
|
||||||
|
|
||||||
Block, shortcode and other front-end styles must work with the Kadence Pro
|
|
||||||
dark mode switcher (and themes that switch palettes the same way).
|
|
||||||
|
|
||||||
- How it switches: Kadence adds `color-switch-dark` or `color-switch-light` to
|
|
||||||
`<body>`. The dark class sets `color-scheme: dark` and redefines
|
|
||||||
`--global-palette1`…`15` and `--wp--preset--color--theme-palette-N` **on
|
|
||||||
`<body>`**. `<html>` stays `color-scheme: light`. Palette 3 is the strongest
|
|
||||||
text and palette 9 the page background in light mode; dark mode swaps them.
|
|
||||||
- Use `currentColor`, `inherit`, translucent neutrals (for example
|
|
||||||
`rgba(127, 127, 127, 0.12)`) or palette variables, never fixed light or dark
|
|
||||||
colours for text, backgrounds or borders.
|
|
||||||
- Do not use `@media (prefers-color-scheme)` to follow the site: it tracks the
|
|
||||||
visitor's system, not the switcher.
|
|
||||||
- Do not define custom properties on `:root` from palette variables; they
|
|
||||||
resolve above `<body>` and keep the light values. Read palette variables
|
|
||||||
where they are used, or define derived ones on the block.
|
|
||||||
- Preset references (`var:preset|color|theme-palette3`) become CSS variables
|
|
||||||
with kebab-cased slugs, as core does: `--wp--preset--color--theme-palette-3`.
|
|
||||||
Use `_wp_to_kebab_case()` in PHP and the same rule in editor JS (a hyphen
|
|
||||||
between letters and digits, lower case).
|
|
||||||
- Embedded pages (iframes) do not follow the switcher: they see the visitor's
|
|
||||||
system setting, and the browser paints their own background behind them, so
|
|
||||||
they stay readable in both modes. Do not make iframes transparent or tint them.
|
|
||||||
- Test light and dark with the Kadence theme by toggling the body class (see
|
|
||||||
Testing, step 5).
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
CI (`.github/workflows/ci.yml`) runs the code checks, the release preflight,
|
|
||||||
Plugin Check and a smoke test on every pull request; details and local
|
|
||||||
commands: `DEVELOPMENT.md`.
|
|
||||||
|
|
||||||
While a repository is private, CI and review apps only advise: nothing is
|
|
||||||
required to merge, for speed. Fix failures your change causes before merging;
|
|
||||||
open an issue for any other failure and merge anyway. Do not turn on branch
|
|
||||||
protection, required checks or paid reviewers. At public launch (owner's say),
|
|
||||||
run the full sweep in `DEVELOPMENT.md` → At public launch, which makes the
|
|
||||||
checks required.
|
|
||||||
|
|
||||||
The checks catch errors, not wrong behaviour, so also verify on real
|
|
||||||
WordPress:
|
|
||||||
|
|
||||||
1. `scripts/lint.sh` (syntax, ShellCheck, PHPCS, PHPStan; run
|
|
||||||
`composer install` once). Fix findings in the code; never grow
|
|
||||||
`phpstan-baseline.neon` or add a `phpcs:ignore` without a reason.
|
|
||||||
2. The user reviews on one local test site per plugin, shared by every
|
|
||||||
session and worktree. It shows a **combined preview**: `origin/main` plus
|
|
||||||
every open pull request from the repository, merged together. Update it
|
|
||||||
only with the script, from any worktree:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scripts/preview-site.sh # the first run on a clone takes the site: scripts/preview-site.sh "<site>"
|
|
||||||
scripts/preview-site.sh --dry-run # report what would be included, copy nothing
|
|
||||||
```
|
|
||||||
|
|
||||||
It fetches `origin`, merges each open PR's branch onto `origin/main` in PR
|
|
||||||
order without touching any checkout, leaves out branches that conflict
|
|
||||||
(and lists them), copies the result with `.distignore` applied (exactly
|
|
||||||
what a release build contains), and writes
|
|
||||||
`<site>/wp-content/{slug}-synced-from.txt` listing what is included.
|
|
||||||
A lock stops two runs at once. Because every run includes everyone's
|
|
||||||
pushed work, no session hides another's.
|
|
||||||
|
|
||||||
- **Never** `rsync` your worktree into the shared site: it hides every
|
|
||||||
other session's work until the next run. Do not use Git hooks either:
|
|
||||||
they are shared by every worktree and would deploy the wrong checkout.
|
|
||||||
- Only pushed work with an open PR is included. Before asking the user to
|
|
||||||
look at unmerged work, push the branch and open a draft PR, then run the
|
|
||||||
script. It says so if the branch you run it from is missing or has
|
|
||||||
commits that are not pushed.
|
|
||||||
- After merging a PR, run the script again, then check that the stamp
|
|
||||||
lists your merge in `main` before telling the user to look.
|
|
||||||
- Conflicts only in `changelog.txt`, `readme.txt` or `README.md` do not
|
|
||||||
leave a branch out: every PR adds lines at the top of the same
|
|
||||||
changelogs, so each merge to `main` would otherwise drop every other
|
|
||||||
open PR. The preview keeps both sides' lines there and says so in the
|
|
||||||
stamp; still merge `origin/main` into your branch before it merges.
|
|
||||||
- If your branch is left out because it conflicts in other files, merge
|
|
||||||
`origin/main` into it (or wait for the other PR), push and run the
|
|
||||||
script again. Tell the user which PRs are left out.
|
|
||||||
- To check your branch on its own, or for checks the user will not look
|
|
||||||
at, use a throwaway site of your own (step 4's Docker image on a free
|
|
||||||
port), which no one else overwrites:
|
|
||||||
`rsync -a --delete --delete-excluded --exclude-from=.distignore ./ "<site>/wp-content/plugins/{slug}/"`.
|
|
||||||
- Size every test site, shared or throwaway, as `DEVELOPMENT.md` → Test
|
|
||||||
site resources says. PHP's defaults fill up and the slowdowns look like
|
|
||||||
bugs.
|
|
||||||
|
|
||||||
3. Exercise the changed feature through the admin UI or HTTP, and check
|
|
||||||
the debug log for new messages mentioning `{slug}` or a plugin it affects
|
|
||||||
(`wp-content/debug.log`, or the file Debug Log Manager writes in
|
|
||||||
`wp-content/uploads/debug-log-manager/` when it is active). For settings
|
|
||||||
imports, seed the replaced plugin's options and delete `{prefix}_options`
|
|
||||||
and `{prefix}_db_version` while the plugin is inactive, then activate it.
|
|
||||||
4. For changes that touch core APIs, also smoke-test on WordPress 6.2 with
|
|
||||||
PHP 7.4 (`scripts/smoke-test.sh --wp 6.2 --php 7.4`, or the
|
|
||||||
`wordpress:php7.4-apache` Docker image with
|
|
||||||
`wp core download --version=6.2 --force`).
|
|
||||||
5. For front-end styling, view the page with the Kadence theme in light and
|
|
||||||
dark mode. Without Kadence Pro, simulate the switcher: print a
|
|
||||||
`body.color-switch-dark { color-scheme: dark; --global-palette1: …; }`
|
|
||||||
rule with a dark palette (palette 3 light, palette 9 dark, and the matching
|
|
||||||
`--wp--preset--color--theme-palette-N: var(--global-paletteN)` lines), then
|
|
||||||
swap the body class between `color-switch-light` and `color-switch-dark`.
|
|
||||||
Check text, backgrounds, borders and palette colours chosen in block
|
|
||||||
settings in both.
|
|
||||||
|
|
||||||
Note: since WordPress 5.6, posts restored from the Bin become drafts. Republish
|
|
||||||
test posts after bulk-trash tests.
|
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* Admin Styles
|
||||||
|
*
|
||||||
|
* @package WPALLSTARS\PluginStarterTemplate
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* General Admin Styles */
|
||||||
|
.wpst-admin-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-admin-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-admin-header h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #23282d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin Form Styles */
|
||||||
|
.wpst-form-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-form-table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 15px 10px 15px 0;
|
||||||
|
width: 200px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-form-table td {
|
||||||
|
padding: 15px 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-form-table input[type="text"],
|
||||||
|
.wpst-form-table input[type="number"],
|
||||||
|
.wpst-form-table select,
|
||||||
|
.wpst-form-table textarea {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-form-table textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-form-description {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin Notices */
|
||||||
|
.wpst-notice {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
border-left: 4px solid #00a0d2;
|
||||||
|
background: #f7fcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-notice.success {
|
||||||
|
border-left-color: #46b450;
|
||||||
|
background: #ecf7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-notice.error {
|
||||||
|
border-left-color: #dc3232;
|
||||||
|
background: #fbeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-notice.warning {
|
||||||
|
border-left-color: #ffb900;
|
||||||
|
background: #fff8e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin Cards */
|
||||||
|
.wpst-card-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 0 -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-card {
|
||||||
|
flex: 1 0 300px;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-card-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-card-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-card-content {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-card-footer {
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Styles */
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.wpst-form-table th {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-form-table td {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-form-table input[type="text"],
|
||||||
|
.wpst-form-table input[type="number"],
|
||||||
|
.wpst-form-table select,
|
||||||
|
.wpst-form-table textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* Update Source Selector Styles
|
||||||
|
*
|
||||||
|
* @package WPALLSTARS\PluginStarterTemplate
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.wpst-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-modal-content {
|
||||||
|
position: relative;
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 10% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 500px;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-modal-header {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-modal-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-modal-close:hover,
|
||||||
|
.wpst-modal-close:focus {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-modal-body {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-modal-footer {
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Source Selection Styles */
|
||||||
|
.wpst-source-options {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-source-option {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-source-option:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-source-option.selected {
|
||||||
|
border-color: #0073aa;
|
||||||
|
background-color: #f0f6fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-source-option input[type="radio"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-source-option-label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-source-option-description {
|
||||||
|
margin-top: 5px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Indicator */
|
||||||
|
.wpst-loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: #0073aa;
|
||||||
|
animation: wpst-spin 1s ease-in-out infinite;
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wpst-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Styles */
|
||||||
|
.wpst-modal-message {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-modal-message.success {
|
||||||
|
background-color: #ecf7ed;
|
||||||
|
border: 1px solid #46b450;
|
||||||
|
color: #2a6f31;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpst-modal-message.error {
|
||||||
|
background-color: #fbeaea;
|
||||||
|
border: 1px solid #dc3232;
|
||||||
|
color: #8a1f1f;
|
||||||
|
}
|
||||||
@@ -1,848 +0,0 @@
|
|||||||
/**
|
|
||||||
* WP Plugin Starter admin screen.
|
|
||||||
*
|
|
||||||
* Scoped to .wps-wrap. Uses core admin tokens so the screen follows the
|
|
||||||
* active admin colour scheme; core buttons, notices and plugin cards keep
|
|
||||||
* their native styles. Logical properties keep RTL working without a
|
|
||||||
* separate stylesheet.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.wps-wrap {
|
|
||||||
--wps-accent: var(--wp-admin-theme-color, #2271b1);
|
|
||||||
--wps-accent-dark: var(--wp-admin-theme-color-darker-10, #135e96);
|
|
||||||
--wps-text: #1d2327;
|
|
||||||
--wps-muted: #646970;
|
|
||||||
--wps-border: #dcdcde;
|
|
||||||
--wps-surface: #fff;
|
|
||||||
--wps-subtle: #f6f7f7;
|
|
||||||
--wps-success: #008a20;
|
|
||||||
--wps-error: #d63638;
|
|
||||||
--wps-radius: 8px;
|
|
||||||
--wps-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
||||||
--wps-focus: 0 0 0 2px var(--wps-surface), 0 0 0 4px var(--wps-accent);
|
|
||||||
|
|
||||||
margin-block-start: 0;
|
|
||||||
margin-inline-start: -20px;
|
|
||||||
margin-inline-end: 0;
|
|
||||||
color: var(--wps-text);
|
|
||||||
/* Start below the Screen Options / Notices tabs, so they never narrow the header. */
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap *,
|
|
||||||
.wps-wrap *::before,
|
|
||||||
.wps-wrap *::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header ------------------------------------------------------------- */
|
|
||||||
|
|
||||||
.wps-header {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px 24px;
|
|
||||||
padding: 14px 24px;
|
|
||||||
background: var(--wps-surface);
|
|
||||||
border-block-end: 1px solid var(--wps-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-header__brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-header__logo {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #fff;
|
|
||||||
background: var(--wps-accent);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-header__title {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-header__actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-inline-start: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-header__support {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* line-height: 1 stops the icon taking the button's taller line height,
|
|
||||||
which draws the glyph below the middle of its box. */
|
|
||||||
.wps-wrap .wps-header__support .dashicons {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search ------------------------------------------------------------- */
|
|
||||||
|
|
||||||
/* Stretch, so the field always matches the button's height, whatever
|
|
||||||
size the admin colour scheme or WordPress version gives buttons. */
|
|
||||||
.wps-search {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-search__field {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__icon {
|
|
||||||
position: absolute;
|
|
||||||
inset-block: 0;
|
|
||||||
inset-inline-start: 9px;
|
|
||||||
margin-block: auto;
|
|
||||||
color: var(--wps-muted);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__input {
|
|
||||||
width: 260px;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
min-height: 30px;
|
|
||||||
margin: 0;
|
|
||||||
padding-block: 0;
|
|
||||||
padding-inline: 34px 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: normal;
|
|
||||||
color: var(--wps-text);
|
|
||||||
background: var(--wps-surface);
|
|
||||||
/* Border and corners stay core's, so they match core buttons in each version. */
|
|
||||||
box-shadow: none;
|
|
||||||
transition: border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__input::placeholder {
|
|
||||||
color: var(--wps-muted);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__input:hover {
|
|
||||||
border-color: var(--wps-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__input:focus {
|
|
||||||
border-color: var(--wps-accent);
|
|
||||||
box-shadow: 0 0 0 1px var(--wps-accent);
|
|
||||||
outline: 2px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__field:focus-within .wps-search__icon {
|
|
||||||
color: var(--wps-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__button {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__group {
|
|
||||||
margin: 24px 0 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__group:first-of-type {
|
|
||||||
margin-block-start: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation --------------------------------------------------------- */
|
|
||||||
|
|
||||||
.wps-nav {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: stretch;
|
|
||||||
padding-inline: 12px;
|
|
||||||
background: var(--wps-surface);
|
|
||||||
border-block-end: 1px solid var(--wps-border);
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-nav__group {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-nav__group + .wps-nav__group {
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
padding-inline-start: 8px;
|
|
||||||
border-inline-start: 1px solid var(--wps-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-nav__tab {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 14px 12px 11px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--wps-muted);
|
|
||||||
text-decoration: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-block-end: 3px solid transparent;
|
|
||||||
transition: color 0.1s ease, border-color 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-nav__tab:hover {
|
|
||||||
color: var(--wps-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-nav__tab:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-nav__tab:focus-visible {
|
|
||||||
outline: 2px solid var(--wps-accent);
|
|
||||||
outline-offset: -4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-nav__tab.is-active {
|
|
||||||
color: var(--wps-text);
|
|
||||||
font-weight: 600;
|
|
||||||
border-block-end-color: var(--wps-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main --------------------------------------------------------------- */
|
|
||||||
|
|
||||||
.wps-wrap > .notice,
|
|
||||||
.wps-wrap > div.updated,
|
|
||||||
.wps-wrap > div.error {
|
|
||||||
margin: 16px 24px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-main {
|
|
||||||
max-width: 1200px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-section {
|
|
||||||
max-width: 860px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-section__intro {
|
|
||||||
margin-block-end: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-section__title {
|
|
||||||
margin: 0 0 4px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-section__desc,
|
|
||||||
.wps-section__hint {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--wps-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-section__hint {
|
|
||||||
margin-block-start: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cards -------------------------------------------------------------- */
|
|
||||||
|
|
||||||
.wps-card {
|
|
||||||
background: var(--wps-surface);
|
|
||||||
border: 1px solid var(--wps-border);
|
|
||||||
border-radius: var(--wps-radius);
|
|
||||||
box-shadow: var(--wps-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-cards {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A settings tab with nothing in it yet. */
|
|
||||||
.wps-empty {
|
|
||||||
padding: 16px 20px;
|
|
||||||
color: var(--wps-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-empty p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--wps-muted);
|
|
||||||
vertical-align: middle;
|
|
||||||
background: var(--wps-subtle);
|
|
||||||
border: 1px solid var(--wps-border);
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-badge--success {
|
|
||||||
color: #00450c;
|
|
||||||
background: #edfaef;
|
|
||||||
border-color: #b8e6bf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Setting cards ------------------------------------------------------ */
|
|
||||||
|
|
||||||
/* Clip the header hover and panel backgrounds to the rounded corners. */
|
|
||||||
.wps-setting {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* "On" stripe drawn above the header/panel backgrounds so hover never hides it. */
|
|
||||||
.wps-setting::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
inset-block: 0;
|
|
||||||
inset-inline-start: 0;
|
|
||||||
width: 3px;
|
|
||||||
background: var(--wps-accent);
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting.is-on::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__header {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting.has-panel .wps-setting__header {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting.has-panel .wps-setting__header:hover {
|
|
||||||
background: #fcfcfc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__text {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status sits beside the title so showing/hiding it never reflows the description. */
|
|
||||||
.wps-setting__title-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px 10px;
|
|
||||||
min-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__desc {
|
|
||||||
margin: 2px 0 0;
|
|
||||||
color: var(--wps-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-replaces {
|
|
||||||
margin: 4px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--wps-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-replaces__active {
|
|
||||||
color: #996800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-setting__expand {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
color: var(--wps-accent);
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-setting__expand:hover {
|
|
||||||
background: var(--wps-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-setting__expand:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: var(--wps-focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__expand .dashicons {
|
|
||||||
transition: transform 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__expand[aria-expanded="true"] .dashicons {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__panel {
|
|
||||||
padding: 4px 20px 8px;
|
|
||||||
border-block-start: 1px solid var(--wps-border);
|
|
||||||
background: #fcfcfc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__panel[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status a feature prints above its options (wpstarter_setting_panel). */
|
|
||||||
.wps-panel-note {
|
|
||||||
padding: 12px 0;
|
|
||||||
border-block-end: 1px solid #f0f0f1;
|
|
||||||
color: var(--wps-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-panel-note p {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-panel-note p:last-child {
|
|
||||||
margin-block-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-panel-note--warning {
|
|
||||||
color: #1d2327;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-panel-note--warning p::before {
|
|
||||||
content: "\f534";
|
|
||||||
margin-inline-end: 6px;
|
|
||||||
font: 16px/1 dashicons;
|
|
||||||
vertical-align: -3px;
|
|
||||||
color: #dba617;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-media__preview {
|
|
||||||
flex: none;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
object-fit: contain;
|
|
||||||
background: repeating-conic-gradient(#f0f0f1 0% 25%, #fff 0% 50%) 0 0 / 16px 16px;
|
|
||||||
border: 1px solid var(--wps-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-code {
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 6px 0 0;
|
|
||||||
padding: 10px 12px;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #1d2327;
|
|
||||||
background: #f6f7f7;
|
|
||||||
border: 1px solid var(--wps-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fields ------------------------------------------------------------- */
|
|
||||||
|
|
||||||
.wps-field {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 200px 1fr;
|
|
||||||
gap: 8px 24px;
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-field + .wps-field {
|
|
||||||
border-block-start: 1px solid #f0f0f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-field__label {
|
|
||||||
padding-block-start: 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-field__control .description {
|
|
||||||
margin-block-start: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-field__unit {
|
|
||||||
color: var(--wps-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-field__control textarea.large-text {
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-media {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-media__preview[hidden],
|
|
||||||
.wps-media__remove[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-checkboxes {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px 20px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 6px 0 0;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-checkbox {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Long lists, such as every active plugin: columns that scroll. */
|
|
||||||
.wps-checkboxes.is-long {
|
|
||||||
display: block;
|
|
||||||
max-height: 320px;
|
|
||||||
overflow-y: auto;
|
|
||||||
columns: 16em auto;
|
|
||||||
column-gap: 20px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border: 1px solid #dcdcde;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-checkboxes.is-long .wps-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-block-end: 6px;
|
|
||||||
break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-checkboxes.is-long .wps-checkbox input {
|
|
||||||
margin-block-start: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-checkboxes__all {
|
|
||||||
display: block;
|
|
||||||
margin-block-end: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-tokens {
|
|
||||||
display: block;
|
|
||||||
margin-block-start: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-token {
|
|
||||||
margin: 2px 4px 2px 0;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-token code {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 1px 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
background: var(--wps-subtle);
|
|
||||||
border: 1px solid var(--wps-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-token:hover code,
|
|
||||||
.wps-token:focus-visible code {
|
|
||||||
color: var(--wps-accent);
|
|
||||||
border-color: var(--wps-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-token:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Switch ------------------------------------------------------------- */
|
|
||||||
|
|
||||||
.wps-switch {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
flex: none;
|
|
||||||
width: 36px;
|
|
||||||
height: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-switch__input {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-switch__track {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #1e1e1e;
|
|
||||||
border-radius: 999px;
|
|
||||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-switch__track::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset-block-start: 3px;
|
|
||||||
inset-inline-start: 3px;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background: #1e1e1e;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: transform 0.15s ease, background-color 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-switch__input:checked + .wps-switch__track {
|
|
||||||
background: var(--wps-accent);
|
|
||||||
border-color: var(--wps-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-switch__input:checked + .wps-switch__track::after {
|
|
||||||
background: #fff;
|
|
||||||
transform: translateX(16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .wps-switch__input:checked + .wps-switch__track::after {
|
|
||||||
transform: translateX(-16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-switch__input:focus-visible + .wps-switch__track {
|
|
||||||
box-shadow: var(--wps-focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-switch__input:disabled + .wps-switch__track {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Save status -------------------------------------------------------- */
|
|
||||||
|
|
||||||
.wps-status {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--wps-muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-field__control .wps-status {
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-status.is-saving::before {
|
|
||||||
content: "";
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border: 2px solid var(--wps-border);
|
|
||||||
border-block-start-color: var(--wps-accent);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: wps-spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-status.is-saved {
|
|
||||||
color: var(--wps-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-status.is-saved::before {
|
|
||||||
content: "\f147";
|
|
||||||
font: normal 18px/1 dashicons;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-status.is-error {
|
|
||||||
color: var(--wps-error);
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes wps-spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Read Me ------------------------------------------------------------ */
|
|
||||||
|
|
||||||
.wps-readme {
|
|
||||||
max-width: 820px;
|
|
||||||
padding: 24px 32px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme-image {
|
|
||||||
margin: 0 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme-image img {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: var(--wps-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The banner at the top of README.md fills the top of the card. */
|
|
||||||
.wps-readme > .wps-readme-image:first-child {
|
|
||||||
margin: -24px -32px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme > .wps-readme-image:first-child img {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: calc(var(--wps-radius) - 1px) calc(var(--wps-radius) - 1px) 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-readme h2 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-readme h3 {
|
|
||||||
margin: 24px 0 8px;
|
|
||||||
padding-block-start: 16px;
|
|
||||||
font-size: 16px;
|
|
||||||
border-block-start: 1px solid #f0f0f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-readme h4 {
|
|
||||||
margin: 16px 0 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme ul,
|
|
||||||
.wps-readme ol {
|
|
||||||
margin: 8px 0 12px;
|
|
||||||
padding-inline-start: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme ul {
|
|
||||||
list-style: disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme ol {
|
|
||||||
list-style: decimal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme-table {
|
|
||||||
margin: 8px 0 16px;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme th,
|
|
||||||
.wps-readme td {
|
|
||||||
padding: 6px 10px;
|
|
||||||
text-align: start;
|
|
||||||
vertical-align: top;
|
|
||||||
border: 1px solid rgba(127, 127, 127, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-readme th {
|
|
||||||
background: rgba(127, 127, 127, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive --------------------------------------------------------- */
|
|
||||||
|
|
||||||
@media screen and (max-width: 782px) {
|
|
||||||
.wps-wrap {
|
|
||||||
margin-inline-start: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-header,
|
|
||||||
.wps-main {
|
|
||||||
padding-inline: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-nav {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Brand and website link on the first row, search across the second. */
|
|
||||||
.wps-search {
|
|
||||||
order: 1;
|
|
||||||
flex: 1 1 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-search__field {
|
|
||||||
flex: 1 1 0;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-wrap .wps-search__input {
|
|
||||||
flex: 1 1 0;
|
|
||||||
width: auto;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 40px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-search .button {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__header {
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-setting__expand {
|
|
||||||
grid-column: 2;
|
|
||||||
justify-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wps-field {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.wps-wrap *,
|
|
||||||
.wps-wrap *::before,
|
|
||||||
.wps-wrap *::after {
|
|
||||||
transition: none !important;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Read Me content for the WP Plugin Starter admin tab (from README.md).
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* README.md contents, with a short fallback if the file is missing.
|
|
||||||
*
|
|
||||||
* @return array{title:string,content:string}
|
|
||||||
*/
|
|
||||||
function wpstarter_get_readme_content() {
|
|
||||||
$readme_path = WPSTARTER_DIR . 'README.md';
|
|
||||||
|
|
||||||
if (is_readable($readme_path)) {
|
|
||||||
$content = (string) file_get_contents($readme_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file.
|
|
||||||
} else {
|
|
||||||
$content = "# WP Plugin Starter\n\nVersion: " . WPSTARTER_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array(
|
|
||||||
'title' => __('Read Me', 'wp-plugin-starter-template'),
|
|
||||||
'content' => $content,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,434 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!--
|
|
||||||
WP Plugin Starter banner (WordPress.org banners and the Read Me tab).
|
|
||||||
Source: .wordpress-org/banner.svg. Edit that, then run scripts/build-banner.sh.
|
|
||||||
Branding follows wpallstars.com: navy, red and cream, Zilla Slab italic
|
|
||||||
headings, the wpallstars plugin stack in red only, the logo's arc of five stars and its red double stripes (here with cream between).
|
|
||||||
-->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="1544"
|
|
||||||
height="500"
|
|
||||||
viewBox="0 0 1544 500"
|
|
||||||
version="1.1"
|
|
||||||
id="svg51"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<title
|
|
||||||
id="title1">WPALLSTARS WP Plugin Starter: Built with AI</title>
|
|
||||||
<defs
|
|
||||||
id="defs21">
|
|
||||||
<linearGradient
|
|
||||||
id="bg"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="1"
|
|
||||||
y2="1">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#000c1f"
|
|
||||||
id="stop1" />
|
|
||||||
<stop
|
|
||||||
offset="0.55"
|
|
||||||
stop-color="#001537"
|
|
||||||
id="stop2" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#0d264d"
|
|
||||||
id="stop3" />
|
|
||||||
</linearGradient>
|
|
||||||
<radialGradient
|
|
||||||
id="glow-blue"
|
|
||||||
cx="1214"
|
|
||||||
cy="270"
|
|
||||||
r="440"
|
|
||||||
gradientUnits="userSpaceOnUse">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#bb0000"
|
|
||||||
stop-opacity="0.45"
|
|
||||||
id="stop4" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#bb0000"
|
|
||||||
stop-opacity="0"
|
|
||||||
id="stop5" />
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient
|
|
||||||
id="glow-red"
|
|
||||||
cx="200"
|
|
||||||
cy="580"
|
|
||||||
r="520"
|
|
||||||
gradientUnits="userSpaceOnUse">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#bb0000"
|
|
||||||
stop-opacity="0.2"
|
|
||||||
id="stop6" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#bb0000"
|
|
||||||
stop-opacity="0"
|
|
||||||
id="stop7" />
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient
|
|
||||||
id="glow-gold"
|
|
||||||
cx="1214"
|
|
||||||
cy="76"
|
|
||||||
r="120"
|
|
||||||
gradientUnits="userSpaceOnUse">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#ffc94d"
|
|
||||||
stop-opacity="0.45"
|
|
||||||
id="stop8" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#ffc94d"
|
|
||||||
stop-opacity="0"
|
|
||||||
id="stop9" />
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient
|
|
||||||
id="dots-fade"
|
|
||||||
cx="1214"
|
|
||||||
cy="260"
|
|
||||||
r="560"
|
|
||||||
gradientUnits="userSpaceOnUse">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#fff"
|
|
||||||
stop-opacity="1"
|
|
||||||
id="stop10" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#fff"
|
|
||||||
stop-opacity="0"
|
|
||||||
id="stop11" />
|
|
||||||
</radialGradient>
|
|
||||||
<pattern
|
|
||||||
id="dots"
|
|
||||||
width="28"
|
|
||||||
height="28"
|
|
||||||
patternUnits="userSpaceOnUse">
|
|
||||||
<circle
|
|
||||||
cx="14"
|
|
||||||
cy="14"
|
|
||||||
r="1.5"
|
|
||||||
fill="#ffffea"
|
|
||||||
fill-opacity="0.12"
|
|
||||||
id="circle11" />
|
|
||||||
</pattern>
|
|
||||||
<mask
|
|
||||||
id="dots-mask">
|
|
||||||
<rect
|
|
||||||
width="1544"
|
|
||||||
height="500"
|
|
||||||
fill="url(#dots-fade)"
|
|
||||||
id="rect11" />
|
|
||||||
</mask>
|
|
||||||
<linearGradient
|
|
||||||
id="gold"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="0"
|
|
||||||
y2="1">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#ffe08a"
|
|
||||||
id="stop12" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#f5a524"
|
|
||||||
id="stop13" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="gold-text"
|
|
||||||
x1="96"
|
|
||||||
y1="280"
|
|
||||||
x2="700"
|
|
||||||
y2="340"
|
|
||||||
gradientUnits="userSpaceOnUse">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#ffd970"
|
|
||||||
id="stop14" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#f5a524"
|
|
||||||
id="stop15" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="top-red"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="1"
|
|
||||||
y2="1">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#ef4444"
|
|
||||||
id="stop16" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#bb0000"
|
|
||||||
id="stop17" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="top-cream"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="1"
|
|
||||||
y2="1">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#ff8a8a"
|
|
||||||
id="stop18" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#e03131"
|
|
||||||
id="stop19" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="top-blue"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="1"
|
|
||||||
y2="1">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
stop-color="#c81e1e"
|
|
||||||
id="stop20" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
stop-color="#7a0000"
|
|
||||||
id="stop21" />
|
|
||||||
</linearGradient>
|
|
||||||
<!-- A five-pointed star, radius 1, centred on 0,0 -->
|
|
||||||
<polygon
|
|
||||||
id="star"
|
|
||||||
points="0,-1 0.2245,-0.309 0.9511,-0.309 0.3633,0.118 0.5878,0.809 0,0.382 -0.5878,0.809 -0.3633,0.118 -0.9511,-0.309 -0.2245,-0.309" />
|
|
||||||
</defs>
|
|
||||||
<!-- Background -->
|
|
||||||
<rect
|
|
||||||
width="1544"
|
|
||||||
height="500"
|
|
||||||
fill="url(#bg)"
|
|
||||||
id="rect21" />
|
|
||||||
<rect
|
|
||||||
width="1544"
|
|
||||||
height="500"
|
|
||||||
fill="url(#glow-red)"
|
|
||||||
id="rect22" />
|
|
||||||
<rect
|
|
||||||
width="1544"
|
|
||||||
height="500"
|
|
||||||
fill="url(#glow-blue)"
|
|
||||||
id="rect23" />
|
|
||||||
<rect
|
|
||||||
width="1544"
|
|
||||||
height="500"
|
|
||||||
fill="url(#dots)"
|
|
||||||
mask="url(#dots-mask)"
|
|
||||||
id="rect24" />
|
|
||||||
<!-- wpallstars.com's red double lines, with a cream line between them. -->
|
|
||||||
<g
|
|
||||||
id="g30">
|
|
||||||
<rect
|
|
||||||
y="0"
|
|
||||||
width="1544"
|
|
||||||
height="6"
|
|
||||||
fill="#bb0000"
|
|
||||||
id="rect25" />
|
|
||||||
<rect
|
|
||||||
y="6"
|
|
||||||
width="1544"
|
|
||||||
height="5"
|
|
||||||
fill="#ffffea"
|
|
||||||
id="rect26" />
|
|
||||||
<rect
|
|
||||||
y="11"
|
|
||||||
width="1544"
|
|
||||||
height="6"
|
|
||||||
fill="#bb0000"
|
|
||||||
id="rect27" />
|
|
||||||
<rect
|
|
||||||
y="483"
|
|
||||||
width="1544"
|
|
||||||
height="6"
|
|
||||||
fill="#bb0000"
|
|
||||||
id="rect28" />
|
|
||||||
<rect
|
|
||||||
y="489"
|
|
||||||
width="1544"
|
|
||||||
height="5"
|
|
||||||
fill="#ffffea"
|
|
||||||
id="rect29" />
|
|
||||||
<rect
|
|
||||||
y="494"
|
|
||||||
width="1544"
|
|
||||||
height="6"
|
|
||||||
fill="#bb0000"
|
|
||||||
id="rect30" />
|
|
||||||
</g>
|
|
||||||
<!-- The picture: scaled down a little so it keeps clear of the stripes -->
|
|
||||||
<g
|
|
||||||
transform="translate(1214 248) scale(0.86) translate(-1214 -248)"
|
|
||||||
id="g48">
|
|
||||||
<!-- The stack: red, cream and blue layers, each with a top face and two sides -->
|
|
||||||
<g
|
|
||||||
stroke-linejoin="round"
|
|
||||||
id="g43">
|
|
||||||
<g
|
|
||||||
transform="translate(0 350)"
|
|
||||||
id="g33">
|
|
||||||
<polygon
|
|
||||||
points="1064,0 1214,87 1214,117 1064,30"
|
|
||||||
fill="#5c0000"
|
|
||||||
id="polygon30" />
|
|
||||||
<polygon
|
|
||||||
points="1214,87 1364,0 1364,30 1214,117"
|
|
||||||
fill="#420000"
|
|
||||||
id="polygon31" />
|
|
||||||
<polygon
|
|
||||||
points="1214,-87 1364,0 1214,87 1064,0"
|
|
||||||
fill="url(#top-blue)"
|
|
||||||
id="polygon32" />
|
|
||||||
<polygon
|
|
||||||
points="1214,-61 1319,0 1214,61 1109,0"
|
|
||||||
fill="none"
|
|
||||||
stroke="#ffffff"
|
|
||||||
stroke-opacity="0.3"
|
|
||||||
stroke-width="2"
|
|
||||||
id="polygon33" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(0 294)"
|
|
||||||
id="g37">
|
|
||||||
<polygon
|
|
||||||
points="1064,0 1214,87 1214,117 1064,30"
|
|
||||||
fill="#c22424"
|
|
||||||
id="polygon34" />
|
|
||||||
<polygon
|
|
||||||
points="1214,87 1364,0 1364,30 1214,117"
|
|
||||||
fill="#9e1414"
|
|
||||||
id="polygon35" />
|
|
||||||
<polygon
|
|
||||||
points="1214,-87 1364,0 1214,87 1064,0"
|
|
||||||
fill="url(#top-cream)"
|
|
||||||
id="polygon36" />
|
|
||||||
<polygon
|
|
||||||
points="1214,-61 1319,0 1214,61 1109,0"
|
|
||||||
fill="none"
|
|
||||||
stroke="#001537"
|
|
||||||
stroke-opacity="0.18"
|
|
||||||
stroke-width="2"
|
|
||||||
id="polygon37" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(0 238)"
|
|
||||||
id="g42">
|
|
||||||
<polygon
|
|
||||||
points="1064,0 1214,87 1214,117 1064,30"
|
|
||||||
fill="#940000"
|
|
||||||
id="polygon38" />
|
|
||||||
<polygon
|
|
||||||
points="1214,87 1364,0 1364,30 1214,117"
|
|
||||||
fill="#700000"
|
|
||||||
id="polygon39" />
|
|
||||||
<polygon
|
|
||||||
points="1214,-87 1364,0 1214,87 1064,0"
|
|
||||||
fill="url(#top-red)"
|
|
||||||
id="polygon40" />
|
|
||||||
<!-- The WordPress Plugins icon (Dashicons admin-plugins, GPLv2 or later),
|
|
||||||
laid flat on the top face: icon x runs along the top-right edge and
|
|
||||||
icon y along the top-left edge, each 20-unit side 66% of the face.
|
|
||||||
Turned 45 degrees so the plug points along the back-left edge. -->
|
|
||||||
<g
|
|
||||||
transform="matrix(4.95 2.871 -4.95 2.871 1214 -57.42)"
|
|
||||||
id="g41">
|
|
||||||
<path
|
|
||||||
fill="#5c0000"
|
|
||||||
fill-opacity="0.55"
|
|
||||||
transform="translate(0.3 0.3) rotate(-45 10 10)"
|
|
||||||
d="M13.11 4.36L9.87 7.6 8 5.73l3.24-3.24c.35-.34 1.05-.2 1.56.32.52.51.66 1.21.31 1.55zm-8 1.77l.91-1.12 9.01 9.01-1.19.84c-.71.71-2.63 1.16-3.82 1.16H6.14L4.9 17.26c-.59.59-1.54.59-2.12 0-.59-.58-.59-1.53 0-2.12l1.24-1.24v-3.88c0-1.13.4-3.19 1.09-3.89zm7.26 3.97l3.24-3.24c.34-.35 1.04-.21 1.55.31.52.51.66 1.21.31 1.55l-3.24 3.25z"
|
|
||||||
id="path40" />
|
|
||||||
<path
|
|
||||||
fill="#ffffea"
|
|
||||||
transform="rotate(-45 10 10)"
|
|
||||||
d="M13.11 4.36L9.87 7.6 8 5.73l3.24-3.24c.35-.34 1.05-.2 1.56.32.52.51.66 1.21.31 1.55zm-8 1.77l.91-1.12 9.01 9.01-1.19.84c-.71.71-2.63 1.16-3.82 1.16H6.14L4.9 17.26c-.59.59-1.54.59-2.12 0-.59-.58-.59-1.53 0-2.12l1.24-1.24v-3.88c0-1.13.4-3.19 1.09-3.89zm7.26 3.97l3.24-3.24c.34-.35 1.04-.21 1.55.31.52.51.66 1.21.31 1.55l-3.24 3.25z"
|
|
||||||
id="path41" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<!-- The logo's arc of five stars: gold in the middle, floating above the stack -->
|
|
||||||
<circle
|
|
||||||
cx="1214"
|
|
||||||
cy="76"
|
|
||||||
r="120"
|
|
||||||
fill="url(#glow-gold)"
|
|
||||||
id="circle43" />
|
|
||||||
<g
|
|
||||||
stroke-linejoin="round"
|
|
||||||
id="g47">
|
|
||||||
<g
|
|
||||||
fill="#bb0000"
|
|
||||||
stroke="#ffffea"
|
|
||||||
stroke-width="5"
|
|
||||||
id="g46">
|
|
||||||
<use
|
|
||||||
xlink:href="#star"
|
|
||||||
transform="translate(1043 150) scale(17)"
|
|
||||||
stroke-width="0.3"
|
|
||||||
id="use43" />
|
|
||||||
<use
|
|
||||||
xlink:href="#star"
|
|
||||||
transform="translate(1110 100) scale(26)"
|
|
||||||
stroke-width="0.2"
|
|
||||||
id="use44" />
|
|
||||||
<use
|
|
||||||
xlink:href="#star"
|
|
||||||
transform="translate(1318 100) scale(26)"
|
|
||||||
stroke-width="0.2"
|
|
||||||
id="use45" />
|
|
||||||
<use
|
|
||||||
xlink:href="#star"
|
|
||||||
transform="translate(1385 150) scale(17)"
|
|
||||||
stroke-width="0.3"
|
|
||||||
id="use46" />
|
|
||||||
</g>
|
|
||||||
<use
|
|
||||||
xlink:href="#star"
|
|
||||||
transform="translate(1214 78) scale(54)"
|
|
||||||
fill="url(#gold)"
|
|
||||||
stroke="#ffffea"
|
|
||||||
stroke-width="0.11"
|
|
||||||
id="use47" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<!-- Words -->
|
|
||||||
<g
|
|
||||||
font-family="Zilla Slab"
|
|
||||||
text-rendering="geometricPrecision"
|
|
||||||
id="g51">
|
|
||||||
<path
|
|
||||||
d="m 123.893,113.015 h -1.431 L 117.683,128 h -2.214 l -4.293,-12.366 h -0.135 L 106.883,128 h -2.241 l -5.076,-14.985 h -1.512 v -2.565 h 6.669 v 2.565 h -1.944 l 3.024,10.044 h 0.135 l 3.483,-10.044 h -1.539 v -2.565 h 6.642 v 2.565 h -1.431 l 3.483,10.179 h 0.135 l 2.754,-10.179 h -2.16 v -2.565 h 6.588 z m 20.60601,3.024 q 0,2.889 -1.863,4.374 -1.836,1.458 -4.428,1.458 h -2.97 v 3.564 h 2.808 V 128 h -7.83 v -2.565 h 2.025 v -12.42 h -2.025 v -2.565 h 7.83 q 3.267,0 4.86,1.566 1.593,1.566 1.593,4.023 z m -3.213,0.054 q 0,-1.458 -0.837,-2.268 -0.81,-0.81 -2.754,-0.81 h -2.457 v 6.318 h 2.457 q 1.917,0 2.754,-0.864 0.837,-0.891 0.837,-2.376 z M 166.72505,128 h -6.723 v -2.565 h 1.782 l -0.891,-2.403 h -6.561 l -0.945,2.403 h 1.971 V 128 h -6.642 v -2.565 h 1.62 l 5.292,-12.42 h -2.565 v -2.565 h 5.643 l 6.318,14.985 h 1.701 z m -6.642,-7.155 -2.295,-6.102 h -0.162 l -2.43,6.102 z M 185.71107,128 h -12.987 v -2.565 h 2.025 v -12.42 h -2.025 v -2.565 h 7.641 v 2.565 h -2.619 v 12.42 h 5.4 l 0.54,-3.645 2.565,0.216 z m 19.93099,0 h -12.987 v -2.565 h 2.025 v -12.42 h -2.025 v -2.565 h 7.641 v 2.565 h -2.619 v 12.42 h 5.4 l 0.54,-3.645 2.565,0.216 z m 19.68799,-5.157 q 0,2.619 -1.836,4.023 -1.809,1.404 -4.806,1.404 -1.782,0 -3.456,-0.432 -1.647,-0.432 -2.619,-1.242 l 0.486,-4.023 2.403,0.243 -0.054,2.16 q 0.891,0.459 1.62,0.594 0.756,0.108 1.404,0.108 1.566,0 2.592,-0.486 1.053,-0.513 1.053,-2.025 0,-1.647 -1.188,-2.187 -1.188,-0.567 -2.727,-0.864 -1.242,-0.243 -2.511,-0.648 -1.269,-0.405 -2.106,-1.35 -0.837,-0.945 -0.837,-2.781 0,-2.754 1.863,-3.942 1.89,-1.215 4.212,-1.215 1.62,0 3.132,0.486 1.539,0.486 2.403,1.269 l 0.162,3.699 -2.403,0.216 -0.243,-2.268 q -0.756,-0.432 -1.512,-0.594 -0.756,-0.162 -1.458,-0.162 -1.269,0 -2.106,0.513 -0.837,0.513 -0.837,1.647 0,1.323 1.026,1.809 1.053,0.459 2.349,0.756 1.107,0.243 2.511,0.648 1.404,0.405 2.43,1.458 1.053,1.026 1.053,3.186 z m 22.65801,-6.669 -2.592,0.216 -0.54,-3.375 h -3.429 v 12.42 h 2.943 V 128 h -8.775 v -2.565 h 2.835 v -12.42 h -3.456 l -0.54,3.375 -2.565,-0.216 0.54,-5.724 h 15.039 z M 270.18705,128 h -6.723 v -2.565 h 1.782 l -0.891,-2.403 h -6.561 l -0.945,2.403 h 1.971 V 128 h -6.642 v -2.565 h 1.62 l 5.292,-12.42 h -2.565 v -2.565 h 5.643 l 6.318,14.985 h 1.701 z m -6.642,-7.155 -2.295,-6.102 h -0.162 l -2.43,6.102 z M 291.81908,128 h -4.104 l -3.618,-6.939 h -2.889 v 4.374 h 2.052 V 128 h -7.074 v -2.565 h 2.025 v -12.42 h -2.025 v -2.565 h 7.803 q 3.267,0 4.887,1.377 1.62,1.35 1.62,3.807 0,1.755 -0.945,3.024 -0.918,1.242 -2.565,1.782 l 2.673,4.995 h 2.16 z m -4.509,-12.285 q 0,-1.485 -0.864,-2.079 -0.864,-0.621 -2.943,-0.621 h -2.295 v 5.643 h 2.268 q 2.16,0 2.997,-0.756 0.837,-0.783 0.837,-2.187 z m 23.62998,7.128 q 0,2.619 -1.836,4.023 -1.809,1.404 -4.806,1.404 -1.782,0 -3.456,-0.432 -1.647,-0.432 -2.619,-1.242 l 0.486,-4.023 2.403,0.243 -0.054,2.16 q 0.891,0.459 1.62,0.594 0.756,0.108 1.404,0.108 1.566,0 2.592,-0.486 1.053,-0.513 1.053,-2.025 0,-1.647 -1.188,-2.187 -1.188,-0.567 -2.727,-0.864 -1.242,-0.243 -2.511,-0.648 -1.269,-0.405 -2.106,-1.35 -0.837,-0.945 -0.837,-2.781 0,-2.754 1.863,-3.942 1.89,-1.215 4.212,-1.215 1.62,0 3.132,0.486 1.539,0.486 2.403,1.269 l 0.162,3.699 -2.403,0.216 -0.243,-2.268 q -0.756,-0.432 -1.512,-0.594 -0.756,-0.162 -1.458,-0.162 -1.269,0 -2.106,0.513 -0.837,0.513 -0.837,1.647 0,1.323 1.026,1.809 1.053,0.459 2.349,0.756 1.107,0.243 2.511,0.648 1.404,0.405 2.43,1.458 1.053,1.026 1.053,3.186 z"
|
|
||||||
id="text48"
|
|
||||||
style="font-weight:600;font-size:27px;letter-spacing:5;fill:#ff999b"
|
|
||||||
aria-label="WPALLSTARS" />
|
|
||||||
<path
|
|
||||||
d="M 192.464,184.256 H 187.16 L 163.552,240 h -9.464 l -10.712,-43.68 h -0.52 L 123.72,240 h -9.672 L 100.944,184.256 H 95.328 L 96.576,172.4 h 25.792 l -1.248,11.856 h -5.928 l 6.24,32.656 h 0.624 l 14.976,-32.656 h -5.096 l 1.248,-11.856 h 25.896 l -1.248,11.856 h -4.68 l 8.008,33.28 h 0.624 l 11.96,-33.28 h -6.864 l 1.248,-11.856 h 25.584 z m -0.37598,43.888 h 6.968 l 4.576,-43.888 h -6.968 l 1.248,-11.856 h 29.744 q 11.128,0 16.848,5.616 5.72,5.512 5.72,14.976 0,12.272 -8.216,18.304 -8.112,6.032 -18.408,6.032 h -9.984 l -1.144,10.816 h 9.984 l -1.248,11.856 h -30.368 z m 30.264,-22.568 q 7.696,0 10.504,-3.432 2.912,-3.536 2.912,-8.528 0,-4.576 -2.392,-6.968 -2.392,-2.392 -8.632,-2.392 h -7.696 l -2.184,21.32 z m 46.464,22.568 h 6.968 l 4.576,-43.888 h -6.968 l 1.248,-11.856 h 29.744 q 11.128,0 16.848,5.616 5.72,5.512 5.72,14.976 0,12.272 -8.216,18.304 -8.112,6.032 -18.408,6.032 h -9.984 l -1.144,10.816 h 9.984 l -1.248,11.856 h -30.368 z m 30.264,-22.568 q 7.696,0 10.504,-3.432 2.912,-3.536 2.912,-8.528 0,-4.576 -2.392,-6.968 -2.392,-2.392 -8.632,-2.392 h -7.696 l -2.184,21.32 z m 31.55191,-34.32 3.224,-2.808 h 16.432 l -7.488,61.36 h 6.032 l -0.936,7.592 -3.12,2.6 h -16.12 l 7.488,-61.36 h -6.448 z m 73.98399,58.552 -0.936,7.384 -3.224,2.808 h -14.456 l -0.52,-9.048 q -2.392,4.472 -6.136,7.28 -3.744,2.704 -8.944,2.704 -6.344,0 -10.608,-3.952 -4.16,-3.952 -3.12,-12.272 l 2.6,-21.424 h -6.032 l 0.936,-7.592 3.12,-2.6 h 16.016 l -3.224,26.416 q -0.728,6.136 0.832,8.736 1.664,2.496 5.512,2.496 4.68,0 6.968,-3.12 2.392,-3.12 3.016,-8.008 v 0.104 l 3.224,-26.624 h 12.896 l -4.472,36.712 z m 41.32793,-36.712 h 16.952 l -0.936,7.592 -3.12,2.6 h -3.328 l -4.264,34.32 q -1.248,9.984 -6.864,17.68 -5.616,7.8 -18.616,7.8 -6.656,0 -12.584,-3.328 -5.824,-3.224 -7.592,-11.336 l 9.152,-5.2 q 3.432,8.736 11.856,8.736 9.984,0 11.752,-14.352 l 0.728,-6.448 q -2.392,4.056 -6.24,6.968 -3.848,2.912 -9.36,2.912 -7.696,0 -11.336,-5.824 -3.64,-5.824 -3.64,-13.936 0,-6.552 2.392,-13.208 2.392,-6.76 7.176,-11.336 4.888,-4.576 12.48,-4.576 10.088,0 13.104,8.32 z m -13.312,9.256 q -4.992,0 -7.592,5.2 -2.6,5.2 -2.6,12.272 0,4.888 1.456,8.008 1.456,3.016 4.888,3.016 4.16,0 7.488,-3.952 3.328,-3.952 4.368,-8.424 l 0.624,-4.992 q 0.208,-5.408 -2.496,-8.216 -2.704,-2.912 -6.136,-2.912 z m 39.5601,-17.576 1.456,-13.936 h 12.48 l -1.456,13.936 z m -8.216,11.128 3.224,-2.808 h 16.536 l -4.472,36.712 h 5.928 l -0.936,7.592 -3.12,2.6 h -16.016 l 4.472,-36.712 h -6.552 z m 30.51203,-2.808 h 14.456 l 0.52,8.944 q 2.392,-4.472 6.136,-7.176 3.744,-2.704 8.944,-2.704 6.344,0 10.504,3.952 4.264,3.952 3.224,12.272 l -2.6,21.424 h 6.032 l -0.936,7.592 -3.12,2.6 h -16.016 l 3.224,-26.416 q 0.728,-6.032 -0.936,-8.632 -1.56,-2.6 -5.408,-2.6 -4.368,0 -6.76,2.704 -2.392,2.704 -3.12,7.176 l -3.328,27.768 h -12.896 l 4.472,-36.712 h -6.552 l 0.936,-7.384 z m 118.53613,25.688 q 0,10.816 -7.176,16.536 -7.072,5.72 -19.76,5.72 -6.968,0 -13.52,-1.768 -6.448,-1.768 -9.984,-5.304 l 3.848,-16.744 10.816,1.04 -0.936,8.216 q 3.224,1.872 5.616,2.288 2.392,0.312 3.744,0.312 5.824,0 9.152,-1.768 3.432,-1.872 3.432,-7.488 0,-5.096 -2.912,-6.552 -2.912,-1.56 -8.632,-2.6 -4.576,-0.832 -9.048,-2.496 -4.368,-1.768 -7.28,-5.304 -2.912,-3.64 -2.912,-10.4 0,-7.8 3.848,-12.376 3.952,-4.68 9.88,-6.656 6.032,-2.08 12.272,-2.08 6.864,0 12.48,2.08 5.72,1.976 8.632,5.408 l -0.936,14.664 -11.024,1.04 -0.104,-7.904 q -2.392,-1.768 -4.888,-2.392 -2.496,-0.624 -4.992,-0.624 -4.368,0 -7.488,1.664 -3.12,1.56 -3.12,5.408 0,4.056 3.328,5.616 3.328,1.56 7.696,2.496 4.368,0.936 8.944,2.704 4.68,1.664 7.8,5.616 3.224,3.952 3.224,11.648 z m 5.44796,-22.464 3.536,-3.224 h 3.016 l 1.248,-10.088 4.056,-4.16 h 9.36 l -1.768,14.248 h 14.04 l -0.936,6.864 -3.536,3.328 h -10.816 l -2.6,21.424 q -0.728,6.136 3.12,6.136 1.664,0 3.224,-1.456 1.664,-1.56 3.016,-6.968 l 9.048,3.016 q -1.976,8.112 -5.824,11.856 -3.848,3.744 -10.816,3.744 -7.488,0 -11.648,-3.848 -4.056,-3.952 -3.224,-11.232 l 2.808,-22.672 h -6.24 z m 85.11207,-3.224 q -1.352,4.264 -2.392,8.32 -0.936,4.056 -1.456,8.32 l -2.496,20.072 h 6.032 l -0.936,7.592 -3.12,2.6 h -14.04 l -0.416,-9.568 q -2.184,4.368 -5.928,7.488 -3.744,3.12 -9.464,3.12 -7.696,0 -11.544,-5.824 -3.848,-5.824 -3.848,-14.248 0,-6.344 2.288,-13 2.288,-6.656 6.968,-11.232 4.784,-4.576 12.376,-4.576 10.192,0 13.312,8.736 0.52,-1.976 0.936,-3.848 0.52,-1.976 1.248,-3.952 z m -25.896,9.256 q -4.992,0 -7.384,5.304 -2.392,5.2 -2.392,11.752 0,11.44 6.656,11.44 4.264,0 7.28,-4.056 3.016,-4.056 3.952,-8.632 l 0.416,-3.536 q 0.624,-5.824 -2.184,-9.048 -2.704,-3.224 -6.344,-3.224 z m 35.50396,-9.256 h 13.416 l 0.728,12.896 q 1.56,-4.888 4.264,-9.36 2.808,-4.472 7.072,-4.472 2.912,0 5.096,1.872 l -2.08,13.832 q -2.912,-1.56 -5.408,-1.56 -7.072,0 -8.736,7.28 l -2.288,16.224 h 8.944 l -0.936,7.384 -3.224,2.808 h -25.064 l 0.936,-7.384 3.224,-2.808 h 3.12 l 3.224,-26.52 h -6.448 l 0.936,-7.384 z m 31.13605,3.224 3.536,-3.224 h 3.016 l 1.248,-10.088 4.056,-4.16 h 9.36 l -1.768,14.2Line truncated
|
|
||||||
id="text49"
|
|
||||||
style="font-style:italic;font-weight:700;font-size:104px;letter-spacing:-1;fill:#ffffea"
|
|
||||||
aria-label="WP Plugin Starter" />
|
|
||||||
<path
|
|
||||||
d="m 124.944,279.2 q 5.76,0 9.144,2.736 3.456,2.664 3.456,7.272 0,3.816 -2.448,6.912 -2.376,3.096 -6.768,3.816 v 0.288 q 4.248,0.72 6.408,3.528 2.16,2.808 2.16,7.056 0,5.904 -4.104,10.584 Q 128.688,326 120.336,326 H 96.648 l 0.864,-8.208 h 4.824 l 3.168,-30.384 h -4.824 l 0.864,-8.208 z m -5.112,19.152 q 4.392,0 5.904,-1.944 1.584,-1.944 1.584,-4.248 0,-2.88 -1.656,-3.816 -1.584,-0.936 -4.032,-0.936 h -6.84 l -1.152,10.944 z m -1.08,19.44 q 8.208,0 8.208,-6.624 0,-5.328 -6.336,-5.328 h -7.776 l -1.224,11.952 z m 57.24003,1.152 -0.648,5.112 -2.232,1.944 h -10.008 l -0.36,-6.264 q -1.656,3.096 -4.248,5.04 -2.592,1.872 -6.192,1.872 -4.392,0 -7.344,-2.736 -2.88,-2.736 -2.16,-8.496 l 1.8,-14.832 h -4.176 l 0.648,-5.256 2.16,-1.8 h 11.088 l -2.232,18.288 q -0.504,4.248 0.576,6.048 1.152,1.728 3.816,1.728 3.24,0 4.824,-2.16 1.656,-2.16 2.088,-5.544 v 0.072 l 2.232,-18.432 h 8.928 l -3.096,25.416 z m 10.29594,-31.176 1.008,-9.648 h 8.64 l -1.008,9.648 z m -5.688,7.704 2.232,-1.944 h 11.448 l -3.096,25.416 h 4.104 l -0.648,5.256 -2.16,1.8 h -11.088 l 3.096,-25.416 h -4.536 z m 20.80802,-17.064 2.232,-1.944 h 11.376 l -5.184,42.48 h 4.176 l -0.648,5.256 -2.16,1.8 h -11.16 l 5.184,-42.48 h -4.464 z m 16.84799,17.352 2.448,-2.232 h 2.088 l 0.864,-6.984 2.808,-2.88 h 6.48 l -1.224,9.864 h 9.72 l -0.648,4.752 -2.448,2.304 h -7.488 l -1.8,14.832 q -0.504,4.248 2.16,4.248 1.152,0 2.232,-1.008 1.152,-1.08 2.088,-4.824 l 6.264,2.088 q -1.368,5.616 -4.032,8.208 -2.664,2.592 -7.488,2.592 -5.184,0 -8.064,-2.664 -2.808,-2.736 -2.232,-7.776 l 1.944,-15.696 h -4.32 z m 72.21606,19.584 h 0.36 q 3.312,-4.608 4.68,-7.92 1.44,-3.384 1.44,-6.912 0,-3.456 -1.44,-6.984 h 10.008 q 0.792,4.392 -0.216,9.072 -1.008,4.68 -4.824,9.792 -1.872,2.448 -5.328,6.48 -3.456,4.032 -6.624,7.128 h -3.744 l -4.752,-18.72 h -0.432 l -10.512,18.72 h -4.752 l -6.912,-25.416 h -3.168 l 0.648,-5.112 2.232,-1.944 h 14.616 l -0.648,5.112 -2.232,1.944 h -2.232 l 2.52,12.816 h 0.432 l 10.368,-19.872 h 5.328 z m 24.04794,-27.576 1.008,-9.648 h 8.64 l -1.008,9.648 z m -5.688,7.704 2.232,-1.944 h 11.448 l -3.096,25.416 h 4.104 l -0.648,5.256 -2.16,1.8 h -11.088 l 3.096,-25.416 h -4.536 z m 19.15202,0.288 2.448,-2.232 h 2.088 l 0.864,-6.984 2.808,-2.88 h 6.48 l -1.224,9.864 h 9.72 l -0.648,4.752 -2.448,2.304 h -7.488 l -1.8,14.832 q -0.504,4.248 2.16,4.248 1.152,0 2.232,-1.008 1.152,-1.08 2.088,-4.824 l 6.264,2.088 q -1.368,5.616 -4.032,8.208 -2.664,2.592 -7.488,2.592 -5.184,0 -8.064,-2.664 -2.808,-2.736 -2.232,-7.776 l 1.944,-15.696 h -4.32 z m 27.28803,-17.352 2.232,-1.944 h 11.088 l -2.736,22.536 q 1.584,-2.736 4.104,-4.392 2.52,-1.728 5.904,-1.728 4.392,0 7.272,2.736 2.952,2.736 2.232,8.496 l -1.8,14.832 h 4.176 l -0.648,5.256 -2.16,1.8 h -11.088 l 2.232,-18.288 q 0.576,-4.176 -0.576,-5.976 -1.152,-1.8 -3.816,-1.8 -3.096,0 -4.68,1.944 -1.584,1.944 -2.16,5.112 l -2.304,19.008 h -8.928 l 5.184,-42.48 h -4.176 z m 45.14409,39.384 h 4.104 l 15.984,-30.384 h -6.192 l 0.864,-8.208 h 15.408 l 12.168,38.592 h 4.248 l -0.864,8.208 h -17.856 l 0.864,-8.208 h 3.6 l -1.224,-4.752 h -15.264 l -2.304,4.752 h 4.176 l -0.864,8.208 h -17.712 z m 25.632,-24.84 -6.408,13.176 h 10.152 l -3.312,-13.176 z m 23.75999,24.84 h 4.824 l 3.168,-30.384 h -4.824 l 0.864,-8.208 h 18.936 l -0.864,8.208 h -4.896 l -3.168,30.384 h 4.896 l -0.864,8.208 h -18.936 z"
|
|
||||||
id="text50"
|
|
||||||
style="font-style:italic;font-weight:700;font-size:72px;fill:url(#gold-text)"
|
|
||||||
aria-label="Built with AI" />
|
|
||||||
<path
|
|
||||||
d="m 119.912,396 h -7.887 v -2.607 h 2.541 l -1.32,-3.564 H 104.6 l -1.419,3.564 h 2.805 V 396 h -7.821 v -2.607 h 1.98 l 6.765,-16.236 h -3.3 v -2.607 h 6.468 l 7.722,18.843 h 2.112 z m -7.524,-8.415 -3.267,-8.712 h -0.198 l -3.432,8.712 z m 27.852,3.3 q -0.264,1.947 -1.749,3.696 -1.452,1.749 -4.653,1.749 -3.102,0 -5.016,-1.914 -1.881,-1.947 -1.881,-5.544 0,-3.333 1.881,-5.61 1.881,-2.31 5.247,-2.31 1.386,0 2.673,0.33 1.287,0.33 2.442,1.155 l 0.594,3.828 -2.343,0.264 -0.528,-2.475 q -1.254,-0.726 -2.871,-0.726 -2.079,0 -3.069,1.518 -0.99,1.485 -0.99,3.861 0,2.475 1.056,3.861 1.056,1.386 3.102,1.386 3.069,0 3.663,-3.564 z m 9.24002,5.115 h -7.656 v -2.244 h 2.376 v -18.183 h -2.475 v -2.277 h 5.379 v 20.46 h 2.376 z m 15.27898,-4.356 q -0.231,1.914 -1.881,3.3 -1.65,1.386 -4.488,1.386 -3.201,0 -5.181,-1.914 -1.98,-1.914 -1.98,-5.544 0,-3.432 1.848,-5.676 1.848,-2.244 5.115,-2.244 3.036,0 4.719,1.881 1.683,1.881 1.716,4.554 0,0.891 -0.198,1.881 h -10.164 q 0.198,4.686 4.323,4.686 1.716,0 2.574,-0.825 0.891,-0.825 1.188,-1.98 z m -3.135,-4.323 q 0.132,-1.452 -0.726,-2.772 -0.825,-1.353 -2.772,-1.353 -1.815,0 -2.739,1.221 -0.891,1.188 -1.089,2.904 z m 6.73203,-4.95 q 1.353,-0.792 2.64,-1.089 1.32,-0.33 2.871,-0.33 2.508,0 3.96,1.221 1.485,1.221 1.485,3.762 v 6.831 q 0,1.452 1.155,1.452 0.462,0 0.891,-0.165 l 0.033,1.716 q -0.924,0.561 -2.145,0.561 -2.673,0 -2.838,-2.97 v -0.033 q -0.66,1.155 -1.914,2.079 -1.221,0.924 -3.003,0.924 -1.518,0 -3.036,-0.924 -1.485,-0.924 -1.485,-3.234 0,-2.772 2.178,-3.63 2.211,-0.891 4.719,-0.891 1.518,0 2.541,0.132 v -1.089 q 0,-1.452 -0.561,-2.475 -0.561,-1.023 -2.508,-1.023 -1.584,0 -2.673,0.561 l -0.561,2.409 -2.343,-0.264 z m 8.052,7.953 v -0.858 q -0.594,-0.066 -1.254,-0.099 -0.627,-0.066 -1.287,-0.066 -1.518,0 -2.706,0.495 -1.188,0.495 -1.188,2.046 0,1.188 0.726,1.716 0.759,0.528 1.716,0.528 1.419,0 2.64,-0.99 1.221,-0.99 1.353,-2.772 z M 200.72902,396 h -7.656 v -2.244 h 2.376 v -6.369 q 0,-2.244 -0.858,-3.168 -0.858,-0.957 -2.277,-0.957 -1.848,0 -2.937,1.221 -1.089,1.221 -1.155,2.838 v 6.435 h 2.376 V 396 h -7.656 v -2.244 h 2.376 v -10.23 h -2.475 v -2.277 h 5.379 v 2.508 q 1.551,-2.805 5.016,-2.805 2.145,0 3.63,1.32 1.485,1.32 1.485,4.125 v 7.359 h 2.376 z m 20.95501,-4.323 q 0,2.211 -1.848,3.432 -1.815,1.221 -4.62,1.221 -3.399,0 -5.775,-1.452 l 0.462,-3.564 2.277,0.231 -0.066,1.914 q 0.66,0.33 1.419,0.462 0.792,0.132 1.551,0.132 1.419,0 2.541,-0.528 1.155,-0.528 1.155,-1.65 0,-1.089 -0.924,-1.518 -0.924,-0.462 -2.31,-0.693 -1.353,-0.231 -2.706,-0.561 -1.353,-0.363 -2.277,-1.188 -0.924,-0.858 -0.924,-2.574 0,-2.475 1.815,-3.432 1.848,-0.957 3.927,-0.957 1.518,0 2.838,0.396 1.353,0.363 2.442,0.957 l 0.33,3.564 -2.31,0.264 -0.33,-2.31 q -1.353,-0.627 -2.838,-0.627 -1.221,0 -2.013,0.462 -0.792,0.462 -0.792,1.518 0,1.023 0.891,1.452 0.924,0.429 2.244,0.66 1.353,0.198 2.673,0.561 1.353,0.363 2.244,1.221 0.924,0.858 0.924,2.607 z m 13.62901,0.165 q -0.627,4.488 -4.95,4.488 -2.277,0 -3.564,-1.221 -1.254,-1.254 -1.254,-3.465 v -8.184 h -2.277 v -2.211 h 2.277 v -4.653 h 2.871 v 4.653 h 5.643 v 2.211 h -5.643 v 7.755 q 0,2.64 2.277,2.64 0.726,0 1.452,-0.462 0.726,-0.495 1.023,-2.244 z m 2.60698,-9.471 q 1.353,-0.792 2.64,-1.089 1.32,-0.33 2.871,-0.33 2.508,0 3.96,1.221 1.485,1.221 1.485,3.762 v 6.831 q 0,1.452 1.155,1.452 0.462,0 0.891,-0.165 l 0.033,1.716 q -0.924,0.561 -2.145,0.561 -2.673,0 -2.838,-2.97 v -0.033 q -0.66,1.155 -1.914,2.079 -1.221,0.924 -3.003,0.924 -1.518,0 -3.036,-0.924 -1.485,-0.924 -1.485,-3.234 0,-2.772 2.178,-3.63 2.211,-0.891 4.719,-0.891 1.518,0 2.541,0.132 v -1.089 q 0,-1.452 -0.561,-2.475 -0.561,-1.023 -2.508,-1.023 -1.584,0 -2.673,0.561 l -0.561,2.409 -2.343,-0.264 z m 8.052,7.953 v -0.858 q -0.594,-0.066 -1.254,-0.099 -0.627,-0.066 -1.287,-0.066 -1.518,0 -2.706,0.495 -1.188,0.495 -1.188,2.046 0,1.188 0.726,1.716 0.759,0.528 1.716,0.528 1.419,0 2.64,-0.99 1.221,-0.99 1.353,-2.772 z m 15.213,-9.306 q 1.551,0 2.64,0.792 l 0.594,4.125 -2.277,0.264 -0.561,-2.541 q -0.561,-0.231 -1.155,-0.231 -1.122,0 -1.881,0.957 -0.759,0.924 -0.759,2.442 v 6.93 h 3.168 V 396 h -8.448 v -2.244 h 2.376 v -10.23 h -2.475 v -2.277 h 5.181 v 2.706 q 0.429,-1.254 1.32,-2.079 0.924,-0.858 2.277,-0.858 z m 16.86302,10.824 q -0.627,4.488 -4.95,4.488 -2.277,0 -3.564,-1.221 -1.254,-1.254 -1.254,-3.465 v -8.184 h -2.277 v -2.211 h 2.277 v -4.653 h 2.871 v 4.653 h 5.643 v 2.211 h -5.643 v 7.755 q 0,2.64 2.277,2.64 0.726,0 1.452,-0.462 0.726,-0.495 1.023,-2.244 z m 15.80697,-18.876 q 1.023,0 1.947,0.231 0.957,0.231 1.782,0.891 l 0.594,3.597 -2.31,0.297 -0.627,-2.475 q -0.693,-0.231 -1.551,-0.231 -2.343,0 -2.343,3.036 v 2.937 h 3.795 v 2.211 h -3.795 v 10.296 h 3.102 V 396 h -8.382 v -2.244 h 2.409 V 383.46 h -2.046 v -2.211 h 2.046 v -2.607 q 0,-2.508 1.518,-4.092 1.518,-1.584 3.861,-1.584 z m 17.88599,15.675 q 0,3.399 -2.112,5.544 -2.079,2.145 -5.511,2.145 -3.366,0 -5.346,-2.079 -1.947,-2.079 -1.947,-5.478 0,-3.234 1.881,-5.544 1.881,-2.31 5.61,-2.31 3.729,0 5.577,2.31 1.848,2Line truncated
|
|
||||||
id="text51"
|
|
||||||
style="font-weight:500;font-size:33px;fill:#c2cad6"
|
|
||||||
aria-label="A clean start for WordPress plugins, made with aidevops." />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,360 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* WP Plugin Starter admin screen.
|
|
||||||
*
|
|
||||||
* Owns the Settings → WP Plugin Starter page: tab registry, page chrome and the
|
|
||||||
* single admin script/stylesheet. Tab content is delegated to the manager
|
|
||||||
* classes. Settings tabs and header links come from WPStarter_Setup; add
|
|
||||||
* other tabs with the `wpstarter_admin_tabs` filter.
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
* @since 0.2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WPStarter_Admin_Manager {
|
|
||||||
|
|
||||||
/** Menu/page slug. */
|
|
||||||
const PAGE = 'wp-plugin-starter-template';
|
|
||||||
|
|
||||||
/** Hook suffix returned by add_options_page(). */
|
|
||||||
const HOOK = 'settings_page_' . self::PAGE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register hooks (once).
|
|
||||||
*/
|
|
||||||
public static function init() {
|
|
||||||
add_action('admin_menu', array(__CLASS__, 'register_admin_menu'));
|
|
||||||
add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_assets'));
|
|
||||||
add_filter('plugin_action_links_' . plugin_basename(WPSTARTER_FILE), array(__CLASS__, 'plugin_action_links'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Slug of the search results screen (not shown in the navigation). */
|
|
||||||
const SEARCH = 'search';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings tabs, in navigation order (WPStarter_Setup::settings_tabs()).
|
|
||||||
*
|
|
||||||
* @return array<string,array{label:string,description:string}>
|
|
||||||
*/
|
|
||||||
public static function settings_tabs() {
|
|
||||||
return WPStarter_Setup::settings_tabs();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registered tabs.
|
|
||||||
*
|
|
||||||
* @return array<string,array{label:string,group:string,render:callable}>
|
|
||||||
*/
|
|
||||||
public static function get_tabs() {
|
|
||||||
$tabs = array();
|
|
||||||
|
|
||||||
// Settings tabs without settings are hidden (they can be filled via
|
|
||||||
// the schema filter). With no settings at all, the first tab shows
|
|
||||||
// and says so, so a new plugin still has a settings screen.
|
|
||||||
$settings_tabs = self::settings_tabs();
|
|
||||||
$has_settings = false;
|
|
||||||
foreach (array_keys($settings_tabs) as $slug) {
|
|
||||||
if (WPStarter_Settings::fields_for_tab($slug)) {
|
|
||||||
$has_settings = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($settings_tabs as $slug => $tab) {
|
|
||||||
if ($has_settings && !WPStarter_Settings::fields_for_tab($slug)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!$has_settings && $tabs) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$tabs[$slug] = array(
|
|
||||||
'label' => $tab['label'],
|
|
||||||
'group' => 'settings',
|
|
||||||
'render' => function () use ($slug, $tab) {
|
|
||||||
WPStarter_Settings_Manager::render_tab($slug, $tab['label'], $tab['description']);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$tabs += array(
|
|
||||||
'readme' => array(
|
|
||||||
'label' => __('Read Me', 'wp-plugin-starter-template'),
|
|
||||||
'group' => 'about',
|
|
||||||
'render' => array('WPStarter_Readme_Manager', 'display_tab_content'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the admin tabs.
|
|
||||||
*
|
|
||||||
* @param array $tabs Tabs keyed by slug: label, group (settings|discover|about),
|
|
||||||
* render callback, optional capability.
|
|
||||||
*/
|
|
||||||
$tabs = (array) apply_filters('wpstarter_admin_tabs', $tabs);
|
|
||||||
|
|
||||||
// Hide tabs the current user cannot use.
|
|
||||||
return array_filter($tabs, function ($tab) {
|
|
||||||
return empty($tab['capability']) || current_user_can($tab['capability']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Active tab slug, falling back to the first registered tab.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function get_active_tab() {
|
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only navigation.
|
|
||||||
$tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : '';
|
|
||||||
if (self::SEARCH === $tab && '' !== self::search_query()) {
|
|
||||||
return $tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tabs = self::get_tabs();
|
|
||||||
$tab = WPStarter_Settings::resolve_tab($tab);
|
|
||||||
if (!isset($tabs[$tab])) {
|
|
||||||
$tab = (string) key($tabs);
|
|
||||||
}
|
|
||||||
return $tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current settings search text.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function search_query() {
|
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only search.
|
|
||||||
$query = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : '';
|
|
||||||
return trim(function_exists('mb_substr') ? mb_substr($query, 0, 100) : substr($query, 0, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* URL of a tab.
|
|
||||||
*
|
|
||||||
* @param string $tab Tab slug.
|
|
||||||
* @param array $args Extra query args.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function tab_url($tab, array $args = array()) {
|
|
||||||
return add_query_arg(array_merge(array('page' => self::PAGE, 'tab' => $tab), $args), admin_url('options-general.php'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the Settings link on the Plugins screen.
|
|
||||||
*
|
|
||||||
* @param array $links Action links.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function plugin_action_links($links) {
|
|
||||||
array_unshift($links, sprintf('<a href="%s">%s</a>', esc_url(add_query_arg('page', self::PAGE, admin_url('options-general.php'))), esc_html__('Settings', 'wp-plugin-starter-template')));
|
|
||||||
return $links;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register Settings → WP Plugin Starter.
|
|
||||||
*/
|
|
||||||
public static function register_admin_menu() {
|
|
||||||
add_options_page(
|
|
||||||
__('WP Plugin Starter', 'wp-plugin-starter-template'),
|
|
||||||
__('WP Plugin Starter', 'wp-plugin-starter-template'),
|
|
||||||
'manage_options',
|
|
||||||
self::PAGE,
|
|
||||||
array(__CLASS__, 'render_settings_page')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enqueue the admin stylesheet and script on our screen only.
|
|
||||||
*
|
|
||||||
* @param string $hook Current admin page hook.
|
|
||||||
*/
|
|
||||||
public static function enqueue_assets($hook) {
|
|
||||||
if (self::HOOK !== $hook) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tab = self::get_active_tab();
|
|
||||||
$css = file_exists(WPSTARTER_DIR . 'admin/css/wpstarter-admin.css') ? filemtime(WPSTARTER_DIR . 'admin/css/wpstarter-admin.css') : WPSTARTER_VERSION;
|
|
||||||
$js = file_exists(WPSTARTER_DIR . 'admin/js/wpstarter-admin.js') ? filemtime(WPSTARTER_DIR . 'admin/js/wpstarter-admin.js') : WPSTARTER_VERSION;
|
|
||||||
|
|
||||||
wp_enqueue_style('wpstarter-admin', WPSTARTER_URL . 'admin/css/wpstarter-admin.css', array('dashicons'), $css);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the admin script's dependencies; enqueue what a tab needs.
|
|
||||||
*
|
|
||||||
* @param string[] $deps Script handles.
|
|
||||||
* @param string $tab Active tab.
|
|
||||||
*/
|
|
||||||
$deps = (array) apply_filters('wpstarter_admin_script_deps', array('jquery', 'wp-a11y', 'wp-i18n'), $tab);
|
|
||||||
|
|
||||||
if (self::shows_media_field($tab)) {
|
|
||||||
wp_enqueue_media();
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_enqueue_script('wpstarter-admin', WPSTARTER_URL . 'admin/js/wpstarter-admin.js', $deps, $js, true);
|
|
||||||
wp_set_script_translations('wpstarter-admin', 'wp-plugin-starter-template');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the data the admin script reads (wpstarterAdmin).
|
|
||||||
*
|
|
||||||
* @param array $data Script data.
|
|
||||||
* @param string $tab Active tab.
|
|
||||||
*/
|
|
||||||
wp_localize_script('wpstarter-admin', 'wpstarterAdmin', (array) apply_filters('wpstarter_admin_script_data', array(
|
|
||||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
|
||||||
'nonce' => wp_create_nonce(WPStarter_Settings::NONCE),
|
|
||||||
'tab' => $tab,
|
|
||||||
'i18n' => array(
|
|
||||||
'saving' => __('Saving…', 'wp-plugin-starter-template'),
|
|
||||||
'saved' => __('Saved', 'wp-plugin-starter-template'),
|
|
||||||
'saveFailed' => __('Could not save. Please try again.', 'wp-plugin-starter-template'),
|
|
||||||
'chooseImage' => __('Choose a picture', 'wp-plugin-starter-template'),
|
|
||||||
'useImage' => __('Use this picture', 'wp-plugin-starter-template'),
|
|
||||||
),
|
|
||||||
), $tab));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires after the admin stylesheet and script are enqueued: enqueue
|
|
||||||
* the plugin's own, depending on 'wpstarter-admin'.
|
|
||||||
*
|
|
||||||
* @param string $tab Active tab.
|
|
||||||
*/
|
|
||||||
do_action('wpstarter_admin_enqueue', $tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a tab (or the search results) may show a media field, which
|
|
||||||
* needs the media dialog.
|
|
||||||
*
|
|
||||||
* @param string $tab Tab slug.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private static function shows_media_field($tab) {
|
|
||||||
$schema = WPStarter_Settings::schema();
|
|
||||||
foreach ($schema as $field) {
|
|
||||||
if (!isset($field['type']) || 'media' !== $field['type']) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (self::SEARCH === $tab) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
$field_tab = isset($field['tab']) ? $field['tab'] : '';
|
|
||||||
if (!$field_tab && isset($field['parent'], $schema[$field['parent']]['tab'])) {
|
|
||||||
$field_tab = $schema[$field['parent']]['tab'];
|
|
||||||
}
|
|
||||||
if ($field_tab === $tab) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the page chrome and the active tab.
|
|
||||||
*/
|
|
||||||
public static function render_settings_page() {
|
|
||||||
if (!current_user_can('manage_options')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tabs = self::get_tabs();
|
|
||||||
$active = self::get_active_tab();
|
|
||||||
$links = WPStarter_Setup::header_links();
|
|
||||||
$groups = array(
|
|
||||||
'settings' => __('Settings', 'wp-plugin-starter-template'),
|
|
||||||
'discover' => __('Discover', 'wp-plugin-starter-template'),
|
|
||||||
'about' => __('About', 'wp-plugin-starter-template'),
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
<div class="wrap wps-wrap">
|
|
||||||
<header class="wps-header">
|
|
||||||
<div class="wps-header__brand">
|
|
||||||
<span class="wps-header__logo dashicons dashicons-star-filled" aria-hidden="true"></span>
|
|
||||||
<h1 class="wps-header__title"><?php esc_html_e('WP Plugin Starter', 'wp-plugin-starter-template'); ?></h1>
|
|
||||||
<span class="wps-badge"><?php echo esc_html('v' . WPSTARTER_VERSION); ?></span>
|
|
||||||
</div>
|
|
||||||
<form class="wps-search" role="search" method="get" action="<?php echo esc_url(admin_url('options-general.php')); ?>">
|
|
||||||
<input type="hidden" name="page" value="<?php echo esc_attr(self::PAGE); ?>" />
|
|
||||||
<input type="hidden" name="tab" value="<?php echo esc_attr(self::SEARCH); ?>" />
|
|
||||||
<label class="screen-reader-text" for="wps-search-input"><?php esc_html_e('Search features', 'wp-plugin-starter-template'); ?></label>
|
|
||||||
<span class="wps-search__field">
|
|
||||||
<span class="wps-search__icon dashicons dashicons-search" aria-hidden="true"></span>
|
|
||||||
<input type="search"
|
|
||||||
id="wps-search-input"
|
|
||||||
class="wps-search__input"
|
|
||||||
name="s"
|
|
||||||
maxlength="100"
|
|
||||||
value="<?php echo esc_attr(self::search_query()); ?>"
|
|
||||||
placeholder="<?php esc_attr_e('Search features', 'wp-plugin-starter-template'); ?>" />
|
|
||||||
</span>
|
|
||||||
<button type="submit" class="button wps-search__button"><?php esc_html_e('Search', 'wp-plugin-starter-template'); ?></button>
|
|
||||||
</form>
|
|
||||||
<div class="wps-header__actions">
|
|
||||||
<?php if (!empty($links['website'])) : ?>
|
|
||||||
<a class="button" href="<?php echo esc_url($links['website']); ?>" target="_blank" rel="noopener noreferrer">
|
|
||||||
<?php esc_html_e('Visit website', 'wp-plugin-starter-template'); ?>
|
|
||||||
<span class="screen-reader-text"><?php esc_html_e('(opens in a new tab)', 'wp-plugin-starter-template'); ?></span>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($links['support'])) : ?>
|
|
||||||
<a class="button wps-header__support" href="<?php echo esc_url($links['support']); ?>" target="_blank" rel="noopener noreferrer">
|
|
||||||
<span class="dashicons dashicons-sos" aria-hidden="true"></span>
|
|
||||||
<?php esc_html_e('Report a problem', 'wp-plugin-starter-template'); ?>
|
|
||||||
<span class="screen-reader-text"><?php esc_html_e('(opens in a new tab)', 'wp-plugin-starter-template'); ?></span>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($links['donate'])) : ?>
|
|
||||||
<a class="button wps-header__support wps-header__donate" href="<?php echo esc_url($links['donate']); ?>" target="_blank" rel="noopener noreferrer">
|
|
||||||
<span class="dashicons dashicons-coffee" aria-hidden="true"></span>
|
|
||||||
<?php esc_html_e('Buy me a coffee', 'wp-plugin-starter-template'); ?>
|
|
||||||
<span class="screen-reader-text"><?php esc_html_e('(opens in a new tab)', 'wp-plugin-starter-template'); ?></span>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<nav class="wps-nav" aria-label="<?php esc_attr_e('WP Plugin Starter sections', 'wp-plugin-starter-template'); ?>">
|
|
||||||
<?php foreach ($groups as $group => $group_label) : ?>
|
|
||||||
<?php
|
|
||||||
$group_tabs = array_filter($tabs, function ($tab) use ($group) {
|
|
||||||
return isset($tab['group']) && $tab['group'] === $group;
|
|
||||||
});
|
|
||||||
if (!$group_tabs) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<div class="wps-nav__group" role="group" aria-label="<?php echo esc_attr($group_label); ?>">
|
|
||||||
<?php foreach ($group_tabs as $slug => $tab) : ?>
|
|
||||||
<a href="<?php echo esc_url(self::tab_url($slug)); ?>"
|
|
||||||
class="wps-nav__tab<?php echo $slug === $active ? ' is-active' : ''; ?>"
|
|
||||||
<?php echo $slug === $active ? 'aria-current="page"' : ''; ?>>
|
|
||||||
<?php echo esc_html($tab['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<?php // Core moves admin notices after this marker instead of into the header. ?>
|
|
||||||
<hr class="wp-header-end" />
|
|
||||||
|
|
||||||
<?php settings_errors(); ?>
|
|
||||||
|
|
||||||
<?php // Not <main>: core's #wpbody already carries role="main". ?>
|
|
||||||
<div class="wps-main wps-tab-<?php echo esc_attr($active); ?>" id="wps-tab-<?php echo esc_attr($active); ?>">
|
|
||||||
<?php
|
|
||||||
if (self::SEARCH === $active) {
|
|
||||||
WPStarter_Settings_Manager::render_search(self::search_query(), $tabs);
|
|
||||||
} elseif (isset($tabs[$active]['render']) && is_callable($tabs[$active]['render'])) {
|
|
||||||
call_user_func($tabs[$active]['render']);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* WP Plugin Starter Read Me tab.
|
|
||||||
*
|
|
||||||
* Renders README.md with a small, escaping Markdown subset
|
|
||||||
* (headings with GitHub-style IDs, lists, tables, bold, italic, inline code,
|
|
||||||
* http(s) links, links to headings and images from the plugin's folder).
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
* @since 0.2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WPStarter_Readme_Manager {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the tab.
|
|
||||||
*/
|
|
||||||
public static function display_tab_content() {
|
|
||||||
$readme = wpstarter_get_readme_content();
|
|
||||||
?>
|
|
||||||
<article class="wps-card wps-readme">
|
|
||||||
<?php echo self::parse_markdown($readme['content']); // phpcs:ignore WordPress.Security.EscapeOutput -- built from escaped text. ?>
|
|
||||||
</article>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert Markdown to HTML. Input is escaped first; only the generated
|
|
||||||
* tags and validated http(s) links are emitted.
|
|
||||||
*
|
|
||||||
* @param string $markdown Markdown.
|
|
||||||
* @return string HTML.
|
|
||||||
*/
|
|
||||||
public static function parse_markdown($markdown) {
|
|
||||||
$markdown = str_replace('{WPSTARTER_VERSION}', WPSTARTER_VERSION, $markdown);
|
|
||||||
$lines = preg_split('/\r\n|\r|\n/', $markdown);
|
|
||||||
$html = '';
|
|
||||||
$list = '';
|
|
||||||
$table = ''; // '', 'head' (header row written) or 'body'.
|
|
||||||
$ids = array();
|
|
||||||
|
|
||||||
$close_list = function () use (&$html, &$list, &$table) {
|
|
||||||
if ($list) {
|
|
||||||
$html .= '</' . $list . '>';
|
|
||||||
$list = '';
|
|
||||||
}
|
|
||||||
if ($table) {
|
|
||||||
$html .= ('head' === $table ? '</thead>' : '</tbody>') . '</table></div>';
|
|
||||||
$table = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
$trim = trim($line);
|
|
||||||
|
|
||||||
if ('' === $trim || '#' === $trim) {
|
|
||||||
$close_list();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// An image on a line of its own, from the plugin's own folder.
|
|
||||||
if (preg_match('/^!\[([^\]]*)\]\(([^)\s]+)\)$/', $trim, $m)) {
|
|
||||||
$close_list();
|
|
||||||
$html .= self::image($m[2], $m[1]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^(#{1,4})\s+(.+)$/', $trim, $m)) {
|
|
||||||
$close_list();
|
|
||||||
$level = min(4, strlen($m[1]) + 1); // h1 is reserved for the page title.
|
|
||||||
$html .= sprintf('<h%1$d id="%2$s">%3$s</h%1$d>', $level, esc_attr(self::anchor($m[2], $ids)), self::inline($m[2]));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tables: a header row, a |---| separator, then body rows.
|
|
||||||
if (strlen($trim) > 1 && '|' === $trim[0] && '|' === substr($trim, -1)) {
|
|
||||||
if ('head' === $table && preg_match('/^\|[\s:|-]+\|$/', $trim)) {
|
|
||||||
$html .= '</thead><tbody>';
|
|
||||||
$table = 'body';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!$table) {
|
|
||||||
$close_list();
|
|
||||||
$html .= '<div class="wps-readme-table"><table><thead>';
|
|
||||||
$table = 'head';
|
|
||||||
}
|
|
||||||
$tag = 'head' === $table ? 'th' : 'td';
|
|
||||||
$html .= '<tr>';
|
|
||||||
foreach (explode('|', substr($trim, 1, -1)) as $cell) {
|
|
||||||
$html .= '<' . $tag . '>' . self::inline(trim($cell)) . '</' . $tag . '>';
|
|
||||||
}
|
|
||||||
$html .= '</tr>';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^[-*]\s+(.+)$/', $trim, $m) || preg_match('/^\d+\.\s+(.+)$/', $trim, $n)) {
|
|
||||||
$type = isset($n[1]) ? 'ol' : 'ul';
|
|
||||||
$text = isset($n[1]) ? $n[1] : $m[1];
|
|
||||||
if ($list !== $type) {
|
|
||||||
$close_list();
|
|
||||||
$html .= '<' . $type . '>';
|
|
||||||
$list = $type;
|
|
||||||
}
|
|
||||||
$html .= '<li>' . self::inline($text) . '</li>';
|
|
||||||
unset($n);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$close_list();
|
|
||||||
$html .= '<p>' . self::inline($trim) . '</p>';
|
|
||||||
}
|
|
||||||
$close_list();
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Heading ID as GitHub makes it, so README links such as
|
|
||||||
* `[Credits](#credits)` work in the tab too. Repeats get -1, -2...
|
|
||||||
*
|
|
||||||
* @param string $text Heading Markdown.
|
|
||||||
* @param array $ids IDs used so far (updated).
|
|
||||||
* @return string ID.
|
|
||||||
*/
|
|
||||||
private static function anchor($text, array &$ids) {
|
|
||||||
$text = preg_replace('/\[([^\]]+)\]\([^)]*\)/', '$1', $text);
|
|
||||||
$text = str_replace(array('`', '*'), '', $text);
|
|
||||||
$id = function_exists('mb_strtolower') ? mb_strtolower($text, 'UTF-8') : strtolower($text);
|
|
||||||
$id = preg_replace('/[^\p{L}\p{N}\s_-]/u', '', $id);
|
|
||||||
$id = preg_replace('/\s/u', '-', trim($id));
|
|
||||||
$base = '' === $id ? 'section' : $id;
|
|
||||||
$id = $base;
|
|
||||||
$n = 0;
|
|
||||||
while (isset($ids[$id])) {
|
|
||||||
$id = $base . '-' . (++$n);
|
|
||||||
}
|
|
||||||
$ids[$id] = true;
|
|
||||||
|
|
||||||
return $id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An image from the plugin's folder, such as the banner at the top of
|
|
||||||
* README.md. Only relative paths to image files that exist are shown, so
|
|
||||||
* the tab never loads anything from another site.
|
|
||||||
*
|
|
||||||
* @param string $path Path relative to the plugin folder.
|
|
||||||
* @param string $alt Alternative text.
|
|
||||||
* @return string HTML, or '' for anything else.
|
|
||||||
*/
|
|
||||||
private static function image($path, $alt) {
|
|
||||||
if (!preg_match('#^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*\.(?:svg|png|jpe?g|gif|webp)$#i', $path)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
$file = WPSTARTER_DIR . $path;
|
|
||||||
if (!is_file($file)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Width and height stop the page jumping while the image loads.
|
|
||||||
$size = '';
|
|
||||||
if ('svg' === strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
|
|
||||||
$head = (string) file_get_contents($file, false, null, 0, 2048); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file.
|
|
||||||
if (preg_match('/<svg\b[^>]*?\swidth="(\d+)"[^>]*?\sheight="(\d+)"/', $head, $m)) {
|
|
||||||
$size = array((int) $m[1], (int) $m[2]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$size = wp_getimagesize($file);
|
|
||||||
}
|
|
||||||
$dims = is_array($size) && !empty($size[0]) && !empty($size[1])
|
|
||||||
? sprintf(' width="%d" height="%d"', $size[0], $size[1])
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// The file's time in the address, so browsers fetch a changed image
|
|
||||||
// (such as a new banner after an update) instead of a cached one.
|
|
||||||
$url = add_query_arg('ver', (string) filemtime($file), WPSTARTER_URL . $path);
|
|
||||||
|
|
||||||
return sprintf(
|
|
||||||
'<p class="wps-readme-image"><img src="%1$s" alt="%2$s"%3$s decoding="async" /></p>',
|
|
||||||
esc_url($url),
|
|
||||||
esc_attr($alt),
|
|
||||||
$dims
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inline Markdown on escaped text.
|
|
||||||
*
|
|
||||||
* @param string $text Raw text.
|
|
||||||
* @return string HTML.
|
|
||||||
*/
|
|
||||||
private static function inline($text) {
|
|
||||||
$text = esc_html($text);
|
|
||||||
$text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
|
|
||||||
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
|
|
||||||
$text = preg_replace('/(?<![*\w])\*(?!\s)(.+?)(?<!\s)\*(?![*\w])/', '<em>$1</em>', $text);
|
|
||||||
|
|
||||||
return preg_replace_callback('/\[([^\]]+)\]\(([^)\s]+)\)/', function ($m) {
|
|
||||||
$url = esc_url(html_entity_decode($m[2]), array('http', 'https'));
|
|
||||||
if ('#' === $m[2][0]) {
|
|
||||||
return sprintf('<a href="#%1$s">%2$s</a>', esc_attr(substr($m[2], 1)), $m[1]);
|
|
||||||
}
|
|
||||||
if ('' === $url) {
|
|
||||||
return $m[1];
|
|
||||||
}
|
|
||||||
return sprintf('<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', $url, $m[1]);
|
|
||||||
}, $text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,431 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Plugins screen: plugins WP Plugin Starter can replace.
|
|
||||||
*
|
|
||||||
* Lists installed plugins that a setting's `replaces` names:
|
|
||||||
* - active, with every setting that replaces it on: the plugin can go, with a
|
|
||||||
* Deactivate link (the settings wait until it is deactivated);
|
|
||||||
* - active, with a replacing setting off: WP Plugin Starter can do this job, with
|
|
||||||
* a link to the settings (only for people who can change them);
|
|
||||||
* - installed but inactive, with every replacing setting on: no longer
|
|
||||||
* needed, with a Delete link (single sites; on multisite a plugin may be
|
|
||||||
* active on another site).
|
|
||||||
* While an active plugin does things here that WP Plugin Starter does not (the
|
|
||||||
* `wpstarter_replaced_plugin_extras` filter), the notice names them
|
|
||||||
* instead of saying the plugin can go.
|
|
||||||
*
|
|
||||||
* Each of those plugins, and inactive ones whose replacing setting is off,
|
|
||||||
* also gets a note under its own row with the same step.
|
|
||||||
*
|
|
||||||
* Shown on the Plugins screen to people who can activate plugins. "Hide"
|
|
||||||
* hides the plugins listed at the time for that person; a plugin that needs
|
|
||||||
* a different step later shows again.
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
* @since 0.7.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WPStarter_Replaced_Plugins {
|
|
||||||
|
|
||||||
/** admin-post action, also the nonce action. */
|
|
||||||
const HIDE = 'wpstarter_hide_replaced_plugins';
|
|
||||||
|
|
||||||
/** User meta: list of "slug:step" items the person hid. */
|
|
||||||
const HIDDEN = 'wpstarter_replaced_plugins_hidden';
|
|
||||||
|
|
||||||
/** admin-post action that deactivates a replaced plugin and comes back. */
|
|
||||||
const DEACTIVATE = 'wpstarter_deactivate_replaced';
|
|
||||||
|
|
||||||
/** Query argument on the page it comes back to. */
|
|
||||||
const DONE = 'wpstarter-deactivated';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register hooks.
|
|
||||||
*/
|
|
||||||
public static function init() {
|
|
||||||
add_action('load-plugins.php', array(__CLASS__, 'load_screen'));
|
|
||||||
add_action('admin_post_' . self::HIDE, array(__CLASS__, 'hide'));
|
|
||||||
add_action('admin_post_' . self::DEACTIVATE, array(__CLASS__, 'deactivate'));
|
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only shows a message.
|
|
||||||
if (isset($_GET[self::DONE])) {
|
|
||||||
add_action('admin_notices', array(__CLASS__, 'deactivated_notice'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugins screen: add the notice and the notes on each plugin's row.
|
|
||||||
*/
|
|
||||||
public static function load_screen() {
|
|
||||||
if (!current_user_can('activate_plugins')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
add_action(is_network_admin() ? 'network_admin_notices' : 'admin_notices', array(__CLASS__, 'notice'));
|
|
||||||
add_action('after_plugin_row', array(__CLASS__, 'row_note'), 10, 1);
|
|
||||||
add_action('admin_head', array(__CLASS__, 'row_note_style'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Join a row note to its plugin's row, as core does for update notes.
|
|
||||||
*/
|
|
||||||
public static function row_note_style() {
|
|
||||||
echo '<style>.plugins tr:has(+ tr.wps-replaced-row) th, .plugins tr:has(+ tr.wps-replaced-row) td { box-shadow: none; }</style>' . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Link that deactivates a replaced plugin on this site and comes back to
|
|
||||||
* the page it was clicked on.
|
|
||||||
*
|
|
||||||
* @param string $file Plugin file.
|
|
||||||
* @return string Unescaped URL.
|
|
||||||
*/
|
|
||||||
public static function deactivate_url($file) {
|
|
||||||
return wp_nonce_url(add_query_arg(array(
|
|
||||||
'action' => self::DEACTIVATE,
|
|
||||||
'plugin' => rawurlencode($file),
|
|
||||||
'_wp_http_referer' => rawurlencode(remove_query_arg(array(self::DONE, 'deactivate', 'activate'))),
|
|
||||||
), admin_url('admin-post.php')), self::DEACTIVATE . '_' . $file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core's deactivate link on the Plugins screen, keeping the list's
|
|
||||||
* status, page and search, so it comes back to the same list.
|
|
||||||
*
|
|
||||||
* @param string $file Plugin file.
|
|
||||||
* @return string Unescaped URL.
|
|
||||||
*/
|
|
||||||
private static function plugins_deactivate_url($file) {
|
|
||||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- the list being shown.
|
|
||||||
$args = array(
|
|
||||||
'action' => 'deactivate',
|
|
||||||
'plugin' => rawurlencode($file),
|
|
||||||
'plugin_status' => isset($_GET['plugin_status']) ? sanitize_key(wp_unslash($_GET['plugin_status'])) : 'all',
|
|
||||||
'paged' => isset($_GET['paged']) ? absint($_GET['paged']) : 1,
|
|
||||||
's' => isset($_GET['s']) ? rawurlencode(sanitize_text_field(wp_unslash($_GET['s']))) : '',
|
|
||||||
);
|
|
||||||
// phpcs:enable
|
|
||||||
return wp_nonce_url(add_query_arg($args, self_admin_url('plugins.php')), 'deactivate-plugin_' . $file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* admin-post: deactivate a replaced plugin on this site, then go back.
|
|
||||||
*/
|
|
||||||
public static function deactivate() {
|
|
||||||
$file = isset($_GET['plugin']) ? sanitize_text_field(wp_unslash($_GET['plugin'])) : '';
|
|
||||||
check_admin_referer(self::DEACTIVATE . '_' . $file);
|
|
||||||
if ('' === $file || validate_file($file) || !current_user_can('deactivate_plugin', $file)) {
|
|
||||||
wp_die(esc_html__('You are not allowed to deactivate this plugin.', 'wp-plugin-starter-template'), '', array('response' => 403));
|
|
||||||
}
|
|
||||||
if (!isset(self::replaced()[dirname($file)])) {
|
|
||||||
wp_die(esc_html__('WP Plugin Starter only deactivates plugins it replaces.', 'wp-plugin-starter-template'), '', array('response' => 400));
|
|
||||||
}
|
|
||||||
if (!function_exists('is_plugin_active')) {
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
||||||
}
|
|
||||||
$back = wp_get_referer();
|
|
||||||
$back = $back ? $back : admin_url('plugins.php');
|
|
||||||
// Network-wide plugins are deactivated in the network admin.
|
|
||||||
if (is_plugin_active($file) && !is_plugin_active_for_network($file)) {
|
|
||||||
deactivate_plugins($file);
|
|
||||||
update_option('recently_activated', array($file => time()) + (array) get_option('recently_activated'), false);
|
|
||||||
$back = add_query_arg(self::DONE, 1, $back);
|
|
||||||
}
|
|
||||||
wp_safe_redirect($back);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Plugin deactivated." on the page the link came back to.
|
|
||||||
*/
|
|
||||||
public static function deactivated_notice() {
|
|
||||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__('Plugin deactivated.', 'wp-plugin-starter-template') . '</p></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A note under a replaced plugin's row on the Plugins screen: which
|
|
||||||
* setting does its job and what to do next.
|
|
||||||
*
|
|
||||||
* @param string $file Plugin file.
|
|
||||||
*/
|
|
||||||
public static function row_note($file) {
|
|
||||||
global $wp_list_table;
|
|
||||||
$states = self::states();
|
|
||||||
if (!isset($states[$file])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$item = $states[$file];
|
|
||||||
$labels = esc_html(implode(', ', array_map(function ($label) {
|
|
||||||
return '‘' . $label . '’';
|
|
||||||
}, $item['settings'])));
|
|
||||||
$setting = '';
|
|
||||||
if (WPStarter_Settings::can_change()) {
|
|
||||||
$setting = sprintf(' <a href="%1$s">%2$s</a>', esc_url(WPStarter_Admin_Manager::tab_url(WPStarter_Admin_Manager::SEARCH, array('s' => $item['name']))), esc_html__('Show the setting', 'wp-plugin-starter-template'));
|
|
||||||
}
|
|
||||||
$extras = esc_html(implode(', ', $item['extras']));
|
|
||||||
|
|
||||||
switch ($item['step']) {
|
|
||||||
case 'deactivate':
|
|
||||||
/* translators: %s: WP Plugin Starter setting names */
|
|
||||||
$text = sprintf(esc_html__('WP Plugin Starter makes this plugin redundant: %s is on and takes over once you deactivate this plugin. Then you can delete it.', 'wp-plugin-starter-template'), $labels);
|
|
||||||
break;
|
|
||||||
case 'partial':
|
|
||||||
/* translators: 1: WP Plugin Starter setting names, 2: the plugin's settings WP Plugin Starter does not have */
|
|
||||||
$text = sprintf(esc_html__('WP Plugin Starter can do this plugin\'s job: %1$s is on and takes over once you deactivate it, except for these, which WP Plugin Starter does not do: %2$s.', 'wp-plugin-starter-template'), $labels, $extras);
|
|
||||||
break;
|
|
||||||
case 'switch_on':
|
|
||||||
/* translators: %s: WP Plugin Starter setting names */
|
|
||||||
$text = sprintf(esc_html__('WP Plugin Starter makes this plugin redundant: turn on %s in WP Plugin Starter, then deactivate and delete this plugin.', 'wp-plugin-starter-template'), $labels) . $setting;
|
|
||||||
break;
|
|
||||||
case 'switch_on_partial':
|
|
||||||
/* translators: 1: WP Plugin Starter setting names, 2: the plugin's settings WP Plugin Starter does not have */
|
|
||||||
$text = sprintf(esc_html__('WP Plugin Starter can do part of this plugin\'s job with %1$s. It does not do these, which this plugin has on: %2$s.', 'wp-plugin-starter-template'), $labels, $extras) . $setting;
|
|
||||||
break;
|
|
||||||
case 'switch_on_inactive':
|
|
||||||
/* translators: %s: WP Plugin Starter setting names */
|
|
||||||
$text = sprintf(esc_html__('WP Plugin Starter makes this plugin redundant: turn on %s in WP Plugin Starter, then you can delete this plugin.', 'wp-plugin-starter-template'), $labels) . $setting;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
/* translators: %s: WP Plugin Starter setting names */
|
|
||||||
$text = sprintf(esc_html__('No longer needed: %s in WP Plugin Starter does this plugin\'s job. You can delete it.', 'wp-plugin-starter-template'), $labels);
|
|
||||||
}
|
|
||||||
$columns = ($wp_list_table instanceof WP_List_Table) ? $wp_list_table->get_column_count() : 4;
|
|
||||||
$active = in_array($item['step'], array('deactivate', 'partial', 'switch_on', 'switch_on_partial'), true) ? ' active' : ' inactive';
|
|
||||||
printf(
|
|
||||||
'<tr class="plugin-update-tr wps-replaced-row%1$s"><td colspan="%2$d" class="plugin-update colspanchange"><div class="notice inline notice-info notice-alt"><p>%3$s</p></div></td></tr>',
|
|
||||||
esc_attr($active),
|
|
||||||
(int) $columns,
|
|
||||||
$text // phpcs:ignore WordPress.Security.EscapeOutput -- built from escaped parts above.
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaced plugins, keyed by folder name, with the settings that replace
|
|
||||||
* them.
|
|
||||||
*
|
|
||||||
* @return array<string,array{name:string,settings:array<string,string>}> slug => name and setting key => label.
|
|
||||||
*/
|
|
||||||
private static function replaced() {
|
|
||||||
$replaced = array();
|
|
||||||
foreach (WPStarter_Settings::schema() as $key => $field) {
|
|
||||||
if (empty($field['replaces']) || !is_array($field['replaces']) || !empty($field['parent'])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach ($field['replaces'] as $slug => $name) {
|
|
||||||
if (!isset($replaced[$slug])) {
|
|
||||||
$replaced[$slug] = array('name' => (string) $name, 'settings' => array());
|
|
||||||
}
|
|
||||||
$replaced[$slug]['settings'][$key] = isset($field['label']) ? (string) $field['label'] : (string) $key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $replaced;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What to show in the notice: one item per installed replaced plugin that
|
|
||||||
* needs a step, leaving out what the person hid.
|
|
||||||
*
|
|
||||||
* @return array<int,array{slug:string,file:string,name:string,step:string,settings:array<string,string>}>
|
|
||||||
*/
|
|
||||||
private static function items() {
|
|
||||||
$items = array_filter(self::states(), function ($item) {
|
|
||||||
// Inactive with the setting off: only a note on its row.
|
|
||||||
return 'switch_on_inactive' !== $item['step']
|
|
||||||
&& ('switch_on' !== $item['step'] || WPStarter_Settings::can_change());
|
|
||||||
});
|
|
||||||
$hidden = (array) get_user_meta(get_current_user_id(), self::HIDDEN, true);
|
|
||||||
return array_values(array_filter($items, function ($item) use ($hidden) {
|
|
||||||
return !in_array($item['slug'] . ':' . $item['step'], $hidden, true);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every installed replaced plugin that needs a step, keyed by plugin file.
|
|
||||||
*
|
|
||||||
* Steps: deactivate, partial (active, setting on, it does more),
|
|
||||||
* switch_on, switch_on_partial, delete (inactive, setting on) and
|
|
||||||
* switch_on_inactive (inactive, setting off; single sites only).
|
|
||||||
*
|
|
||||||
* @return array<string,array{slug:string,file:string,name:string,step:string,settings:array<string,string>,extras:string[]}>
|
|
||||||
*/
|
|
||||||
private static function states() {
|
|
||||||
static $states = null;
|
|
||||||
if (null !== $states) {
|
|
||||||
return $states;
|
|
||||||
}
|
|
||||||
if (!function_exists('get_plugins')) {
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
||||||
}
|
|
||||||
$installed = array();
|
|
||||||
foreach (array_keys(get_plugins()) as $file) {
|
|
||||||
$slug = dirname($file);
|
|
||||||
if ('.' !== $slug) {
|
|
||||||
$installed[$slug] = $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$network = is_multisite() && is_network_admin();
|
|
||||||
// Active where this screen can deactivate it: network-wide in the
|
|
||||||
// network admin, on this site otherwise. Stored lists, so plugins
|
|
||||||
// skipped on this screen still count.
|
|
||||||
$here = $network
|
|
||||||
? array_keys((array) get_site_option('active_sitewide_plugins', array()))
|
|
||||||
: WPStarter_Feature::stored_active_plugins();
|
|
||||||
$all = WPStarter_Feature::active_plugins();
|
|
||||||
|
|
||||||
$items = array();
|
|
||||||
foreach (self::replaced() as $slug => $plugin) {
|
|
||||||
if (!isset($installed[$slug])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$file = $installed[$slug];
|
|
||||||
$all_on = true;
|
|
||||||
foreach (array_keys($plugin['settings']) as $key) {
|
|
||||||
$all_on = $all_on && (bool) WPStarter_Settings::get($key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($file, $here, true)) {
|
|
||||||
$step = $all_on ? 'deactivate' : 'switch_on';
|
|
||||||
} elseif (!isset($all[$slug]) && !is_multisite()) {
|
|
||||||
$step = $all_on ? 'delete' : 'switch_on_inactive';
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$extras = array();
|
|
||||||
if ('deactivate' === $step || 'switch_on' === $step) {
|
|
||||||
/**
|
|
||||||
* What a replaced plugin does on this site that WP Plugin Starter,
|
|
||||||
* as set up, does not. When there is any, the notice lists it
|
|
||||||
* instead of saying the plugin can go.
|
|
||||||
*
|
|
||||||
* @param string[] $extras Plain names.
|
|
||||||
* @param string $slug Plugin folder.
|
|
||||||
*/
|
|
||||||
$extras = array_values(array_filter(array_map('strval', (array) apply_filters('wpstarter_replaced_plugin_extras', array(), (string) $slug))));
|
|
||||||
}
|
|
||||||
if ($extras) {
|
|
||||||
$step = 'deactivate' === $step ? 'partial' : 'switch_on_partial';
|
|
||||||
}
|
|
||||||
$items[$file] = array(
|
|
||||||
'slug' => (string) $slug,
|
|
||||||
'file' => $file,
|
|
||||||
'name' => $plugin['name'],
|
|
||||||
'step' => $step,
|
|
||||||
'settings' => $plugin['settings'],
|
|
||||||
'extras' => $extras,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$states = $items;
|
|
||||||
return $states;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The notice.
|
|
||||||
*/
|
|
||||||
public static function notice() {
|
|
||||||
$items = self::items();
|
|
||||||
if (!$items) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$rows = array();
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$rows[] = self::row($item);
|
|
||||||
}
|
|
||||||
$keys = array_map(function ($item) {
|
|
||||||
return $item['slug'] . ':' . $item['step'];
|
|
||||||
}, $items);
|
|
||||||
$hide = wp_nonce_url(add_query_arg(array('action' => self::HIDE, 'items' => implode(',', $keys)), admin_url('admin-post.php')), self::HIDE);
|
|
||||||
?>
|
|
||||||
<div class="notice notice-info wps-replaced-plugins">
|
|
||||||
<p><strong><?php esc_html_e('WP Plugin Starter can do the job of these plugins:', 'wp-plugin-starter-template'); ?></strong></p>
|
|
||||||
<ul class="ul-disc">
|
|
||||||
<?php foreach ($rows as $row) : ?>
|
|
||||||
<li><?php echo $row; // phpcs:ignore WordPress.Security.EscapeOutput -- built from escaped parts in row(). ?></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<p><a href="<?php echo esc_url($hide); ?>"><?php esc_html_e('Hide', 'wp-plugin-starter-template'); ?></a></p>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One line of the notice.
|
|
||||||
*
|
|
||||||
* @param array $item Item from items().
|
|
||||||
* @return string HTML.
|
|
||||||
*/
|
|
||||||
private static function row(array $item) {
|
|
||||||
$labels = implode(', ', array_map(function ($label) {
|
|
||||||
return '‘' . $label . '’';
|
|
||||||
}, $item['settings']));
|
|
||||||
$name = '<strong>' . esc_html($item['name']) . '</strong>';
|
|
||||||
$base = self_admin_url('plugins.php');
|
|
||||||
|
|
||||||
if ('deactivate' === $item['step']) {
|
|
||||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names */
|
|
||||||
$text = sprintf(esc_html__('%1$s: %2$s is on and takes over once you deactivate it. Then you can delete it.', 'wp-plugin-starter-template'), $name, esc_html($labels));
|
|
||||||
if (current_user_can('deactivate_plugin', $item['file'])) {
|
|
||||||
$url = self::plugins_deactivate_url($item['file']);
|
|
||||||
/* translators: %s: plugin name */
|
|
||||||
$text .= sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html(sprintf(__('Deactivate %s', 'wp-plugin-starter-template'), $item['name'])));
|
|
||||||
}
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('partial' === $item['step'] || 'switch_on_partial' === $item['step']) {
|
|
||||||
$extras = esc_html(implode(', ', $item['extras']));
|
|
||||||
if ('partial' === $item['step']) {
|
|
||||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names, 3: the plugin's settings WP Plugin Starter does not have */
|
|
||||||
$text = sprintf(esc_html__('%1$s: %2$s is on and takes over once you deactivate it, except for these, which WP Plugin Starter does not do: %3$s. Deactivate it only if you no longer need them.', 'wp-plugin-starter-template'), $name, esc_html($labels), $extras);
|
|
||||||
if (current_user_can('deactivate_plugin', $item['file'])) {
|
|
||||||
$url = self::plugins_deactivate_url($item['file']);
|
|
||||||
/* translators: %s: plugin name */
|
|
||||||
$text .= sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html(sprintf(__('Deactivate %s', 'wp-plugin-starter-template'), $item['name'])));
|
|
||||||
}
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names, 3: the plugin's settings WP Plugin Starter does not have */
|
|
||||||
$text = sprintf(esc_html__('%1$s: %2$s can do part of its job. WP Plugin Starter does not do these, which it has on: %3$s.', 'wp-plugin-starter-template'), $name, esc_html($labels), $extras);
|
|
||||||
$url = WPStarter_Admin_Manager::tab_url(WPStarter_Admin_Manager::SEARCH, array('s' => $item['name']));
|
|
||||||
return $text . sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html__('Show the setting', 'wp-plugin-starter-template'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('delete' === $item['step']) {
|
|
||||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names */
|
|
||||||
$text = sprintf(esc_html__('%1$s is inactive and no longer needed: %2$s does its job.', 'wp-plugin-starter-template'), $name, esc_html($labels));
|
|
||||||
if (current_user_can('delete_plugins')) {
|
|
||||||
$url = wp_nonce_url(add_query_arg(array('action' => 'delete-selected', 'checked[]' => $item['file'], 'plugin_status' => 'all'), $base), 'bulk-plugins');
|
|
||||||
/* translators: %s: plugin name */
|
|
||||||
$text .= sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html(sprintf(__('Delete %s', 'wp-plugin-starter-template'), $item['name'])));
|
|
||||||
}
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names */
|
|
||||||
$text = sprintf(esc_html__('%1$s: switch on %2$s, then deactivate and delete it.', 'wp-plugin-starter-template'), $name, esc_html($labels));
|
|
||||||
$url = WPStarter_Admin_Manager::tab_url(WPStarter_Admin_Manager::SEARCH, array('s' => $item['name']));
|
|
||||||
return $text . sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html__('Show the setting', 'wp-plugin-starter-template'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* admin-post: hide the listed items for this person.
|
|
||||||
*/
|
|
||||||
public static function hide() {
|
|
||||||
check_admin_referer(self::HIDE);
|
|
||||||
if (!current_user_can('activate_plugins')) {
|
|
||||||
wp_die(esc_html__('You are not allowed to manage plugins on this site.', 'wp-plugin-starter-template'), '', array('response' => 403));
|
|
||||||
}
|
|
||||||
$items = isset($_GET['items']) ? explode(',', sanitize_text_field(wp_unslash($_GET['items']))) : array();
|
|
||||||
$items = array_filter($items, function ($item) {
|
|
||||||
return (bool) preg_match('/^[A-Za-z0-9._-]+:(deactivate|switch_on|delete|partial|switch_on_partial)$/', $item);
|
|
||||||
});
|
|
||||||
$hidden = (array) get_user_meta(get_current_user_id(), self::HIDDEN, true);
|
|
||||||
update_user_meta(get_current_user_id(), self::HIDDEN, array_values(array_unique(array_filter(array_merge($hidden, $items)))));
|
|
||||||
|
|
||||||
$back = wp_get_referer();
|
|
||||||
wp_safe_redirect($back ? $back : self_admin_url('plugins.php'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* WP Plugin Starter settings tabs.
|
|
||||||
*
|
|
||||||
* Renders setting cards from WPStarter_Settings::schema(). Each top-level
|
|
||||||
* setting is a card with a switch; child settings appear in an expandable
|
|
||||||
* panel. Controls save instantly via AJAX (see admin/js/wpstarter-admin.js).
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
* @since 0.2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WPStarter_Settings_Manager {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render matching settings from every tab, grouped by tab.
|
|
||||||
*
|
|
||||||
* Cards are the same as on their own tab, so they can be switched on
|
|
||||||
* and changed here.
|
|
||||||
*
|
|
||||||
* @param string $query Search text.
|
|
||||||
* @param array $tabs Registered tabs (see WPStarter_Admin_Manager::get_tabs()).
|
|
||||||
*/
|
|
||||||
public static function render_search($query, array $tabs) {
|
|
||||||
$groups = array();
|
|
||||||
foreach (WPStarter_Settings::search($query) as $key => $field) {
|
|
||||||
if (isset($tabs[$field['tab']])) {
|
|
||||||
$groups[$field['tab']][$key] = $field;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$count = array_sum(array_map('count', $groups));
|
|
||||||
?>
|
|
||||||
<div class="wps-section">
|
|
||||||
<div class="wps-section__intro">
|
|
||||||
<h2 class="wps-section__title">
|
|
||||||
<?php
|
|
||||||
/* translators: %s: search text */
|
|
||||||
echo esc_html(sprintf(__('Results for “%s”', 'wp-plugin-starter-template'), $query));
|
|
||||||
?>
|
|
||||||
</h2>
|
|
||||||
<p class="wps-section__desc" role="status">
|
|
||||||
<?php
|
|
||||||
echo esc_html($count
|
|
||||||
/* translators: %d: number of matching features */
|
|
||||||
? sprintf(_n('%d feature found.', '%d features found.', $count, 'wp-plugin-starter-template'), $count)
|
|
||||||
: __('No features match. Try another word.', 'wp-plugin-starter-template'));
|
|
||||||
?>
|
|
||||||
</p>
|
|
||||||
<?php if ($count) : ?>
|
|
||||||
<p class="wps-section__hint"><?php esc_html_e('Changes are saved automatically.', 'wp-plugin-starter-template'); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php foreach ($groups as $tab => $fields) : ?>
|
|
||||||
<h3 class="wps-search__group">
|
|
||||||
<a href="<?php echo esc_url(WPStarter_Admin_Manager::tab_url($tab)); ?>"><?php echo esc_html($tabs[$tab]['label']); ?></a>
|
|
||||||
</h3>
|
|
||||||
<div class="wps-cards">
|
|
||||||
<?php
|
|
||||||
foreach ($fields as $key => $field) {
|
|
||||||
self::render_card($key, $field);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render every top-level setting for a tab.
|
|
||||||
*
|
|
||||||
* @param string $tab Tab slug.
|
|
||||||
* @param string $title Section heading.
|
|
||||||
* @param string $description Section intro.
|
|
||||||
*/
|
|
||||||
public static function render_tab($tab, $title, $description) {
|
|
||||||
$fields = WPStarter_Settings::fields_for_tab($tab);
|
|
||||||
?>
|
|
||||||
<div class="wps-section">
|
|
||||||
<div class="wps-section__intro">
|
|
||||||
<h2 class="wps-section__title"><?php echo esc_html($title); ?></h2>
|
|
||||||
<?php if ($description) : ?>
|
|
||||||
<p class="wps-section__desc"><?php echo esc_html($description); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($fields) : ?>
|
|
||||||
<p class="wps-section__hint"><?php esc_html_e('Changes are saved automatically.', 'wp-plugin-starter-template'); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php if ($fields) : ?>
|
|
||||||
<div class="wps-cards">
|
|
||||||
<?php
|
|
||||||
foreach ($fields as $key => $field) {
|
|
||||||
self::render_card($key, $field);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<div class="wps-card wps-empty">
|
|
||||||
<p><?php esc_html_e('No settings yet.', 'wp-plugin-starter-template'); ?></p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Fires after a settings tab's cards, for sections that are not
|
|
||||||
* settings (such as example data).
|
|
||||||
*
|
|
||||||
* @param string $tab Tab slug.
|
|
||||||
*/
|
|
||||||
do_action('wpstarter_settings_tab_after', $tab);
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a setting card.
|
|
||||||
*
|
|
||||||
* @param string $key Setting key.
|
|
||||||
* @param array $field Schema entry.
|
|
||||||
*/
|
|
||||||
public static function render_card($key, array $field) {
|
|
||||||
// Hidden options are wiring set by starter data or code, not choices.
|
|
||||||
$children = array_filter(WPStarter_Settings::children_of($key), function ($child) {
|
|
||||||
return empty($child['hidden']);
|
|
||||||
});
|
|
||||||
$value = WPStarter_Settings::get($key);
|
|
||||||
$id = 'wps-' . $key;
|
|
||||||
$panel_id = $id . '-panel';
|
|
||||||
$is_bool = 'bool' === $field['type'];
|
|
||||||
?>
|
|
||||||
<section class="wps-card wps-setting<?php echo ($is_bool && $value) ? ' is-on' : ''; ?><?php echo $children ? ' has-panel' : ''; ?>" data-setting-card="<?php echo esc_attr($key); ?>">
|
|
||||||
<?php // Clicking the header (outside the switch) opens the options; only the switch changes the value. ?>
|
|
||||||
<div class="wps-setting__header"<?php echo $children ? ' data-wps-panel-toggle' : ''; ?>>
|
|
||||||
<?php if ($is_bool) : ?>
|
|
||||||
<span class="wps-switch">
|
|
||||||
<input type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
class="wps-switch__input"
|
|
||||||
id="<?php echo esc_attr($id); ?>"
|
|
||||||
data-wps-setting="<?php echo esc_attr($key); ?>"
|
|
||||||
aria-labelledby="<?php echo esc_attr($id); ?>-title"
|
|
||||||
aria-describedby="<?php echo esc_attr($id); ?>-desc"
|
|
||||||
<?php checked((bool) $value); ?> />
|
|
||||||
<span class="wps-switch__track" aria-hidden="true"></span>
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="wps-setting__text">
|
|
||||||
<div class="wps-setting__title-row">
|
|
||||||
<span class="wps-setting__title" id="<?php echo esc_attr($id); ?>-title"><?php echo esc_html($field['label']); ?></span>
|
|
||||||
<span class="wps-status" data-wps-status="<?php echo esc_attr($key); ?>" aria-hidden="true"></span>
|
|
||||||
</div>
|
|
||||||
<?php if (!empty($field['description'])) : ?>
|
|
||||||
<p class="wps-setting__desc" id="<?php echo esc_attr($id); ?>-desc"><?php echo esc_html($field['description']); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php self::render_replaces($field); ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($children) : ?>
|
|
||||||
<button type="button"
|
|
||||||
class="wps-setting__expand button-link"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-controls="<?php echo esc_attr($panel_id); ?>">
|
|
||||||
<span class="wps-setting__expand-label"><?php esc_html_e('Options', 'wp-plugin-starter-template'); ?></span>
|
|
||||||
<span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span>
|
|
||||||
<span class="screen-reader-text"><?php echo esc_html(sprintf(/* translators: %s: setting name */ __('for %s', 'wp-plugin-starter-template'), $field['label'])); ?></span>
|
|
||||||
</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($children) : ?>
|
|
||||||
<div class="wps-setting__panel" id="<?php echo esc_attr($panel_id); ?>" hidden>
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Fires at the top of a setting's options panel, for status
|
|
||||||
* such as progress (wrap output in .wps-panel-note).
|
|
||||||
*
|
|
||||||
* @param string $key Setting key.
|
|
||||||
* @param array $field Schema entry.
|
|
||||||
*/
|
|
||||||
do_action('wpstarter_setting_panel', $key, $field);
|
|
||||||
foreach ($children as $child_key => $child) {
|
|
||||||
self::render_field($child_key, $child);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note which plugins a setting replaces. While one is active the setting
|
|
||||||
* waits (WPStarter_Feature::replaced_active()); say so, with a
|
|
||||||
* deactivate link where the user can deactivate it here.
|
|
||||||
*
|
|
||||||
* @param array $field Schema entry.
|
|
||||||
*/
|
|
||||||
private static function render_replaces(array $field) {
|
|
||||||
if (empty($field['replaces']) || !is_array($field['replaces'])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$active = WPStarter_Feature::active_plugins();
|
|
||||||
$site = self::active_plugins_by_slug();
|
|
||||||
$items = array();
|
|
||||||
$links = array();
|
|
||||||
$busy = array();
|
|
||||||
foreach ($field['replaces'] as $slug => $name) {
|
|
||||||
$items[] = esc_html($name);
|
|
||||||
if (!isset($active[$slug])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$busy[] = (string) $name;
|
|
||||||
if (isset($site[$slug]) && current_user_can('deactivate_plugin', $site[$slug])) {
|
|
||||||
// Comes back to this tab, where the setting then takes over.
|
|
||||||
$url = WPStarter_Replaced_Plugins::deactivate_url($site[$slug]);
|
|
||||||
/* translators: %s: plugin name */
|
|
||||||
$links[] = sprintf('<a href="%1$s">%2$s</a>', esc_url($url), esc_html(sprintf(__('Deactivate %s', 'wp-plugin-starter-template'), $name)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
printf(
|
|
||||||
'<p class="wps-replaces">%1$s %2$s</p>',
|
|
||||||
esc_html__('Replaces:', 'wp-plugin-starter-template'),
|
|
||||||
implode(', ', $items) // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
);
|
|
||||||
if ($busy) {
|
|
||||||
printf(
|
|
||||||
'<p class="wps-replaces wps-replaces__active">%1$s %2$s</p>',
|
|
||||||
/* translators: %s: plugin names */
|
|
||||||
esc_html(sprintf(__('%s is still active, so it does this job and this setting waits until it is deactivated.', 'wp-plugin-starter-template'), implode(', ', $busy))),
|
|
||||||
implode(' · ', $links) // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugins active on this site (not network-wide) keyed by directory slug.
|
|
||||||
*
|
|
||||||
* @return array<string,string> slug => plugin file
|
|
||||||
*/
|
|
||||||
private static function active_plugins_by_slug() {
|
|
||||||
static $active = null;
|
|
||||||
if (null === $active) {
|
|
||||||
$active = array();
|
|
||||||
foreach (WPStarter_Feature::stored_active_plugins() as $file) {
|
|
||||||
$slug = dirname((string) $file);
|
|
||||||
if ('.' !== $slug) {
|
|
||||||
$active[$slug] = (string) $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $active;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a child field row.
|
|
||||||
*
|
|
||||||
* @param string $key Setting key.
|
|
||||||
* @param array $field Schema entry.
|
|
||||||
*/
|
|
||||||
public static function render_field($key, array $field) {
|
|
||||||
$value = WPStarter_Settings::get($key);
|
|
||||||
$id = 'wps-' . $key;
|
|
||||||
$desc_id = $id . '-desc';
|
|
||||||
$attrs = sprintf('id="%1$s" data-wps-setting="%2$s" aria-describedby="%3$s"', esc_attr($id), esc_attr($key), esc_attr($desc_id));
|
|
||||||
$is_multi = 'multi' === $field['type'];
|
|
||||||
?>
|
|
||||||
<div class="wps-field">
|
|
||||||
<?php if ($is_multi) : ?>
|
|
||||||
<span class="wps-field__label" id="<?php echo esc_attr($id); ?>-label"><?php echo esc_html($field['label']); ?></span>
|
|
||||||
<?php else : ?>
|
|
||||||
<label class="wps-field__label" for="<?php echo esc_attr($id); ?>"><?php echo esc_html($field['label']); ?></label>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="wps-field__control">
|
|
||||||
<?php
|
|
||||||
switch ($field['type']) {
|
|
||||||
case 'select':
|
|
||||||
printf('<select %s>', $attrs); // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
$choices = WPStarter_Settings::options_for($field);
|
|
||||||
if (!empty($field['open']) && '' !== (string) $value && !array_key_exists((string) $value, $choices)) {
|
|
||||||
// A saved choice whose plugin is not loaded here stays shown.
|
|
||||||
$choices[(string) $value] = (string) $value;
|
|
||||||
}
|
|
||||||
foreach ($choices as $option_value => $option_label) {
|
|
||||||
printf(
|
|
||||||
'<option value="%1$s"%2$s>%3$s</option>',
|
|
||||||
esc_attr((string) $option_value),
|
|
||||||
selected((string) $value, (string) $option_value, false),
|
|
||||||
esc_html($option_label)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
echo '</select>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'multi':
|
|
||||||
$chosen = array_map('strval', (array) $value);
|
|
||||||
$choices = WPStarter_Settings::options_for($field);
|
|
||||||
// With some plugins skipped on this request, a saved
|
|
||||||
// choice may belong to one of them; keep it too.
|
|
||||||
if (!empty($field['open']) || WPStarter_Feature::plugins_skipped()) {
|
|
||||||
// Saved items that are not registered right now stay visible so they can be unticked.
|
|
||||||
foreach (array_diff($chosen, array_map('strval', array_keys($choices))) as $missing) {
|
|
||||||
$choices[$missing] = $missing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$long = count($choices) > 12;
|
|
||||||
if ($long) {
|
|
||||||
// Long lists (such as every active plugin) scroll, with quick choices.
|
|
||||||
printf(
|
|
||||||
'<span class="wps-checkboxes__all"><button type="button" class="button-link" data-wps-check-all="%1$s">%2$s</button> · <button type="button" class="button-link" data-wps-check-none="%1$s">%3$s</button></span>',
|
|
||||||
esc_attr($id),
|
|
||||||
esc_html__('Select all', 'wp-plugin-starter-template'),
|
|
||||||
esc_html__('Clear', 'wp-plugin-starter-template')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// The group carries data-wps-setting; the JS saves every checked value.
|
|
||||||
printf(
|
|
||||||
'<fieldset class="wps-checkboxes%3$s" data-wps-multi %1$s aria-labelledby="%2$s">',
|
|
||||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
esc_attr($id . '-label'),
|
|
||||||
$long ? ' is-long' : ''
|
|
||||||
);
|
|
||||||
foreach ($choices as $option_value => $option_label) {
|
|
||||||
printf(
|
|
||||||
'<label class="wps-checkbox"><input type="checkbox" value="%1$s"%2$s /> %3$s</label>',
|
|
||||||
esc_attr((string) $option_value),
|
|
||||||
checked(in_array((string) $option_value, $chosen, true), true, false),
|
|
||||||
esc_html($option_label)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
echo '</fieldset>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'url':
|
|
||||||
printf(
|
|
||||||
'<input type="text" class="regular-text code" inputmode="url" %1$s value="%2$s" placeholder="%3$s" />',
|
|
||||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
esc_attr((string) $value),
|
|
||||||
esc_attr(isset($field['placeholder']) ? $field['placeholder'] : '')
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'times':
|
|
||||||
printf(
|
|
||||||
'<input type="text" class="regular-text" %1$s value="%2$s" placeholder="%3$s" autocomplete="off" />',
|
|
||||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
esc_attr((string) $value),
|
|
||||||
esc_attr(isset($field['placeholder']) ? $field['placeholder'] : '')
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'int':
|
|
||||||
printf(
|
|
||||||
'<input type="number" class="small-text" %1$s value="%2$s" min="%3$s" max="%4$s" step="1" inputmode="numeric" />',
|
|
||||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
esc_attr((string) $value),
|
|
||||||
esc_attr(isset($field['min']) ? (string) $field['min'] : ''),
|
|
||||||
esc_attr(isset($field['max']) ? (string) $field['max'] : '')
|
|
||||||
);
|
|
||||||
if (!empty($field['unit'])) {
|
|
||||||
echo ' <span class="wps-field__unit">' . esc_html($field['unit']) . '</span>';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'lines':
|
|
||||||
case 'domains':
|
|
||||||
printf(
|
|
||||||
'<textarea class="large-text code" rows="%1$d" %2$s placeholder="%3$s">%4$s</textarea>',
|
|
||||||
isset($field['rows']) ? (int) $field['rows'] : 3,
|
|
||||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
esc_attr(isset($field['placeholder']) ? $field['placeholder'] : ''),
|
|
||||||
esc_textarea((string) $value)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'media':
|
|
||||||
// The hidden input carries data-wps-setting; the JS sets it
|
|
||||||
// from the media dialog and saves it.
|
|
||||||
$image_id = (int) $value;
|
|
||||||
$preview = $image_id ? wp_get_attachment_image_url($image_id, 'thumbnail') : '';
|
|
||||||
echo '<div class="wps-media" data-wps-media>';
|
|
||||||
printf(
|
|
||||||
'<input type="hidden" data-wps-setting="%1$s" value="%2$s" />',
|
|
||||||
esc_attr($key),
|
|
||||||
esc_attr((string) $image_id)
|
|
||||||
);
|
|
||||||
if ($preview) {
|
|
||||||
printf('<img class="wps-media__preview" src="%s" alt="" />', esc_url($preview));
|
|
||||||
} else {
|
|
||||||
echo '<img class="wps-media__preview" alt="" hidden />';
|
|
||||||
}
|
|
||||||
printf(
|
|
||||||
'<button type="button" class="button wps-media__choose" id="%1$s" aria-describedby="%2$s">%3$s</button>',
|
|
||||||
esc_attr($id),
|
|
||||||
esc_attr($desc_id),
|
|
||||||
esc_html__('Choose picture', 'wp-plugin-starter-template')
|
|
||||||
);
|
|
||||||
printf(
|
|
||||||
'<button type="button" class="button-link wps-media__remove"%1$s>%2$s</button>',
|
|
||||||
$image_id ? '' : ' hidden',
|
|
||||||
esc_html__('Remove', 'wp-plugin-starter-template')
|
|
||||||
);
|
|
||||||
echo '</div>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'bool':
|
|
||||||
printf(
|
|
||||||
'<span class="wps-switch"><input type="checkbox" role="switch" class="wps-switch__input" %1$s %2$s /><span class="wps-switch__track" aria-hidden="true"></span></span>',
|
|
||||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
checked((bool) $value, true, false)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
default:
|
|
||||||
printf(
|
|
||||||
'<input type="text" class="regular-text" %1$s value="%2$s" placeholder="%3$s" />',
|
|
||||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
|
||||||
esc_attr((string) $value),
|
|
||||||
esc_attr(isset($field['placeholder']) ? $field['placeholder'] : '')
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<span class="wps-status" data-wps-status="<?php echo esc_attr($key); ?>" aria-hidden="true"></span>
|
|
||||||
<?php if (!empty($field['description']) || !empty($field['tokens'])) : ?>
|
|
||||||
<p class="description" id="<?php echo esc_attr($desc_id); ?>">
|
|
||||||
<?php echo esc_html(isset($field['description']) ? $field['description'] : ''); ?>
|
|
||||||
<?php if (!empty($field['tokens'])) : ?>
|
|
||||||
<span class="wps-tokens">
|
|
||||||
<?php esc_html_e('Tokens:', 'wp-plugin-starter-template'); ?>
|
|
||||||
<?php foreach ($field['tokens'] as $token) : ?>
|
|
||||||
<button type="button" class="wps-token" data-token="<?php echo esc_attr($token); ?>" data-target="<?php echo esc_attr($id); ?>" title="<?php esc_attr_e('Insert token', 'wp-plugin-starter-template'); ?>"><code><?php echo esc_html($token); ?></code></button>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Admin Scripts
|
||||||
|
*
|
||||||
|
* @package WPALLSTARS\PluginStarterTemplate
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin functionality
|
||||||
|
*/
|
||||||
|
const WPSTAdmin = {
|
||||||
|
/**
|
||||||
|
* Initialize
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
// Initialize components
|
||||||
|
this.initComponents();
|
||||||
|
|
||||||
|
// Bind events
|
||||||
|
this.bindEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize components
|
||||||
|
*/
|
||||||
|
initComponents: function() {
|
||||||
|
// Initialize any components here
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind events
|
||||||
|
*/
|
||||||
|
bindEvents: function() {
|
||||||
|
// Example: Toggle sections
|
||||||
|
$('.wpst-toggle-section').on('click', this.toggleSection);
|
||||||
|
|
||||||
|
// Example: Form submission
|
||||||
|
$('#wpst-settings-form').on('submit', this.handleFormSubmit);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle section visibility
|
||||||
|
*
|
||||||
|
* @param {Event} e Click event
|
||||||
|
*/
|
||||||
|
toggleSection: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const $this = $(this);
|
||||||
|
const target = $this.data('target');
|
||||||
|
|
||||||
|
$(target).slideToggle(200);
|
||||||
|
$this.toggleClass('open');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle form submission
|
||||||
|
*
|
||||||
|
* @param {Event} e Submit event
|
||||||
|
*/
|
||||||
|
handleFormSubmit: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const $form = $(this);
|
||||||
|
const $submitButton = $form.find('input[type="submit"]');
|
||||||
|
const formData = $form.serialize();
|
||||||
|
|
||||||
|
// Disable submit button and show loading state
|
||||||
|
$submitButton.prop('disabled', true).addClass('loading');
|
||||||
|
|
||||||
|
// Send AJAX request
|
||||||
|
$.ajax({
|
||||||
|
url: wpstData.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wpst_save_settings',
|
||||||
|
nonce: wpstData.nonce,
|
||||||
|
formData: formData
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
WPSTAdmin.showNotice('success', response.data.message);
|
||||||
|
} else {
|
||||||
|
WPSTAdmin.showNotice('error', response.data.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
WPSTAdmin.showNotice('error', 'An error occurred. Please try again.');
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
// Re-enable submit button and remove loading state
|
||||||
|
$submitButton.prop('disabled', false).removeClass('loading');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show admin notice
|
||||||
|
*
|
||||||
|
* @param {string} type Notice type (success, error, warning)
|
||||||
|
* @param {string} message Notice message
|
||||||
|
*/
|
||||||
|
showNotice: function(type, message) {
|
||||||
|
const $notice = $('<div class="wpst-notice ' + type + '"><p>' + message + '</p></div>');
|
||||||
|
|
||||||
|
// Add notice to the page
|
||||||
|
$('.wpst-notices').html($notice);
|
||||||
|
|
||||||
|
// Automatically remove notice after 5 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
$notice.fadeOut(300, function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize when document is ready
|
||||||
|
$(document).ready(function() {
|
||||||
|
WPSTAdmin.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Update Source Selector Scripts
|
||||||
|
*
|
||||||
|
* @package WPALLSTARS\PluginStarterTemplate
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Source Selector functionality
|
||||||
|
*/
|
||||||
|
const WPSTUpdateSourceSelector = {
|
||||||
|
/**
|
||||||
|
* Modal element
|
||||||
|
*/
|
||||||
|
$modal: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected source
|
||||||
|
*/
|
||||||
|
selectedSource: '',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
// Cache DOM elements
|
||||||
|
this.$modal = $('#wpst-update-source-modal');
|
||||||
|
|
||||||
|
// Bind events
|
||||||
|
this.bindEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind events
|
||||||
|
*/
|
||||||
|
bindEvents: function() {
|
||||||
|
// Open modal when clicking on the update source link
|
||||||
|
$(document).on('click', '.wpst-update-source-selector', this.openModal.bind(this));
|
||||||
|
|
||||||
|
// Close modal when clicking on the close button or outside the modal
|
||||||
|
this.$modal.on('click', '.wpst-modal-close', this.closeModal.bind(this));
|
||||||
|
$(document).on('click', '.wpst-modal', function(e) {
|
||||||
|
if ($(e.target).hasClass('wpst-modal')) {
|
||||||
|
WPSTUpdateSourceSelector.closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select source option
|
||||||
|
this.$modal.on('click', '.wpst-source-option', this.selectSource.bind(this));
|
||||||
|
|
||||||
|
// Save source selection
|
||||||
|
this.$modal.on('click', '#wpst-save-source', this.saveSource.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the modal
|
||||||
|
*
|
||||||
|
* @param {Event} e Click event
|
||||||
|
*/
|
||||||
|
openModal: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.$modal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal
|
||||||
|
*/
|
||||||
|
closeModal: function() {
|
||||||
|
this.$modal.hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a source option
|
||||||
|
*
|
||||||
|
* @param {Event} e Click event
|
||||||
|
*/
|
||||||
|
selectSource: function(e) {
|
||||||
|
const $option = $(e.currentTarget);
|
||||||
|
|
||||||
|
// Update selected state
|
||||||
|
this.$modal.find('.wpst-source-option').removeClass('selected');
|
||||||
|
$option.addClass('selected');
|
||||||
|
|
||||||
|
// Update radio button
|
||||||
|
$option.find('input[type="radio"]').prop('checked', true);
|
||||||
|
|
||||||
|
// Store selected source
|
||||||
|
this.selectedSource = $option.find('input[type="radio"]').val();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the selected source
|
||||||
|
*/
|
||||||
|
saveSource: function() {
|
||||||
|
// Validate selection
|
||||||
|
if (!this.selectedSource) {
|
||||||
|
this.showMessage('error', 'Please select an update source.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const $saveButton = $('#wpst-save-source');
|
||||||
|
$saveButton.prop('disabled', true).html('<span class="wpst-loading"></span> Saving...');
|
||||||
|
|
||||||
|
// Send AJAX request
|
||||||
|
$.ajax({
|
||||||
|
url: wpstModalData.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wpst_set_update_source',
|
||||||
|
nonce: wpstModalData.nonce,
|
||||||
|
source: this.selectedSource
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
WPSTUpdateSourceSelector.showMessage('success', response.data.message);
|
||||||
|
|
||||||
|
// Close modal after a short delay
|
||||||
|
setTimeout(function() {
|
||||||
|
WPSTUpdateSourceSelector.closeModal();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
WPSTUpdateSourceSelector.showMessage('error', response.data.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
WPSTUpdateSourceSelector.showMessage('error', 'An error occurred. Please try again.');
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
// Reset button state
|
||||||
|
$saveButton.prop('disabled', false).text(wpstModalData.i18n.confirm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a message in the modal
|
||||||
|
*
|
||||||
|
* @param {string} type Message type (success, error)
|
||||||
|
* @param {string} message Message text
|
||||||
|
*/
|
||||||
|
showMessage: function(type, message) {
|
||||||
|
const $message = this.$modal.find('.wpst-modal-message');
|
||||||
|
|
||||||
|
// Set message content and type
|
||||||
|
$message.html(message).removeClass('success error').addClass(type).show();
|
||||||
|
|
||||||
|
// Hide message after a delay for success messages
|
||||||
|
if (type === 'success') {
|
||||||
|
setTimeout(function() {
|
||||||
|
$message.fadeOut(300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize when document is ready
|
||||||
|
$(document).ready(function() {
|
||||||
|
WPSTUpdateSourceSelector.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
/**
|
|
||||||
* WP Plugin Starter admin screen.
|
|
||||||
*
|
|
||||||
* One delegated controller per concern:
|
|
||||||
* - Settings: instant save for [data-wps-setting] controls.
|
|
||||||
* - Panels: expandable setting options.
|
|
||||||
* - Tokens: insert pattern tokens into text fields.
|
|
||||||
* - Media fields: choose a Media Library picture with the media dialog.
|
|
||||||
*
|
|
||||||
* The plugin's own tabs bring their own script (wpstarter_admin_enqueue),
|
|
||||||
* which can use window.wpstarterAdmin.api.
|
|
||||||
*
|
|
||||||
* Localized data: window.wpstarterAdmin (see WPStarter_Admin_Manager::enqueue_assets()).
|
|
||||||
*/
|
|
||||||
(function ($, wp, cfg) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (!cfg) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var i18n = cfg.i18n || {};
|
|
||||||
|
|
||||||
function speak(message, politeness) {
|
|
||||||
if (wp && wp.a11y && wp.a11y.speak) {
|
|
||||||
wp.a11y.speak(message, politeness || 'polite');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some plugins redirect the first admin request after activation to a
|
|
||||||
// welcome screen, and admin-ajax.php runs admin_init too. The redirect
|
|
||||||
// happens once, so a reply that is a page instead of JSON is retried
|
|
||||||
// once. Every action here is safe to repeat.
|
|
||||||
function post(action, data) {
|
|
||||||
var deferred = $.Deferred();
|
|
||||||
var current;
|
|
||||||
var send = function (retry) {
|
|
||||||
current = $.ajax({
|
|
||||||
url: cfg.ajaxUrl,
|
|
||||||
type: 'POST',
|
|
||||||
dataType: 'json',
|
|
||||||
data: $.extend({ action: action, nonce: cfg.nonce }, data)
|
|
||||||
})
|
|
||||||
.done(deferred.resolve)
|
|
||||||
.fail(function (xhr, status, error) {
|
|
||||||
if (status === 'parsererror' && retry) {
|
|
||||||
send(false);
|
|
||||||
} else {
|
|
||||||
deferred.reject(xhr, status, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
send(true);
|
|
||||||
|
|
||||||
var promise = deferred.promise();
|
|
||||||
promise.abort = function () {
|
|
||||||
current.abort();
|
|
||||||
};
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorMessage(xhr, fallback) {
|
|
||||||
var json = xhr && xhr.responseJSON;
|
|
||||||
if (json && json.data && json.data.message) {
|
|
||||||
return json.data.message;
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Settings */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
var Settings = {
|
|
||||||
pending: {},
|
|
||||||
sequence: 0,
|
|
||||||
// Saves run one at a time so each reads the settings the last one wrote.
|
|
||||||
queue: $.Deferred().resolve().promise(),
|
|
||||||
|
|
||||||
init: function () {
|
|
||||||
$(document).on('change', '[data-wps-setting]', function () {
|
|
||||||
Settings.save($(this));
|
|
||||||
});
|
|
||||||
// "Select all" and "Clear" for long checkbox lists: one save for the lot.
|
|
||||||
$(document).on('click', '[data-wps-check-all], [data-wps-check-none]', function () {
|
|
||||||
var all = this.hasAttribute('data-wps-check-all');
|
|
||||||
var $group = $('#' + $(this).attr(all ? 'data-wps-check-all' : 'data-wps-check-none'));
|
|
||||||
$group.find(':checkbox').prop('checked', all);
|
|
||||||
Settings.save($group);
|
|
||||||
});
|
|
||||||
// Forms that change data elsewhere ask first.
|
|
||||||
$(document).on('submit', 'form[data-wps-confirm]', function (event) {
|
|
||||||
if (!window.confirm($(this).attr('data-wps-confirm'))) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
valueOf: function ($input) {
|
|
||||||
if ($input.is('[data-wps-multi]')) {
|
|
||||||
return $input.find(':checkbox:checked').map(function () {
|
|
||||||
return this.value;
|
|
||||||
}).get();
|
|
||||||
}
|
|
||||||
if ($input.is(':checkbox')) {
|
|
||||||
return $input.is(':checked') ? '1' : '0';
|
|
||||||
}
|
|
||||||
return $input.val();
|
|
||||||
},
|
|
||||||
|
|
||||||
status: function (key, state, text, keep) {
|
|
||||||
var $status = $('[data-wps-status="' + key + '"]');
|
|
||||||
$status.removeClass('is-saving is-saved is-error').addClass(state ? 'is-' + state : '').text(text || '');
|
|
||||||
clearTimeout($status.data('timer'));
|
|
||||||
// "Reload the page" stays until the next change.
|
|
||||||
if (state === 'saved' && !keep) {
|
|
||||||
$status.data('timer', setTimeout(function () {
|
|
||||||
$status.removeClass('is-saved').text('');
|
|
||||||
}, 2000));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
save: function ($input) {
|
|
||||||
var key = $input.data('wps-setting');
|
|
||||||
var value = this.valueOf($input);
|
|
||||||
var previous = $input.data('wps-saved');
|
|
||||||
var seq = ++this.sequence;
|
|
||||||
|
|
||||||
if (previous === undefined) {
|
|
||||||
previous = $input.is(':checkbox') ? ($input.is(':checked') ? '0' : '1') : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pending[key] = seq;
|
|
||||||
this.status(key, 'saving', i18n.saving);
|
|
||||||
|
|
||||||
var run = function () {
|
|
||||||
return post('wpstarter_save_setting', { key: key, value: value })
|
|
||||||
.done(done)
|
|
||||||
.fail(failed);
|
|
||||||
};
|
|
||||||
var done = function (response) {
|
|
||||||
if (Settings.pending[key] !== seq) {
|
|
||||||
return; // A newer save for this key is queued.
|
|
||||||
}
|
|
||||||
if (!response || !response.success) {
|
|
||||||
Settings.fail($input, key, previous, i18n.saveFailed);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var saved = response.data.value;
|
|
||||||
if ($input.is('[data-wps-multi]')) {
|
|
||||||
var chosen = $.map(saved || [], String);
|
|
||||||
$input.find(':checkbox').each(function () {
|
|
||||||
this.checked = $.inArray(this.value, chosen) !== -1;
|
|
||||||
});
|
|
||||||
} else if ($input.is(':checkbox')) {
|
|
||||||
$input.data('wps-saved', saved ? '1' : '0');
|
|
||||||
} else if (saved !== undefined && String(saved) !== String($input.val())) {
|
|
||||||
$input.val(saved); // Show the sanitized value.
|
|
||||||
}
|
|
||||||
if (!$input.is(':checkbox')) {
|
|
||||||
$input.data('wps-saved', $input.val());
|
|
||||||
}
|
|
||||||
var reload = !!response.data.reload;
|
|
||||||
var message = reload && response.data.message ? response.data.message : i18n.saved;
|
|
||||||
Settings.status(key, 'saved', message, reload);
|
|
||||||
speak(message);
|
|
||||||
$(document).trigger('wpstarter:setting-saved', [key, saved]);
|
|
||||||
};
|
|
||||||
var failed = function (xhr) {
|
|
||||||
if (Settings.pending[key] === seq) {
|
|
||||||
Settings.fail($input, key, previous, errorMessage(xhr, i18n.saveFailed));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A failed save must not block the ones after it.
|
|
||||||
this.queue = this.queue.then(run, run);
|
|
||||||
|
|
||||||
if ($input.is(':checkbox')) {
|
|
||||||
$input.closest('[data-setting-card]').filter(function () {
|
|
||||||
return $(this).data('setting-card') === key;
|
|
||||||
}).toggleClass('is-on', $input.is(':checked'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fail: function ($input, key, previous, message) {
|
|
||||||
if ($input.is(':checkbox')) {
|
|
||||||
var on = previous === '1' || previous === true;
|
|
||||||
$input.prop('checked', on);
|
|
||||||
$input.closest('[data-setting-card]').toggleClass('is-on', on);
|
|
||||||
}
|
|
||||||
this.status(key, 'error', message);
|
|
||||||
speak(message, 'assertive');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Expandable panels and tokens */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
var Panels = {
|
|
||||||
toggle: function ($button) {
|
|
||||||
if (!$button.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var expanded = $button.attr('aria-expanded') === 'true';
|
|
||||||
var panel = document.getElementById($button.attr('aria-controls'));
|
|
||||||
$button.attr('aria-expanded', expanded ? 'false' : 'true');
|
|
||||||
if (panel) {
|
|
||||||
panel.hidden = expanded;
|
|
||||||
}
|
|
||||||
$button.closest('.wps-setting').toggleClass('is-expanded', !expanded);
|
|
||||||
},
|
|
||||||
|
|
||||||
init: function () {
|
|
||||||
$(document).on('click', '.wps-setting__expand', function (event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
Panels.toggle($(this));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mouse convenience: clicking anywhere on the header opens/closes the
|
|
||||||
// options. The switch (and any other control) keeps its own behaviour;
|
|
||||||
// keyboard users use the Options button.
|
|
||||||
$(document).on('click', '[data-wps-panel-toggle]', function (event) {
|
|
||||||
if ($(event.target).closest('input, button, a, label, select, textarea, .wps-switch').length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (window.getSelection && String(window.getSelection()).length) {
|
|
||||||
return; // Let people select text.
|
|
||||||
}
|
|
||||||
Panels.toggle($(this).find('.wps-setting__expand').first());
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on('click', '.wps-token', function () {
|
|
||||||
var input = document.getElementById($(this).data('target'));
|
|
||||||
var token = String($(this).data('token'));
|
|
||||||
if (!input) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var start = typeof input.selectionStart === 'number' ? input.selectionStart : input.value.length;
|
|
||||||
var end = typeof input.selectionEnd === 'number' ? input.selectionEnd : input.value.length;
|
|
||||||
input.value = input.value.slice(0, start) + token + input.value.slice(end);
|
|
||||||
input.focus();
|
|
||||||
input.setSelectionRange(start + token.length, start + token.length);
|
|
||||||
$(input).trigger('change');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Media fields: choose a Media Library picture */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
var MediaField = {
|
|
||||||
init: function () {
|
|
||||||
$(document).on('click', '.wps-media__choose', function () {
|
|
||||||
MediaField.open($(this).closest('[data-wps-media]'));
|
|
||||||
});
|
|
||||||
$(document).on('click', '.wps-media__remove', function () {
|
|
||||||
MediaField.set($(this).closest('[data-wps-media]'), 0, '');
|
|
||||||
$(this).siblings('.wps-media__choose').trigger('focus');
|
|
||||||
});
|
|
||||||
// A saved 0 means the choice was not a picture.
|
|
||||||
$(document).on('wpstarter:setting-saved', function (event, key, value) {
|
|
||||||
var $field = $('[data-wps-media]').has('[data-wps-setting="' + key + '"]');
|
|
||||||
if ($field.length && !parseInt(value, 10)) {
|
|
||||||
MediaField.preview($field, '');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
open: function ($field) {
|
|
||||||
if (!wp || !wp.media) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var frame = $field.data('wps-frame');
|
|
||||||
if (!frame) {
|
|
||||||
frame = wp.media({
|
|
||||||
title: i18n.chooseImage,
|
|
||||||
library: { type: 'image' },
|
|
||||||
multiple: false,
|
|
||||||
button: { text: i18n.useImage }
|
|
||||||
});
|
|
||||||
frame.on('select', function () {
|
|
||||||
var item = frame.state().get('selection').first();
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var data = item.toJSON();
|
|
||||||
var url = data.sizes && data.sizes.thumbnail ? data.sizes.thumbnail.url : data.url;
|
|
||||||
MediaField.set($field, data.id, url);
|
|
||||||
});
|
|
||||||
$field.data('wps-frame', frame);
|
|
||||||
}
|
|
||||||
frame.open();
|
|
||||||
},
|
|
||||||
|
|
||||||
set: function ($field, id, url) {
|
|
||||||
this.preview($field, url);
|
|
||||||
$field.find('[data-wps-setting]').val(String(id || 0)).trigger('change');
|
|
||||||
},
|
|
||||||
|
|
||||||
preview: function ($field, url) {
|
|
||||||
var $img = $field.find('.wps-media__preview');
|
|
||||||
if (url) {
|
|
||||||
$img.attr('src', url).prop('hidden', false);
|
|
||||||
} else {
|
|
||||||
$img.removeAttr('src').prop('hidden', true);
|
|
||||||
}
|
|
||||||
$field.find('.wps-media__remove').prop('hidden', !url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// For the plugin's own admin scripts, which load after this one
|
|
||||||
// (wpstarter_admin_enqueue): the same AJAX, screen reader and error helpers.
|
|
||||||
cfg.api = { post: post, speak: speak, errorMessage: errorMessage };
|
|
||||||
|
|
||||||
$(function () {
|
|
||||||
Settings.init();
|
|
||||||
Panels.init();
|
|
||||||
MediaField.init();
|
|
||||||
});
|
|
||||||
})(jQuery, window.wp, window.wpstarterAdmin);
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?php
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?php
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* WP Plugin Starter admin loader.
|
|
||||||
*
|
|
||||||
* Loads the settings screen, the Read Me tab and the Plugins screen notes
|
|
||||||
* for replaced plugins, then the plugin's own admin parts
|
|
||||||
* (WPStarter_Setup::admin()). Included from the main plugin file for
|
|
||||||
* admin requests only.
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$wpstarter_admin_files = array(
|
|
||||||
'admin/data/readme.php',
|
|
||||||
'admin/includes/class-settings-manager.php',
|
|
||||||
'admin/includes/class-readme-manager.php',
|
|
||||||
'admin/includes/class-admin-manager.php',
|
|
||||||
'admin/includes/class-replaced-plugins.php',
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($wpstarter_admin_files as $wpstarter_file) {
|
|
||||||
require_once WPSTARTER_DIR . $wpstarter_file;
|
|
||||||
}
|
|
||||||
unset($wpstarter_admin_files, $wpstarter_file);
|
|
||||||
|
|
||||||
WPStarter_Admin_Manager::init();
|
|
||||||
WPStarter_Replaced_Plugins::init();
|
|
||||||
WPStarter_Setup::admin();
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Modal Template
|
||||||
|
*
|
||||||
|
* @package WPALLSTARS\PluginStarterTemplate
|
||||||
|
*/
|
||||||
|
|
||||||
|
// If this file is called directly, abort.
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Update Source Modal -->
|
||||||
|
<div id="wpst-update-source-modal" class="wpst-modal">
|
||||||
|
<div class="wpst-modal-content">
|
||||||
|
<div class="wpst-modal-header">
|
||||||
|
<h2 class="wpst-modal-title"><?php esc_html_e('Select Update Source', 'wp-plugin-starter-template'); ?></h2>
|
||||||
|
<span class="wpst-modal-close">×</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wpst-modal-body">
|
||||||
|
<p><?php esc_html_e('Choose your preferred source for plugin updates:', 'wp-plugin-starter-template'); ?></p>
|
||||||
|
|
||||||
|
<div class="wpst-modal-message"></div>
|
||||||
|
|
||||||
|
<div class="wpst-source-options">
|
||||||
|
<?php
|
||||||
|
// Get current update source
|
||||||
|
$current_source = get_option('wpst_update_source', 'wordpress.org');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<label class="wpst-source-option <?php echo $current_source === 'wordpress.org' ? 'selected' : ''; ?>">
|
||||||
|
<input type="radio" name="update_source" value="wordpress.org" <?php checked($current_source, 'wordpress.org'); ?>>
|
||||||
|
<span class="wpst-source-option-label"><?php esc_html_e('WordPress.org', 'wp-plugin-starter-template'); ?></span>
|
||||||
|
<div class="wpst-source-option-description">
|
||||||
|
<?php esc_html_e('Receive updates from the official WordPress.org repository. Recommended for most users.', 'wp-plugin-starter-template'); ?>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="wpst-source-option <?php echo $current_source === 'github' ? 'selected' : ''; ?>">
|
||||||
|
<input type="radio" name="update_source" value="github" <?php checked($current_source, 'github'); ?>>
|
||||||
|
<span class="wpst-source-option-label"><?php esc_html_e('GitHub', 'wp-plugin-starter-template'); ?></span>
|
||||||
|
<div class="wpst-source-option-description">
|
||||||
|
<?php esc_html_e('Receive updates from the GitHub repository. May include pre-release versions.', 'wp-plugin-starter-template'); ?>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="wpst-source-option <?php echo $current_source === 'gitea' ? 'selected' : ''; ?>">
|
||||||
|
<input type="radio" name="update_source" value="gitea" <?php checked($current_source, 'gitea'); ?>>
|
||||||
|
<span class="wpst-source-option-label"><?php esc_html_e('Gitea', 'wp-plugin-starter-template'); ?></span>
|
||||||
|
<div class="wpst-source-option-description">
|
||||||
|
<?php esc_html_e('Receive updates from the Gitea repository. May include pre-release versions.', 'wp-plugin-starter-template'); ?>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wpst-modal-footer">
|
||||||
|
<button type="button" id="wpst-save-source" class="button button-primary">
|
||||||
|
<?php esc_html_e('Save', 'wp-plugin-starter-template'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Plugin Assets
|
||||||
|
|
||||||
|
This directory contains assets for the plugin, including:
|
||||||
|
|
||||||
|
- `banner/`: Banner images for the plugin
|
||||||
|
- `icon/`: Icon images for the plugin
|
||||||
|
- `screenshots/`: Screenshots of the plugin in action
|
||||||
|
|
||||||
|
These assets are used in the plugin's WordPress.org page and in the plugin itself.
|
||||||
|
|
||||||
|
## Asset Requirements
|
||||||
|
|
||||||
|
### Banner
|
||||||
|
|
||||||
|
- Regular: 772x250 pixels
|
||||||
|
- Retina: 1544x500 pixels
|
||||||
|
- PNG or JPG format
|
||||||
|
|
||||||
|
### Icon
|
||||||
|
|
||||||
|
- Regular: 128x128 pixels
|
||||||
|
- Retina: 256x256 pixels
|
||||||
|
- PNG format with transparency
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
- Should be at least 1200 pixels wide
|
||||||
|
- PNG or JPG format
|
||||||
|
- Should demonstrate the plugin's functionality
|
||||||
|
|
||||||
|
## Adding Assets
|
||||||
|
|
||||||
|
When adding new assets:
|
||||||
|
|
||||||
|
1. Place the files in the appropriate directory
|
||||||
|
2. Update the readme.txt file to reference the new screenshots
|
||||||
|
3. Update the WordPress.org assets if the plugin is published there
|
||||||
|
|
||||||
|
For more information about WordPress.org plugin assets, see the `.wordpress-org/WORDPRESS_ORG_ASSETS.md` file.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Banner Images
|
||||||
|
|
||||||
|
This directory contains banner images for the plugin.
|
||||||
|
|
||||||
|
## Required Files
|
||||||
|
|
||||||
|
- `banner-772x250.png`: Regular banner image (772x250 pixels)
|
||||||
|
- `banner-1544x500.png`: Retina banner image (1544x500 pixels)
|
||||||
|
|
||||||
|
## Design Guidelines
|
||||||
|
|
||||||
|
- Keep the design simple and focused
|
||||||
|
- Ensure text is readable
|
||||||
|
- Use high-quality images
|
||||||
|
- Use consistent branding (colors, fonts, etc.)
|
||||||
|
- Test how the banner looks at different sizes
|
||||||
|
|
||||||
|
## Creating Banner Images
|
||||||
|
|
||||||
|
1. Use a design tool like Photoshop, GIMP, or Figma
|
||||||
|
2. Create a canvas with the correct dimensions
|
||||||
|
3. Add your plugin name, logo, and a brief description
|
||||||
|
4. Use colors that match your plugin's branding
|
||||||
|
5. Export as PNG or JPG
|
||||||
|
|
||||||
|
For more information about WordPress.org plugin assets, see the `.wordpress-org/WORDPRESS_ORG_ASSETS.md` file.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Icon Images
|
||||||
|
|
||||||
|
This directory contains icon images for the plugin.
|
||||||
|
|
||||||
|
## Required Files
|
||||||
|
|
||||||
|
- `icon-128x128.png`: Regular icon image (128x128 pixels)
|
||||||
|
- `icon-256x256.png`: Retina icon image (256x256 pixels)
|
||||||
|
|
||||||
|
## Design Guidelines
|
||||||
|
|
||||||
|
- Keep the design simple and recognizable
|
||||||
|
- Use a transparent background
|
||||||
|
- Ensure the icon is visible at small sizes
|
||||||
|
- Use consistent branding (colors, fonts, etc.)
|
||||||
|
- Test how the icon looks at different sizes
|
||||||
|
|
||||||
|
## Creating Icon Images
|
||||||
|
|
||||||
|
1. Use a design tool like Photoshop, GIMP, or Figma
|
||||||
|
2. Create a canvas with the correct dimensions
|
||||||
|
3. Add your plugin logo or a simple graphic that represents your plugin
|
||||||
|
4. Use colors that match your plugin's branding
|
||||||
|
5. Export as PNG with transparency
|
||||||
|
|
||||||
|
For more information about WordPress.org plugin assets, see the `.wordpress-org/WORDPRESS_ORG_ASSETS.md` file.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Screenshots
|
||||||
|
|
||||||
|
This directory contains screenshots of the plugin in action.
|
||||||
|
|
||||||
|
## Required Files
|
||||||
|
|
||||||
|
- `screenshot-1.png`: First screenshot
|
||||||
|
- `screenshot-2.png`: Second screenshot (if applicable)
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
## Screenshot Guidelines
|
||||||
|
|
||||||
|
- Screenshots should be at least 1200 pixels wide
|
||||||
|
- Use PNG or JPG format
|
||||||
|
- Show the plugin in action
|
||||||
|
- Focus on one feature per screenshot
|
||||||
|
- Use annotations to highlight important elements
|
||||||
|
- Use real-world examples
|
||||||
|
|
||||||
|
## Creating Screenshots
|
||||||
|
|
||||||
|
1. Set up a test WordPress site with your plugin activated
|
||||||
|
2. Configure the plugin with realistic settings
|
||||||
|
3. Use a screen capture tool to take screenshots
|
||||||
|
4. Edit the screenshots to add annotations if needed
|
||||||
|
5. Save as PNG or JPG
|
||||||
|
|
||||||
|
## Screenshot Descriptions
|
||||||
|
|
||||||
|
Screenshot descriptions are added in the `readme.txt` file under the "Screenshots" section:
|
||||||
|
|
||||||
|
```
|
||||||
|
== Screenshots ==
|
||||||
|
|
||||||
|
1. This is the first screenshot
|
||||||
|
2. This is the second screenshot
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information about WordPress.org plugin assets, see the `.wordpress-org/WORDPRESS_ORG_ASSETS.md` file.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# WordPress Plugin Build Script
|
||||||
|
# This script creates a clean build of the plugin for distribution
|
||||||
|
|
||||||
|
# Check if version is provided
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "❌ Error: Version number is required"
|
||||||
|
echo "Usage: ./build.sh <version>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
PLUGIN_SLUG="wp-plugin-starter-template"
|
||||||
|
BUILD_DIR="build/$PLUGIN_SLUG"
|
||||||
|
ZIP_FILE="$PLUGIN_SLUG-$VERSION.zip"
|
||||||
|
|
||||||
|
# Create build directory
|
||||||
|
echo "Creating build directory..."
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
# Install composer dependencies
|
||||||
|
echo "Installing composer dependencies..."
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
# Copy plugin files to build directory
|
||||||
|
echo "Copying plugin files..."
|
||||||
|
cp -R *.php "$BUILD_DIR/"
|
||||||
|
cp -R README.md LICENSE CHANGELOG.md readme.txt composer.json "$BUILD_DIR/"
|
||||||
|
|
||||||
|
# Copy directories
|
||||||
|
echo "Copying directories..."
|
||||||
|
mkdir -p "$BUILD_DIR/admin" "$BUILD_DIR/includes" "$BUILD_DIR/languages" "$BUILD_DIR/assets"
|
||||||
|
cp -R admin/* "$BUILD_DIR/admin/"
|
||||||
|
cp -R includes/* "$BUILD_DIR/includes/"
|
||||||
|
cp -R languages/* "$BUILD_DIR/languages/"
|
||||||
|
|
||||||
|
# Create assets directory structure
|
||||||
|
mkdir -p "$BUILD_DIR/assets/banner" "$BUILD_DIR/assets/icon" "$BUILD_DIR/assets/screenshots"
|
||||||
|
|
||||||
|
# Copy assets if they exist
|
||||||
|
if [ -d "assets/banner" ]; then
|
||||||
|
cp -R assets/banner/* "$BUILD_DIR/assets/banner/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "assets/icon" ]; then
|
||||||
|
cp -R assets/icon/* "$BUILD_DIR/assets/icon/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "assets/screenshots" ]; then
|
||||||
|
cp -R assets/screenshots/* "$BUILD_DIR/assets/screenshots/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy vendor directory if it exists
|
||||||
|
if [ -d "vendor" ]; then
|
||||||
|
cp -R vendor "$BUILD_DIR/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create ZIP file
|
||||||
|
echo "Creating ZIP file..."
|
||||||
|
cd build
|
||||||
|
zip -r "../$ZIP_FILE" "$PLUGIN_SLUG" -x "*.DS_Store" -x "*.git*" -x "*.github*"
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Check if ZIP file was created successfully
|
||||||
|
if [ -f "$ZIP_FILE" ]; then
|
||||||
|
echo "✅ Build successful: $ZIP_FILE created"
|
||||||
|
echo "File path: $(pwd)/$ZIP_FILE"
|
||||||
|
|
||||||
|
# Deploy to local WordPress installation if environment variable is set
|
||||||
|
if [ -n "$WP_LOCAL_PLUGIN_DIR" ]; then
|
||||||
|
echo "\nDeploying to local WordPress installation..."
|
||||||
|
echo "Deploying to local WordPress installation..."
|
||||||
|
|
||||||
|
# Remove existing plugin directory
|
||||||
|
rm -rf "$WP_LOCAL_PLUGIN_DIR/$PLUGIN_SLUG"
|
||||||
|
|
||||||
|
# Copy files to local WordPress installation
|
||||||
|
rsync -av --exclude=".git" --exclude=".github" --exclude=".DS_Store" \
|
||||||
|
"$BUILD_DIR/" "$WP_LOCAL_PLUGIN_DIR/$PLUGIN_SLUG"
|
||||||
|
|
||||||
|
# Clear WordPress transients if WP-CLI is available
|
||||||
|
if command -v wp &> /dev/null; then
|
||||||
|
echo "Clearing WordPress transients..."
|
||||||
|
wp transient delete --all --path="$WP_LOCAL_PLUGIN_DIR/../.."
|
||||||
|
else
|
||||||
|
echo "⚠️ WP-CLI not found, skipping transient clearing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Local deployment successful!"
|
||||||
|
echo "Plugin deployed to: $WP_LOCAL_PLUGIN_DIR/$PLUGIN_SLUG"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ Build failed: ZIP file was not created"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
== Changelog ==
|
|
||||||
|
|
||||||
Every change to WP Plugin Starter. readme.txt lists the latest version in short.
|
|
||||||
|
|
||||||
= Unreleased =
|
|
||||||
* Fix: the shared GitHub updater (copy 1.0.1) could not update from private repositories on sites where another plugin raises every request's redirection count (HTTP Requests Manager in a blocking mode sets it to 1): GitHub's redirect was followed, so the signed download address was lost and the token went along to the download host. Its requests to GitHub now never follow redirects, whatever other plugins set. Public repositories' release checks had the same problem.
|
|
||||||
* Fix: the shared GitHub updater's private downloads survive updaters that reset upgrader_pre_download (GPLVault Updater).
|
|
||||||
|
|
||||||
= 1.0.0 =
|
|
||||||
* A fresh start, made from the parts every wpallstars plugin needs, taken from plugins in daily use: the settings screen with search, the Read Me tab, features as classes, replaced plugins, the shared GitHub updater, the release and check scripts, CI and the shared rules (STANDARDS.md, DEVELOPMENT.md, RELEASING.md). Earlier versions of this starter are retired; start again from this one.
|
|
||||||
* New: scripts/rename-plugin.sh gives a copy of the starter its own names, and scripts/sync-core.sh keeps a plugin's core files the same as the starter's.
|
|
||||||
+34
-32
@@ -1,34 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "wpallstars/wp-plugin-starter-template-for-ai-coding",
|
"name": "wpallstars/wp-plugin-starter-template",
|
||||||
"description": "WP Plugin Starter WordPress plugin. Development tools only: the plugin has no Composer dependencies and ships without vendor/.",
|
"description": "A comprehensive starter template for WordPress plugins with best practices for AI-assisted development.",
|
||||||
"type": "wordpress-plugin",
|
"type": "wordpress-plugin",
|
||||||
"license": "GPL-2.0-or-later",
|
"license": "GPL-2.0-or-later",
|
||||||
"require": {
|
"authors": [
|
||||||
"php": ">=7.4"
|
{
|
||||||
},
|
"name": "WPALLSTARS",
|
||||||
"require-dev": {
|
"email": "info@wpallstars.com"
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
}
|
||||||
"phpcompatibility/phpcompatibility-wp": "^2.1",
|
],
|
||||||
"phpstan/extension-installer": "^1.4",
|
"minimum-stability": "stable",
|
||||||
"phpstan/phpstan": "^2.1",
|
"require": {
|
||||||
"szepeviktor/phpstan-wordpress": "^2.0",
|
"php": ">=7.0"
|
||||||
"wp-coding-standards/wpcs": "^3.1"
|
},
|
||||||
},
|
"require-dev": {
|
||||||
"config": {
|
"phpunit/phpunit": "^7.5",
|
||||||
"allow-plugins": {
|
"squizlabs/php_codesniffer": "^3.5",
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
"wp-coding-standards/wpcs": "^2.3"
|
||||||
"phpstan/extension-installer": true
|
},
|
||||||
},
|
"autoload": {
|
||||||
"platform": {
|
"psr-4": {
|
||||||
"php": "7.4.33"
|
"WPALLSTARS\\PluginStarterTemplate\\": "includes/"
|
||||||
},
|
}
|
||||||
"sort-packages": true
|
},
|
||||||
},
|
"scripts": {
|
||||||
"scripts": {
|
"phpcs": "phpcs --standard=WordPress",
|
||||||
"lint": "scripts/lint.sh",
|
"phpcbf": "phpcbf --standard=WordPress",
|
||||||
"phpcs": "phpcs",
|
"test": "phpunit"
|
||||||
"phpcbf": "phpcbf",
|
},
|
||||||
"phpstan": "phpstan analyse --memory-limit=4G",
|
"config": {
|
||||||
"baseline": "phpstan analyse --memory-limit=4G --generate-baseline=phpstan-baseline.neon"
|
"allow-plugins": {
|
||||||
}
|
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Generated
-880
@@ -1,880 +0,0 @@
|
|||||||
{
|
|
||||||
"_readme": [
|
|
||||||
"This file locks the dependencies of your project to a known state",
|
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
|
||||||
"This file is @generated automatically"
|
|
||||||
],
|
|
||||||
"content-hash": "0914721d7ea1987ea4e5e41b78d877a3",
|
|
||||||
"packages": [],
|
|
||||||
"packages-dev": [
|
|
||||||
{
|
|
||||||
"name": "dealerdirect/phpcodesniffer-composer-installer",
|
|
||||||
"version": "v1.2.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/PHPCSStandards/composer-installer.git",
|
|
||||||
"reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd",
|
|
||||||
"reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"composer-plugin-api": "^2.2",
|
|
||||||
"php": ">=5.4",
|
|
||||||
"squizlabs/php_codesniffer": "^3.1.0 || ^4.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"composer/composer": "^2.2",
|
|
||||||
"ext-json": "*",
|
|
||||||
"ext-zip": "*",
|
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.4.0",
|
|
||||||
"phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev",
|
|
||||||
"yoast/phpunit-polyfills": "^1.0"
|
|
||||||
},
|
|
||||||
"type": "composer-plugin",
|
|
||||||
"extra": {
|
|
||||||
"class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Franck Nijhof",
|
|
||||||
"email": "opensource@frenck.dev",
|
|
||||||
"homepage": "https://frenck.dev",
|
|
||||||
"role": "Open source developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Contributors",
|
|
||||||
"homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "PHP_CodeSniffer Standards Composer Installer Plugin",
|
|
||||||
"keywords": [
|
|
||||||
"PHPCodeSniffer",
|
|
||||||
"PHP_CodeSniffer",
|
|
||||||
"code quality",
|
|
||||||
"codesniffer",
|
|
||||||
"composer",
|
|
||||||
"installer",
|
|
||||||
"phpcbf",
|
|
||||||
"phpcs",
|
|
||||||
"plugin",
|
|
||||||
"qa",
|
|
||||||
"quality",
|
|
||||||
"standard",
|
|
||||||
"standards",
|
|
||||||
"style guide",
|
|
||||||
"stylecheck",
|
|
||||||
"tests"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/PHPCSStandards/composer-installer/issues",
|
|
||||||
"security": "https://github.com/PHPCSStandards/composer-installer/security/policy",
|
|
||||||
"source": "https://github.com/PHPCSStandards/composer-installer"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/PHPCSStandards",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/jrfnl",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://opencollective.com/php_codesniffer",
|
|
||||||
"type": "open_collective"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://thanks.dev/u/gh/phpcsstandards",
|
|
||||||
"type": "thanks_dev"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-05-06T08:26:05+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "php-stubs/wordpress-stubs",
|
|
||||||
"version": "v7.1.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/php-stubs/wordpress-stubs.git",
|
|
||||||
"reference": "394570301894799e69a4aa117c9c995427e3ba04"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/394570301894799e69a4aa117c9c995427e3ba04",
|
|
||||||
"reference": "394570301894799e69a4aa117c9c995427e3ba04",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"conflict": {
|
|
||||||
"phpdocumentor/reflection-docblock": "5.6.1"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
|
||||||
"nikic/php-parser": "^5.5",
|
|
||||||
"php": "^7.4 || ^8.0",
|
|
||||||
"php-stubs/generator": "^0.9",
|
|
||||||
"phpdocumentor/reflection-docblock": "^6.0",
|
|
||||||
"phpstan/phpstan": "^2.2",
|
|
||||||
"phpunit/phpunit": "^9.5",
|
|
||||||
"symfony/polyfill-php80": "*",
|
|
||||||
"szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.2",
|
|
||||||
"wp-coding-standards/wpcs": "3.4.1 as 2.3.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"paragonie/sodium_compat": "Pure PHP implementation of libsodium",
|
|
||||||
"symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
|
|
||||||
"szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "WordPress function and class declaration stubs for static analysis.",
|
|
||||||
"homepage": "https://github.com/php-stubs/wordpress-stubs",
|
|
||||||
"keywords": [
|
|
||||||
"PHPStan",
|
|
||||||
"static analysis",
|
|
||||||
"wordpress"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/php-stubs/wordpress-stubs/issues",
|
|
||||||
"source": "https://github.com/php-stubs/wordpress-stubs/tree/v7.1.0"
|
|
||||||
},
|
|
||||||
"time": "2026-08-28T00:32:06+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phpcompatibility/php-compatibility",
|
|
||||||
"version": "9.3.5",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/PHPCompatibility/PHPCompatibility.git",
|
|
||||||
"reference": "9fb324479acf6f39452e0655d2429cc0d3914243"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243",
|
|
||||||
"reference": "9fb324479acf6f39452e0655d2429cc0d3914243",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=5.3",
|
|
||||||
"squizlabs/php_codesniffer": "^2.3 || ^3.0.2"
|
|
||||||
},
|
|
||||||
"conflict": {
|
|
||||||
"squizlabs/php_codesniffer": "2.6.2"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.",
|
|
||||||
"roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
|
|
||||||
},
|
|
||||||
"type": "phpcodesniffer-standard",
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-3.0-or-later"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Wim Godden",
|
|
||||||
"homepage": "https://github.com/wimg",
|
|
||||||
"role": "lead"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Juliette Reinders Folmer",
|
|
||||||
"homepage": "https://github.com/jrfnl",
|
|
||||||
"role": "lead"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Contributors",
|
|
||||||
"homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.",
|
|
||||||
"homepage": "http://techblog.wimgodden.be/tag/codesniffer/",
|
|
||||||
"keywords": [
|
|
||||||
"compatibility",
|
|
||||||
"phpcs",
|
|
||||||
"standards"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues",
|
|
||||||
"source": "https://github.com/PHPCompatibility/PHPCompatibility"
|
|
||||||
},
|
|
||||||
"time": "2019-12-27T09:44:58+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phpcompatibility/phpcompatibility-paragonie",
|
|
||||||
"version": "1.3.4",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git",
|
|
||||||
"reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf",
|
|
||||||
"reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"phpcompatibility/php-compatibility": "^9.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
|
||||||
"paragonie/random_compat": "dev-master",
|
|
||||||
"paragonie/sodium_compat": "dev-master"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.",
|
|
||||||
"roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
|
|
||||||
},
|
|
||||||
"type": "phpcodesniffer-standard",
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-3.0-or-later"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Wim Godden",
|
|
||||||
"role": "lead"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Juliette Reinders Folmer",
|
|
||||||
"role": "lead"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.",
|
|
||||||
"homepage": "http://phpcompatibility.com/",
|
|
||||||
"keywords": [
|
|
||||||
"compatibility",
|
|
||||||
"paragonie",
|
|
||||||
"phpcs",
|
|
||||||
"polyfill",
|
|
||||||
"standards",
|
|
||||||
"static analysis"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues",
|
|
||||||
"security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy",
|
|
||||||
"source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/PHPCompatibility",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/jrfnl",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://opencollective.com/php_codesniffer",
|
|
||||||
"type": "open_collective"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://thanks.dev/u/gh/phpcompatibility",
|
|
||||||
"type": "thanks_dev"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2025-09-19T17:43:28+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phpcompatibility/phpcompatibility-wp",
|
|
||||||
"version": "2.1.8",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git",
|
|
||||||
"reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa",
|
|
||||||
"reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"phpcompatibility/php-compatibility": "^9.0",
|
|
||||||
"phpcompatibility/phpcompatibility-paragonie": "^1.0",
|
|
||||||
"squizlabs/php_codesniffer": "^3.3"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.",
|
|
||||||
"roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
|
|
||||||
},
|
|
||||||
"type": "phpcodesniffer-standard",
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-3.0-or-later"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Wim Godden",
|
|
||||||
"role": "lead"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Juliette Reinders Folmer",
|
|
||||||
"role": "lead"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.",
|
|
||||||
"homepage": "http://phpcompatibility.com/",
|
|
||||||
"keywords": [
|
|
||||||
"compatibility",
|
|
||||||
"phpcs",
|
|
||||||
"standards",
|
|
||||||
"static analysis",
|
|
||||||
"wordpress"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues",
|
|
||||||
"security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy",
|
|
||||||
"source": "https://github.com/PHPCompatibility/PHPCompatibilityWP"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/PHPCompatibility",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/jrfnl",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://opencollective.com/php_codesniffer",
|
|
||||||
"type": "open_collective"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://thanks.dev/u/gh/phpcompatibility",
|
|
||||||
"type": "thanks_dev"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2025-10-18T00:05:59+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phpcsstandards/phpcsextra",
|
|
||||||
"version": "1.5.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/PHPCSStandards/PHPCSExtra.git",
|
|
||||||
"reference": "39467533fdb742446d68c1d10ac33d625ee0311c"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/39467533fdb742446d68c1d10ac33d625ee0311c",
|
|
||||||
"reference": "39467533fdb742446d68c1d10ac33d625ee0311c",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=5.4",
|
|
||||||
"phpcsstandards/phpcsutils": "^1.2.3",
|
|
||||||
"squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"php-parallel-lint/php-console-highlighter": "^1.0",
|
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.4.0",
|
|
||||||
"phpcsstandards/phpcsdevcs": "^1.2.0",
|
|
||||||
"phpcsstandards/phpcsdevtools": "^1.2.1",
|
|
||||||
"phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
|
|
||||||
},
|
|
||||||
"type": "phpcodesniffer-standard",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-stable": "1.x-dev",
|
|
||||||
"dev-develop": "1.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-3.0-or-later"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Juliette Reinders Folmer",
|
|
||||||
"homepage": "https://github.com/jrfnl",
|
|
||||||
"role": "lead"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Contributors",
|
|
||||||
"homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A collection of sniffs and standards for use with PHP_CodeSniffer.",
|
|
||||||
"keywords": [
|
|
||||||
"PHP_CodeSniffer",
|
|
||||||
"phpcbf",
|
|
||||||
"phpcodesniffer-standard",
|
|
||||||
"phpcs",
|
|
||||||
"standards",
|
|
||||||
"static analysis"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues",
|
|
||||||
"security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy",
|
|
||||||
"source": "https://github.com/PHPCSStandards/PHPCSExtra"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/PHPCSStandards",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/jrfnl",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://opencollective.com/php_codesniffer",
|
|
||||||
"type": "open_collective"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://thanks.dev/u/gh/phpcsstandards",
|
|
||||||
"type": "thanks_dev"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-07-27T11:13:17+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phpcsstandards/phpcsutils",
|
|
||||||
"version": "1.2.3",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
|
|
||||||
"reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/5f35d9408c54d7b529501f3c688b6eae562aea1f",
|
|
||||||
"reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0",
|
|
||||||
"php": ">=5.4",
|
|
||||||
"squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"ext-filter": "*",
|
|
||||||
"php-parallel-lint/php-console-highlighter": "^1.0",
|
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.4.0",
|
|
||||||
"phpcsstandards/phpcsdevcs": "^1.2.0",
|
|
||||||
"yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0"
|
|
||||||
},
|
|
||||||
"type": "phpcodesniffer-standard",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-stable": "1.x-dev",
|
|
||||||
"dev-develop": "1.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"classmap": [
|
|
||||||
"PHPCSUtils/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-3.0-or-later"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Juliette Reinders Folmer",
|
|
||||||
"homepage": "https://github.com/jrfnl",
|
|
||||||
"role": "lead"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Contributors",
|
|
||||||
"homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A suite of utility functions for use with PHP_CodeSniffer",
|
|
||||||
"homepage": "https://phpcsutils.com/",
|
|
||||||
"keywords": [
|
|
||||||
"PHP_CodeSniffer",
|
|
||||||
"phpcbf",
|
|
||||||
"phpcodesniffer-standard",
|
|
||||||
"phpcs",
|
|
||||||
"phpcs3",
|
|
||||||
"phpcs4",
|
|
||||||
"standards",
|
|
||||||
"static analysis",
|
|
||||||
"tokens",
|
|
||||||
"utility"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"docs": "https://phpcsutils.com/",
|
|
||||||
"issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues",
|
|
||||||
"security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy",
|
|
||||||
"source": "https://github.com/PHPCSStandards/PHPCSUtils"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/PHPCSStandards",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/jrfnl",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://opencollective.com/php_codesniffer",
|
|
||||||
"type": "open_collective"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://thanks.dev/u/gh/phpcsstandards",
|
|
||||||
"type": "thanks_dev"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-07-27T10:28:41+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phpstan/extension-installer",
|
|
||||||
"version": "1.4.3",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/phpstan/extension-installer.git",
|
|
||||||
"reference": "85e90b3942d06b2326fba0403ec24fe912372936"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
|
|
||||||
"reference": "85e90b3942d06b2326fba0403ec24fe912372936",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"composer-plugin-api": "^2.0",
|
|
||||||
"php": "^7.2 || ^8.0",
|
|
||||||
"phpstan/phpstan": "^1.9.0 || ^2.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"composer/composer": "^2.0",
|
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.2.0",
|
|
||||||
"phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
|
|
||||||
},
|
|
||||||
"type": "composer-plugin",
|
|
||||||
"extra": {
|
|
||||||
"class": "PHPStan\\ExtensionInstaller\\Plugin"
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"PHPStan\\ExtensionInstaller\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "Composer plugin for automatic installation of PHPStan extensions",
|
|
||||||
"keywords": [
|
|
||||||
"dev",
|
|
||||||
"static analysis"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/phpstan/extension-installer/issues",
|
|
||||||
"source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
|
|
||||||
},
|
|
||||||
"time": "2024-09-04T20:21:43+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phpstan/phpstan",
|
|
||||||
"version": "2.2.16",
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92",
|
|
||||||
"reference": "46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.4|^8.0"
|
|
||||||
},
|
|
||||||
"conflict": {
|
|
||||||
"phpstan/phpstan-shim": "*"
|
|
||||||
},
|
|
||||||
"bin": [
|
|
||||||
"phpstan",
|
|
||||||
"phpstan.phar"
|
|
||||||
],
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"bootstrap.php"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Ondřej Mirtes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Markus Staab"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Vincent Langlet"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "PHPStan - PHP Static Analysis Tool",
|
|
||||||
"keywords": [
|
|
||||||
"dev",
|
|
||||||
"static analysis"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"docs": "https://phpstan.org/user-guide/getting-started",
|
|
||||||
"forum": "https://github.com/phpstan/phpstan/discussions",
|
|
||||||
"issues": "https://github.com/phpstan/phpstan/issues",
|
|
||||||
"security": "https://github.com/phpstan/phpstan/security/policy",
|
|
||||||
"source": "https://github.com/phpstan/phpstan-src"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/ondrejmirtes",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/phpstan",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-09-25T09:31:51+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "squizlabs/php_codesniffer",
|
|
||||||
"version": "3.13.6",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
|
|
||||||
"reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91",
|
|
||||||
"reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-simplexml": "*",
|
|
||||||
"ext-tokenizer": "*",
|
|
||||||
"ext-xmlwriter": "*",
|
|
||||||
"php": ">=5.4.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
|
|
||||||
},
|
|
||||||
"bin": [
|
|
||||||
"bin/phpcbf",
|
|
||||||
"bin/phpcs"
|
|
||||||
],
|
|
||||||
"type": "library",
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"BSD-3-Clause"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Greg Sherwood",
|
|
||||||
"role": "Former lead"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Juliette Reinders Folmer",
|
|
||||||
"role": "Current lead"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Contributors",
|
|
||||||
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
|
|
||||||
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
|
|
||||||
"keywords": [
|
|
||||||
"phpcs",
|
|
||||||
"standards",
|
|
||||||
"static analysis"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
|
|
||||||
"security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
|
|
||||||
"source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
|
|
||||||
"wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/PHPCSStandards",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/jrfnl",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://opencollective.com/php_codesniffer",
|
|
||||||
"type": "open_collective"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://thanks.dev/u/gh/phpcsstandards",
|
|
||||||
"type": "thanks_dev"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-08-06T00:17:32+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "szepeviktor/phpstan-wordpress",
|
|
||||||
"version": "v2.0.4",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/szepeviktor/phpstan-wordpress.git",
|
|
||||||
"reference": "84577e2ea1e4c2a95537fe5ea2ac8e18f4ceab1a"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/84577e2ea1e4c2a95537fe5ea2ac8e18f4ceab1a",
|
|
||||||
"reference": "84577e2ea1e4c2a95537fe5ea2ac8e18f4ceab1a",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.4 || ^8.0",
|
|
||||||
"php-stubs/wordpress-stubs": ">=6.6.2",
|
|
||||||
"phpstan/phpstan": "^2.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"composer/composer": "^2.1.14",
|
|
||||||
"composer/semver": "^3.4",
|
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.1",
|
|
||||||
"phpstan/phpstan-strict-rules": "^2.0",
|
|
||||||
"phpunit/phpunit": "^9.0",
|
|
||||||
"szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0",
|
|
||||||
"wp-coding-standards/wpcs": "3.1.0 as 2.3.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods"
|
|
||||||
},
|
|
||||||
"type": "phpstan-extension",
|
|
||||||
"extra": {
|
|
||||||
"phpstan": {
|
|
||||||
"includes": [
|
|
||||||
"extension.neon"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"SzepeViktor\\PHPStan\\WordPress\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "WordPress extensions for PHPStan",
|
|
||||||
"keywords": [
|
|
||||||
"PHPStan",
|
|
||||||
"code analyse",
|
|
||||||
"code analysis",
|
|
||||||
"static analysis",
|
|
||||||
"wordpress"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/szepeviktor/phpstan-wordpress/issues",
|
|
||||||
"source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.4"
|
|
||||||
},
|
|
||||||
"time": "2026-05-22T16:22:09+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wp-coding-standards/wpcs",
|
|
||||||
"version": "3.4.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/WordPress/WordPress-Coding-Standards.git",
|
|
||||||
"reference": "ec2ff942335f33683a5957a85d138753876a05cf"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/ec2ff942335f33683a5957a85d138753876a05cf",
|
|
||||||
"reference": "ec2ff942335f33683a5957a85d138753876a05cf",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-filter": "*",
|
|
||||||
"ext-libxml": "*",
|
|
||||||
"ext-tokenizer": "*",
|
|
||||||
"ext-xmlreader": "*",
|
|
||||||
"php": ">=7.2",
|
|
||||||
"phpcsstandards/phpcsextra": "^1.5.1",
|
|
||||||
"phpcsstandards/phpcsutils": "^1.2.3",
|
|
||||||
"squizlabs/php_codesniffer": "^3.13.5"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.4.0",
|
|
||||||
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
|
||||||
"phpcsstandards/phpcsdevtools": "^1.2.0",
|
|
||||||
"phpunit/phpunit": "^8.0 || ^9.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-iconv": "For improved results",
|
|
||||||
"ext-mbstring": "For improved results"
|
|
||||||
},
|
|
||||||
"type": "phpcodesniffer-standard",
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Contributors",
|
|
||||||
"homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions",
|
|
||||||
"keywords": [
|
|
||||||
"phpcs",
|
|
||||||
"standards",
|
|
||||||
"static analysis",
|
|
||||||
"wordpress"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues",
|
|
||||||
"source": "https://github.com/WordPress/WordPress-Coding-Standards",
|
|
||||||
"wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://opencollective.com/php_codesniffer",
|
|
||||||
"type": "custom"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-07-27T11:53:23+00:00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"aliases": [],
|
|
||||||
"minimum-stability": "stable",
|
|
||||||
"stability-flags": {},
|
|
||||||
"prefer-stable": false,
|
|
||||||
"prefer-lowest": false,
|
|
||||||
"platform": {
|
|
||||||
"php": ">=7.4"
|
|
||||||
},
|
|
||||||
"platform-dev": {},
|
|
||||||
"platform-overrides": {
|
|
||||||
"php": "7.4.33"
|
|
||||||
},
|
|
||||||
"plugin-api-version": "2.6.0"
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Base class for WP Plugin Starter features.
|
|
||||||
*
|
|
||||||
* A feature is a self-contained class that:
|
|
||||||
* - declares its settings in settings() (the on/off switch first, keyed by KEY,
|
|
||||||
* then any options with 'parent' => KEY);
|
|
||||||
* - registers its hooks in boot(), which runs on `init` (priority 0, before
|
|
||||||
* widgets_init) for every registered feature. Disabled features should
|
|
||||||
* return early so they cost nothing;
|
|
||||||
* - optionally imports settings from the plugin it replaces in migrate().
|
|
||||||
*
|
|
||||||
* While a plugin that a setting replaces is active, the setting waits:
|
|
||||||
* enabled() is false and that plugin keeps doing the job, so the two never
|
|
||||||
* run side by side (two admin bar menus, analytics loaded twice). The
|
|
||||||
* settings card says so, with a deactivate link.
|
|
||||||
*
|
|
||||||
* Register extra features with the `wpstarter_features` filter.
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
* @since 0.3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class WPStarter_Feature {
|
|
||||||
|
|
||||||
/** Setting key of the feature's on/off switch. */
|
|
||||||
const KEY = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings schema entries for this feature.
|
|
||||||
*
|
|
||||||
* @return array<string,array>
|
|
||||||
*/
|
|
||||||
public static function settings() {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the feature is switched on and not waiting for a plugin it
|
|
||||||
* replaces to be deactivated.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function enabled() {
|
|
||||||
return static::switched_on() && !self::replaced_active(static::KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the feature's switch is on, even if it is waiting.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function switched_on() {
|
|
||||||
return '' !== static::KEY && (bool) WPStarter_Settings::get(static::KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Names of the active plugins that a setting replaces.
|
|
||||||
*
|
|
||||||
* @param string $key Setting key.
|
|
||||||
* @return array<string,string> slug => name
|
|
||||||
*/
|
|
||||||
public static function replaced_active($key) {
|
|
||||||
$schema = WPStarter_Settings::schema();
|
|
||||||
if (empty($schema[$key]['replaces']) || !is_array($schema[$key]['replaces'])) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
return array_intersect_key($schema[$key]['replaces'], self::active_plugins());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugins active on this site or network-wide, keyed by folder name.
|
|
||||||
*
|
|
||||||
* @return array<string,string> slug => plugin file
|
|
||||||
*/
|
|
||||||
public static function active_plugins() {
|
|
||||||
static $active = null;
|
|
||||||
if (null === $active) {
|
|
||||||
$files = self::stored_active_plugins();
|
|
||||||
if (is_multisite()) {
|
|
||||||
$files = array_merge($files, array_keys((array) get_site_option('active_sitewide_plugins', array())));
|
|
||||||
}
|
|
||||||
$active = array();
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$slug = dirname((string) $file);
|
|
||||||
if ('.' !== $slug) {
|
|
||||||
$active[$slug] = (string) $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $active;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin files active on this site (not network-wide), as stored: a
|
|
||||||
* plugin skipped on this request still counts.
|
|
||||||
*
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
public static function stored_active_plugins() {
|
|
||||||
$files = get_option('active_plugins', array());
|
|
||||||
$files = is_array($files) ? array_values(array_filter($files, 'is_string')) : array();
|
|
||||||
/**
|
|
||||||
* Filter the active plugin files as stored, for code that skips
|
|
||||||
* plugins on some requests (WP Plugin Starter's "Load plugins only
|
|
||||||
* where needed" filters the option itself).
|
|
||||||
*
|
|
||||||
* @param string[] $files Plugin files, such as "akismet/akismet.php".
|
|
||||||
*/
|
|
||||||
$files = apply_filters('wpstarter_stored_active_plugins', $files);
|
|
||||||
return is_array($files) ? array_values(array_filter($files, 'is_string')) : array();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether some active plugins are skipped on this request, so things
|
|
||||||
* they register (post types, widgets) may be missing.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function plugins_skipped() {
|
|
||||||
/**
|
|
||||||
* Filter whether some active plugins are skipped on this request.
|
|
||||||
*
|
|
||||||
* @param bool $skipped Whether they are.
|
|
||||||
*/
|
|
||||||
return (bool) apply_filters('wpstarter_plugins_skipped', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import settings once, when the stored settings version is older than
|
|
||||||
* WPStarter_Settings::DB_VERSION. Only fill keys that are not stored yet.
|
|
||||||
*
|
|
||||||
* @param array $options Stored settings (raw, without defaults).
|
|
||||||
* @param int $from_version Stored settings version before this upgrade.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function migrate(array $options, $from_version) {
|
|
||||||
return $options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a setting during migrate() unless it is already stored.
|
|
||||||
* (Named import_setting() so features may keep their own import() methods.)
|
|
||||||
*
|
|
||||||
* @param array $options Stored settings.
|
|
||||||
* @param string $key Setting key.
|
|
||||||
* @param mixed $value Raw value; null skips.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected static function import_setting(array $options, $key, $value) {
|
|
||||||
$schema = WPStarter_Settings::schema();
|
|
||||||
if (null !== $value && !array_key_exists($key, $options) && isset($schema[$key])) {
|
|
||||||
$options[$key] = WPStarter_Settings::sanitize_value($value, $schema[$key]);
|
|
||||||
}
|
|
||||||
return $options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Role options for multi settings.
|
|
||||||
*
|
|
||||||
* @return array<string,string>
|
|
||||||
*/
|
|
||||||
public static function role_options() {
|
|
||||||
$roles = array();
|
|
||||||
foreach (wp_roles()->get_names() as $role => $name) {
|
|
||||||
$roles[$role] = translate_user_role($name);
|
|
||||||
}
|
|
||||||
return $roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Role options for settings that restrict people, without roles that can
|
|
||||||
* manage options (such as Administrator): current_user_in_roles() never
|
|
||||||
* matches them, so offering them would only mislead.
|
|
||||||
*
|
|
||||||
* @return array<string,string>
|
|
||||||
*/
|
|
||||||
public static function restrictable_role_options() {
|
|
||||||
$roles = array();
|
|
||||||
foreach (wp_roles()->roles as $role => $data) {
|
|
||||||
if (empty($data['capabilities']['manage_options'])) {
|
|
||||||
$roles[$role] = translate_user_role($data['name']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the current user has one of the roles. Users who can manage
|
|
||||||
* options are never matched, so admins cannot lock themselves out.
|
|
||||||
*
|
|
||||||
* @param mixed $roles Role slugs.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function current_user_in_roles($roles) {
|
|
||||||
$user = wp_get_current_user();
|
|
||||||
if (!$user->exists() || user_can($user, 'manage_options')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (bool) array_intersect((array) $user->roles, (array) $roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a batch that began at $start has time for one more item.
|
|
||||||
*
|
|
||||||
* The batch keeps to its own budget, and also to PHP's time limit for
|
|
||||||
* the whole request: WP-Cron runs every due batch in one request, so two
|
|
||||||
* 20-second batches would pass a 30-second limit. Where the host allows
|
|
||||||
* it, the limit is restarted for the next item instead.
|
|
||||||
*
|
|
||||||
* @param float $start microtime(true) when the batch began.
|
|
||||||
* @param int $budget Seconds the batch may run.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function more_time($start, $budget) {
|
|
||||||
$now = microtime(true);
|
|
||||||
if ($now - $start >= $budget) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$limit = (int) ini_get('max_execution_time');
|
|
||||||
if ($limit <= 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// set_time_limit() restarts the count; hosts may disable it.
|
|
||||||
if (function_exists('set_time_limit') && @set_time_limit(max($limit, 60))) { // phpcs:ignore WordPress.PHP.NoSilencedErrors, Squiz.PHP.DiscouragedFunctions -- may be disabled by the host.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
$begun = isset($_SERVER['REQUEST_TIME_FLOAT']) ? (float) $_SERVER['REQUEST_TIME_FLOAT'] : $start;
|
|
||||||
$used = $now - $begun;
|
|
||||||
// On Linux the limit counts CPU time of every thread, and image
|
|
||||||
// libraries use several, so CPU time can run ahead of the clock.
|
|
||||||
// getrusage() counts the whole process, which under PHP-FPM serves
|
|
||||||
// many requests (thousands of seconds on a test site), so only CPU
|
|
||||||
// time since the first call in this request counts, plus the clock
|
|
||||||
// time before it.
|
|
||||||
$cpu = self::cpu_seconds();
|
|
||||||
if (null !== $cpu) {
|
|
||||||
if (null === self::$cpu_base) {
|
|
||||||
self::$cpu_base = array($cpu, $now - $begun);
|
|
||||||
}
|
|
||||||
$used = max($used, $cpu - self::$cpu_base[0] + self::$cpu_base[1]);
|
|
||||||
}
|
|
||||||
return $used < $limit - min(10, $limit / 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** CPU seconds and request seconds at the first more_time() check. @var array|null */
|
|
||||||
private static $cpu_base = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CPU seconds this process has used, or null when unknown.
|
|
||||||
*
|
|
||||||
* @return float|null
|
|
||||||
*/
|
|
||||||
private static function cpu_seconds() {
|
|
||||||
if (!function_exists('getrusage')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$usage = getrusage();
|
|
||||||
if (!is_array($usage) || !isset($usage['ru_utime.tv_sec'], $usage['ru_stime.tv_sec'], $usage['ru_utime.tv_usec'], $usage['ru_stime.tv_usec'])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return $usage['ru_utime.tv_sec'] + $usage['ru_stime.tv_sec'] + ($usage['ru_utime.tv_usec'] + $usage['ru_stime.tv_usec']) / 1e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register hooks.
|
|
||||||
*/
|
|
||||||
abstract public static function boot();
|
|
||||||
}
|
|
||||||
@@ -1,599 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* WP Plugin Starter settings store.
|
|
||||||
*
|
|
||||||
* Every setting lives in a single `wpstarter_options` array and is described
|
|
||||||
* by a schema entry (type, default, UI metadata) declared by its feature.
|
|
||||||
* Features read values with WPStarter_Settings::get(); the admin UI renders
|
|
||||||
* cards from the same schema; the AJAX endpoint and the Settings API sanitize
|
|
||||||
* through it. Add settings with the `wpstarter_settings_schema` filter or by
|
|
||||||
* registering a feature (see WPStarter_Feature).
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
* @since 0.3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WPStarter_Settings {
|
|
||||||
|
|
||||||
/** Option that stores every setting. */
|
|
||||||
const OPTION = 'wpstarter_options';
|
|
||||||
|
|
||||||
/** Settings API group. */
|
|
||||||
const GROUP = 'wpstarter_settings';
|
|
||||||
|
|
||||||
/** Nonce action shared by admin AJAX requests. */
|
|
||||||
const NONCE = 'wpstarter_admin';
|
|
||||||
|
|
||||||
/** Stored schema version, used for one-off migrations. */
|
|
||||||
const DB_VERSION_OPTION = 'wpstarter_db_version';
|
|
||||||
|
|
||||||
/** Current schema version; its history is in WPStarter_Setup. */
|
|
||||||
const DB_VERSION = WPStarter_Setup::DB_VERSION;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request-level cache of the resolved schema.
|
|
||||||
*
|
|
||||||
* @var array|null
|
|
||||||
*/
|
|
||||||
private static $schema = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request-level cache of defaults.
|
|
||||||
*
|
|
||||||
* @var array|null
|
|
||||||
*/
|
|
||||||
private static $defaults = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register hooks.
|
|
||||||
*/
|
|
||||||
public static function init() {
|
|
||||||
// Priority 0, before WPStarter::boot_features() so features read
|
|
||||||
// migrated values, and before widgets_init (init:1).
|
|
||||||
add_action('init', array(__CLASS__, 'maybe_migrate'), 0);
|
|
||||||
add_action('admin_init', array(__CLASS__, 'register_setting'));
|
|
||||||
add_action('wp_ajax_wpstarter_save_setting', array(__CLASS__, 'ajax_save'));
|
|
||||||
add_filter('option_page_capability_' . self::GROUP, array(__CLASS__, 'capability'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the current user may change WP Plugin Starter's settings.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function can_change() {
|
|
||||||
/**
|
|
||||||
* Filter whether the current user may change WP Plugin Starter's settings
|
|
||||||
* (on top of the manage_options check).
|
|
||||||
*
|
|
||||||
* @param bool $can Whether they may.
|
|
||||||
*/
|
|
||||||
return current_user_can('manage_options') && (bool) apply_filters('wpstarter_can_change_settings', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Capability options.php checks before saving the settings.
|
|
||||||
*
|
|
||||||
* @param string $capability Capability.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function capability($capability) {
|
|
||||||
return self::can_change() ? $capability : 'do_not_allow';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setting definitions, collected from the registered features.
|
|
||||||
*
|
|
||||||
* Keys:
|
|
||||||
* - type: bool | int | text | domains | select | multi | times
|
|
||||||
* - default: default value
|
|
||||||
* - tab: admin tab slug (top-level settings only)
|
|
||||||
* - parent: parent setting key (renders inside the parent's panel)
|
|
||||||
* - options: value => label array, or a callable returning one (select, multi)
|
|
||||||
* - open: multi only; also keep key-like values that are not (yet)
|
|
||||||
* in options, e.g. post types or widgets registered later
|
|
||||||
* - replaces: top-level only; plugin slug => name this setting replaces
|
|
||||||
* - reload: true when the change shows only after a page load; the
|
|
||||||
* saved message then asks to reload the page
|
|
||||||
* - label, description, placeholder, min, max, unit, tokens, rows: UI metadata
|
|
||||||
*
|
|
||||||
* Types also include `url` (one URL or site path), `lines` (one entry
|
|
||||||
* per line, sanitized as plain text) and `media` (a Media Library
|
|
||||||
* picture's ID, chosen with the media dialog).
|
|
||||||
*
|
|
||||||
* @return array<string,array>
|
|
||||||
*/
|
|
||||||
public static function schema() {
|
|
||||||
if (null !== self::$schema) {
|
|
||||||
return self::$schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
$schema = array();
|
|
||||||
foreach (WPStarter::features() as $class) {
|
|
||||||
$schema += (array) $class::settings();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the settings schema to add or adjust settings.
|
|
||||||
*
|
|
||||||
* @param array $schema Setting definitions keyed by setting key.
|
|
||||||
*/
|
|
||||||
$schema = (array) apply_filters('wpstarter_settings_schema', $schema);
|
|
||||||
|
|
||||||
foreach ($schema as $key => $field) {
|
|
||||||
if (isset($field['tab']) && is_string($field['tab'])) {
|
|
||||||
$schema[$key]['tab'] = self::resolve_tab($field['tab']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$schema = $schema;
|
|
||||||
|
|
||||||
return self::$schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a select/multi field's options.
|
|
||||||
*
|
|
||||||
* Options may be a callable so they can depend on data registered at
|
|
||||||
* `init` (post types, roles) without building it for every request.
|
|
||||||
*
|
|
||||||
* @param array $field Schema entry.
|
|
||||||
* @return array<string,string> value => label
|
|
||||||
*/
|
|
||||||
public static function options_for(array $field) {
|
|
||||||
if (empty($field['options'])) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
$options = is_callable($field['options']) ? call_user_func($field['options']) : $field['options'];
|
|
||||||
return is_array($options) ? $options : array();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default values for every setting.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function defaults() {
|
|
||||||
if (null === self::$defaults) {
|
|
||||||
self::$defaults = array();
|
|
||||||
foreach (self::schema() as $key => $field) {
|
|
||||||
self::$defaults[$key] = isset($field['default']) ? $field['default'] : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return self::$defaults;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All settings merged over defaults.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function all() {
|
|
||||||
$stored = get_option(self::OPTION, array());
|
|
||||||
if (!is_array($stored)) {
|
|
||||||
$stored = array();
|
|
||||||
}
|
|
||||||
return array_merge(self::defaults(), array_intersect_key($stored, self::schema()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read one setting.
|
|
||||||
*
|
|
||||||
* @param string $key Setting key.
|
|
||||||
* @return mixed Value, or null for unknown keys.
|
|
||||||
*/
|
|
||||||
public static function get($key) {
|
|
||||||
$all = self::all();
|
|
||||||
return array_key_exists($key, $all) ? $all[$key] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update one setting after sanitizing it.
|
|
||||||
*
|
|
||||||
* @param string $key Setting key.
|
|
||||||
* @param mixed $value Raw value.
|
|
||||||
* @return mixed|WP_Error Sanitized value, or error for unknown keys.
|
|
||||||
*/
|
|
||||||
public static function set($key, $value) {
|
|
||||||
$schema = self::schema();
|
|
||||||
if (!isset($schema[$key])) {
|
|
||||||
return new WP_Error('wpstarter_unknown_setting', __('Unknown setting.', 'wp-plugin-starter-template'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read what is stored now, not the copy loaded when this request
|
|
||||||
// began, so a save made meanwhile is not overwritten.
|
|
||||||
self::flush_cache();
|
|
||||||
$options = self::all();
|
|
||||||
$options[$key] = self::sanitize_value($value, $schema[$key]);
|
|
||||||
update_option(self::OPTION, $options);
|
|
||||||
|
|
||||||
if (!self::stored($key, $options[$key])) {
|
|
||||||
return new WP_Error('wpstarter_not_saved', __('The setting could not be saved. Please try again.', 'wp-plugin-starter-template'));
|
|
||||||
}
|
|
||||||
return $options[$key];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop cached copies of the settings so the next read hits the database.
|
|
||||||
*/
|
|
||||||
private static function flush_cache() {
|
|
||||||
wp_cache_delete(self::OPTION, 'options');
|
|
||||||
wp_cache_delete('alloptions', 'options');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the database holds a value for a setting (update_option()
|
|
||||||
* returns false both for "unchanged" and for a failed write).
|
|
||||||
*
|
|
||||||
* @param string $key Setting key.
|
|
||||||
* @param mixed $value Expected value.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private static function stored($key, $value) {
|
|
||||||
self::flush_cache();
|
|
||||||
$stored = get_option(self::OPTION);
|
|
||||||
return is_array($stored) && array_key_exists($key, $stored) && $stored[$key] === $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Top-level settings for a tab.
|
|
||||||
*
|
|
||||||
* @param string $tab Tab slug.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function fields_for_tab($tab) {
|
|
||||||
return array_filter(self::schema(), function ($field) use ($tab) {
|
|
||||||
return empty($field['parent']) && isset($field['tab']) && $field['tab'] === $tab;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a renamed tab slug to its current slug
|
|
||||||
* (WPStarter_Setup::RENAMED_TABS).
|
|
||||||
*
|
|
||||||
* @param string $tab Tab slug.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function resolve_tab($tab) {
|
|
||||||
$renamed = WPStarter_Setup::RENAMED_TABS;
|
|
||||||
return array_key_exists($tab, $renamed) ? $renamed[$tab] : $tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Top-level settings that match a search, in schema order.
|
|
||||||
*
|
|
||||||
* Matches the label, description, replaced plugin names and the labels
|
|
||||||
* of child options, ignoring case.
|
|
||||||
*
|
|
||||||
* @param string $query Search text.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function search($query) {
|
|
||||||
$query = trim((string) $query);
|
|
||||||
if ('' === $query) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
$matches = array();
|
|
||||||
foreach (self::schema() as $key => $field) {
|
|
||||||
if (!empty($field['parent']) || empty($field['tab'])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$haystack = array(
|
|
||||||
$key,
|
|
||||||
isset($field['label']) ? $field['label'] : '',
|
|
||||||
isset($field['description']) ? $field['description'] : '',
|
|
||||||
);
|
|
||||||
if (!empty($field['replaces']) && is_array($field['replaces'])) {
|
|
||||||
$haystack = array_merge($haystack, array_keys($field['replaces']), array_values($field['replaces']));
|
|
||||||
}
|
|
||||||
foreach (self::children_of($key) as $child) {
|
|
||||||
$haystack[] = isset($child['label']) && empty($child['hidden']) ? (string) $child['label'] : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$text = implode(' ', array_map('strval', $haystack));
|
|
||||||
$found = function_exists('mb_stripos') ? mb_stripos($text, $query) : stripos($text, $query);
|
|
||||||
if (false !== $found) {
|
|
||||||
$matches[$key] = $field;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Child settings rendered in a parent's panel.
|
|
||||||
*
|
|
||||||
* @param string $parent Parent setting key.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function children_of($parent) {
|
|
||||||
return array_filter(self::schema(), function ($field) use ($parent) {
|
|
||||||
return isset($field['parent']) && $field['parent'] === $parent;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitize a value according to its schema entry.
|
|
||||||
*
|
|
||||||
* Must be idempotent: update_option() re-runs sanitize_all() on the
|
|
||||||
* already-sanitized array.
|
|
||||||
*
|
|
||||||
* @param mixed $value Raw value.
|
|
||||||
* @param array $field Schema entry.
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public static function sanitize_value($value, array $field) {
|
|
||||||
$type = isset($field['type']) ? $field['type'] : 'text';
|
|
||||||
$default = isset($field['default']) ? $field['default'] : null;
|
|
||||||
|
|
||||||
switch ($type) {
|
|
||||||
case 'bool':
|
|
||||||
return rest_sanitize_boolean($value);
|
|
||||||
|
|
||||||
case 'int':
|
|
||||||
$value = is_numeric($value) ? (int) $value : (int) $default;
|
|
||||||
if (isset($field['min'])) {
|
|
||||||
$value = max((int) $field['min'], $value);
|
|
||||||
}
|
|
||||||
if (isset($field['max'])) {
|
|
||||||
$value = min((int) $field['max'], $value);
|
|
||||||
}
|
|
||||||
return $value;
|
|
||||||
|
|
||||||
case 'domains':
|
|
||||||
return implode("\n", self::parse_domains($value));
|
|
||||||
|
|
||||||
case 'media':
|
|
||||||
// A Media Library picture's ID, or 0.
|
|
||||||
$value = is_numeric($value) ? absint($value) : 0;
|
|
||||||
return $value && wp_attachment_is_image($value) ? $value : 0;
|
|
||||||
|
|
||||||
case 'select':
|
|
||||||
$value = is_scalar($value) ? (string) $value : '';
|
|
||||||
if (array_key_exists($value, self::options_for($field))) {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
// Open selects (another plugin's board or course) keep an
|
|
||||||
// identifier whose plugin is not loaded on this request.
|
|
||||||
return !empty($field['open']) && strlen($value) <= 200 && preg_match('/^[A-Za-z0-9_-]+$/', $value) ? $value : $default;
|
|
||||||
|
|
||||||
case 'multi':
|
|
||||||
$values = is_array($value) ? $value : preg_split('/\s*,\s*/', (string) $value, -1, PREG_SPLIT_NO_EMPTY);
|
|
||||||
$values = array_map('strval', array_filter((array) $values, 'is_scalar'));
|
|
||||||
// Keep the options' order so stored values are stable.
|
|
||||||
$known = array_values(array_intersect(array_map('strval', array_keys(self::options_for($field))), $values));
|
|
||||||
if (empty($field['open'])) {
|
|
||||||
return $known;
|
|
||||||
}
|
|
||||||
// Open lists also keep identifiers that are not registered right now
|
|
||||||
// (class names may contain namespace separators).
|
|
||||||
$extra = array_filter(array_diff($values, $known), function ($item) {
|
|
||||||
return strlen($item) <= 200 && (bool) preg_match('/^[A-Za-z0-9_\\\\-]+$/', $item);
|
|
||||||
});
|
|
||||||
return array_values(array_unique(array_merge($known, $extra)));
|
|
||||||
|
|
||||||
case 'times':
|
|
||||||
return implode(', ', self::parse_times($value));
|
|
||||||
|
|
||||||
case 'url':
|
|
||||||
$value = trim((string) $value);
|
|
||||||
if ('' === $value) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if ('/' === $value[0] && '/' !== substr($value, 1, 1)) {
|
|
||||||
// Site path: keep it relative so it survives domain changes.
|
|
||||||
return '/' . ltrim(preg_replace('/\s+/', '', sanitize_text_field($value)), '/');
|
|
||||||
}
|
|
||||||
return esc_url_raw($value, array('http', 'https'));
|
|
||||||
|
|
||||||
case 'lines':
|
|
||||||
$lines = preg_split('/[\r\n]+/', (string) $value);
|
|
||||||
// Not sanitize_text_field(): it strips %xx, which URL paths need.
|
|
||||||
$lines = array_filter(array_map(function ($line) {
|
|
||||||
return trim(preg_replace('/[\x00-\x1F\x7F]+/', '', wp_strip_all_tags($line)));
|
|
||||||
}, $lines), function ($line) {
|
|
||||||
return '' !== $line;
|
|
||||||
});
|
|
||||||
return implode("\n", array_values(array_unique($lines)));
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
default:
|
|
||||||
$value = (string) $value;
|
|
||||||
if (empty($field['tokens'])) {
|
|
||||||
return sanitize_text_field($value);
|
|
||||||
}
|
|
||||||
// sanitize_text_field() drops "%" plus two hex digits, which
|
|
||||||
// breaks tokens such as %date% and %day%; set them aside.
|
|
||||||
$tokens = array();
|
|
||||||
foreach (array_values((array) $field['tokens']) as $i => $token) {
|
|
||||||
$tokens["\u{E000}{$i}\u{E001}"] = (string) $token;
|
|
||||||
}
|
|
||||||
$value = str_replace(array_values($tokens), array_keys($tokens), $value);
|
|
||||||
return str_replace(array_keys($tokens), array_values($tokens), sanitize_text_field($value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalise a newline/comma separated domain list to bare lowercase hosts.
|
|
||||||
*
|
|
||||||
* @param mixed $value Raw list.
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
public static function parse_domains($value) {
|
|
||||||
$lines = preg_split('/[\r\n,]+/', (string) $value);
|
|
||||||
$domains = array();
|
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
$line = trim($line);
|
|
||||||
if ('' === $line) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$host = wp_parse_url(false === strpos($line, '://') ? 'http://' . $line : $line, PHP_URL_HOST);
|
|
||||||
$host = $host ? strtolower(preg_replace('/^www\./i', '', $host)) : '';
|
|
||||||
if ('' !== $host && preg_match('/^[a-z0-9.-]+$/', $host)) {
|
|
||||||
$domains[] = $host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_values(array_unique($domains));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a host is one of the domains or a subdomain of one.
|
|
||||||
*
|
|
||||||
* @param string $host Host name.
|
|
||||||
* @param string[] $domains Normalised domains from parse_domains().
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function host_matches($host, array $domains) {
|
|
||||||
$host = strtolower(preg_replace('/^www\./i', '', (string) $host));
|
|
||||||
foreach ($domains as $domain) {
|
|
||||||
if ($host === $domain || substr($host, -strlen('.' . $domain)) === '.' . $domain) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalise a list of times of day to sorted, unique 24-hour "HH:MM".
|
|
||||||
*
|
|
||||||
* Accepts "9", "9:30", "09:30", "9.30", "9pm", "9:30 am" separated by
|
|
||||||
* commas, spaces or new lines.
|
|
||||||
*
|
|
||||||
* @param mixed $value Raw list.
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
public static function parse_times($value) {
|
|
||||||
preg_match_all('/(\d{1,2})(?:[:.](\d{2}))?\s*(am|pm)?/i', (string) $value, $matches, PREG_SET_ORDER);
|
|
||||||
$times = array();
|
|
||||||
|
|
||||||
foreach ($matches as $match) {
|
|
||||||
$hour = (int) $match[1];
|
|
||||||
$minute = isset($match[2]) && '' !== $match[2] ? (int) $match[2] : 0;
|
|
||||||
$suffix = isset($match[3]) ? strtolower($match[3]) : '';
|
|
||||||
if ('pm' === $suffix && $hour < 12) {
|
|
||||||
$hour += 12;
|
|
||||||
} elseif ('am' === $suffix && 12 === $hour) {
|
|
||||||
$hour = 0;
|
|
||||||
}
|
|
||||||
if ($hour > 23 || $minute > 59) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$times[] = sprintf('%02d:%02d', $hour, $minute);
|
|
||||||
}
|
|
||||||
|
|
||||||
$times = array_values(array_unique($times));
|
|
||||||
sort($times);
|
|
||||||
return $times;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitize the whole array when saved through options.php or update_option().
|
|
||||||
*
|
|
||||||
* @param mixed $input Raw option value.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function sanitize_all($input) {
|
|
||||||
$input = is_array($input) ? $input : array();
|
|
||||||
$schema = self::schema();
|
|
||||||
$clean = self::all();
|
|
||||||
|
|
||||||
foreach ($input as $key => $value) {
|
|
||||||
if (isset($schema[$key])) {
|
|
||||||
$clean[$key] = self::sanitize_value($value, $schema[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $clean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the option with the Settings API.
|
|
||||||
*/
|
|
||||||
public static function register_setting() {
|
|
||||||
register_setting(self::GROUP, self::OPTION, array(
|
|
||||||
'type' => 'object',
|
|
||||||
'sanitize_callback' => array(__CLASS__, 'sanitize_all'),
|
|
||||||
'default' => self::defaults(),
|
|
||||||
'show_in_rest' => false,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AJAX: save a single setting (used by instant-save controls).
|
|
||||||
*/
|
|
||||||
public static function ajax_save() {
|
|
||||||
check_ajax_referer(self::NONCE, 'nonce');
|
|
||||||
|
|
||||||
if (!self::can_change()) {
|
|
||||||
wp_send_json_error(array('message' => __('You are not allowed to change these settings.', 'wp-plugin-starter-template')), 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = isset($_POST['key']) ? sanitize_key(wp_unslash($_POST['key'])) : '';
|
|
||||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized per schema in set().
|
|
||||||
$raw = isset($_POST['value']) ? wp_unslash($_POST['value']) : '';
|
|
||||||
|
|
||||||
$value = self::set($key, $raw);
|
|
||||||
if (is_wp_error($value)) {
|
|
||||||
wp_send_json_error(array('message' => $value->get_error_message()), 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires after a setting is saved from the admin UI.
|
|
||||||
*
|
|
||||||
* @param string $key Setting key.
|
|
||||||
* @param mixed $value Sanitized value.
|
|
||||||
*/
|
|
||||||
do_action('wpstarter_setting_saved', $key, $value);
|
|
||||||
|
|
||||||
$schema = self::schema();
|
|
||||||
$reload = !empty($schema[$key]['reload']);
|
|
||||||
wp_send_json_success(array(
|
|
||||||
'key' => $key,
|
|
||||||
'value' => $value,
|
|
||||||
'message' => $reload ? __('Saved. Reload the page to see the change.', 'wp-plugin-starter-template') : __('Saved', 'wp-plugin-starter-template'),
|
|
||||||
'reload' => $reload,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One-off migrations, once per DB_VERSION: WPStarter_Setup::migrate()
|
|
||||||
* (imports that belong to no feature), then each feature's migrate().
|
|
||||||
* The history of versions is in WPStarter_Setup::DB_VERSION.
|
|
||||||
*
|
|
||||||
* Old options are left in place so a downgrade keeps working;
|
|
||||||
* uninstall.php removes ours. Other plugins' options are never touched.
|
|
||||||
*/
|
|
||||||
public static function maybe_migrate() {
|
|
||||||
$from = (int) get_option(self::DB_VERSION_OPTION, 0);
|
|
||||||
if ($from >= self::DB_VERSION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$options = get_option(self::OPTION, array());
|
|
||||||
$options = is_array($options) ? $options : array();
|
|
||||||
$options = (array) WPStarter_Setup::migrate($options, $from);
|
|
||||||
|
|
||||||
foreach (WPStarter::features() as $class) {
|
|
||||||
$options = (array) $class::migrate($options, $from);
|
|
||||||
}
|
|
||||||
|
|
||||||
$clean = self::sanitize_all($options);
|
|
||||||
update_option(self::OPTION, $clean);
|
|
||||||
|
|
||||||
// Only record the version once the settings are stored, so a failed
|
|
||||||
// write is retried on the next request instead of losing imports.
|
|
||||||
self::flush_cache();
|
|
||||||
if (get_option(self::OPTION) != $clean) { // phpcs:ignore Universal.Operators.StrictComparisons -- key order may differ.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
update_option(self::DB_VERSION_OPTION, self::DB_VERSION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* What makes this plugin WP Plugin Starter: its features, settings tabs,
|
|
||||||
* links, settings history and the helpers only it needs.
|
|
||||||
*
|
|
||||||
* The other files in includes/ and admin/ that the starter plugin also has
|
|
||||||
* (the feature registry, settings store, base feature and admin screen)
|
|
||||||
* read this class and differ from the starter's only in names. Keep
|
|
||||||
* anything that only this plugin needs here, in features or in its own
|
|
||||||
* files loaded from here.
|
|
||||||
*
|
|
||||||
* The starter has no features yet, so its settings screen shows the first
|
|
||||||
* settings tab with "No settings yet" and the Read Me tab. Add a feature:
|
|
||||||
* STANDARDS.md → Structure and README.md → Developers.
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
final class WPStarter_Setup {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Built-in features, in the order their cards appear within each tab.
|
|
||||||
* Each lives in includes/features/class-{lowercase-hyphenated-name}.php,
|
|
||||||
* for example WPStarter_Example in class-wpstarter-example.php.
|
|
||||||
*/
|
|
||||||
const FEATURES = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Features that some builds leave out, loaded only when their file is
|
|
||||||
* present (for example one listed in .distignore-wporg).
|
|
||||||
*/
|
|
||||||
const OPTIONAL_FEATURES = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings version. WPStarter_Settings::maybe_migrate() runs
|
|
||||||
* migrate() below and every feature's migrate() once per version.
|
|
||||||
* After a release, bump it for a new or changed import and add a line.
|
|
||||||
*
|
|
||||||
* v1: first version.
|
|
||||||
*/
|
|
||||||
const DB_VERSION = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renamed tab slugs, old => new. Settings that still use an old slug
|
|
||||||
* land on the new tab, and old admin links open the new tab.
|
|
||||||
*/
|
|
||||||
const RENAMED_TABS = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Links in the settings screen header; leave one out for no button.
|
|
||||||
*
|
|
||||||
* - website: the maker's website
|
|
||||||
* - support: where people report problems (the plugin's GitHub issues)
|
|
||||||
* - donate: where people can support the maker
|
|
||||||
*
|
|
||||||
* @return array<string,string>
|
|
||||||
*/
|
|
||||||
public static function header_links() {
|
|
||||||
return array(
|
|
||||||
'website' => 'https://www.wpallstars.com/',
|
|
||||||
'support' => 'https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/issues',
|
|
||||||
'donate' => 'https://buymeacoffee.com/marcusquinn',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the plugin's own helpers, before the features.
|
|
||||||
*/
|
|
||||||
public static function load() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the helpers' hooks, after the settings store's.
|
|
||||||
*/
|
|
||||||
public static function init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports that belong to no feature, run once per DB_VERSION before
|
|
||||||
* the features' own (for example settings from the plugin's earlier
|
|
||||||
* names). Never write or delete other plugins' options.
|
|
||||||
*
|
|
||||||
* @param array $options Stored settings (raw, without defaults).
|
|
||||||
* @param int $from_version Stored settings version before this upgrade.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function migrate(array $options, $from_version) {
|
|
||||||
unset($from_version);
|
|
||||||
return $options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings tabs, in navigation order. A tab shows only when a setting
|
|
||||||
* uses it; with no settings at all, the first one shows, empty.
|
|
||||||
*
|
|
||||||
* @return array<string,array{label:string,description:string}>
|
|
||||||
*/
|
|
||||||
public static function settings_tabs() {
|
|
||||||
return array(
|
|
||||||
'general' => array(
|
|
||||||
'label' => __('General', 'wp-plugin-starter-template'),
|
|
||||||
'description' => __('WP Plugin Starter\'s settings appear here as features are added.', 'wp-plugin-starter-template'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Admin requests: load and start the plugin's own admin parts (other
|
|
||||||
* tabs with the wpstarter_admin_tabs filter, scripts with the
|
|
||||||
* wpstarter_admin_enqueue action).
|
|
||||||
*/
|
|
||||||
public static function admin() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* WP Plugin Starter bootstrap and feature registry. The features and anything
|
|
||||||
* else only this plugin needs come from WPStarter_Setup.
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
* @since 0.3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
final class WPStarter {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature classes whose files are loaded: WPStarter_Setup::FEATURES,
|
|
||||||
* plus each of WPStarter_Setup::OPTIONAL_FEATURES this build has.
|
|
||||||
*
|
|
||||||
* @var string[]
|
|
||||||
*/
|
|
||||||
private static $core_features = array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolved feature classes.
|
|
||||||
*
|
|
||||||
* @var string[]|null
|
|
||||||
*/
|
|
||||||
private static $features = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load files and register hooks.
|
|
||||||
*/
|
|
||||||
public static function load() {
|
|
||||||
require_once WPSTARTER_DIR . 'includes/class-wpstarter-settings.php';
|
|
||||||
require_once WPSTARTER_DIR . 'includes/class-wpstarter-feature.php';
|
|
||||||
require_once WPSTARTER_DIR . 'includes/class-wpstarter-setup.php';
|
|
||||||
WPStarter_Setup::load();
|
|
||||||
|
|
||||||
foreach (WPStarter_Setup::FEATURES as $class) {
|
|
||||||
require_once self::feature_file($class);
|
|
||||||
self::$core_features[] = $class;
|
|
||||||
}
|
|
||||||
// Features that some builds leave out (the WordPress.org build has
|
|
||||||
// no GitHub updates) load only when their file is present.
|
|
||||||
foreach (WPStarter_Setup::OPTIONAL_FEATURES as $class) {
|
|
||||||
if (is_readable(self::feature_file($class))) {
|
|
||||||
require_once self::feature_file($class);
|
|
||||||
self::$core_features[] = $class;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The shared GitHub updater (GitHub builds only): registers this
|
|
||||||
// copy; the newest copy on the site loads on plugins_loaded.
|
|
||||||
if (is_readable(WPSTARTER_DIR . 'includes/github-updater/load.php')) {
|
|
||||||
require_once WPSTARTER_DIR . 'includes/github-updater/load.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
WPStarter_Settings::init();
|
|
||||||
WPStarter_Setup::init();
|
|
||||||
// Priority 0, added after WPStarter_Settings::maybe_migrate() so features
|
|
||||||
// read migrated values, and early enough to hook widgets_init (init:1).
|
|
||||||
add_action('init', array(__CLASS__, 'boot_features'), 0);
|
|
||||||
|
|
||||||
if (is_admin()) {
|
|
||||||
require_once WPSTARTER_DIR . 'admin/settings.php';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* File of a feature class.
|
|
||||||
*
|
|
||||||
* @param string $class Class name.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private static function feature_file($class) {
|
|
||||||
return WPSTARTER_DIR . 'includes/features/class-' . str_replace('_', '-', strtolower($class)) . '.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registered feature classes.
|
|
||||||
*
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
public static function features() {
|
|
||||||
if (null === self::$features) {
|
|
||||||
/**
|
|
||||||
* Filter the feature classes. Each must extend WPStarter_Feature.
|
|
||||||
*
|
|
||||||
* @param string[] $features Class names.
|
|
||||||
*/
|
|
||||||
$features = (array) apply_filters('wpstarter_features', self::$core_features);
|
|
||||||
self::$features = array_values(array_filter($features, function ($class) {
|
|
||||||
return is_string($class) && class_exists($class) && is_subclass_of($class, 'WPStarter_Feature');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return self::$features;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boot every feature.
|
|
||||||
*/
|
|
||||||
public static function boot_features() {
|
|
||||||
foreach (self::features() as $class) {
|
|
||||||
$class::boot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin deactivated: let features that changed files or scheduled
|
|
||||||
* work outside WordPress's options undo it (a static deactivate() method).
|
|
||||||
*
|
|
||||||
* @param bool $network_wide Deactivated for the whole network.
|
|
||||||
*/
|
|
||||||
public static function deactivate($network_wide = false) {
|
|
||||||
foreach (self::features() as $class) {
|
|
||||||
if (method_exists($class, 'deactivate')) {
|
|
||||||
$class::deactivate((bool) $network_wide);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?php
|
||||||
@@ -1,828 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Shared GitHub updater: updates from GitHub releases for every installed
|
|
||||||
* plugin that names its GitHub repository.
|
|
||||||
*
|
|
||||||
* Loaded by load.php, which picks the newest copy on the site; see there.
|
|
||||||
* Only in builds made from GitHub releases: the WordPress.org build leaves
|
|
||||||
* this folder out.
|
|
||||||
*
|
|
||||||
* - A plugin takes part by naming its repository in its main file's header:
|
|
||||||
* `GitHub Plugin URI: owner/repo` (the header Git Updater reads, so the
|
|
||||||
* two stay compatible). `Release Asset: true` means "install only the zip
|
|
||||||
* attached to the release, never the source code".
|
|
||||||
* - WordPress does the rest itself: this only tells it that a newer release
|
|
||||||
* exists and where its zip is. The Updates screen, auto-updates, "View
|
|
||||||
* details", the download, the install and the rollback on failure are
|
|
||||||
* WordPress's own. Nothing is removed from or blocked in its update check.
|
|
||||||
* - The latest release of each repository is asked for at most every 12
|
|
||||||
* hours (an hour after a failure), when WordPress checks for updates, and
|
|
||||||
* again when someone presses "Check again" on the Updates screen.
|
|
||||||
* - Public repositories are read from github.com's own pages (the
|
|
||||||
* releases/latest redirect, the asset's download address and the main
|
|
||||||
* file on raw.githubusercontent.com), not the API: without a token the API
|
|
||||||
* allows 60 requests an hour per server address, shared by every site on
|
|
||||||
* a host. The asset must be named {folder}-{version}.zip.
|
|
||||||
* - Private repositories need a GitHub token in wp-config.php
|
|
||||||
* (WPALLSTARS_GITHUB_TOKEN) or from the `wpallstars_github_token` filter,
|
|
||||||
* and are read through the API. The token is sent only to api.github.com,
|
|
||||||
* never stored and never shown.
|
|
||||||
* - Plugins also on WordPress.org keep updating from there unless the
|
|
||||||
* `wpallstars_github_updater_early` filter returns true.
|
|
||||||
* - While Git Updater is active, this waits and Git Updater does the job.
|
|
||||||
* The `wpallstars_github_updater_enabled` filter can turn it off too.
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
final class WPAllStars_GitHub_Updater {
|
|
||||||
|
|
||||||
/** Site transient: latest release of each repository, keyed by owner/repo. */
|
|
||||||
const CACHE = 'wpallstars_github_releases';
|
|
||||||
|
|
||||||
/** How long a release answer and a failed request are kept. */
|
|
||||||
const FRESH = 12 * HOUR_IN_SECONDS;
|
|
||||||
const RETRY = HOUR_IN_SECONDS;
|
|
||||||
const RECENT = MINUTE_IN_SECONDS;
|
|
||||||
|
|
||||||
/** Update IDs this file adds, so its own entries can be told apart. */
|
|
||||||
const ID_PREFIX = 'github.com/';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release answers read this request.
|
|
||||||
*
|
|
||||||
* @var array|null
|
|
||||||
*/
|
|
||||||
private static $cache = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register hooks (plugins_loaded). Whether to run is decided on init,
|
|
||||||
* once plugins have added their filters.
|
|
||||||
*/
|
|
||||||
public static function load() {
|
|
||||||
add_action('init', array(__CLASS__, 'boot'), 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the update hooks, unless switched off or Git Updater is active.
|
|
||||||
*/
|
|
||||||
public static function boot() {
|
|
||||||
/**
|
|
||||||
* Whether to add GitHub releases to WordPress's update check.
|
|
||||||
*
|
|
||||||
* @param bool $enabled False while Git Updater is active.
|
|
||||||
*/
|
|
||||||
if (!apply_filters('wpallstars_github_updater_enabled', !self::git_updater_active())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Update checks run in the admin, in cron and in WP-CLI.
|
|
||||||
add_filter('pre_set_site_transient_update_plugins', array(__CLASS__, 'add_updates'));
|
|
||||||
add_filter('plugins_api', array(__CLASS__, 'plugin_information'), 20, 3);
|
|
||||||
add_filter('upgrader_package_options', array(__CLASS__, 'private_package'));
|
|
||||||
add_filter('upgrader_pre_download', array(__CLASS__, 'private_download'), 10, 3);
|
|
||||||
add_filter('upgrader_source_selection', array(__CLASS__, 'fix_folder'), 10, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether Git Updater is active on this site or network-wide.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private static function git_updater_active() {
|
|
||||||
$files = (array) get_option('active_plugins', array());
|
|
||||||
if (is_multisite()) {
|
|
||||||
$files = array_merge($files, array_keys((array) get_site_option('active_sitewide_plugins', array())));
|
|
||||||
}
|
|
||||||
foreach ($files as $file) {
|
|
||||||
if ('git-updater' === dirname((string) $file)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Installed plugins that name a GitHub repository.
|
|
||||||
*
|
|
||||||
* @return array<string,array{repo:string,asset_only:bool,version:string,name:string}> Plugin file => details.
|
|
||||||
*/
|
|
||||||
public static function plugins() {
|
|
||||||
static $found = null;
|
|
||||||
if (null !== $found) {
|
|
||||||
return $found;
|
|
||||||
}
|
|
||||||
if (!function_exists('get_plugins')) {
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
$found = array();
|
|
||||||
foreach (get_plugins() as $file => $data) {
|
|
||||||
// Plugins in a folder only: an update replaces the whole folder.
|
|
||||||
if ('.' === dirname($file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$headers = get_file_data(WP_PLUGIN_DIR . '/' . $file, array(
|
|
||||||
'repo' => 'GitHub Plugin URI',
|
|
||||||
'asset' => 'Release Asset',
|
|
||||||
));
|
|
||||||
$repo = self::repo_name($headers['repo']);
|
|
||||||
if ('' === $repo) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$found[$file] = array(
|
|
||||||
'repo' => $repo,
|
|
||||||
'asset_only' => in_array(strtolower(trim($headers['asset'])), array('true', 'yes', '1'), true),
|
|
||||||
'version' => isset($data['Version']) ? (string) $data['Version'] : '',
|
|
||||||
'name' => isset($data['Name']) ? (string) $data['Name'] : $file,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the plugins updated from GitHub releases.
|
|
||||||
*
|
|
||||||
* @param array $found Plugin file => array(repo, asset_only, version, name).
|
|
||||||
*/
|
|
||||||
$filtered = (array) apply_filters('wpallstars_github_plugins', $found);
|
|
||||||
$found = array();
|
|
||||||
foreach ($filtered as $file => $plugin) {
|
|
||||||
$repo = is_array($plugin) && isset($plugin['repo']) ? self::repo_name($plugin['repo']) : '';
|
|
||||||
if ('' === $repo || '.' === dirname((string) $file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$found[(string) $file] = array(
|
|
||||||
'repo' => $repo,
|
|
||||||
'asset_only' => !empty($plugin['asset_only']),
|
|
||||||
'version' => isset($plugin['version']) ? (string) $plugin['version'] : '',
|
|
||||||
'name' => isset($plugin['name']) ? (string) $plugin['name'] : (string) $file,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return $found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* owner/repo from a header value ("owner/repo" or a github.com address).
|
|
||||||
*
|
|
||||||
* @param mixed $value Header value.
|
|
||||||
* @return string owner/repo, or '' when it is not one.
|
|
||||||
*/
|
|
||||||
public static function repo_name($value) {
|
|
||||||
$value = trim((string) $value);
|
|
||||||
$value = preg_replace('#^(?:https?://)?(?:www\.)?github\.com/#i', '', $value);
|
|
||||||
$value = preg_replace('#(?:\.git)?/*$#', '', (string) $value);
|
|
||||||
return preg_match('#^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$#', (string) $value) ? (string) $value : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Token for a repository, if the site has one.
|
|
||||||
*
|
|
||||||
* @param string $repo owner/repo.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private static function token($repo) {
|
|
||||||
$token = defined('WPALLSTARS_GITHUB_TOKEN') ? (string) WPALLSTARS_GITHUB_TOKEN : '';
|
|
||||||
/**
|
|
||||||
* Filter the GitHub token used for a repository (private repositories
|
|
||||||
* need one with read access to its contents). Return '' for none.
|
|
||||||
*
|
|
||||||
* @param string $token Token.
|
|
||||||
* @param string $repo owner/repo.
|
|
||||||
*/
|
|
||||||
return trim((string) apply_filters('wpallstars_github_token', $token, $repo));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET from the GitHub API.
|
|
||||||
*
|
|
||||||
* @param string $repo owner/repo (for the token).
|
|
||||||
* @param string $path Path after /repos/{repo}.
|
|
||||||
* @param string $accept Accept header.
|
|
||||||
* @return array|WP_Error
|
|
||||||
*/
|
|
||||||
private static function api_get($repo, $path, $accept = 'application/vnd.github+json') {
|
|
||||||
$headers = array(
|
|
||||||
'Accept' => $accept,
|
|
||||||
'X-GitHub-Api-Version' => '2022-11-28',
|
|
||||||
);
|
|
||||||
$token = self::token($repo);
|
|
||||||
if ('' !== $token) {
|
|
||||||
$headers['Authorization'] = 'Bearer ' . $token;
|
|
||||||
}
|
|
||||||
// No redirects: the token must never travel to another address.
|
|
||||||
return self::remote_no_redirect('GET', 'https://api.github.com/repos/' . $repo . $path, array(
|
|
||||||
'timeout' => 10,
|
|
||||||
'headers' => $headers,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A request that never follows a redirect, so the answer's Location can
|
|
||||||
* be read and a token never travels to another address.
|
|
||||||
*
|
|
||||||
* Some plugins raise every request's redirection count (HTTP Requests
|
|
||||||
* Manager sets it to 1), which would follow GitHub's redirect: the token
|
|
||||||
* would go along and the Location would be lost. Only this request is
|
|
||||||
* held at 0, after every other filter.
|
|
||||||
*
|
|
||||||
* @param string $method GET or HEAD.
|
|
||||||
* @param string $url Address.
|
|
||||||
* @param array $args Request arguments.
|
|
||||||
* @return array|WP_Error
|
|
||||||
*/
|
|
||||||
private static function remote_no_redirect($method, $url, array $args) {
|
|
||||||
$args['redirection'] = 0;
|
|
||||||
$hold = function ($parsed_args, $request_url) use ($url) {
|
|
||||||
if ($request_url === $url && is_array($parsed_args)) {
|
|
||||||
$parsed_args['redirection'] = 0;
|
|
||||||
}
|
|
||||||
return $parsed_args;
|
|
||||||
};
|
|
||||||
add_filter('http_request_args', $hold, PHP_INT_MAX, 2);
|
|
||||||
$response = 'HEAD' === $method ? wp_safe_remote_head($url, $args) : wp_safe_remote_get($url, $args);
|
|
||||||
remove_filter('http_request_args', $hold, PHP_INT_MAX);
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stored release answers.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
private static function cache() {
|
|
||||||
if (null === self::$cache) {
|
|
||||||
$stored = get_site_transient(self::CACHE);
|
|
||||||
self::$cache = is_array($stored) ? $stored : array();
|
|
||||||
}
|
|
||||||
return self::$cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether someone pressed "Check again" on the Updates screen.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private static function forced() {
|
|
||||||
global $pagenow;
|
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- core's own link; it only refreshes data.
|
|
||||||
return is_admin() && 'update-core.php' === $pagenow && !empty($_GET['force-check']) && current_user_can('update_plugins');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Latest release of a repository, from the cache or GitHub.
|
|
||||||
*
|
|
||||||
* @param string $repo owner/repo.
|
|
||||||
* @param string $folder Plugin folder (picks the release asset).
|
|
||||||
* @param string $main Main file name in the folder (reads its requirements).
|
|
||||||
* @return array|null Release, or null when there is none or GitHub did not answer.
|
|
||||||
*/
|
|
||||||
public static function release($repo, $folder, $main) {
|
|
||||||
$cache = self::cache();
|
|
||||||
// The asset and requirements depend on the plugin, not only the repository.
|
|
||||||
$key = $repo . '|' . $folder . '/' . $main;
|
|
||||||
$entry = isset($cache[$key]) && is_array($cache[$key]) ? $cache[$key] : null;
|
|
||||||
$age = $entry && isset($entry['checked']) ? time() - (int) $entry['checked'] : PHP_INT_MAX;
|
|
||||||
$keep = $entry && !empty($entry['failed']) ? self::RETRY : self::FRESH;
|
|
||||||
|
|
||||||
if ($entry && $age < $keep && !(self::forced() && $age > self::RECENT)) {
|
|
||||||
return isset($entry['release']) ? $entry['release'] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$release = self::fetch($repo, $folder, $main);
|
|
||||||
if (is_wp_error($release)) {
|
|
||||||
// Keep the last good answer, and try again in an hour.
|
|
||||||
$entry = array(
|
|
||||||
'checked' => time(),
|
|
||||||
'failed' => $release->get_error_message(),
|
|
||||||
'release' => $entry && isset($entry['release']) ? $entry['release'] : null,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$entry = array('checked' => time(), 'release' => $release);
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$cache[$key] = $entry;
|
|
||||||
set_site_transient(self::CACHE, self::$cache, 2 * DAY_IN_SECONDS);
|
|
||||||
return $entry['release'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ask GitHub for a repository's latest release.
|
|
||||||
*
|
|
||||||
* @param string $repo owner/repo.
|
|
||||||
* @param string $folder Plugin folder.
|
|
||||||
* @param string $main Main file name.
|
|
||||||
* @return array|null|WP_Error Release, null when there is no usable one, or the error.
|
|
||||||
*/
|
|
||||||
private static function fetch($repo, $folder, $main) {
|
|
||||||
return '' !== self::token($repo) ? self::fetch_api($repo, $folder, $main) : self::fetch_web($repo, $folder, $main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An error for a GitHub answer that was not the expected one.
|
|
||||||
*
|
|
||||||
* @param int $code HTTP status code.
|
|
||||||
* @return WP_Error
|
|
||||||
*/
|
|
||||||
private static function status_error($code) {
|
|
||||||
/* translators: %d: HTTP status code */
|
|
||||||
return new WP_Error('wpallstars_github', sprintf(__('GitHub answered with status %d.', 'wp-plugin-starter-template'), $code));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Latest release from github.com's own pages, for public repositories.
|
|
||||||
* The GitHub API allows 60 requests an hour per server address without
|
|
||||||
* a token, which hosts share between many sites; these pages do not
|
|
||||||
* count towards it.
|
|
||||||
*
|
|
||||||
* @param string $repo owner/repo.
|
|
||||||
* @param string $folder Plugin folder.
|
|
||||||
* @param string $main Main file name.
|
|
||||||
* @return array|null|WP_Error
|
|
||||||
*/
|
|
||||||
private static function fetch_web($repo, $folder, $main) {
|
|
||||||
// Redirects to the newest release that is not a draft or pre-release.
|
|
||||||
$response = self::remote_no_redirect('HEAD', 'https://github.com/' . $repo . '/releases/latest', array('timeout' => 10));
|
|
||||||
if (is_wp_error($response)) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
$code = (int) wp_remote_retrieve_response_code($response);
|
|
||||||
$location = (string) wp_remote_retrieve_header($response, 'location');
|
|
||||||
if (404 === $code) {
|
|
||||||
// No such public repository (private ones need a token).
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if ($code < 300 || $code > 399) {
|
|
||||||
return self::status_error($code);
|
|
||||||
}
|
|
||||||
if (!preg_match('#^https://github\.com/' . preg_quote($repo, '#') . '/releases/tag/([^/?\#]+)$#i', $location, $match)) {
|
|
||||||
// No releases yet: GitHub sends the releases list instead.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tag = rawurldecode($match[1]);
|
|
||||||
$version = preg_replace('/^v/i', '', $tag);
|
|
||||||
if (!preg_match('/^[0-9]+(\.[0-9]+)*$/', $version)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The release zip, by its usual name: {folder}-{version}.zip.
|
|
||||||
$asset = null;
|
|
||||||
$download = 'https://github.com/' . $repo . '/releases/download/' . rawurlencode($tag) . '/' . rawurlencode($folder . '-' . $version . '.zip');
|
|
||||||
$head = self::remote_no_redirect('HEAD', $download, array('timeout' => 10));
|
|
||||||
if (is_wp_error($head)) {
|
|
||||||
return $head;
|
|
||||||
}
|
|
||||||
$code = (int) wp_remote_retrieve_response_code($head);
|
|
||||||
if ($code >= 300 && $code < 400) {
|
|
||||||
$asset = array('public' => $download, 'api' => '');
|
|
||||||
} elseif (404 !== $code) {
|
|
||||||
return self::status_error($code);
|
|
||||||
}
|
|
||||||
|
|
||||||
$release = array(
|
|
||||||
'tag' => $tag,
|
|
||||||
'version' => $version,
|
|
||||||
'url' => $location,
|
|
||||||
'published' => '',
|
|
||||||
'notes' => '',
|
|
||||||
'asset' => $asset,
|
|
||||||
'zipball' => 'https://github.com/' . $repo . '/archive/refs/tags/' . rawurlencode($tag) . '.zip',
|
|
||||||
);
|
|
||||||
$file = wp_safe_remote_get('https://raw.githubusercontent.com/' . $repo . '/' . rawurlencode($tag) . '/' . rawurlencode($main), array(
|
|
||||||
'timeout' => 10,
|
|
||||||
'headers' => array('Range' => 'bytes=0-8191'),
|
|
||||||
));
|
|
||||||
$body = !is_wp_error($file) && in_array((int) wp_remote_retrieve_response_code($file), array(200, 206), true) ? (string) wp_remote_retrieve_body($file) : '';
|
|
||||||
return $release + self::requirements($body);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Latest release from the GitHub API, with a token (private repositories).
|
|
||||||
*
|
|
||||||
* @param string $repo owner/repo.
|
|
||||||
* @param string $folder Plugin folder.
|
|
||||||
* @param string $main Main file name.
|
|
||||||
* @return array|null|WP_Error
|
|
||||||
*/
|
|
||||||
private static function fetch_api($repo, $folder, $main) {
|
|
||||||
// The newest release that is not a draft or pre-release.
|
|
||||||
$response = self::api_get($repo, '/releases/latest');
|
|
||||||
if (is_wp_error($response)) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
$code = (int) wp_remote_retrieve_response_code($response);
|
|
||||||
if (404 === $code) {
|
|
||||||
// No releases yet, or a private repository without a token.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (200 !== $code) {
|
|
||||||
return self::status_error($code);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode(wp_remote_retrieve_body($response), true);
|
|
||||||
$tag = is_array($data) && isset($data['tag_name']) ? (string) $data['tag_name'] : '';
|
|
||||||
$version = preg_replace('/^v/i', '', $tag);
|
|
||||||
// Numbers only: anything else is not a finished version.
|
|
||||||
if (!preg_match('/^[0-9]+(\.[0-9]+)*$/', $version)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$release = array(
|
|
||||||
'tag' => $tag,
|
|
||||||
'version' => $version,
|
|
||||||
'url' => isset($data['html_url']) ? (string) $data['html_url'] : 'https://github.com/' . $repo . '/releases',
|
|
||||||
'published' => isset($data['published_at']) ? (string) $data['published_at'] : '',
|
|
||||||
'notes' => isset($data['body']) ? substr((string) $data['body'], 0, 20000) : '',
|
|
||||||
'asset' => self::pick_asset($repo, $folder, isset($data['assets']) && is_array($data['assets']) ? $data['assets'] : array()),
|
|
||||||
'zipball' => 'https://api.github.com/repos/' . $repo . '/zipball/' . rawurlencode($tag),
|
|
||||||
);
|
|
||||||
$file = self::api_get($repo, '/contents/' . rawurlencode($main) . '?ref=' . rawurlencode($tag), 'application/vnd.github.raw+json');
|
|
||||||
$body = !is_wp_error($file) && 200 === (int) wp_remote_retrieve_response_code($file) ? (string) wp_remote_retrieve_body($file) : '';
|
|
||||||
return $release + self::requirements($body);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The release zip for a plugin folder: a .zip asset whose name starts
|
|
||||||
* with the folder name (never the "wordpress-org-…" build).
|
|
||||||
*
|
|
||||||
* @param string $repo owner/repo.
|
|
||||||
* @param string $folder Plugin folder.
|
|
||||||
* @param array $assets Release assets.
|
|
||||||
* @return array{public:string,api:string}|null Download addresses.
|
|
||||||
*/
|
|
||||||
private static function pick_asset($repo, $folder, array $assets) {
|
|
||||||
foreach ($assets as $asset) {
|
|
||||||
$name = isset($asset['name']) ? (string) $asset['name'] : '';
|
|
||||||
if (0 !== stripos($name, $folder) || '.zip' !== strtolower(substr($name, -4))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$public = isset($asset['browser_download_url']) ? (string) $asset['browser_download_url'] : '';
|
|
||||||
$api = isset($asset['id']) ? 'https://api.github.com/repos/' . $repo . '/releases/assets/' . (int) $asset['id'] : '';
|
|
||||||
if (0 === strpos($public, 'https://github.com/' . $repo . '/releases/download/')) {
|
|
||||||
return array('public' => $public, 'api' => $api);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Requires at least" and "Requires PHP" of the released main file, so
|
|
||||||
* WordPress does not offer an update the site cannot run.
|
|
||||||
*
|
|
||||||
* @param string $file Start of the released main file ('' when unknown).
|
|
||||||
* @return array{requires:string,requires_php:string}
|
|
||||||
*/
|
|
||||||
private static function requirements($file) {
|
|
||||||
$found = array('requires' => '', 'requires_php' => '');
|
|
||||||
$head = substr((string) $file, 0, 8192);
|
|
||||||
foreach (array('requires' => 'Requires at least', 'requires_php' => 'Requires PHP') as $key => $header) {
|
|
||||||
if (preg_match('/^(?:[ \t]*<\?php)?[ \t\/*#@]*' . preg_quote($header, '/') . ':(.*)$/mi', $head, $match)) {
|
|
||||||
$value = trim(preg_replace('/\s*(?:\*\/|\?>).*/', '', $match[1]));
|
|
||||||
if (preg_match('/^[0-9]+(\.[0-9]+)*$/', $value)) {
|
|
||||||
$found[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $found;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download address WordPress installs from.
|
|
||||||
*
|
|
||||||
* @param string $repo owner/repo.
|
|
||||||
* @param array $release Release.
|
|
||||||
* @param bool $asset_only Install the release asset only.
|
|
||||||
* @return string '' when the release has nothing to install.
|
|
||||||
*/
|
|
||||||
private static function package($repo, array $release, $asset_only) {
|
|
||||||
$private = '' !== self::token($repo);
|
|
||||||
if (!empty($release['asset'])) {
|
|
||||||
// Assets of private repositories download through the API (see private_package()).
|
|
||||||
return $private && $release['asset']['api'] ? $release['asset']['api'] : $release['asset']['public'];
|
|
||||||
}
|
|
||||||
return $asset_only ? '' : (string) $release['zipball'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether WordPress.org offers updates for a plugin.
|
|
||||||
*
|
|
||||||
* @param object $transient update_plugins.
|
|
||||||
* @param string $file Plugin file.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private static function on_wordpress_org($transient, $file) {
|
|
||||||
foreach (array('response', 'no_update') as $list) {
|
|
||||||
if (isset($transient->{$list}[$file])) {
|
|
||||||
$item = (object) $transient->{$list}[$file];
|
|
||||||
$id = isset($item->id) ? (string) $item->id : '';
|
|
||||||
if (0 !== strpos($id, self::ID_PREFIX)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tell WordPress about GitHub releases when it saves its update check.
|
|
||||||
*
|
|
||||||
* @param mixed $transient update_plugins.
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public static function add_updates($transient) {
|
|
||||||
if (!is_object($transient)) {
|
|
||||||
return $transient;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Whether plugins also on WordPress.org take each version from GitHub
|
|
||||||
* as soon as it is released, instead of waiting for WordPress.org.
|
|
||||||
*
|
|
||||||
* @param bool $early Default false.
|
|
||||||
*/
|
|
||||||
$early = (bool) apply_filters('wpallstars_github_updater_early', false);
|
|
||||||
|
|
||||||
foreach (self::plugins() as $file => $plugin) {
|
|
||||||
if (!$early && self::on_wordpress_org($transient, $file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$folder = dirname($file);
|
|
||||||
$release = self::release($plugin['repo'], $folder, basename($file));
|
|
||||||
$current = isset($transient->checked[$file]) ? (string) $transient->checked[$file] : $plugin['version'];
|
|
||||||
if (!$release) {
|
|
||||||
// Keep WordPress.org's answer, or say nothing.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$dot_org = isset($transient->response[$file]->new_version) ? (string) $transient->response[$file]->new_version : '';
|
|
||||||
if ('' !== $dot_org && 0 !== strpos((string) ($transient->response[$file]->id ?? ''), self::ID_PREFIX) && version_compare($dot_org, $release['version'], '>=')) {
|
|
||||||
// WordPress.org already offers this version or a newer one.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$item = (object) array(
|
|
||||||
'id' => self::ID_PREFIX . $plugin['repo'],
|
|
||||||
'slug' => $folder,
|
|
||||||
'plugin' => $file,
|
|
||||||
'new_version' => $release['version'],
|
|
||||||
'url' => 'https://github.com/' . $plugin['repo'],
|
|
||||||
'package' => self::package($plugin['repo'], $release, $plugin['asset_only']),
|
|
||||||
'requires' => $release['requires'],
|
|
||||||
'requires_php' => $release['requires_php'],
|
|
||||||
'tested' => '',
|
|
||||||
'icons' => array(),
|
|
||||||
'banners' => array(),
|
|
||||||
'banners_rtl' => array(),
|
|
||||||
'compatibility' => new stdClass(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!is_array($transient->response ?? null)) {
|
|
||||||
$transient->response = array();
|
|
||||||
}
|
|
||||||
if (!is_array($transient->no_update ?? null)) {
|
|
||||||
$transient->no_update = array();
|
|
||||||
}
|
|
||||||
if ('' === $item->package && self::on_wordpress_org($transient, $file)) {
|
|
||||||
// Nothing to install from GitHub: keep WordPress.org's answer.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
unset($transient->response[$file], $transient->no_update[$file]);
|
|
||||||
if ('' !== $item->package && version_compare($release['version'], $current, '>')) {
|
|
||||||
$transient->response[$file] = $item;
|
|
||||||
} else {
|
|
||||||
// Listed as up to date, so the Plugins screen offers auto-updates.
|
|
||||||
$transient->no_update[$file] = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $transient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The plugin a "View details" request is about, if it is ours to answer.
|
|
||||||
*
|
|
||||||
* @param string $slug Plugin folder.
|
|
||||||
* @return string Plugin file, or ''.
|
|
||||||
*/
|
|
||||||
private static function file_for_slug($slug) {
|
|
||||||
$updates = get_site_transient('update_plugins');
|
|
||||||
foreach (self::plugins() as $file => $plugin) {
|
|
||||||
if (dirname($file) !== $slug) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach (array('response', 'no_update') as $list) {
|
|
||||||
if (is_object($updates) && isset($updates->{$list}[$file]->id) && 0 === strpos((string) $updates->{$list}[$file]->id, self::ID_PREFIX)) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "View details" for plugins updated from GitHub.
|
|
||||||
*
|
|
||||||
* @param false|object|array $result Result so far.
|
|
||||||
* @param string $action plugins_api action.
|
|
||||||
* @param object $args Request arguments.
|
|
||||||
* @return false|object|array
|
|
||||||
*/
|
|
||||||
public static function plugin_information($result, $action, $args) {
|
|
||||||
if ('plugin_information' !== $action || !is_object($args) || empty($args->slug)) {
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
$file = self::file_for_slug((string) $args->slug);
|
|
||||||
if ('' === $file) {
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
$plugins = self::plugins();
|
|
||||||
$plugin = $plugins[$file];
|
|
||||||
$release = self::release($plugin['repo'], dirname($file), basename($file));
|
|
||||||
$data = get_plugin_data(WP_PLUGIN_DIR . '/' . $file, false, true);
|
|
||||||
|
|
||||||
$sections = array(
|
|
||||||
'description' => wpautop(esc_html(isset($data['Description']) ? wp_strip_all_tags($data['Description']) : '')),
|
|
||||||
);
|
|
||||||
if ($release) {
|
|
||||||
$sections['changelog'] = '<h4>' . esc_html($release['version']) . '</h4>'
|
|
||||||
. ('' !== trim($release['notes']) ? self::notes_html($release['notes']) : '')
|
|
||||||
. sprintf('<p><a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a></p>', esc_url($release['url']), esc_html__('Release notes on GitHub', 'wp-plugin-starter-template'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (object) array(
|
|
||||||
'name' => $plugin['name'],
|
|
||||||
'slug' => dirname($file),
|
|
||||||
'version' => $release ? $release['version'] : $plugin['version'],
|
|
||||||
'author' => isset($data['Author']) ? $data['Author'] : '',
|
|
||||||
'homepage' => 'https://github.com/' . $plugin['repo'],
|
|
||||||
'requires' => $release ? $release['requires'] : '',
|
|
||||||
'requires_php' => $release ? $release['requires_php'] : '',
|
|
||||||
'last_updated' => $release ? $release['published'] : '',
|
|
||||||
'download_link' => $release ? self::package($plugin['repo'], $release, $plugin['asset_only']) : '',
|
|
||||||
'sections' => $sections,
|
|
||||||
'banners' => array(),
|
|
||||||
'external' => true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release notes (Markdown) as simple HTML: headings, lists, paragraphs,
|
|
||||||
* bold and code. Everything is escaped first.
|
|
||||||
*
|
|
||||||
* @param string $markdown Release notes.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private static function notes_html($markdown) {
|
|
||||||
$html = '';
|
|
||||||
$list = false;
|
|
||||||
foreach (preg_split('/\r\n|\r|\n/', (string) $markdown) as $line) {
|
|
||||||
$line = rtrim($line);
|
|
||||||
$text = esc_html(ltrim(preg_replace('/^(#{1,6}|[-*+])\s+/', '', trim($line))));
|
|
||||||
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
|
|
||||||
$text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
|
|
||||||
$item = (bool) preg_match('/^\s*[-*+]\s+/', $line);
|
|
||||||
if ($list && !$item) {
|
|
||||||
$html .= '</ul>';
|
|
||||||
$list = false;
|
|
||||||
}
|
|
||||||
if ('' === trim($line)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($item) {
|
|
||||||
if (!$list) {
|
|
||||||
$html .= '<ul>';
|
|
||||||
$list = true;
|
|
||||||
}
|
|
||||||
$html .= '<li>' . $text . '</li>';
|
|
||||||
} elseif (preg_match('/^#{1,6}\s/', $line)) {
|
|
||||||
$html .= '<h4>' . $text . '</h4>';
|
|
||||||
} else {
|
|
||||||
$html .= '<p>' . $text . '</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $html . ($list ? '</ul>' : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads from private repositories: before WordPress downloads, swap
|
|
||||||
* the API address for the signed, short-lived address GitHub sends the
|
|
||||||
* token holder to, so WordPress downloads it itself without the token.
|
|
||||||
*
|
|
||||||
* This runs before upgrader_pre_download on purpose: some plugins' own
|
|
||||||
* updaters return false there for every package (GPLVault Updater does,
|
|
||||||
* at priority 999999999), which throws away a file already downloaded
|
|
||||||
* and leaves WordPress fetching the API address without the token
|
|
||||||
* (GitHub answers 404).
|
|
||||||
*
|
|
||||||
* @param array $options Upgrader options; 'package' is the address.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function private_package($options) {
|
|
||||||
if (!is_array($options) || !isset($options['package'])) {
|
|
||||||
return $options;
|
|
||||||
}
|
|
||||||
$address = self::signed_address($options['package']);
|
|
||||||
if (is_string($address)) {
|
|
||||||
$options['package'] = $address;
|
|
||||||
}
|
|
||||||
return $options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback for downloads that skip upgrader_package_options: ask the API
|
|
||||||
* with the token, then fetch the file from where GitHub sends us without
|
|
||||||
* it.
|
|
||||||
*
|
|
||||||
* @param mixed $reply False to let WordPress download.
|
|
||||||
* @param string $package Download address.
|
|
||||||
* @param WP_Upgrader $upgrader Upgrader.
|
|
||||||
* @return mixed File path, WP_Error or $reply.
|
|
||||||
*/
|
|
||||||
public static function private_download($reply, $package, $upgrader) {
|
|
||||||
if (false !== $reply) {
|
|
||||||
return $reply;
|
|
||||||
}
|
|
||||||
$address = self::signed_address($package);
|
|
||||||
if (null === $address) {
|
|
||||||
return $reply;
|
|
||||||
}
|
|
||||||
return is_wp_error($address) ? $address : download_url($address, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The signed, short-lived download address of a private repository's
|
|
||||||
* release asset or source zip, asked for with the token. No token is
|
|
||||||
* needed (or wanted) there.
|
|
||||||
*
|
|
||||||
* @param mixed $package Download address.
|
|
||||||
* @return string|WP_Error|null Address; error; null when the address is
|
|
||||||
* not one of ours or there is no token.
|
|
||||||
*/
|
|
||||||
private static function signed_address($package) {
|
|
||||||
if (!is_string($package) || !preg_match('#^https://api\.github\.com/repos/([^/]+/[^/]+)/(?:releases/assets/[0-9]+|zipball/[^/?]+)$#', $package, $match)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$repo = $match[1];
|
|
||||||
$known = false;
|
|
||||||
foreach (self::plugins() as $plugin) {
|
|
||||||
$known = $known || $plugin['repo'] === $repo;
|
|
||||||
}
|
|
||||||
$token = self::token($repo);
|
|
||||||
if (!$known || '' === $token) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = self::remote_no_redirect('GET', $package, array(
|
|
||||||
'timeout' => 30,
|
|
||||||
'headers' => array(
|
|
||||||
'Accept' => false !== strpos($package, '/releases/assets/') ? 'application/octet-stream' : 'application/vnd.github+json',
|
|
||||||
'Authorization' => 'Bearer ' . $token,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
if (is_wp_error($response)) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
$location = (string) wp_remote_retrieve_header($response, 'location');
|
|
||||||
if ('' === $location || !in_array((int) wp_remote_retrieve_response_code($response), array(301, 302, 303, 307, 308), true)) {
|
|
||||||
return new WP_Error('wpallstars_github_download', __('GitHub did not give a download address. Check the token’s access to the repository.', 'wp-plugin-starter-template'));
|
|
||||||
}
|
|
||||||
return $location;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keep a plugin in its folder when the zip's top folder has another name
|
|
||||||
* (GitHub's source zips are named owner-repo-commit).
|
|
||||||
*
|
|
||||||
* @param string|WP_Error $source Unpacked folder.
|
|
||||||
* @param string $remote_source Folder it was unpacked in.
|
|
||||||
* @param WP_Upgrader $upgrader Upgrader.
|
|
||||||
* @param array $hook_extra Context.
|
|
||||||
* @return string|WP_Error
|
|
||||||
*/
|
|
||||||
public static function fix_folder($source, $remote_source, $upgrader, $hook_extra = array()) {
|
|
||||||
global $wp_filesystem;
|
|
||||||
if (is_wp_error($source) || empty($hook_extra['plugin']) || !$wp_filesystem) {
|
|
||||||
return $source;
|
|
||||||
}
|
|
||||||
$file = (string) $hook_extra['plugin'];
|
|
||||||
if (!isset(self::plugins()[$file])) {
|
|
||||||
return $source;
|
|
||||||
}
|
|
||||||
$folder = dirname($file);
|
|
||||||
if (basename(untrailingslashit($source)) === $folder) {
|
|
||||||
return $source;
|
|
||||||
}
|
|
||||||
$target = trailingslashit($remote_source) . $folder . '/';
|
|
||||||
$from = untrailingslashit($source);
|
|
||||||
if ($from === untrailingslashit($remote_source)) {
|
|
||||||
// A zip without a top folder: move its files aside, then into the folder.
|
|
||||||
$from = untrailingslashit($remote_source) . '-github-updater';
|
|
||||||
if (!$wp_filesystem->move(untrailingslashit($remote_source), $from, true) || !$wp_filesystem->mkdir(untrailingslashit($remote_source), FS_CHMOD_DIR)) {
|
|
||||||
return new WP_Error('wpallstars_github_folder', __('The update could not be unpacked into the plugin’s folder.', 'wp-plugin-starter-template'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$wp_filesystem->move($from, untrailingslashit($target), true)) {
|
|
||||||
return new WP_Error('wpallstars_github_folder', __('The update could not be unpacked into the plugin’s folder.', 'wp-plugin-starter-template'));
|
|
||||||
}
|
|
||||||
return $target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Shared GitHub updater: registers this copy.
|
|
||||||
*
|
|
||||||
* Every plugin made from the starter carries a copy of includes/github-updater/.
|
|
||||||
* Each copy registers its version here when its plugin loads; on
|
|
||||||
* plugins_loaded only the newest copy loads, once, and serves every installed
|
|
||||||
* plugin with a `GitHub Plugin URI` header. So one update check covers all of
|
|
||||||
* them, however many carry a copy.
|
|
||||||
*
|
|
||||||
* The WordPress.org build leaves this folder out (.distignore-wporg): plugins
|
|
||||||
* hosted there may not install or update code from anywhere else.
|
|
||||||
*
|
|
||||||
* The starter holds the master copy. Change it there, raise the version
|
|
||||||
* below with every change, then copy it to each plugin (scripts/sync-core.sh).
|
|
||||||
* Plugins change what it does only through its filters, never by calling the
|
|
||||||
* class: another plugin's newer or older copy may be the one that runs.
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($GLOBALS['wpallstars_github_updater']) || !is_array($GLOBALS['wpallstars_github_updater'])) {
|
|
||||||
$GLOBALS['wpallstars_github_updater'] = array();
|
|
||||||
}
|
|
||||||
// Version of this copy => its class file.
|
|
||||||
$GLOBALS['wpallstars_github_updater']['1.0.1'] = __DIR__ . '/class-wpallstars-github-updater.php';
|
|
||||||
|
|
||||||
if (!function_exists('wpallstars_github_updater_load')) {
|
|
||||||
/**
|
|
||||||
* Load the newest registered copy of the updater (plugins_loaded).
|
|
||||||
*/
|
|
||||||
function wpallstars_github_updater_load() {
|
|
||||||
if (class_exists('WPAllStars_GitHub_Updater', false) || empty($GLOBALS['wpallstars_github_updater'])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$copies = (array) $GLOBALS['wpallstars_github_updater'];
|
|
||||||
uksort($copies, 'version_compare');
|
|
||||||
$file = (string) end($copies);
|
|
||||||
if (is_readable($file)) {
|
|
||||||
require_once $file;
|
|
||||||
WPAllStars_GitHub_Updater::load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Every plugin's main file has run by then, so every copy is registered.
|
|
||||||
add_action('plugins_loaded', 'wpallstars_github_updater_load', 1);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?php
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?php
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Copyright (C) 2025 WPALLSTARS
|
||||||
|
# This file is distributed under the GPL-2.0+.
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: WordPress Plugin Starter Template 0.1.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/wp-plugin-starter-template\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"POT-Creation-Date: 2025-04-18T00:00:00+00:00\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"X-Generator: WP-CLI 2.9.0\n"
|
||||||
|
"X-Domain: wp-plugin-starter-template\n"
|
||||||
|
|
||||||
|
#. Plugin Name of the plugin
|
||||||
|
msgid "WordPress Plugin Starter Template"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Plugin URI of the plugin
|
||||||
|
msgid "https://www.wpallstars.com"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Description of the plugin
|
||||||
|
msgid "A comprehensive starter template for WordPress plugins with best practices for AI-assisted development."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Author of the plugin
|
||||||
|
msgid "WPALLSTARS"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Author URI of the plugin
|
||||||
|
msgid "https://www.wpallstars.com"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/lib/admin.php:74
|
||||||
|
msgid "Save Changes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/lib/admin.php:75
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/lib/modal.php:67
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/lib/modal.php:68
|
||||||
|
msgid "Confirm"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/templates/modal.php:16
|
||||||
|
msgid "Select Update Source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/templates/modal.php:21
|
||||||
|
msgid "Choose your preferred source for plugin updates:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/templates/modal.php:31
|
||||||
|
msgid "WordPress.org"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/templates/modal.php:33
|
||||||
|
msgid "Receive updates from the official WordPress.org repository. Recommended for most users."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/templates/modal.php:38
|
||||||
|
msgid "GitHub"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/templates/modal.php:40
|
||||||
|
msgid "Receive updates from the GitHub repository. May include pre-release versions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/templates/modal.php:45
|
||||||
|
msgid "Gitea"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/templates/modal.php:47
|
||||||
|
msgid "Receive updates from the Gitea repository. May include pre-release versions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/templates/modal.php:55
|
||||||
|
msgid "Save"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/updater.php:71
|
||||||
|
msgid "Update Source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/updater.php:84
|
||||||
|
msgid "Security check failed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/updater.php:89
|
||||||
|
msgid "You do not have permission to perform this action."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/updater.php:98
|
||||||
|
msgid "Invalid update source."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/updater.php:107
|
||||||
|
msgid "Update source set to %s."
|
||||||
|
msgstr ""
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="WPStarter" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
|
|
||||||
<description>
|
|
||||||
WP Plugin Starter: what WordPress.org reviewers and Plugin Check look for
|
|
||||||
(security, the database, internationalisation, deprecated and
|
|
||||||
discouraged APIs, prefixes) plus compatibility with PHP 7.4 and
|
|
||||||
WordPress 6.2. Whitespace and formatting follow the code's own style
|
|
||||||
(4 spaces, no padding inside parentheses) and are not checked here.
|
|
||||||
|
|
||||||
Every finding fails the check. When code is right as it is, say why
|
|
||||||
on the line: // phpcs:ignore Sniff.Name -- reason.
|
|
||||||
Run: composer phpcs, or scripts/lint.sh.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<file>.</file>
|
|
||||||
<exclude-pattern>/vendor/*</exclude-pattern>
|
|
||||||
<exclude-pattern>/.cache/*</exclude-pattern>
|
|
||||||
<exclude-pattern>/node_modules/*</exclude-pattern>
|
|
||||||
<exclude-pattern>/dist/*</exclude-pattern>
|
|
||||||
<exclude-pattern>/reference-plugins/*</exclude-pattern>
|
|
||||||
<exclude-pattern>/project-documents/*</exclude-pattern>
|
|
||||||
|
|
||||||
<arg name="extensions" value="php"/>
|
|
||||||
<arg name="parallel" value="8"/>
|
|
||||||
<arg value="sp"/>
|
|
||||||
<arg name="colors"/>
|
|
||||||
|
|
||||||
<!-- Minimum versions (wp-plugin-starter-template.php and readme.txt). -->
|
|
||||||
<config name="testVersion" value="7.4-"/>
|
|
||||||
<config name="minimum_wp_version" value="6.2"/>
|
|
||||||
|
|
||||||
<!-- PHP 7.4 syntax and functions, knowing WordPress's polyfills. Everywhere, scripts included. -->
|
|
||||||
<rule ref="PHPCompatibilityWP"/>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
WordPress rules apply to the plugin, not to scripts/*.php: those run with
|
|
||||||
the PHP CLI on a developer's machine or in CI, never in WordPress, and are
|
|
||||||
not in the release zips.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- Security: escape output, verify nonces, sanitise input, safe redirects. -->
|
|
||||||
<rule ref="WordPress.Security">
|
|
||||||
<exclude-pattern>/scripts/*</exclude-pattern>
|
|
||||||
</rule>
|
|
||||||
|
|
||||||
<!-- Database: prepared queries, WordPress's database layer. -->
|
|
||||||
<rule ref="WordPress.DB">
|
|
||||||
<exclude-pattern>/scripts/*</exclude-pattern>
|
|
||||||
</rule>
|
|
||||||
|
|
||||||
<!-- WordPress APIs: deprecated, discouraged, alternatives, i18n, enqueues. -->
|
|
||||||
<rule ref="WordPress.WP">
|
|
||||||
<exclude-pattern>/scripts/*</exclude-pattern>
|
|
||||||
<!-- Capabilities from other plugins are checked in review. -->
|
|
||||||
<exclude name="WordPress.WP.Capabilities.Unknown"/>
|
|
||||||
<!-- Copy is checked in review; "WordPress" in slugs and URLs is not a typo. -->
|
|
||||||
<exclude name="WordPress.WP.CapitalPDangit"/>
|
|
||||||
</rule>
|
|
||||||
<rule ref="WordPress.WP.I18n">
|
|
||||||
<properties>
|
|
||||||
<property name="text_domain" type="array">
|
|
||||||
<element value="wp-plugin-starter-template"/>
|
|
||||||
</property>
|
|
||||||
</properties>
|
|
||||||
</rule>
|
|
||||||
<rule ref="WordPress.DateTime">
|
|
||||||
<exclude-pattern>/scripts/*</exclude-pattern>
|
|
||||||
</rule>
|
|
||||||
<rule ref="WordPress.PHP">
|
|
||||||
<exclude-pattern>/scripts/*</exclude-pattern>
|
|
||||||
<!-- Yoda conditions are a formatting preference. -->
|
|
||||||
<exclude name="WordPress.PHP.YodaConditions"/>
|
|
||||||
</rule>
|
|
||||||
<rule ref="WordPress.CodeAnalysis">
|
|
||||||
<exclude-pattern>/scripts/*</exclude-pattern>
|
|
||||||
</rule>
|
|
||||||
|
|
||||||
<!-- Everything global is prefixed, so it cannot clash with other plugins. -->
|
|
||||||
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
|
|
||||||
<exclude-pattern>/scripts/*</exclude-pattern>
|
|
||||||
<properties>
|
|
||||||
<property name="prefixes" type="array">
|
|
||||||
<element value="wpstarter"/>
|
|
||||||
<element value="WPStarter"/>
|
|
||||||
<!-- The shared GitHub updater (includes/github-updater/), the same in every plugin. -->
|
|
||||||
<element value="wpallstars"/>
|
|
||||||
<element value="WPAllStars"/>
|
|
||||||
</property>
|
|
||||||
</properties>
|
|
||||||
</rule>
|
|
||||||
<rule ref="WordPress.NamingConventions.ValidHookName">
|
|
||||||
<exclude-pattern>/scripts/*</exclude-pattern>
|
|
||||||
<!-- Some plugins name their hooks with slashes; firing theirs needs their names. -->
|
|
||||||
<exclude name="WordPress.NamingConventions.ValidHookName.UseUnderscores"/>
|
|
||||||
</rule>
|
|
||||||
|
|
||||||
<!-- PHP that WordPress.org does not allow, or code that cannot be right. -->
|
|
||||||
<rule ref="Generic.PHP.BacktickOperator"/>
|
|
||||||
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
|
|
||||||
<rule ref="Generic.PHP.DisallowAlternativePHPTags"/>
|
|
||||||
<rule ref="Generic.PHP.DiscourageGoto"/>
|
|
||||||
<rule ref="Generic.Files.ByteOrderMark"/>
|
|
||||||
<rule ref="Generic.Files.LineEndings"/>
|
|
||||||
<rule ref="Generic.CodeAnalysis.EmptyStatement"/>
|
|
||||||
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
|
|
||||||
<rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
|
|
||||||
<rule ref="Squiz.PHP.NonExecutableCode"/>
|
|
||||||
<rule ref="Squiz.PHP.Eval"/>
|
|
||||||
</ruleset>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
parameters:
|
|
||||||
ignoreErrors: []
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# PHPStan settings for WP Plugin Starter's own files, added to phpstan.neon.dist
|
|
||||||
# (which holds the settings every plugin made from the starter shares).
|
|
||||||
# Add the plugin's own folders under paths (for example blocks) and each
|
|
||||||
# ignore with the identifier, the file and the reason.
|
|
||||||
parameters:
|
|
||||||
ignoreErrors:
|
|
||||||
# The starter has no features, tabs to rename or helpers yet:
|
|
||||||
# WPStarter_Setup's lists are empty and its methods do nothing, so
|
|
||||||
# PHPStan reads the core code that uses them as having no effect.
|
|
||||||
# A plugin made from the starter fills them and drops these ignores.
|
|
||||||
-
|
|
||||||
identifiers:
|
|
||||||
- foreach.emptyArray
|
|
||||||
- staticMethod.resultUnused
|
|
||||||
path: includes/class-wpstarter.php
|
|
||||||
-
|
|
||||||
identifier: staticMethod.resultUnused
|
|
||||||
path: admin/settings.php
|
|
||||||
-
|
|
||||||
identifier: function.impossibleType
|
|
||||||
message: '#array_key_exists\(\) with mixed and array\{\}#'
|
|
||||||
path: includes/class-wpstarter-settings.php
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# PHPStan: finds calls to functions, methods and classes that do not exist,
|
|
||||||
# wrong argument counts and types, and code that cannot run, using the
|
|
||||||
# WordPress stubs (szepeviktor/phpstan-wordpress) and PHP 7.4's functions.
|
|
||||||
#
|
|
||||||
# phpstan-baseline.neon is empty: every finding fails the check. Fix the
|
|
||||||
# code. Where PHPStan or the stubs are wrong, add an entry under
|
|
||||||
# ignoreErrors with the identifier, the file and the reason: here for the
|
|
||||||
# files every plugin made from the starter shares, in phpstan-plugin.neon
|
|
||||||
# for this plugin's own files and paths. Never put findings in the baseline
|
|
||||||
# to get a change through.
|
|
||||||
includes:
|
|
||||||
- phpstan-baseline.neon
|
|
||||||
- phpstan-plugin.neon
|
|
||||||
|
|
||||||
parameters:
|
|
||||||
level: 5
|
|
||||||
phpVersion: 70400
|
|
||||||
# Result cache, kept between CI runs so only changed files are analysed again.
|
|
||||||
tmpDir: .cache/phpstan
|
|
||||||
paths:
|
|
||||||
- wp-plugin-starter-template.php
|
|
||||||
- uninstall.php
|
|
||||||
- includes
|
|
||||||
- admin
|
|
||||||
bootstrapFiles:
|
|
||||||
- scripts/phpstan-bootstrap.php
|
|
||||||
reportUnmatchedIgnoredErrors: true
|
|
||||||
treatPhpDocTypesAsCertain: false
|
|
||||||
ignoreErrors:
|
|
||||||
# Closures that change variables passed by reference (use (&$x)):
|
|
||||||
# PHPStan does not see the changes, so it reads them as never set.
|
|
||||||
-
|
|
||||||
identifiers:
|
|
||||||
- if.alwaysFalse
|
|
||||||
- identical.alwaysFalse
|
|
||||||
path: admin/includes/class-readme-manager.php
|
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<phpunit
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
convertErrorsToExceptions="true"
|
||||||
|
convertNoticesToExceptions="true"
|
||||||
|
convertWarningsToExceptions="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="unit">
|
||||||
|
<directory prefix="test-" suffix=".php">./tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory suffix=".php">./includes</directory>
|
||||||
|
<directory suffix=".php">./admin</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory suffix=".php">./vendor</directory>
|
||||||
|
<directory suffix=".php">./tests</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
||||||
+226
-27
@@ -1,52 +1,251 @@
|
|||||||
=== WP Plugin Starter ===
|
=== WordPress Plugin Starter Template for AI Coding ===
|
||||||
Contributors: wpallstars
|
Contributors: wpallstars
|
||||||
Donate link: https://buymeacoffee.com/marcusquinn
|
Donate link: https://www.wpallstars.com/
|
||||||
Tags: starter, boilerplate, settings, developer, template
|
Tags: starter, template, boilerplate, plugin development, ai coding
|
||||||
Requires at least: 6.2
|
Requires at least: 5.0
|
||||||
Tested up to: 7.1
|
Tested up to: 6.4
|
||||||
Requires PHP: 7.4
|
Requires PHP: 7.0
|
||||||
Stable tag: 1.0.0
|
Stable tag: 0.1.5
|
||||||
License: GPLv2 or later
|
License: GPL-2.0+
|
||||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
|
||||||
A clean start for a WordPress plugin: a settings screen, a Read Me tab, updates from GitHub and release scripts, ready for your features.
|
A comprehensive starter template for WordPress plugins with best practices for AI-assisted development.
|
||||||
|
|
||||||
== Description ==
|
== Description ==
|
||||||
|
|
||||||
WP Plugin Starter is what wpallstars plugins are made from. It has no features of its own: it holds the parts every plugin needs, so a new plugin starts with them working.
|
The WordPress Plugin Starter Template provides a solid foundation for developing WordPress plugins. It incorporates best practices, modern coding standards, and a comprehensive structure that makes it easy to get started with plugin development.
|
||||||
|
|
||||||
* **A settings screen** (Settings → WP Plugin Starter) that features fill by declaring their settings, saved instantly, searchable, in tabs. With no features yet it shows one empty tab.
|
This template is based on the experience gained from developing the "Fix 'Plugin file does not exist' Notices" plugin and other successful WordPress plugins.
|
||||||
* **A Read Me tab** that shows the plugin's README.md, banner included.
|
|
||||||
* **Features as classes**, off by default, with settings, hooks, one-off imports from the plugins they replace and clean uninstall.
|
|
||||||
* **Release and check scripts**: lint, smoke test, release build, preflight and Plugin Check.
|
|
||||||
|
|
||||||
Start a plugin from it on GitHub (wpallstars/wp-plugin-starter-template-for-ai-coding): the Read Me tab explains how.
|
= Key Features =
|
||||||
|
|
||||||
= Built with AI =
|
* **Object-Oriented Architecture**: Well-structured, maintainable code using OOP principles
|
||||||
|
* **Namespace Support**: Modern PHP namespacing for better organization and avoiding conflicts
|
||||||
|
* **Comprehensive Documentation**: Detailed documentation for both users and developers
|
||||||
|
* **Testing Framework**: PHPUnit setup for unit testing and Cypress for e2e testing
|
||||||
|
* **Internationalization Ready**: Full support for translation and localization
|
||||||
|
* **Update Source Selection**: Choose between WordPress.org, GitHub, or Gitea for plugin updates
|
||||||
|
* **AI Workflow Documentation**: Detailed guides for AI-assisted development
|
||||||
|
* **Wiki Documentation**: Ready-to-use wiki structure for comprehensive documentation
|
||||||
|
* **Multisite Compatible**: Fully tested and compatible with WordPress multisite installations
|
||||||
|
|
||||||
WP Plugin Starter is built and maintained with aidevops (https://aidevops.sh), the same developer's open-source AI harness for creating and managing anything online with AI.
|
= How to Use This Template =
|
||||||
|
|
||||||
|
1. Clone or download this repository
|
||||||
|
2. **Important**: Begin by reading the Starter Prompt file in the wiki for detailed instructions
|
||||||
|
3. Add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context (pin them if possible)
|
||||||
|
4. Use the prompt in the Starter Prompt file to guide the AI in customizing the template for your plugin
|
||||||
|
5. Rename files and update namespaces to match your plugin
|
||||||
|
6. Customize the functionality for your specific needs
|
||||||
|
7. Update documentation to reflect your plugin's features
|
||||||
|
8. Build and test your plugin
|
||||||
|
|
||||||
|
For detailed instructions, see the [Starter Prompt](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/wiki/Starter-Prompt) file in the wiki.
|
||||||
|
|
||||||
|
= AI-Assisted Development =
|
||||||
|
|
||||||
|
This template includes comprehensive documentation for AI-assisted development:
|
||||||
|
|
||||||
|
* **.ai-assistant.md**: Guide for AI assistants to understand the project structure
|
||||||
|
* **.ai-workflows/**: Detailed workflow documentation for common development tasks
|
||||||
|
* **Starter Prompt**: Comprehensive prompt for AI tools to help customize the template (available in the wiki)
|
||||||
|
|
||||||
|
**Important**: For the best AI assistance, add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context. In most AI IDEs, you can pin these files to ensure they're considered in each message.
|
||||||
|
|
||||||
|
= Development Environment =
|
||||||
|
|
||||||
|
This template includes configuration for WordPress Environment (wp-env) to make local development easier:
|
||||||
|
|
||||||
|
1. Install Node.js dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the WordPress environment:
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
3. For multisite testing:
|
||||||
|
```bash
|
||||||
|
npm run multisite
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Access your local WordPress site at http://localhost:8888 (admin credentials: admin/password)
|
||||||
|
|
||||||
|
= Testing =
|
||||||
|
|
||||||
|
The template includes both PHP unit tests and end-to-end tests:
|
||||||
|
|
||||||
|
**PHP Unit Tests**
|
||||||
|
|
||||||
|
1. Install Composer dependencies:
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run PHP unit tests:
|
||||||
|
```bash
|
||||||
|
npm run test:php
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Check PHP coding standards:
|
||||||
|
```bash
|
||||||
|
npm run lint:php
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Fix PHP coding standards issues:
|
||||||
|
```bash
|
||||||
|
npm run fix:php
|
||||||
|
```
|
||||||
|
|
||||||
|
**End-to-End Tests**
|
||||||
|
|
||||||
|
1. Start the WordPress environment:
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run Cypress tests in interactive mode:
|
||||||
|
```bash
|
||||||
|
npm run test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run Cypress tests in headless mode:
|
||||||
|
```bash
|
||||||
|
npm run test:e2e:headless
|
||||||
|
```
|
||||||
|
|
||||||
|
= Support & Feedback =
|
||||||
|
|
||||||
|
If you need help with this template, there are several ways to get support:
|
||||||
|
|
||||||
|
* [GitHub Issues](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/issues)
|
||||||
|
* [Gitea Issues](https://gitea.wpallstars.com/wpallstars/wp-plugin-starter-template-for-ai-coding/issues)
|
||||||
|
|
||||||
|
= Contributing =
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
1. Fork the repository on [GitHub](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/) or [Gitea](https://gitea.wpallstars.com/wpallstars/wp-plugin-starter-template-for-ai-coding/)
|
||||||
|
2. Create your feature branch: `git checkout -b feature/amazing-feature`
|
||||||
|
3. Commit your changes: `git commit -m 'Add some amazing feature'`
|
||||||
|
4. Push to the branch: `git push origin feature/amazing-feature`
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
== Installation ==
|
== Installation ==
|
||||||
|
|
||||||
1. Upload the `wp-plugin-starter-template` folder to `/wp-content/plugins/`, or install the zip from Plugins → Add New → Upload Plugin.
|
1. Clone or download this repository
|
||||||
2. Activate it, then open Settings → WP Plugin Starter.
|
2. Read the Starter Prompt file in the wiki for detailed instructions
|
||||||
|
3. Add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context
|
||||||
|
4. Use the prompt in the Starter Prompt file to guide the AI in customizing the template
|
||||||
|
5. Rename files and update namespaces to match your plugin
|
||||||
|
6. Customize the functionality for your specific needs
|
||||||
|
7. Update documentation to reflect your plugin's features
|
||||||
|
8. Build and test your plugin
|
||||||
|
|
||||||
|
For detailed instructions, see the [Starter Prompt](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/wiki/Starter-Prompt) file in the wiki.
|
||||||
|
|
||||||
== Frequently Asked Questions ==
|
== Frequently Asked Questions ==
|
||||||
|
|
||||||
= Does it do anything on its own? =
|
= How do I customize this template for my plugin? =
|
||||||
|
|
||||||
No. It adds an empty settings screen and a Read Me tab, ready for a plugin's features.
|
See the [Starter Prompt](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/wiki/Starter-Prompt) file in the wiki for detailed instructions on customizing this template for your specific plugin needs. Make sure to add the .ai-assistant.md file and .ai-workflows/ directory to your AI IDE chat context for the best results.
|
||||||
|
|
||||||
= Does it contact other services? =
|
= What files do I need to update with my plugin information? =
|
||||||
|
|
||||||
No. The WordPress.org build contacts nothing outside WordPress.
|
The main files you need to update include:
|
||||||
|
1. Main plugin file (rename and update header)
|
||||||
|
2. README.md
|
||||||
|
3. readme.txt
|
||||||
|
4. CHANGELOG.md
|
||||||
|
5. composer.json
|
||||||
|
6. package.json
|
||||||
|
7. languages/pot file
|
||||||
|
8. .github/workflows/
|
||||||
|
9. .wiki/
|
||||||
|
10. .ai-assistant.md
|
||||||
|
11. includes/plugin.php
|
||||||
|
12. includes/core.php
|
||||||
|
13. admin/lib/admin.php
|
||||||
|
|
||||||
|
= How do I build and test my plugin? =
|
||||||
|
|
||||||
|
Use the included build.sh script to create a deployable version of your plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh {VERSION}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a ZIP file that you can install in WordPress.
|
||||||
|
|
||||||
|
= How do I add custom functionality to my plugin? =
|
||||||
|
|
||||||
|
Customize the includes/core.php file to implement your core functionality and the admin/lib/admin.php file for admin-specific functionality.
|
||||||
|
|
||||||
|
= How do I update the namespace for my plugin? =
|
||||||
|
|
||||||
|
You'll need to update all namespace references from WPALLSTARS\PluginStarterTemplate to your own namespace throughout the codebase.
|
||||||
|
|
||||||
|
= How do I update the text domain for my plugin? =
|
||||||
|
|
||||||
|
You'll need to update all text domain references from 'wp-plugin-starter-template' to your own text domain throughout the codebase.
|
||||||
|
|
||||||
|
= Is this template compatible with WordPress multisite? =
|
||||||
|
|
||||||
|
Yes, this template is fully compatible with WordPress multisite installations. You can test multisite compatibility by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run multisite
|
||||||
|
```
|
||||||
|
|
||||||
|
== Screenshots ==
|
||||||
|
|
||||||
|
1. This is a placeholder for your plugin's screenshots. Replace with actual screenshots of your plugin in action.
|
||||||
|
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
= 1.0.0 =
|
= 0.1.5 =
|
||||||
* A fresh start, made from the parts every wpallstars plugin needs: settings screen, Read Me tab, features as classes, release and check scripts. Earlier versions are retired; start again from this one.
|
* Fixed: Release workflow to use correct plugin directory name
|
||||||
|
* Added: Testing setup with wp-env and Cypress
|
||||||
|
* Added: Multisite compatibility
|
||||||
|
* Added: npm scripts for development and testing
|
||||||
|
|
||||||
|
= 0.1.3 =
|
||||||
|
* Added: Improved AI IDE context recommendations in documentation
|
||||||
|
* Enhanced: Starter Prompt with guidance on pinning .ai-assistant.md and .ai-workflows/
|
||||||
|
* Moved: Starter Prompt to the wiki for better organization
|
||||||
|
* Updated: README.md and readme.txt with AI IDE context recommendations
|
||||||
|
* Improved: Documentation for AI-assisted development
|
||||||
|
|
||||||
|
= 0.1.2 =
|
||||||
|
* Added: STARTER-PROMPT.md with comprehensive guide for customizing the template
|
||||||
|
* Updated: Documentation files with improved instructions
|
||||||
|
* Added: Additional AI workflow files for better development guidance
|
||||||
|
|
||||||
|
= 0.1.1 =
|
||||||
|
* Updated: LICENSE file with correct GPL-2.0 text
|
||||||
|
|
||||||
|
= 0.1.0 =
|
||||||
|
* Initial release with basic template structure
|
||||||
|
* Added: Core plugin architecture with OOP approach
|
||||||
|
* Added: Admin interface components and styling
|
||||||
|
* Added: Update mechanism with multiple source options
|
||||||
|
* Added: Documentation templates for users and developers
|
||||||
|
* Added: AI workflow documentation for AI-assisted development
|
||||||
|
* Added: GitHub Actions workflows for automated tasks
|
||||||
|
* Added: Wiki documentation templates
|
||||||
|
|
||||||
== Upgrade Notice ==
|
== Upgrade Notice ==
|
||||||
|
|
||||||
= 1.0.0 =
|
= 0.1.5 =
|
||||||
A fresh start. Earlier versions of the starter are retired; start a new plugin from this one.
|
Fixed release workflow and added testing setup with wp-env and Cypress, multisite compatibility, and npm scripts for development and testing.
|
||||||
|
|
||||||
|
= 0.1.3 =
|
||||||
|
Added improved AI IDE context recommendations and moved Starter Prompt to the wiki with guidance on pinning .ai-assistant.md and .ai-workflows/ files.
|
||||||
|
|
||||||
|
= 0.1.1 =
|
||||||
|
Updated LICENSE file with correct GPL-2.0 text.
|
||||||
|
|
||||||
|
= 0.1.0 =
|
||||||
|
Initial release with basic template structure and core functionality.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Reference Plugins
|
||||||
|
|
||||||
|
This directory is intended to contain reference plugins that can be used as inspiration for development. These plugins are not part of the actual plugin but serve as examples of best practices and implementation patterns.
|
||||||
|
|
||||||
|
## Recommended Reference Plugins
|
||||||
|
|
||||||
|
1. **[Fix 'Plugin file does not exist' Notices](https://github.com/wpallstars/wp-fix-plugin-does-not-exist-notices)**
|
||||||
|
- This plugin is the original inspiration for this starter template
|
||||||
|
- Demonstrates clean, modular code structure
|
||||||
|
- Shows implementation of Git Updater integration
|
||||||
|
- Provides examples of WordPress admin UI integration
|
||||||
|
|
||||||
|
2. **[WordPress Plugin Boilerplate](https://github.com/devinvinson/WordPress-Plugin-Boilerplate)**
|
||||||
|
- A standardized, organized, object-oriented foundation for building high-quality WordPress plugins
|
||||||
|
- Follows WordPress coding standards
|
||||||
|
- Provides a clear separation of concerns
|
||||||
|
|
||||||
|
3. **[CMB2](https://github.com/CMB2/CMB2)**
|
||||||
|
- Excellent example of metabox and form handling
|
||||||
|
- Well-documented codebase
|
||||||
|
- Demonstrates extensibility and API design
|
||||||
|
|
||||||
|
4. **[WooCommerce](https://github.com/woocommerce/woocommerce)**
|
||||||
|
- Example of a large-scale WordPress plugin
|
||||||
|
- Demonstrates advanced features like custom post types, taxonomies, and REST API integration
|
||||||
|
- Shows how to structure a complex plugin
|
||||||
|
|
||||||
|
## How to Use Reference Plugins
|
||||||
|
|
||||||
|
1. **Study the Code Structure**:
|
||||||
|
- Examine how the plugin is organized
|
||||||
|
- Note the separation of concerns
|
||||||
|
- Observe naming conventions and coding standards
|
||||||
|
|
||||||
|
2. **Analyze Implementation Patterns**:
|
||||||
|
- Look at how common WordPress patterns are implemented
|
||||||
|
- Study how the plugin integrates with WordPress hooks
|
||||||
|
- Observe error handling and security practices
|
||||||
|
|
||||||
|
3. **Adapt, Don't Copy**:
|
||||||
|
- Use these plugins as inspiration, not as code to copy directly
|
||||||
|
- Adapt patterns to fit your specific needs
|
||||||
|
- Ensure you understand the code before using similar approaches
|
||||||
|
|
||||||
|
## Adding Reference Plugins
|
||||||
|
|
||||||
|
To add a reference plugin to this directory:
|
||||||
|
|
||||||
|
1. Create a subdirectory with the plugin name
|
||||||
|
2. Include a README.md file explaining why this plugin is included as a reference
|
||||||
|
3. Add relevant code snippets or links to the original repository
|
||||||
|
4. Update this README.md file to include the new reference plugin
|
||||||
|
|
||||||
|
## Note on Licensing
|
||||||
|
|
||||||
|
When using code from reference plugins, always respect their licensing terms. Most WordPress plugins are licensed under GPL or compatible licenses, but always verify before using any code.
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Build the plugin's banner from its source, .wordpress-org/banner.svg.
|
|
||||||
#
|
|
||||||
# admin/images/banner.svg
|
|
||||||
# Shipped with the plugin, shown at the top of the Read Me tab (and of
|
|
||||||
# README.md on GitHub). Text is turned into shapes, so it looks the same
|
|
||||||
# without the Zilla Slab font.
|
|
||||||
# .wordpress-org/banner-772x250.png
|
|
||||||
# .wordpress-org/banner-1544x500.png
|
|
||||||
# For the WordPress.org SVN assets/ folder (not in the plugin zip).
|
|
||||||
#
|
|
||||||
# Usage: scripts/build-banner.sh
|
|
||||||
#
|
|
||||||
# Needs Inkscape 1.x (INKSCAPE=/path/to/inkscape to choose one) and the Zilla Slab
|
|
||||||
# font installed (wpallstars.com's heading font).
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SOURCE=".wordpress-org/banner.svg"
|
|
||||||
|
|
||||||
die() {
|
|
||||||
local message="$1"
|
|
||||||
printf 'build-banner: %s\n' "$message" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_inkscape() {
|
|
||||||
if [[ -n "${INKSCAPE:-}" ]]; then
|
|
||||||
printf '%s\n' "$INKSCAPE"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if command -v inkscape >/dev/null 2>&1; then
|
|
||||||
command -v inkscape
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [[ -x /Applications/Inkscape.app/Contents/MacOS/inkscape ]]; then
|
|
||||||
printf '%s\n' /Applications/Inkscape.app/Contents/MacOS/inkscape
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
export_png() {
|
|
||||||
local inkscape="$1"
|
|
||||||
local width="$2"
|
|
||||||
local height="$3"
|
|
||||||
"$inkscape" "$SOURCE" --export-type=png --export-background-opacity=1 \
|
|
||||||
--export-width="$width" --export-height="$height" \
|
|
||||||
--export-filename=".wordpress-org/banner-${width}x${height}.png"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
|
||||||
[[ -f "$SOURCE" ]] || die "missing $SOURCE"
|
|
||||||
|
|
||||||
local inkscape
|
|
||||||
inkscape="$(find_inkscape)" || die "Inkscape not found; set INKSCAPE=/path/to/inkscape"
|
|
||||||
|
|
||||||
if command -v fc-list >/dev/null 2>&1; then
|
|
||||||
local families
|
|
||||||
families="$(fc-list : family)"
|
|
||||||
grep -qi '^Zilla Slab' <<<"$families" || die "the Zilla Slab font is not installed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
"$inkscape" "$SOURCE" --export-text-to-path --export-plain-svg \
|
|
||||||
--export-filename=admin/images/banner.svg
|
|
||||||
export_png "$inkscape" 1544 500
|
|
||||||
export_png "$inkscape" 772 250
|
|
||||||
|
|
||||||
ls -l admin/images/banner.svg .wordpress-org/banner-1544x500.png .wordpress-org/banner-772x250.png
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Build the release zips of the plugin from a Git ref.
|
|
||||||
#
|
|
||||||
# Two builds of the same version, both with one {slug}/ folder inside (the
|
|
||||||
# slug is the main file's name, scripts/lib/plugin.sh):
|
|
||||||
# {slug}-X.Y.Z.zip
|
|
||||||
# GitHub release asset: the files in Git, less .distignore.
|
|
||||||
# wordpress-org-{slug}-X.Y.Z.zip
|
|
||||||
# WordPress.org build: the same, less the files in .distignore-wporg and
|
|
||||||
# the GitHub updater header lines. Its name does not start with the
|
|
||||||
# slug, so no updater picks it even if it is attached to a GitHub
|
|
||||||
# release by mistake (it takes the first asset whose name starts with
|
|
||||||
# the plugin slug). Never attach it to a GitHub release.
|
|
||||||
# SHA256SUMS
|
|
||||||
#
|
|
||||||
# Files come from the Git ref (git archive), never from the working tree, so
|
|
||||||
# uncommitted changes and untracked files are never included.
|
|
||||||
#
|
|
||||||
# Usage: scripts/build-release.sh [--ref REF] [--out DIR] [--quiet]
|
|
||||||
# --ref REF Commit, branch or tag to build (default: HEAD).
|
|
||||||
# --out DIR Output folder (default: dist/ in the repository, gitignored).
|
|
||||||
# --quiet Print only the paths of the zips.
|
|
||||||
#
|
|
||||||
# Needs git, rsync, zip, unzip and shasum or sha256sum.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
umask 022
|
|
||||||
export TZ=UTC
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
readonly SCRIPT_DIR
|
|
||||||
# shellcheck source=scripts/lib/plugin.sh disable=SC1091 # followed only with -x
|
|
||||||
. "$SCRIPT_DIR/lib/plugin.sh"
|
|
||||||
readonly WPORG_IGNORE=".distignore-wporg"
|
|
||||||
# Header lines read only by GitHub updaters (ours and Git Updater); left out of the WordPress.org build.
|
|
||||||
readonly WPORG_STRIP_HEADERS='GitHub Plugin URI|Primary Branch|Release Asset'
|
|
||||||
|
|
||||||
TMP_DIR=""
|
|
||||||
SLUG=""
|
|
||||||
MAIN_FILE=""
|
|
||||||
|
|
||||||
die() {
|
|
||||||
local message="$1"
|
|
||||||
printf 'build-release: %s\n' "$message" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
need() {
|
|
||||||
local tool="$1"
|
|
||||||
command -v "$tool" >/dev/null 2>&1 || die "needs $tool"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Version: header of the main file at a commit.
|
|
||||||
version_at() {
|
|
||||||
local sha="$1"
|
|
||||||
git show "$sha:$MAIN_FILE" | sed -n 's/^[[:space:]*]*Version:[[:space:]]*\([^[:space:]]*\).*/\1/p' | head -n 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
sha256_of() {
|
|
||||||
local file="$1"
|
|
||||||
if command -v shasum >/dev/null 2>&1; then
|
|
||||||
shasum -a 256 "$file" | cut -d' ' -f1
|
|
||||||
else
|
|
||||||
sha256sum "$file" | cut -d' ' -f1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Zip <folder>/<SLUG> as <zip>, with the slug folder at the top. Fixed times,
|
|
||||||
# modes (umask) and entry order make the same ref give the same zip anywhere.
|
|
||||||
make_zip() {
|
|
||||||
local folder="$1"
|
|
||||||
local zip_path="$2"
|
|
||||||
local partial="$zip_path.partial"
|
|
||||||
rm -f "$zip_path" "$partial"
|
|
||||||
(cd "$folder" && find "$SLUG" -exec touch -h -t 202001010000 {} + && find "$SLUG" | LC_ALL=C sort | zip -qX "$partial" -@)
|
|
||||||
mv "$partial" "$zip_path"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove the GitHub updater header lines from the main file of a build.
|
|
||||||
strip_updater_headers() {
|
|
||||||
local main="$1"
|
|
||||||
local stripped="$main.tmp"
|
|
||||||
grep -Ev "^[[:space:]*]*($WPORG_STRIP_HEADERS):" "$main" >"$stripped"
|
|
||||||
mv "$stripped" "$main"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local ref="HEAD"
|
|
||||||
local out=""
|
|
||||||
local quiet=0
|
|
||||||
local arg
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
arg="$1"
|
|
||||||
case "$arg" in
|
|
||||||
--ref)
|
|
||||||
[ $# -ge 2 ] || die "--ref needs a value"
|
|
||||||
ref="$2"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--out)
|
|
||||||
[ $# -ge 2 ] || die "--out needs a value"
|
|
||||||
out="$2"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--quiet) quiet=1 ;;
|
|
||||||
-h | --help)
|
|
||||||
usage
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*) die "unknown argument: $arg" ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
local tool
|
|
||||||
for tool in git rsync zip unzip; do
|
|
||||||
need "$tool"
|
|
||||||
done
|
|
||||||
|
|
||||||
local root sha version
|
|
||||||
root="$(git rev-parse --show-toplevel)" || die "run this inside a checkout of the plugin"
|
|
||||||
cd "$root"
|
|
||||||
sha="$(git rev-parse --verify --quiet "$ref^{commit}")" || die "not a commit: $ref"
|
|
||||||
plugin_identity "$sha" || die "cannot tell which plugin this is at $ref"
|
|
||||||
SLUG="$PLUGIN_SLUG"
|
|
||||||
MAIN_FILE="$PLUGIN_MAIN_FILE"
|
|
||||||
version="$(version_at "$sha")"
|
|
||||||
[ -n "$version" ] || die "no Version: header in $MAIN_FILE at $ref"
|
|
||||||
|
|
||||||
if [ "$ref" = "HEAD" ] && [ -n "$(git status --porcelain --untracked-files=no)" ] && [ "$quiet" -eq 0 ]; then
|
|
||||||
printf 'Note: uncommitted changes are not in the build (it is made from HEAD).\n'
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "$out" ] || out="$root/dist"
|
|
||||||
mkdir -p "$out"
|
|
||||||
out="$(cd "$out" && pwd)"
|
|
||||||
|
|
||||||
trap cleanup EXIT
|
|
||||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/$SLUG-build.XXXXXX")"
|
|
||||||
mkdir -p "$TMP_DIR/src" "$TMP_DIR/github/$SLUG" "$TMP_DIR/wporg/$SLUG"
|
|
||||||
|
|
||||||
git archive --format=tar "$sha" | tar -x -C "$TMP_DIR/src"
|
|
||||||
# .distignore lists itself and is export-ignored, so read it from Git.
|
|
||||||
git show "$sha:.distignore" >"$TMP_DIR/distignore" || die ".distignore missing at $ref"
|
|
||||||
rsync -a --exclude-from="$TMP_DIR/distignore" "$TMP_DIR/src/" "$TMP_DIR/github/$SLUG/"
|
|
||||||
[ -f "$TMP_DIR/github/$SLUG/$MAIN_FILE" ] || die "$MAIN_FILE is not in the build; check .distignore"
|
|
||||||
|
|
||||||
if git cat-file -e "$sha:$WPORG_IGNORE" 2>/dev/null; then
|
|
||||||
git show "$sha:$WPORG_IGNORE" >"$TMP_DIR/wporg-ignore"
|
|
||||||
else
|
|
||||||
: >"$TMP_DIR/wporg-ignore"
|
|
||||||
fi
|
|
||||||
rsync -a --exclude-from="$TMP_DIR/wporg-ignore" "$TMP_DIR/github/$SLUG/" "$TMP_DIR/wporg/$SLUG/"
|
|
||||||
strip_updater_headers "$TMP_DIR/wporg/$SLUG/$MAIN_FILE"
|
|
||||||
|
|
||||||
local github_zip="$out/$SLUG-$version.zip"
|
|
||||||
local wporg_zip="$out/wordpress-org-$SLUG-$version.zip"
|
|
||||||
make_zip "$TMP_DIR/github" "$github_zip"
|
|
||||||
make_zip "$TMP_DIR/wporg" "$wporg_zip"
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '%s %s\n' "$(sha256_of "$github_zip")" "$(basename "$github_zip")"
|
|
||||||
printf '%s %s\n' "$(sha256_of "$wporg_zip")" "$(basename "$wporg_zip")"
|
|
||||||
} >"$out/SHA256SUMS"
|
|
||||||
|
|
||||||
if [ "$quiet" -eq 1 ]; then
|
|
||||||
printf '%s\n%s\n' "$github_zip" "$wporg_zip"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
printf '%s %s from %s (%s)\n' "$PLUGIN_NAME" "$version" "$ref" "${sha:0:12}"
|
|
||||||
printf ' GitHub release: %s (%s files)\n' "$github_zip" "$(unzip -Z1 "$github_zip" | grep -cv '/$')"
|
|
||||||
printf ' WordPress.org: %s (%s files)\n' "$wporg_zip" "$(unzip -Z1 "$wporg_zip" | grep -cv '/$')"
|
|
||||||
printf ' Checksums: %s\n' "$out/SHA256SUMS"
|
|
||||||
printf 'Check them with: scripts/preflight-release.sh --ref %s\n' "$ref"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Core files: the same in every plugin made from the starter, apart from the
|
|
||||||
# names (STANDARDS.md → Structure). scripts/sync-core.sh copies them from
|
|
||||||
# the starter with the plugin's names, and --check lists any that differ.
|
|
||||||
# Paths use the starter's names; a trailing / means every file below.
|
|
||||||
# Change a core file in the starter first, then sync each plugin.
|
|
||||||
|
|
||||||
# Bootstrap, feature registry, base feature and settings store.
|
|
||||||
includes/class-wpstarter.php
|
|
||||||
includes/class-wpstarter-feature.php
|
|
||||||
includes/class-wpstarter-settings.php
|
|
||||||
|
|
||||||
# Shared GitHub updater: only the newest copy on a site runs.
|
|
||||||
includes/github-updater/
|
|
||||||
|
|
||||||
# Admin screen, Read Me tab and the Plugins screen notes for replaced plugins.
|
|
||||||
admin/settings.php
|
|
||||||
admin/data/readme.php
|
|
||||||
admin/includes/class-admin-manager.php
|
|
||||||
admin/includes/class-settings-manager.php
|
|
||||||
admin/includes/class-readme-manager.php
|
|
||||||
admin/includes/class-replaced-plugins.php
|
|
||||||
admin/css/wpstarter-admin.css
|
|
||||||
admin/js/wpstarter-admin.js
|
|
||||||
|
|
||||||
# Build, check, release and preview scripts.
|
|
||||||
scripts/build-banner.sh
|
|
||||||
scripts/build-release.sh
|
|
||||||
scripts/core-files.txt
|
|
||||||
scripts/lib/plugin.sh
|
|
||||||
scripts/lint.sh
|
|
||||||
scripts/phpstan-bootstrap.php
|
|
||||||
scripts/plugin-check.sh
|
|
||||||
scripts/preflight-release.sh
|
|
||||||
scripts/preview-site.sh
|
|
||||||
scripts/rename-plugin.sh
|
|
||||||
scripts/replaced-plugins.php
|
|
||||||
scripts/smoke-test.sh
|
|
||||||
scripts/sync-core.sh
|
|
||||||
|
|
||||||
# CI and tool configuration.
|
|
||||||
.github/
|
|
||||||
.distignore
|
|
||||||
.gitattributes
|
|
||||||
.gitignore
|
|
||||||
.gitleaks.toml
|
|
||||||
composer.json
|
|
||||||
phpcs.xml.dist
|
|
||||||
phpstan.neon.dist
|
|
||||||
|
|
||||||
# Shared docs.
|
|
||||||
STANDARDS.md
|
|
||||||
DEVELOPMENT.md
|
|
||||||
RELEASING.md
|
|
||||||
CONTRIBUTING.md
|
|
||||||
SECURITY.md
|
|
||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# WordPress Plugin Local Deployment Script
|
||||||
|
# This script deploys the plugin to a local WordPress installation for testing
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PLUGIN_SLUG="wp-plugin-starter-template"
|
||||||
|
SOURCE_DIR="$(pwd)"
|
||||||
|
TARGET_DIR="${WP_LOCAL_PLUGIN_DIR:-/path/to/your/local/wordpress/wp-content/plugins}/$PLUGIN_SLUG"
|
||||||
|
|
||||||
|
# Check if WP_LOCAL_PLUGIN_DIR is set
|
||||||
|
if [ -z "$WP_LOCAL_PLUGIN_DIR" ]; then
|
||||||
|
echo "⚠️ Warning: WP_LOCAL_PLUGIN_DIR environment variable is not set."
|
||||||
|
echo "Please set it to your local WordPress plugins directory or edit this script."
|
||||||
|
echo "Example: export WP_LOCAL_PLUGIN_DIR=/path/to/your/local/wordpress/wp-content/plugins"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if target directory exists
|
||||||
|
if [ ! -d "$(dirname "$TARGET_DIR")" ]; then
|
||||||
|
echo "❌ Error: Target directory does not exist: $(dirname "$TARGET_DIR")"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create or clean target directory
|
||||||
|
if [ -d "$TARGET_DIR" ]; then
|
||||||
|
echo "Cleaning existing plugin directory..."
|
||||||
|
rm -rf "$TARGET_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating plugin directory..."
|
||||||
|
mkdir -p "$TARGET_DIR"
|
||||||
|
|
||||||
|
# Copy plugin files
|
||||||
|
echo "Copying plugin files..."
|
||||||
|
rsync -av --exclude=".git" --exclude=".github" --exclude=".DS_Store" \
|
||||||
|
--exclude="node_modules" --exclude="build" --exclude=".wordpress-org" \
|
||||||
|
"$SOURCE_DIR/" "$TARGET_DIR/"
|
||||||
|
|
||||||
|
# Clear WordPress transients if WP-CLI is available
|
||||||
|
if command -v wp &> /dev/null; then
|
||||||
|
echo "Clearing WordPress transients..."
|
||||||
|
wp transient delete --all --path="$(dirname "$(dirname "$TARGET_DIR")")"
|
||||||
|
else
|
||||||
|
echo "⚠️ WP-CLI not found, skipping transient clearing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Deployment successful!"
|
||||||
|
echo "Plugin deployed to: $TARGET_DIR"
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# The PLUGIN_* variables are read by the scripts that source this file.
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
# Who the plugin is, read from its main file, so the scripts in scripts/ work
|
|
||||||
# unchanged in every plugin made from the starter. Source it, then call
|
|
||||||
# plugin_identity [REF] (default HEAD). It sets:
|
|
||||||
#
|
|
||||||
# PLUGIN_MAIN_FILE the PHP file at the top of the repository with a
|
|
||||||
# "Plugin Name:" header, for example wp-plugin-starter-template.php
|
|
||||||
# PLUGIN_SLUG its name without .php: the plugin folder and text domain
|
|
||||||
# PLUGIN_NAME the Plugin Name header, for example WP Plugin Starter
|
|
||||||
# PLUGIN_PACKAGE the @package tag, which is also the class prefix
|
|
||||||
# (WPStarter: WPStarter_Settings, WPStarter::features())
|
|
||||||
# PLUGIN_CONST the constant prefix, from define('<PREFIX>_VERSION', ...)
|
|
||||||
# (WPSTARTER)
|
|
||||||
# PLUGIN_PREFIX the option, hook and transient prefix: PLUGIN_CONST in
|
|
||||||
# lower case (wpstarter)
|
|
||||||
# PLUGIN_CSS the CSS class and data attribute prefix, from the admin
|
|
||||||
# screen's "wrap <css>-wrap" class (wps: .wps-card)
|
|
||||||
# PLUGIN_REPO the GitHub Plugin URI header, owner/repo (may be empty)
|
|
||||||
#
|
|
||||||
# Files come from the Git ref, never the working tree, like the release build.
|
|
||||||
|
|
||||||
PLUGIN_MAIN_FILE=""
|
|
||||||
PLUGIN_SLUG=""
|
|
||||||
PLUGIN_NAME=""
|
|
||||||
PLUGIN_PACKAGE=""
|
|
||||||
PLUGIN_CONST=""
|
|
||||||
PLUGIN_PREFIX=""
|
|
||||||
PLUGIN_CSS=""
|
|
||||||
PLUGIN_REPO=""
|
|
||||||
|
|
||||||
# Value of a "Key: value" header line in the text, case-insensitive.
|
|
||||||
plugin_header_field() {
|
|
||||||
local text="$1"
|
|
||||||
local key="$2"
|
|
||||||
printf '%s\n' "$text" | awk -v k="$key" '
|
|
||||||
BEGIN { k = tolower(k) }
|
|
||||||
{
|
|
||||||
line = $0
|
|
||||||
sub(/^[ \t*]*/, "", line)
|
|
||||||
i = index(line, ":")
|
|
||||||
if (i > 0 && tolower(substr(line, 1, i - 1)) == k) {
|
|
||||||
v = substr(line, i + 1)
|
|
||||||
gsub(/^[ \t]+|[ \t\r]+$/, "", v)
|
|
||||||
print v
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin_identity() {
|
|
||||||
local ref="${1:-HEAD}"
|
|
||||||
local file text found=""
|
|
||||||
while IFS= read -r file; do
|
|
||||||
case "$file" in
|
|
||||||
*.php) ;;
|
|
||||||
*) continue ;;
|
|
||||||
esac
|
|
||||||
# WordPress reads the header from the first 8 KB of the file.
|
|
||||||
text="$(git show "$ref:$file" 2>/dev/null || true)"
|
|
||||||
if [ -n "$(plugin_header_field "${text:0:8192}" "Plugin Name")" ]; then
|
|
||||||
[ -z "$found" ] || {
|
|
||||||
printf 'plugin: more than one main file at %s: %s and %s\n' "$ref" "$found" "$file" >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
found="$file"
|
|
||||||
fi
|
|
||||||
done < <(git ls-tree --name-only "$ref" 2>/dev/null)
|
|
||||||
if [ -z "$found" ]; then
|
|
||||||
printf 'plugin: no PHP file with a Plugin Name: header at the top of %s\n' "$ref" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
text="$(git show "$ref:$found")"
|
|
||||||
PLUGIN_MAIN_FILE="$found"
|
|
||||||
PLUGIN_SLUG="${found%.php}"
|
|
||||||
PLUGIN_NAME="$(plugin_header_field "${text:0:8192}" "Plugin Name")"
|
|
||||||
PLUGIN_PACKAGE="$(printf '%s\n' "$text" | sed -nE '/^[[:space:]*]*@package[[:space:]]+[A-Za-z0-9_]+/{s/^[[:space:]*]*@package[[:space:]]+([A-Za-z0-9_]+).*/\1/p;q;}')"
|
|
||||||
PLUGIN_CONST="$(printf '%s\n' "$text" | sed -nE "/define\([[:space:]]*['\"][A-Z0-9_]+_VERSION['\"]/{s/.*define\([[:space:]]*['\"]([A-Z0-9_]+)_VERSION['\"].*/\1/p;q;}")"
|
|
||||||
if [ -z "$PLUGIN_PACKAGE" ]; then
|
|
||||||
printf 'plugin: %s has no @package tag (the class prefix)\n' "$found" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [ -z "$PLUGIN_CONST" ]; then
|
|
||||||
printf "plugin: %s does not define a <PREFIX>_VERSION constant\n" "$found" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
PLUGIN_PREFIX="$(printf '%s' "$PLUGIN_CONST" | tr '[:upper:]' '[:lower:]')"
|
|
||||||
PLUGIN_REPO="$(plugin_header_field "${text:0:8192}" "GitHub Plugin URI")"
|
|
||||||
PLUGIN_CSS="$(git show "$ref:admin/includes/class-admin-manager.php" 2>/dev/null | sed -nE 's/.*class="wrap ([a-z0-9]+)-wrap.*/\1/p' | head -n 1)"
|
|
||||||
if [ -z "$PLUGIN_CSS" ]; then
|
|
||||||
printf 'plugin: no "wrap <css>-wrap" class in admin/includes/class-admin-manager.php at %s\n' "$ref" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Copy the PLUGIN_* names into FROM_* (from) or TO_* (to), for plugin_map.
|
|
||||||
plugin_names_as() {
|
|
||||||
local side="$1"
|
|
||||||
local name
|
|
||||||
for name in SLUG NAME PACKAGE CONST PREFIX CSS REPO; do
|
|
||||||
eval "export ${side}_${name}=\"\$PLUGIN_${name}\""
|
|
||||||
done
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rewrite text on stdin from one plugin's names (FROM_*) to another's (TO_*):
|
|
||||||
# GitHub repository, name, class prefix, constant prefix, slug, prefix and
|
|
||||||
# CSS prefix, in that order. When the source plugin's slug is also its prefix
|
|
||||||
# (myplugin), the slug is only the quoted word ('myplugin': text domain,
|
|
||||||
# admin page) and the main file name; everywhere else it is the prefix.
|
|
||||||
plugin_map() {
|
|
||||||
perl -pe '
|
|
||||||
BEGIN { %e = map { $_ => $ENV{$_} // "" } grep { /^(FROM|TO)_/ } keys %ENV; }
|
|
||||||
s{\Q$e{FROM_REPO}\E}{$e{TO_REPO}}g if $e{FROM_REPO} ne "" && $e{TO_REPO} ne "";
|
|
||||||
s{\Q$e{FROM_NAME}\E}{$e{TO_NAME}}g;
|
|
||||||
s{\Q$e{FROM_PACKAGE}\E}{$e{TO_PACKAGE}}g;
|
|
||||||
s{\Q$e{FROM_CONST}\E}{$e{TO_CONST}}g;
|
|
||||||
if ($e{FROM_SLUG} eq $e{FROM_PREFIX}) {
|
|
||||||
s{(["\x27])\Q$e{FROM_SLUG}\E\1}{${1}$e{TO_SLUG}${1}}g;
|
|
||||||
s{\b\Q$e{FROM_SLUG}\E\.php\b}{$e{TO_SLUG}.php}g;
|
|
||||||
} else {
|
|
||||||
s{\Q$e{FROM_SLUG}\E}{$e{TO_SLUG}}g;
|
|
||||||
}
|
|
||||||
s{\Q$e{FROM_PREFIX}\E}{$e{TO_PREFIX}}g;
|
|
||||||
s{\b\Q$e{FROM_CSS}\E(?=[-_A-Z])}{$e{TO_CSS}}g;
|
|
||||||
'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Value of the environment variable <PLUGIN_CONST>_<NAME>, or the default.
|
|
||||||
# For example plugin_env PREVIEW_SITE reads WPSTARTER_PREVIEW_SITE.
|
|
||||||
plugin_env() {
|
|
||||||
local name="${PLUGIN_CONST}_$1"
|
|
||||||
local default="${2:-}"
|
|
||||||
printf '%s' "${!name:-$default}"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Paths the WordPress.org build leaves out (.distignore-wporg at the ref),
|
|
||||||
# one per line, without the leading slash: the GitHub updater.
|
|
||||||
plugin_wporg_only() {
|
|
||||||
local ref="${1:-HEAD}"
|
|
||||||
git show "$ref:.distignore-wporg" 2>/dev/null | sed -e 's/[[:space:]]*$//' -e '/^#/d' -e '/^$/d' -e 's|^/||' -e 's|/$||'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
-134
@@ -1,134 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Code checks for the plugin, the same ones CI runs on every pull request
|
|
||||||
# (.github/workflows/ci.yml). Changes nothing.
|
|
||||||
#
|
|
||||||
# Usage: scripts/lint.sh [CHECK]...
|
|
||||||
# php PHP syntax of every PHP file, with the local php.
|
|
||||||
# js JavaScript syntax of every JS file (node --check).
|
|
||||||
# shell ShellCheck on every shell script.
|
|
||||||
# workflows actionlint on .github/workflows (skipped if not installed).
|
|
||||||
# phpcs WordPress Coding Standards, security sniffs and PHP 7.4
|
|
||||||
# compatibility (phpcs.xml.dist).
|
|
||||||
# phpstan Static analysis (phpstan.neon.dist): no new findings beyond
|
|
||||||
# phpstan-baseline.neon.
|
|
||||||
# With no CHECK, runs them all. phpcs and phpstan need `composer install`.
|
|
||||||
#
|
|
||||||
# Exit status: 0 when every check passes, 1 when any fails, 2 on bad usage.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
readonly ALL_CHECKS="php js shell workflows phpcs phpstan"
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
readonly ROOT
|
|
||||||
FAILED=""
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
say() {
|
|
||||||
local message="$1"
|
|
||||||
printf '\n== %s\n' "$message"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
need_vendor() {
|
|
||||||
local tool="$1"
|
|
||||||
if [ ! -x "$ROOT/vendor/bin/$tool" ]; then
|
|
||||||
printf 'lint: vendor/bin/%s is missing; run: composer install\n' "$tool" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
check_php() {
|
|
||||||
local file output status=0
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
if ! output="$(php -l "$file" 2>&1)"; then
|
|
||||||
printf '%s\n' "$output"
|
|
||||||
status=1
|
|
||||||
fi
|
|
||||||
done < <(git -C "$ROOT" ls-files -z '*.php')
|
|
||||||
return "$status"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_js() {
|
|
||||||
local file status=0
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
node --check "$file" || status=1
|
|
||||||
done < <(git -C "$ROOT" ls-files -z '*.js')
|
|
||||||
return "$status"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_shell() {
|
|
||||||
local files=()
|
|
||||||
local file
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
files+=("$file")
|
|
||||||
done < <(git -C "$ROOT" ls-files -z '*.sh')
|
|
||||||
[ "${#files[@]}" -eq 0 ] && return 0
|
|
||||||
# -x follows the scripts' `# shellcheck source=` lines (scripts/lib/).
|
|
||||||
shellcheck -x --severity=style "${files[@]}"
|
|
||||||
return $?
|
|
||||||
}
|
|
||||||
|
|
||||||
check_workflows() {
|
|
||||||
if [ ! -d "$ROOT/.github/workflows" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if ! command -v actionlint >/dev/null 2>&1; then
|
|
||||||
printf 'actionlint is not installed; skipped (CI runs it)\n'
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
actionlint
|
|
||||||
return $?
|
|
||||||
}
|
|
||||||
|
|
||||||
check_phpcs() {
|
|
||||||
need_vendor phpcs || return 1
|
|
||||||
vendor/bin/phpcs -q --report=full
|
|
||||||
return $?
|
|
||||||
}
|
|
||||||
|
|
||||||
check_phpstan() {
|
|
||||||
need_vendor phpstan || return 1
|
|
||||||
vendor/bin/phpstan analyse --memory-limit=4G --no-progress
|
|
||||||
return $?
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local checks="$*"
|
|
||||||
local check
|
|
||||||
if [ -z "$checks" ]; then
|
|
||||||
checks="$ALL_CHECKS"
|
|
||||||
fi
|
|
||||||
for check in $checks; do
|
|
||||||
case " $ALL_CHECKS " in
|
|
||||||
*" $check "*) ;;
|
|
||||||
*)
|
|
||||||
usage >&2
|
|
||||||
return 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
cd "$ROOT"
|
|
||||||
for check in $checks; do
|
|
||||||
say "$check"
|
|
||||||
if "check_$check"; then
|
|
||||||
printf 'ok\n'
|
|
||||||
else
|
|
||||||
FAILED="$FAILED $check"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -n "$FAILED" ]; then
|
|
||||||
printf '\nlint: failed:%s\n' "$FAILED" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
printf '\nlint: all passed (%s)\n' "$checks"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Constants for PHPStan (phpstan.neon.dist). WordPress's functions and
|
|
||||||
* classes come from szepeviktor/phpstan-wordpress; this adds the constants
|
|
||||||
* WordPress defines while it loads (wp-settings.php, wp-config.php) and
|
|
||||||
* those wp-plugin-starter-template.php defines.
|
|
||||||
*
|
|
||||||
* @package WPStarter
|
|
||||||
*/
|
|
||||||
|
|
||||||
define('WPINC', 'wp-includes');
|
|
||||||
define('COOKIEPATH', '/');
|
|
||||||
define('SITECOOKIEPATH', '/');
|
|
||||||
define('DB_NAME', 'wordpress');
|
|
||||||
define('WPSTARTER_VERSION', '0.0.0');
|
|
||||||
define('WPSTARTER_FILE', dirname(__DIR__) . '/wp-plugin-starter-template.php');
|
|
||||||
define('WPSTARTER_DIR', dirname(__DIR__) . '/');
|
|
||||||
define('WPSTARTER_URL', 'https://example.com/wp-content/plugins/wp-plugin-starter-template/');
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Run Plugin Check (the WordPress.org review tool) on the release zips, in a
|
|
||||||
# disposable WordPress in Docker. Nothing is kept: the containers, volume and
|
|
||||||
# network are removed when it ends, and no other site is touched.
|
|
||||||
#
|
|
||||||
# Usage: scripts/plugin-check.sh [--ref REF] [--zip FILE]... [--keep-output DIR]
|
|
||||||
# --ref REF Build both zips from REF (default: HEAD) and check them.
|
|
||||||
# --zip FILE Check this zip instead (repeatable); it must hold one
|
|
||||||
# {slug}/ folder.
|
|
||||||
# --keep-output DIR Save each full report as JSON in DIR.
|
|
||||||
#
|
|
||||||
# Exit status: 0 when no zip has Plugin Check errors. Warnings are listed;
|
|
||||||
# review them before a WordPress.org submission. Updater findings in the
|
|
||||||
# GitHub zip's updater files (.distignore-wporg) are expected and not counted.
|
|
||||||
# Needs Docker and internet access (WordPress and Plugin Check are downloaded).
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
readonly SCRIPT_DIR
|
|
||||||
# shellcheck source=scripts/lib/plugin.sh disable=SC1091 # followed only with -x
|
|
||||||
. "$SCRIPT_DIR/lib/plugin.sh"
|
|
||||||
# Set in main() from the plugin's main file.
|
|
||||||
SLUG=""
|
|
||||||
UPDATER_FILES=""
|
|
||||||
CLI_IMAGE=""
|
|
||||||
DB_IMAGE=""
|
|
||||||
readonly DB_PASSWORD="plugincheck"
|
|
||||||
|
|
||||||
NAME=""
|
|
||||||
TMP_DIR=""
|
|
||||||
STARTED=0
|
|
||||||
|
|
||||||
die() {
|
|
||||||
local message="$1"
|
|
||||||
printf 'plugin-check: %s\n' "$message" >&2
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [ "$STARTED" -eq 1 ]; then
|
|
||||||
local attempt id
|
|
||||||
# wp-cli containers still stopping (after Ctrl-C) keep the volume and
|
|
||||||
# network in use, so remove them first and retry.
|
|
||||||
for attempt in 1 2 3; do
|
|
||||||
docker ps -aq --filter "volume=$NAME-wp" | while IFS= read -r id; do
|
|
||||||
docker rm -f "$id" >/dev/null 2>&1 || true
|
|
||||||
done
|
|
||||||
docker rm -f "$NAME-db" >/dev/null 2>&1 || true
|
|
||||||
docker volume rm "$NAME-wp" >/dev/null 2>&1 || true
|
|
||||||
docker network rm "$NAME" >/dev/null 2>&1 || true
|
|
||||||
if ! docker volume inspect "$NAME-wp" >/dev/null 2>&1 && ! docker network inspect "$NAME" >/dev/null 2>&1; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
[ "$attempt" -eq 3 ] && printf 'plugin-check: could not remove %s-wp or network %s\n' "$NAME" "$NAME" >&2
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# wp-cli in the disposable site; zips are mounted at /zips. The image's
|
|
||||||
# 128 MB PHP memory limit is too small to unpack WordPress.
|
|
||||||
wp_cli() {
|
|
||||||
docker run --rm --network "$NAME" -v "$NAME-wp:/var/www/html" -v "$TMP_DIR/zips:/zips:ro" \
|
|
||||||
--user 33:33 "$CLI_IMAGE" php -d memory_limit=1G /usr/local/bin/wp "$@"
|
|
||||||
return $?
|
|
||||||
}
|
|
||||||
|
|
||||||
start_site() {
|
|
||||||
printf 'Starting a disposable WordPress (%s)...\n' "$NAME"
|
|
||||||
STARTED=1
|
|
||||||
docker network create "$NAME" >/dev/null
|
|
||||||
docker volume create "$NAME-wp" >/dev/null
|
|
||||||
docker run -d --name "$NAME-db" --network "$NAME" \
|
|
||||||
-e MARIADB_ROOT_PASSWORD="$DB_PASSWORD" -e MARIADB_DATABASE=wordpress "$DB_IMAGE" >/dev/null
|
|
||||||
# The volume starts owned by root; let www-data (33) write to it.
|
|
||||||
docker run --rm -v "$NAME-wp:/var/www/html" --user 0:0 "$CLI_IMAGE" chown 33:33 /var/www/html
|
|
||||||
|
|
||||||
# Ping over TCP: the image's first, socket-only server answers on the
|
|
||||||
# socket before the real server is listening.
|
|
||||||
local waited=0
|
|
||||||
until docker exec "$NAME-db" mariadb-admin ping -h127.0.0.1 -uroot -p"$DB_PASSWORD" --silent >/dev/null 2>&1; do
|
|
||||||
[ "$waited" -lt 90 ] || die "the database did not start"
|
|
||||||
sleep 2
|
|
||||||
waited=$((waited + 2))
|
|
||||||
done
|
|
||||||
|
|
||||||
wp_cli core download --quiet
|
|
||||||
wp_cli config create --dbname=wordpress --dbuser=root --dbpass="$DB_PASSWORD" --dbhost="$NAME-db" --skip-check --quiet
|
|
||||||
wp_cli core install --url=http://localhost --title=PluginCheck --admin_user=admin --admin_password="$DB_PASSWORD" \
|
|
||||||
--admin_email=admin@example.com --skip-email --quiet
|
|
||||||
wp_cli plugin install plugin-check --activate --quiet
|
|
||||||
printf 'WordPress %s, Plugin Check %s\n' "$(wp_cli core version)" "$(wp_cli plugin get plugin-check --field=version)"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check one zip; returns 1 when Plugin Check reports errors.
|
|
||||||
check_zip() {
|
|
||||||
local zip_name="$1"
|
|
||||||
local keep="$2"
|
|
||||||
local report errors warnings
|
|
||||||
printf '\n== %s ==\n' "$zip_name"
|
|
||||||
if ! wp_cli plugin install "/zips/$zip_name" --force --quiet; then
|
|
||||||
printf 'Could not install %s.\n' "$zip_name"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
report="$(wp_cli plugin check "$SLUG" --format=json 2>&1 || true)"
|
|
||||||
if [ -n "$keep" ]; then
|
|
||||||
printf '%s\n' "$report" >"$keep/${zip_name%.zip}-plugin-check.json"
|
|
||||||
fi
|
|
||||||
if ! printf '%s\n' "$report" | grep -Eq '^(FILE: |Success: )'; then
|
|
||||||
# Neither findings nor the success line: Plugin Check did not run.
|
|
||||||
printf 'Plugin Check did not run:\n%s\n' "$report"
|
|
||||||
wp_cli plugin delete "$SLUG" --quiet || true
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
errors="$( (printf '%s\n' "$report" | grep -o '"type":"ERROR"' || true) | wc -l | tr -d ' ')"
|
|
||||||
warnings="$( (printf '%s\n' "$report" | grep -o '"type":"WARNING"' || true) | wc -l | tr -d ' ')"
|
|
||||||
# The GitHub zip carries Updates from GitHub on purpose; Plugin Check
|
|
||||||
# reports it as an updater. Those findings are expected there (and only
|
|
||||||
# in those files); in the WordPress.org zip they stay errors. So are
|
|
||||||
# prefix warnings there: the shared updater's names (wpallstars_) are the
|
|
||||||
# same in every plugin, so that one copy can stand in for the others.
|
|
||||||
local expected=0
|
|
||||||
local expected_warnings=0
|
|
||||||
local counts
|
|
||||||
case "$zip_name" in
|
|
||||||
"$SLUG"-*)
|
|
||||||
# The file list goes through the environment: awk -v cannot hold newlines.
|
|
||||||
counts="$(printf '%s\n' "$report" | FILES="$UPDATER_FILES" awk '
|
|
||||||
BEGIN { nfiles = split(ENVIRON["FILES"], list, "\n") }
|
|
||||||
# A listed file, or a file inside a listed folder.
|
|
||||||
function updater(path, i) {
|
|
||||||
for (i = 1; i <= nfiles; i++) {
|
|
||||||
if (list[i] != "" && (path == list[i] || index(path, list[i] "/") == 1)) { return 1 }
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
/^FILE: / { current = substr($0, 7); next }
|
|
||||||
/^\[/ && updater(current) {
|
|
||||||
n = split($0, items, "},{")
|
|
||||||
for (i = 1; i <= n; i++) {
|
|
||||||
if (items[i] ~ /"type":"ERROR"/ && items[i] ~ /"code":"(plugin_updater_detected|update_modification_detected|PluginCheck\.CodeAnalysis\.Offloading\.OffloadedContent)"/) { count++ }
|
|
||||||
if (items[i] ~ /"type":"WARNING"/ && items[i] ~ /"code":"WordPress\.NamingConventions\.PrefixAllGlobals\./) { prefix++ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END { print count + 0, prefix + 0 }')"
|
|
||||||
expected="${counts% *}"
|
|
||||||
expected_warnings="${counts#* }"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
if [ "$expected" -gt 0 ] || [ "$expected_warnings" -gt 0 ]; then
|
|
||||||
printf '%s updater error(s) and %s prefix warning(s) in %s are expected in the GitHub zip.\n' "$expected" "$expected_warnings" "$(printf '%s' "$UPDATER_FILES" | tr '\n' ' ')"
|
|
||||||
errors=$((errors - expected))
|
|
||||||
warnings=$((warnings - expected_warnings))
|
|
||||||
fi
|
|
||||||
# Readable summary: one line per finding (type, code, file:line).
|
|
||||||
printf '%s\n' "$report" | awk '
|
|
||||||
/^FILE: / { file = substr($0, 7); next }
|
|
||||||
/^\[/ {
|
|
||||||
n = split($0, items, "},{")
|
|
||||||
for (i = 1; i <= n; i++) {
|
|
||||||
t = items[i]; c = items[i]; l = items[i]
|
|
||||||
sub(/.*"type":"/, "", t); sub(/".*/, "", t)
|
|
||||||
sub(/.*"code":"/, "", c); sub(/".*/, "", c)
|
|
||||||
sub(/.*"line":/, "", l); sub(/[^0-9].*/, "", l)
|
|
||||||
printf " %-7s %s %s:%s\n", t, c, file, l
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
printf '%s error(s), %s warning(s)\n' "$errors" "$warnings"
|
|
||||||
wp_cli plugin delete "$SLUG" --quiet || true
|
|
||||||
[ "$errors" -eq 0 ] || return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local ref="HEAD"
|
|
||||||
local keep=""
|
|
||||||
local zips=""
|
|
||||||
local arg
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
arg="$1"
|
|
||||||
case "$arg" in
|
|
||||||
--ref)
|
|
||||||
[ $# -ge 2 ] || die "--ref needs a value"
|
|
||||||
ref="$2"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--zip)
|
|
||||||
[ $# -ge 2 ] || die "--zip needs a file"
|
|
||||||
[ -f "$2" ] || die "no such zip: $2"
|
|
||||||
zips="$zips
|
|
||||||
$(cd "$(dirname "$2")" && pwd)/$(basename "$2")"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--keep-output)
|
|
||||||
[ $# -ge 2 ] || die "--keep-output needs a folder"
|
|
||||||
mkdir -p "$2"
|
|
||||||
keep="$(cd "$2" && pwd)"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h | --help)
|
|
||||||
usage
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*) die "unknown argument: $arg" ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
command -v docker >/dev/null 2>&1 || die "needs Docker"
|
|
||||||
docker info >/dev/null 2>&1 || die "Docker is not running"
|
|
||||||
local root
|
|
||||||
root="$(git rev-parse --show-toplevel)" || die "run this inside a checkout of the plugin"
|
|
||||||
cd "$root"
|
|
||||||
plugin_identity "$ref" || die "cannot tell which plugin this is at $ref"
|
|
||||||
SLUG="$PLUGIN_SLUG"
|
|
||||||
UPDATER_FILES="$(plugin_wporg_only "$ref")"
|
|
||||||
CLI_IMAGE="$(plugin_env CLI_IMAGE wordpress:cli-php8.3)"
|
|
||||||
DB_IMAGE="$(plugin_env DB_IMAGE mariadb:10.6)"
|
|
||||||
NAME="$SLUG-plugincheck-$$"
|
|
||||||
|
|
||||||
trap cleanup EXIT
|
|
||||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/$SLUG-plugincheck.XXXXXX")"
|
|
||||||
mkdir -p "$TMP_DIR/zips"
|
|
||||||
chmod 755 "$TMP_DIR" "$TMP_DIR/zips"
|
|
||||||
|
|
||||||
local zip_path
|
|
||||||
if [ -z "$zips" ]; then
|
|
||||||
"$root/scripts/build-release.sh" --ref "$ref" --out "$TMP_DIR/zips" --quiet >/dev/null || die "build failed"
|
|
||||||
else
|
|
||||||
while IFS= read -r zip_path; do
|
|
||||||
[ -n "$zip_path" ] && cp "$zip_path" "$TMP_DIR/zips/"
|
|
||||||
done <<EOF
|
|
||||||
$zips
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
chmod 644 "$TMP_DIR"/zips/*.zip
|
|
||||||
|
|
||||||
start_site
|
|
||||||
local failed=0 zip_name
|
|
||||||
for zip_path in "$TMP_DIR"/zips/*.zip; do
|
|
||||||
zip_name="$(basename "$zip_path")"
|
|
||||||
check_zip "$zip_name" "$keep" || failed=1
|
|
||||||
done
|
|
||||||
|
|
||||||
printf '\n'
|
|
||||||
if [ "$failed" -eq 1 ]; then
|
|
||||||
printf 'Plugin Check found errors.\n'
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
printf 'Plugin Check: no errors.\n'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,586 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Check a version of the plugin before it is released on GitHub or
|
|
||||||
# submitted to WordPress.org. Changes nothing: it builds both zips into a
|
|
||||||
# temporary folder (scripts/build-release.sh) and reads them and the Git ref.
|
|
||||||
#
|
|
||||||
# Errors stop a release. Warnings need a look and a decision; most matter only
|
|
||||||
# for WordPress.org. Notes are for information.
|
|
||||||
#
|
|
||||||
# Usage: scripts/preflight-release.sh [--ref REF] [--offline] [--strict] [--no-docker]
|
|
||||||
# --ref REF Commit, branch or tag to check (default: HEAD).
|
|
||||||
# --offline Skip checks that ask WordPress.org (latest WordPress version,
|
|
||||||
# slug, contributor profiles).
|
|
||||||
# --strict Fail on warnings too (use before a WordPress.org submission).
|
|
||||||
# --no-docker Lint PHP with the local php instead of PHP 7.4 in Docker.
|
|
||||||
#
|
|
||||||
# Exit status: 0 when there are no errors (and, with --strict, no warnings).
|
|
||||||
# Plugin Check runs separately: scripts/plugin-check.sh.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
readonly UPDATER_HEADERS='GitHub Plugin URI|Primary Branch|Release Asset'
|
|
||||||
# Development files that must never be in a release zip (paths inside the slug folder).
|
|
||||||
readonly DEV_FILES='^[^/]+/(\.git|\.agents|\.wordpress-org|\.distignore|\.distignore-wporg|\.gitattributes|\.gitignore|\.woodpecker\.yml|\.github|\.editorconfig|\.gitleaks\.toml|\.aidevops\.json|composer\.(json|lock)|phpcs\.xml(\.dist)?|phpstan(-baseline|-plugin)?\.neon(\.dist)?|vendor|AGENTS\.md|CONTRIBUTING\.md|DEVELOPMENT\.md|LAUNCH\.md|SECURITY\.md|STANDARDS\.md|RELEASING\.md|ROADMAP\.md|STABILITY\.md|TESTING\.md|scripts|dist|node_modules|reference-plugins|project-documents)(/|$)|(^|/)(\.DS_Store|__MACOSX|Thumbs\.db)(/|$)|\.(bak|log|orig|swp)$'
|
|
||||||
readonly README_MAX_BYTES=10240
|
|
||||||
readonly SHORT_DESC_MAX=150
|
|
||||||
readonly MAX_TAGS=5
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
readonly SCRIPT_DIR
|
|
||||||
# shellcheck source=scripts/lib/plugin.sh disable=SC1091 # followed only with -x
|
|
||||||
. "$SCRIPT_DIR/lib/plugin.sh"
|
|
||||||
|
|
||||||
# Set from the main file at the ref (plugin_identity).
|
|
||||||
SLUG=""
|
|
||||||
MAIN_FILE=""
|
|
||||||
VERSION_CONSTANT=""
|
|
||||||
# Paths only in the GitHub build (.distignore-wporg): the GitHub updater.
|
|
||||||
UPDATER_FILES=""
|
|
||||||
|
|
||||||
ERRORS=0
|
|
||||||
WARNINGS=0
|
|
||||||
TMP_DIR=""
|
|
||||||
OFFLINE=0
|
|
||||||
USE_DOCKER=1
|
|
||||||
CHANGELOG_TXT=""
|
|
||||||
|
|
||||||
die() {
|
|
||||||
local message="$1"
|
|
||||||
printf 'preflight: %s\n' "$message" >&2
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
ok() {
|
|
||||||
local message="$1"
|
|
||||||
printf ' ok %s\n' "$message"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
err() {
|
|
||||||
local message="$1"
|
|
||||||
printf ' ERROR %s\n' "$message"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
warn() {
|
|
||||||
local message="$1"
|
|
||||||
printf ' warn %s\n' "$message"
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
note() {
|
|
||||||
local message="$1"
|
|
||||||
printf ' note %s\n' "$message"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
section() {
|
|
||||||
local title="$1"
|
|
||||||
printf '\n%s\n' "$title"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Value of "Key: value" in a header (plugin file comment or readme), case-insensitive.
|
|
||||||
field() {
|
|
||||||
local text="$1"
|
|
||||||
local key="$2"
|
|
||||||
printf '%s\n' "$text" | awk -v k="$key" '
|
|
||||||
BEGIN { k = tolower(k) }
|
|
||||||
{
|
|
||||||
line = $0
|
|
||||||
sub(/^[ \t*]*/, "", line)
|
|
||||||
i = index(line, ":")
|
|
||||||
if (i > 0 && tolower(substr(line, 1, i - 1)) == k) {
|
|
||||||
v = substr(line, i + 1)
|
|
||||||
gsub(/^[ \t]+|[ \t\r]+$/, "", v)
|
|
||||||
print v
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 1 if version a < b (numeric parts), else 0.
|
|
||||||
version_lt() {
|
|
||||||
local a="$1"
|
|
||||||
local b="$2"
|
|
||||||
awk -v a="$a" -v b="$b" 'BEGIN {
|
|
||||||
na = split(a, x, "."); nb = split(b, y, "."); n = (na > nb) ? na : nb
|
|
||||||
for (i = 1; i <= n; i++) { if ((x[i] + 0) < (y[i] + 0)) { print 1; exit } if ((x[i] + 0) > (y[i] + 0)) { print 0; exit } }
|
|
||||||
print 0 }'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTP status of a URL (000 when offline or unreachable).
|
|
||||||
http_status() {
|
|
||||||
local url="$1"
|
|
||||||
local code
|
|
||||||
# curl prints 000 itself when it cannot connect, and exits non-zero.
|
|
||||||
code="$(curl -sL -o /dev/null -m 20 -w '%{http_code}' "$url" || true)"
|
|
||||||
printf '%s' "${code:-000}"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
check_versions() {
|
|
||||||
local plugin_header="$1"
|
|
||||||
local readme="$2"
|
|
||||||
local main_php="$3"
|
|
||||||
section "Versions and headers"
|
|
||||||
|
|
||||||
local version constant stable
|
|
||||||
version="$(field "$plugin_header" "Version")"
|
|
||||||
constant="$(printf '%s\n' "$main_php" | sed -nE "/define\([[:space:]]*['\"]${VERSION_CONSTANT}['\"]/{s/.*,[[:space:]]*['\"]([^'\"]+)['\"].*/\1/p;q;}")"
|
|
||||||
stable="$(field "$readme" "Stable tag")"
|
|
||||||
|
|
||||||
if printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
|
|
||||||
ok "Version: $version (numbers only, so GitHub updates and WordPress.org treat it as stable)"
|
|
||||||
else
|
|
||||||
err "Version: '$version' is not X.Y.Z; pre-release versions on main are offered to sites still using Git Updater"
|
|
||||||
fi
|
|
||||||
if [ "$constant" = "$version" ]; then ok "version constant matches ($constant)"; else err "version constant is '$constant', Version: is '$version'"; fi
|
|
||||||
if [ "$stable" = "$version" ]; then ok "readme.txt Stable tag matches"; else err "readme.txt Stable tag is '$stable', Version: is '$version'"; fi
|
|
||||||
if [ "$(printf '%s' "$stable" | tr '[:upper:]' '[:lower:]')" = "trunk" ]; then err "Stable tag: trunk is not allowed for new plugins"; fi
|
|
||||||
|
|
||||||
local key header_value readme_value
|
|
||||||
for key in "Requires at least" "Requires PHP"; do
|
|
||||||
header_value="$(field "$plugin_header" "$key")"
|
|
||||||
readme_value="$(field "$readme" "$key")"
|
|
||||||
if [ -z "$header_value" ]; then
|
|
||||||
err "$MAIN_FILE has no '$key' header (WordPress reads it from the plugin file)"
|
|
||||||
elif [ -n "$readme_value" ] && [ "$header_value" != "$readme_value" ]; then
|
|
||||||
err "$key: $header_value in $MAIN_FILE, $readme_value in readme.txt"
|
|
||||||
else
|
|
||||||
ok "$key: $header_value"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if printf '%s\n' "$main_php" | grep -Eiq '^[[:space:]*]*Update URI:'; then
|
|
||||||
err "Update URI header found: WordPress.org rejects it and Plugin Check reports an updater"
|
|
||||||
else
|
|
||||||
ok "no Update URI header"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local domain license plugin_uri author_uri
|
|
||||||
domain="$(field "$plugin_header" "Text Domain")"
|
|
||||||
if [ "$domain" = "$SLUG" ]; then ok "Text Domain: $domain"; else err "Text Domain is '$domain'; it must be the slug ($SLUG) for language packs"; fi
|
|
||||||
license="$(field "$plugin_header" "License")"
|
|
||||||
if printf '%s' "$license" | grep -Eiq 'GPL'; then ok "License: $license"; else err "License '$license' is not GPL-compatible as written"; fi
|
|
||||||
plugin_uri="$(field "$plugin_header" "Plugin URI")"
|
|
||||||
author_uri="$(field "$plugin_header" "Author URI")"
|
|
||||||
if [ -n "$plugin_uri" ] && [ "$plugin_uri" = "$author_uri" ]; then
|
|
||||||
warn "Plugin URI and Author URI are the same ($plugin_uri); WordPress.org asks for a Plugin URI unique to the plugin, or none"
|
|
||||||
fi
|
|
||||||
VERSION="$version"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Changelog text (a readme section or changelog.txt): an entry for the version,
|
|
||||||
# and no Unreleased section left.
|
|
||||||
check_changelog() {
|
|
||||||
local label="$1"
|
|
||||||
local text="$2"
|
|
||||||
local version="$3"
|
|
||||||
if printf '%s\n' "$text" | grep -Eq "^= *v?$version *="; then
|
|
||||||
ok "$label changelog has $version"
|
|
||||||
else
|
|
||||||
warn "$label changelog has no '= $version =' entry"
|
|
||||||
fi
|
|
||||||
if printf '%s\n' "$text" | grep -Eiq '^= *unreleased *='; then
|
|
||||||
warn "$label changelog still has an Unreleased section; name it $version when releasing"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
check_readme() {
|
|
||||||
local readme="$1"
|
|
||||||
local plugin_header="$2"
|
|
||||||
local version="$3"
|
|
||||||
section "readme.txt"
|
|
||||||
|
|
||||||
local bytes
|
|
||||||
bytes="$(printf '%s' "$readme" | wc -c | tr -d ' ')"
|
|
||||||
if [ "$bytes" -le "$README_MAX_BYTES" ]; then
|
|
||||||
ok "size ${bytes} bytes ($((README_MAX_BYTES - bytes)) left)"
|
|
||||||
else
|
|
||||||
warn "size ${bytes} bytes; WordPress.org says over 10 KB may cause errors (keep the current changelog, move older entries to changelog.txt, trim the description)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local name readme_name
|
|
||||||
name="$(field "$plugin_header" "Plugin Name")"
|
|
||||||
readme_name="$(printf '%s\n' "$readme" | sed -n '1s/^===[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*===.*/\1/p')"
|
|
||||||
if [ "$readme_name" = "$name" ]; then ok "name matches the plugin header ($name)"; else warn "readme name '$readme_name' differs from Plugin Name '$name'"; fi
|
|
||||||
|
|
||||||
local short
|
|
||||||
short="$(printf '%s\n' "$readme" | awk 'NR == 1 { next } !h && /^[ \t]*$/ { h = 1; next } h && /^==/ { exit } h && !/^[ \t]*$/ { print; exit }')"
|
|
||||||
if [ -z "$short" ]; then
|
|
||||||
err "no short description (the line after the header)"
|
|
||||||
elif [ "${#short}" -le "$SHORT_DESC_MAX" ]; then
|
|
||||||
ok "short description ${#short} characters"
|
|
||||||
else
|
|
||||||
warn "short description ${#short} characters; WordPress.org cuts it at $SHORT_DESC_MAX"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local tags tag_count
|
|
||||||
tags="$(field "$readme" "Tags")"
|
|
||||||
tag_count="$(printf '%s\n' "$tags" | awk -F',' '{ print ($0 == "") ? 0 : NF }')"
|
|
||||||
if [ "$tag_count" -ge 1 ] && [ "$tag_count" -le "$MAX_TAGS" ]; then ok "$tag_count tags"; else warn "$tag_count tags; WordPress.org shows at most $MAX_TAGS"; fi
|
|
||||||
|
|
||||||
local tested
|
|
||||||
tested="$(field "$readme" "Tested up to")"
|
|
||||||
if printf '%s' "$tested" | grep -Eq '^[0-9]+\.[0-9]+$'; then
|
|
||||||
ok "Tested up to: $tested"
|
|
||||||
else
|
|
||||||
err "Tested up to '$tested' should be a major version such as 6.8"
|
|
||||||
fi
|
|
||||||
if [ "$OFFLINE" -eq 0 ]; then
|
|
||||||
local latest latest_major
|
|
||||||
# The first offer is the latest release; later ones are older branches.
|
|
||||||
latest="$(curl -s -m 20 'https://api.wordpress.org/core/version-check/1.7/' | grep -o '"current":"[0-9.]*"' | head -n 1 | cut -d'"' -f4 || true)"
|
|
||||||
latest_major="$(printf '%s' "$latest" | cut -d. -f1-2)"
|
|
||||||
if [ -z "$latest_major" ]; then
|
|
||||||
note "could not read the latest WordPress version"
|
|
||||||
elif [ "$(version_lt "$tested" "$latest_major")" = "1" ]; then
|
|
||||||
warn "Tested up to $tested, latest WordPress is $latest; test and raise it"
|
|
||||||
else
|
|
||||||
ok "Tested up to is the latest WordPress ($latest)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
check_changelog "readme.txt" "$(printf '%s\n' "$readme" | awk '/^== *[Cc]hangelog *==/ { c = 1; next } c && /^== / { exit } c')" "$version"
|
|
||||||
if [ -n "$CHANGELOG_TXT" ]; then
|
|
||||||
check_changelog "changelog.txt" "$CHANGELOG_TXT" "$version"
|
|
||||||
fi
|
|
||||||
if printf '%s\n' "$readme" | grep -Eiq '^== *upgrade notice *=='; then
|
|
||||||
if printf '%s\n' "$readme" | awk '/^== *[Uu]pgrade [Nn]otice *==/ { u = 1; next } u && /^== / { exit } u' | grep -Eq "^= *v?$version *="; then
|
|
||||||
ok "upgrade notice has $version"
|
|
||||||
else
|
|
||||||
note "no upgrade notice for $version (optional)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
check_wporg() {
|
|
||||||
local readme="$1"
|
|
||||||
local plugin_header="$2"
|
|
||||||
section "WordPress.org listing"
|
|
||||||
|
|
||||||
local name derived
|
|
||||||
name="$(field "$plugin_header" "Plugin Name")"
|
|
||||||
derived="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g')"
|
|
||||||
if [ "$derived" = "$SLUG" ]; then
|
|
||||||
ok "the slug WordPress.org makes from '$name' is $SLUG"
|
|
||||||
else
|
|
||||||
warn "WordPress.org makes the slug from Plugin Name: '$name' becomes '$derived', not '$SLUG'. Ask for '$SLUG' in the submission notes (it can change only before approval), or the text domain will not match"
|
|
||||||
fi
|
|
||||||
if printf '%s' "$name" | grep -Eiq '^(wordpress|wp|woocommerce|woo|gutenberg)([^a-z]|$)'; then
|
|
||||||
warn "Plugin Name starts with a trademark ('$name'); WordPress.org does not allow that"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$OFFLINE" -eq 1 ]; then
|
|
||||||
note "offline: slug and contributor profiles not checked"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local info
|
|
||||||
info="$(curl -s -m 20 "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request%5Bslug%5D=$SLUG&request%5Bfields%5D%5Bsections%5D=0" || true)"
|
|
||||||
if printf '%s' "$info" | grep -q '"error"'; then
|
|
||||||
note "slug $SLUG is not listed on WordPress.org yet"
|
|
||||||
elif [ -n "$info" ]; then
|
|
||||||
note "slug $SLUG is already listed on WordPress.org; check it is this plugin"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local contributors user status
|
|
||||||
contributors="$(field "$readme" "Contributors")"
|
|
||||||
if [ -z "$contributors" ]; then
|
|
||||||
err "readme.txt has no Contributors"
|
|
||||||
fi
|
|
||||||
for user in $(printf '%s' "$contributors" | tr ',' ' '); do
|
|
||||||
status="$(http_status "https://profiles.wordpress.org/$user/")"
|
|
||||||
case "$status" in
|
|
||||||
200) ok "contributor $user has a WordPress.org profile" ;;
|
|
||||||
000) note "could not check contributor $user" ;;
|
|
||||||
*) warn "contributor '$user' has no WordPress.org profile (HTTP $status); Contributors must be WordPress.org usernames, case-sensitive" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Lint PHP (PHP 7.4 in Docker when possible) and JS in an unpacked build.
|
|
||||||
check_syntax() {
|
|
||||||
local dir="$1"
|
|
||||||
local label="$2"
|
|
||||||
local out php_label
|
|
||||||
local expected
|
|
||||||
expected="$(find "$dir" -name '*.php' | wc -l | tr -d ' ')"
|
|
||||||
if [ "$USE_DOCKER" -eq 1 ] && command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
|
||||||
local image="php:7.4-cli"
|
|
||||||
if docker image inspect wordpress:php7.4-apache >/dev/null 2>&1; then image="wordpress:php7.4-apache"; fi
|
|
||||||
out="$(docker run --rm -v "$dir:/src:ro" "$image" sh -c 'find /src -name "*.php" -exec php -l {} \;' 2>&1 || true)"
|
|
||||||
php_label="PHP 7.4 syntax ($image)"
|
|
||||||
elif command -v php >/dev/null 2>&1; then
|
|
||||||
out="$(find "$dir" -name '*.php' -exec php -l {} \; 2>&1 || true)"
|
|
||||||
php_label="PHP syntax ($(php -r 'echo PHP_VERSION;'); not 7.4)"
|
|
||||||
else
|
|
||||||
out=""
|
|
||||||
php_label=""
|
|
||||||
warn "$label: no PHP to lint with"
|
|
||||||
fi
|
|
||||||
if [ -n "$php_label" ]; then
|
|
||||||
# Count the files php -l passed, so a lint that never ran cannot pass.
|
|
||||||
local passed problems
|
|
||||||
passed="$(printf '%s\n' "$out" | grep -c '^No syntax errors' || true)"
|
|
||||||
problems="$(printf '%s\n' "$out" | grep -v '^No syntax errors' | grep -v '^[[:space:]]*$' || true)"
|
|
||||||
if [ -z "$problems" ] && [ "$passed" -eq "$expected" ] && [ "$expected" -gt 0 ]; then
|
|
||||||
ok "$label: $php_label, $passed files"
|
|
||||||
elif [ -z "$problems" ]; then
|
|
||||||
err "$label: PHP lint checked $passed of $expected files"
|
|
||||||
else
|
|
||||||
err "$label: $php_label errors:"
|
|
||||||
printf '%s\n' "$problems" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if command -v node >/dev/null 2>&1; then
|
|
||||||
out="$(find "$dir" -name '*.js' -exec node --check {} \; 2>&1 || true)"
|
|
||||||
if [ -z "$out" ]; then ok "$label: JS syntax"; else err "$label: JS syntax errors:"; printf '%s\n' "$out" | sed 's/^/ /'; fi
|
|
||||||
else
|
|
||||||
warn "$label: node not found, JS not checked"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Shared checks for a release zip; prints its unpacked folder in UNPACKED.
|
|
||||||
check_zip() {
|
|
||||||
local zip_path="$1"
|
|
||||||
local label="$2"
|
|
||||||
local dir="$TMP_DIR/unpacked-$label"
|
|
||||||
local list tops dev size
|
|
||||||
list="$(unzip -Z1 "$zip_path")"
|
|
||||||
tops="$(printf '%s\n' "$list" | cut -d/ -f1 | sort -u | tr '\n' ' ')"
|
|
||||||
if [ "$tops" = "$SLUG " ]; then ok "$label: one $SLUG/ folder"; else err "$label: top level is '$tops', must be only $SLUG/"; fi
|
|
||||||
if printf '%s\n' "$list" | grep -qx "$SLUG/$MAIN_FILE"; then ok "$label: $SLUG/$MAIN_FILE present"; else err "$label: $SLUG/$MAIN_FILE missing"; fi
|
|
||||||
dev="$(printf '%s\n' "$list" | grep -E "$DEV_FILES" || true)"
|
|
||||||
if [ -z "$dev" ]; then ok "$label: no development files"; else err "$label: development files: $(printf '%s' "$dev" | tr '\n' ' ')"; fi
|
|
||||||
size="$(wc -c <"$zip_path" | tr -d ' ')"
|
|
||||||
note "$label: $(basename "$zip_path"), $((size / 1024)) KB, $(printf '%s\n' "$list" | grep -cv '/$') files"
|
|
||||||
mkdir -p "$dir"
|
|
||||||
unzip -q "$zip_path" -d "$dir"
|
|
||||||
check_syntax "$dir" "$label"
|
|
||||||
UNPACKED="$dir/$SLUG"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
check_builds() {
|
|
||||||
local github_zip="$1"
|
|
||||||
local wporg_zip="$2"
|
|
||||||
local version="$3"
|
|
||||||
section "Release zips"
|
|
||||||
|
|
||||||
if [ "$(basename "$github_zip")" = "$SLUG-$version.zip" ]; then
|
|
||||||
ok "GitHub asset name $SLUG-$version.zip (sites pick assets starting with $SLUG)"
|
|
||||||
else
|
|
||||||
err "GitHub asset is $(basename "$github_zip"), expected $SLUG-$version.zip"
|
|
||||||
fi
|
|
||||||
case "$(basename "$wporg_zip")" in
|
|
||||||
"$SLUG"*) err "WordPress.org zip name starts with $SLUG; sites could install it from GitHub" ;;
|
|
||||||
*) ok "WordPress.org zip name does not start with $SLUG" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
UNPACKED=""
|
|
||||||
check_zip "$github_zip" "github"
|
|
||||||
local github_dir="$UNPACKED" path
|
|
||||||
if [ -z "$UPDATER_FILES" ]; then
|
|
||||||
note "no .distignore-wporg: both builds hold the same files"
|
|
||||||
fi
|
|
||||||
for path in $UPDATER_FILES; do
|
|
||||||
if [ -e "$github_dir/$path" ]; then ok "github: has $path"; else err "github: $path missing (listed in .distignore-wporg)"; fi
|
|
||||||
done
|
|
||||||
if grep -Eq "^[[:space:]*]*GitHub Plugin URI:" "$github_dir/$MAIN_FILE"; then ok "github: has the GitHub Plugin URI header"; else err "github: no GitHub Plugin URI header, sites cannot update it from GitHub"; fi
|
|
||||||
|
|
||||||
check_zip "$wporg_zip" "wporg"
|
|
||||||
local wporg_dir="$UNPACKED"
|
|
||||||
for path in $UPDATER_FILES; do
|
|
||||||
if [ -e "$wporg_dir/$path" ]; then err "wporg: $path must not be in the WordPress.org build"; else ok "wporg: no $path"; fi
|
|
||||||
done
|
|
||||||
if grep -Eq "^[[:space:]*]*($UPDATER_HEADERS):" "$wporg_dir/$MAIN_FILE"; then err "wporg: GitHub updater header lines still in $MAIN_FILE"; else ok "wporg: no GitHub updater header lines"; fi
|
|
||||||
local hits
|
|
||||||
hits="$(grep -rEl --include='*.php' --include='*.js' 'gu_override_dot_org|api\.github\.com/repos|Plugin_Upgrader|Theme_Upgrader|site_transient_update_plugins|auto_update_(plugin|theme)' "$wporg_dir" 2>/dev/null | sed "s|^$wporg_dir/||" || true)"
|
|
||||||
if [ -z "$hits" ]; then
|
|
||||||
ok "wporg: no code that installs or updates plugins from elsewhere"
|
|
||||||
else
|
|
||||||
warn "wporg: check these install or update code (guideline 8 allows only WordPress.org sources): $(printf '%s' "$hits" | tr '\n' ' ')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
section "Remote assets and services (WordPress.org build)"
|
|
||||||
hits="$(grep -rEn "(wp_(enqueue|register)_(script|style)|<script|<link)[^;]*['\"](https?:)?//" "$wporg_dir" --include='*.php' 2>/dev/null | sed "s|^$wporg_dir/||" | cut -c1-160 || true)"
|
|
||||||
if [ -z "$hits" ]; then ok "no scripts or styles loaded from other sites"; else warn "scripts or styles from other sites (guideline 8: ship them in the plugin unless they are part of a service):"; printf '%s\n' "$hits" | sed 's/^/ /'; fi
|
|
||||||
local readme_text hosts host missing=""
|
|
||||||
readme_text="$(cat "$wporg_dir/readme.txt")"
|
|
||||||
hosts="$(grep -rEoh 'https?://[A-Za-z0-9.-]+\.[a-z]{2,}' "$wporg_dir/includes" "$wporg_dir/blocks" 2>/dev/null | sed -E 's|https?://||; s|^www\.||' | sort -u || true)"
|
|
||||||
for host in $hosts; do
|
|
||||||
case "$host" in
|
|
||||||
w3.org | gnu.org | wordpress.org | *.wordpress.org | w.org | *.w.org | wp.org | *.wp.org | example.com | *.example.com) continue ;;
|
|
||||||
esac
|
|
||||||
if ! printf '%s' "$readme_text" | grep -Fqi "$host"; then missing="$missing $host"; fi
|
|
||||||
done
|
|
||||||
if [ -z "$missing" ]; then
|
|
||||||
ok "every host in includes/ and blocks/ is named in readme.txt"
|
|
||||||
else
|
|
||||||
note "hosts in code not named in readme.txt (fine if they are only links; services the plugin contacts need an External services entry):$missing"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Checks for parts only some plugins have; each runs when its files exist.
|
|
||||||
# A plugin's own checks: scripts/preflight-plugin.sh defines plugin_preflight,
|
|
||||||
# which gets the unpacked GitHub build and can use section, ok, warn and err.
|
|
||||||
if [ -f "$SCRIPT_DIR/preflight-plugin.sh" ]; then
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
. "$SCRIPT_DIR/preflight-plugin.sh"
|
|
||||||
plugin_preflight "$github_dir"
|
|
||||||
fi
|
|
||||||
if [ -f "$SCRIPT_DIR/replaced-plugins.php" ]; then
|
|
||||||
check_replaced_count "$github_dir"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# README.md's "<Plugin Name> replaces **N plugins**" line matches the
|
|
||||||
# 'replaces' entries in the code (scripts/replaced-plugins.php).
|
|
||||||
check_replaced_count() {
|
|
||||||
local dir="$1"
|
|
||||||
section "Replaced plugins"
|
|
||||||
if ! command -v php >/dev/null 2>&1; then
|
|
||||||
warn "php not found, the replaced plugins count in README.md was not checked"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local out
|
|
||||||
if out="$(php "$SCRIPT_DIR/replaced-plugins.php" --check "$dir" 2>&1)"; then
|
|
||||||
ok "$out"
|
|
||||||
else
|
|
||||||
err "$out"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Core files match the starter's (scripts/sync-core.sh --check), when a
|
|
||||||
# checkout of the starter is at hand. A warning, not an error: the starter's
|
|
||||||
# checkout may be behind, and only a person can tell which side is right.
|
|
||||||
check_core_files() {
|
|
||||||
section "Core files"
|
|
||||||
local out status=0
|
|
||||||
out="$(bash "$SCRIPT_DIR/sync-core.sh" --check 2>&1)" || status=$?
|
|
||||||
case "$status" in
|
|
||||||
0) ok "$(printf '%s\n' "$out" | tail -n 1)" ;;
|
|
||||||
1) warn "$(printf '%s\n' "$out" | sed -n '2,$p' | tr '\n' ' ')" ;;
|
|
||||||
*) note "not compared: ${out#sync-core: }" ;;
|
|
||||||
esac
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
check_git() {
|
|
||||||
local ref="$1"
|
|
||||||
local sha="$2"
|
|
||||||
local version="$3"
|
|
||||||
section "Git"
|
|
||||||
local tag_sha
|
|
||||||
if tag_sha="$(git rev-parse --verify --quiet "refs/tags/v$version^{commit}")"; then
|
|
||||||
if [ "$tag_sha" = "$sha" ]; then ok "tag v$version is this commit"; else warn "tag v$version exists on another commit (${tag_sha:0:12}); bump the version"; fi
|
|
||||||
else
|
|
||||||
note "no tag v$version yet"
|
|
||||||
fi
|
|
||||||
local main_sha
|
|
||||||
if main_sha="$(git rev-parse --verify --quiet "refs/remotes/origin/main^{commit}")"; then
|
|
||||||
if git merge-base --is-ancestor "$sha" "$main_sha"; then ok "$ref is on origin/main"; else note "$ref is not on origin/main yet; release only from main"; fi
|
|
||||||
fi
|
|
||||||
if [ "$ref" = "HEAD" ] && [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
|
||||||
note "uncommitted changes are not checked (the build comes from HEAD)"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local ref="HEAD"
|
|
||||||
local strict=0
|
|
||||||
local arg
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
arg="$1"
|
|
||||||
case "$arg" in
|
|
||||||
--ref)
|
|
||||||
[ $# -ge 2 ] || die "--ref needs a value"
|
|
||||||
ref="$2"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--offline) OFFLINE=1 ;;
|
|
||||||
--strict) strict=1 ;;
|
|
||||||
--no-docker) USE_DOCKER=0 ;;
|
|
||||||
-h | --help)
|
|
||||||
usage
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*) die "unknown argument: $arg" ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
local root sha
|
|
||||||
root="$(git rev-parse --show-toplevel)" || die "run this inside a checkout of the plugin"
|
|
||||||
cd "$root"
|
|
||||||
sha="$(git rev-parse --verify --quiet "$ref^{commit}")" || die "not a commit: $ref"
|
|
||||||
plugin_identity "$sha" || die "cannot tell which plugin this is at $ref"
|
|
||||||
SLUG="$PLUGIN_SLUG"
|
|
||||||
MAIN_FILE="$PLUGIN_MAIN_FILE"
|
|
||||||
VERSION_CONSTANT="${PLUGIN_CONST}_VERSION"
|
|
||||||
UPDATER_FILES="$(plugin_wporg_only "$sha")"
|
|
||||||
|
|
||||||
trap cleanup EXIT
|
|
||||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/$SLUG-preflight.XXXXXX")"
|
|
||||||
|
|
||||||
local main_php readme plugin_header
|
|
||||||
main_php="$(git show "$sha:$MAIN_FILE")"
|
|
||||||
readme="$(git show "$sha:readme.txt")" || die "readme.txt missing at $ref"
|
|
||||||
if git cat-file -e "$sha:changelog.txt" 2>/dev/null; then
|
|
||||||
CHANGELOG_TXT="$(git show "$sha:changelog.txt")"
|
|
||||||
fi
|
|
||||||
plugin_header="$(printf '%s\n' "$main_php" | sed -n '1,/\*\//p')"
|
|
||||||
|
|
||||||
printf '%s preflight: %s (%s)\n' "$PLUGIN_NAME" "$ref" "${sha:0:12}"
|
|
||||||
VERSION=""
|
|
||||||
check_versions "$plugin_header" "$readme" "$main_php"
|
|
||||||
check_readme "$readme" "$plugin_header" "$VERSION"
|
|
||||||
check_wporg "$readme" "$plugin_header"
|
|
||||||
|
|
||||||
local zips github_zip wporg_zip
|
|
||||||
zips="$("$root/scripts/build-release.sh" --ref "$sha" --out "$TMP_DIR/dist" --quiet)" || die "build failed"
|
|
||||||
github_zip="$(printf '%s\n' "$zips" | sed -n 1p)"
|
|
||||||
wporg_zip="$(printf '%s\n' "$zips" | sed -n 2p)"
|
|
||||||
check_builds "$github_zip" "$wporg_zip" "$VERSION"
|
|
||||||
if [ -f "$SCRIPT_DIR/sync-core.sh" ]; then
|
|
||||||
check_core_files
|
|
||||||
fi
|
|
||||||
check_git "$ref" "$sha" "$VERSION"
|
|
||||||
|
|
||||||
printf '\n%s error(s), %s warning(s).\n' "$ERRORS" "$WARNINGS"
|
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
|
||||||
printf 'Not ready: fix the errors.\n'
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [ "$strict" -eq 1 ] && [ "$WARNINGS" -gt 0 ]; then
|
|
||||||
printf 'Not ready (--strict): resolve or accept the warnings.\n'
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
printf 'Next: scripts/plugin-check.sh (Plugin Check on both zips).\n'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user