diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2323038 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[php]": { + "editor.defaultFormatter": "wongjn.php-sniffer", + "editor.formatOnSave": true + } +} diff --git a/IMPLEMENTATION-GUIDE.md b/IMPLEMENTATION-GUIDE.md new file mode 100644 index 0000000..9b90cd2 --- /dev/null +++ b/IMPLEMENTATION-GUIDE.md @@ -0,0 +1,265 @@ +# StellarWP Changelog Embed Implementation Guide + +This guide provides detailed instructions for implementing, customizing, and extending the StellarWP Changelog Embed plugin. + +## Installation and Setup + +### Prerequisites + +- WordPress 5.8 or higher +- PHP 7.4 or higher +- Node.js and npm (for development only) + +### Installation Steps + +1. **Create Plugin Directory**: + Create a directory named `stellar-changelog-embed` in your WordPress plugins folder (`wp-content/plugins/`). + +2. **Add Plugin Files**: + Copy all the plugin files to the directory, maintaining the folder structure as described in the README. + +3. **Install Development Dependencies** (for development only): + ```bash + cd wp-content/plugins/stellar-changelog-embed + npm install + ``` + +4. **Build Block Assets** (for development only): + ```bash + npm run build + ``` + +5. **Activate the Plugin**: + Log in to your WordPress admin area and activate the plugin from the Plugins menu. + +## Configuration + +### GitHub API Token + +To avoid rate limiting and access private repositories: + +1. Create a Personal Access Token on GitHub: + - Go to GitHub > Settings > Developer Settings > Personal Access Tokens + - Generate a new token with the `repo` scope + +2. Add the token to the plugin: + - Go to WordPress admin > Settings > StellarWP Changelog Embed + - Paste your GitHub token + - Save changes + +## Technical Details + +### Processing Flow + +1. User inserts StellarWP Changelog Embed block and configures repo details +2. Plugin saves these settings as block attributes +3. When page loads, plugin: + - Fetches changelog file from GitHub (using cached version if available) + - Parses the changelog text into structured data + - Renders the changelog using the template + - Applies JavaScript for interactive elements + +### Caching System + +- GitHub API responses are cached using WordPress transients +- Default cache duration: 1 hour +- Cache key format: `stellar_changelog_embed_[MD5_HASH]` +- Cache can be manually cleared from Settings > StellarWP Changelog Embed + +### Block Implementation + +The plugin implements a Gutenberg block with: + +- Server-side rendering for improved performance and SEO +- Block inspector controls for configuration options +- Live preview during editing + +## Customization + +### Adding Custom Change Types + +The parser automatically detects change types from the format `* Type - Description`. If you need to style additional types beyond the default ones (Fix, Feature, Tweak, Security): + +1. Add CSS styles for the new types in `assets/css/changelog-viewer.css`: + ```css + .stellar-changelog-embed__section[data-type="YourType"] .stellar-changelog-embed__section-header { + background-color: #your-color; + } + + .stellar-changelog-embed__section[data-type="YourType"] .stellar-changelog-embed__section-title { + color: #your-text-color; + } + + .stellar-changelog-embed__section[data-type="YourType"] .stellar-changelog-embed__section-count { + background-color: #your-lighter-color; + color: #your-darker-color; + } + ``` + +2. Update the template if needed in `src/views/changelog.php` + +### Advanced Template Customization + +To create a completely custom template: + +1. Copy `src/views/changelog.php` to your theme: + ``` + wp-content/themes/your-theme/stellar-changelog-embed/changelog.php + ``` + +2. Add a filter to WordPress to use your custom template: + ```php + add_filter( 'stellar_changelog_embed_template_path', function( $template_path ) { + $custom_template = get_stylesheet_directory() . '/stellar-changelog-embed/changelog.php'; + if ( file_exists( $custom_template ) ) { + return $custom_template; + } + return $template_path; + } ); + ``` + +3. Customize your template file. The template uses BEM CSS methodology with the `stellar-changelog-embed` block and has access to: + - `$changelog_data`: Array of parsed changelog entries + - CSS classes follow BEM structure: `stellar-changelog-embed__element--modifier` + +### JavaScript Event Hooks + +The frontend JavaScript triggers custom events you can hook into: + +```javascript +// Listen for when a changelog version is expanded +jQuery(document).on('stellar_changelog_embed_version_expanded', function(event, versionData) { + console.log('Version expanded:', versionData.version); +}); + +// Listen for when a changelog version is collapsed +jQuery(document).on('stellar_changelog_embed_version_collapsed', function(event, versionData) { + console.log('Version collapsed:', versionData.version); +}); +``` + +### CSS Class Structure + +The plugin uses BEM (Block Element Modifier) methodology for CSS classes: + +- **Block**: `stellar-changelog-embed` (main container) +- **Elements**: `stellar-changelog-embed__element` (using double underscores) +- **Modifiers**: `stellar-changelog-embed__element--modifier` (using double hyphens) + +Key CSS classes: +- `.stellar-changelog-embed__header` - Main header section +- `.stellar-changelog-embed__version` - Individual version container +- `.stellar-changelog-embed__version-header` - Version header with toggle +- `.stellar-changelog-embed__section` - Change type sections (Fix, Feature, etc.) +- `.stellar-changelog-embed__pagination` - Pagination controls +- `.stellar-changelog-embed__version-tag--latest` - Latest version indicator +- `.stellar-changelog-embed__pagination-btn--active` - Active page button + +### Hook Naming Convention + +The plugin uses a consistent naming convention for all hooks and events: + +- **PHP Filters/Actions**: `stellar_changelog_embed_*` +- **JavaScript Events**: `stellar_changelog_embed_*` +- **Cache Keys**: `stellar_changelog_embed_*` +- **Class Names**: `Stellar_Changelog_Embed_*` + +Available hooks: +- `stellar_changelog_embed_template_path` - Customize template path +- `stellar_changelog_embed_default_branch` - Change default branch +- `stellar_changelog_embed_parser` - Custom parser class +- `stellar_changelog_embed_cache_duration` - Cache duration +- `stellar_changelog_embed_version_expanded` - JavaScript event +- `stellar_changelog_embed_version_collapsed` - JavaScript event + +## Integration with Other Plugins + +### Integration with WP GitHub Updater + +If you're using GitHub to host your plugins and want auto-updates: + +1. Ensure your changelog file is at the root of your GitHub repository +2. Set up GitHub Updater with the same repository details +3. This plugin will automatically display the latest changes when updates are available + +### Integration Examples + +The changelog embed block can be used anywhere WordPress supports blocks: + +1. Add the changelog block to any post, page, or custom post type +2. Configure it to point to your GitHub repository +3. Display up-to-date changelog information anywhere on your site + +## Common Code Modifications + +### Change Default Branch + +To change the default branch from "main" to something else: + +```php +// Add to your theme's functions.php. +add_filter( 'stellar_changelog_embed_default_branch', function() { + return 'master'; // Or any other branch name. +} ); +``` + +### Custom Parsing Rules + +To support a different changelog format: + +1. Create a custom parser class that extends `Stellar_Changelog_Embed_Parser`. +2. Override the `parse()` method with your custom logic. +3. Replace the default parser with your custom one: + +```php +add_filter( 'stellar_changelog_embed_parser', function() { + return new Your_Custom_Parser(); +} ); +``` + +### Enable Shortcode Support + +To add shortcode support (in addition to the block): + +```php +// Add to your plugin or theme. +function changelog_viewer_shortcode( $atts ) { + $attributes = shortcode_atts( [ + 'owner' => '', + 'repo' => '', + 'file_path' => 'changelog.txt', + 'branch' => 'main', + 'max_versions' => 5, + ], $atts ); + + return Stellar_Changelog_Embed::render_block( $attributes ); +} +add_shortcode( 'changelog_viewer', 'changelog_viewer_shortcode' ); +``` + +## Security Considerations + +- The plugin validates and sanitizes all user inputs +- GitHub API requests are made server-side to avoid exposing API tokens +- Content from GitHub is sanitized before display to prevent XSS attacks +- WordPress nonces are used for all admin actions +- AJAX requests include permission checks + +## Performance Optimization + +- Use browser caching for CSS/JS files (configure in your web server) +- Consider increasing the cache duration for changelog data: + ```php + add_filter( 'stellar_changelog_embed_cache_duration', function() { + return DAY_IN_SECONDS; // Cache for 24 hours. + } ); + ``` +- If you have multiple blocks showing the same repository, they will share the cache + +## Support and Maintenance + +For future plugin updates, you may need to: + +1. Update the JavaScript dependencies in package.json +2. Rebuild the block editor scripts with `npm run build` +3. Test compatibility with new WordPress versions diff --git a/README.md b/README.md index da71f5d..c090691 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,120 @@ # StellarWP Changelog Embed -A simple block for embedding a changelog text file as content. +A WordPress plugin that allows you to display changelogs from your GitHub repositories in a clean, organized format using a Gutenberg block. + +## Features + +- Connects to GitHub to fetch changelog.txt files directly from your repositories +- Parses WordPress plugin-style changelog entries +- Displays changelogs in an expandable/collapsible interface +- Groups changes by type (Fix, Feature, Tweak, Security, etc.) +- Caches GitHub API responses to improve performance +- Full Gutenberg block integration with live preview +- Responsive design that works on all devices +- Preserves user preferences for expanded/collapsed versions + +## Installation + +1. Upload the `stellar-changelog-embed` folder to the `/wp-content/plugins/` directory +2. Activate the plugin through the 'Plugins' menu in WordPress +3. Add a StellarWP Changelog Embed block to any post or page + +## Usage + +### Using the Block + +1. Add a "StellarWP Changelog Embed" block to your post or page +2. In the block settings, enter: + - Repository Owner (GitHub username or organization) + - Repository Name + - Changelog File Path (defaults to "changelog.txt") + - Branch (defaults to "main") + - Maximum Versions to display +3. Save your post or page + +### GitHub API Configuration + +To avoid GitHub API rate limits or to access private repositories: + +1. Go to Settings > StellarWP Changelog Embed +2. Enter your GitHub Personal Access Token +3. Save Changes + +## Changelog Format + +The plugin expects changelog files in this format: + +``` += [4.21.1 2025-08-07] = +* Fix - Fixed missing quiz points in the activity report widget. +* Tweak - Improved the UX of the quiz template saving process. +* Feature - Added new functionality for users. +* Security - Fixed potential vulnerability in the login system. + += [4.21.0 2025-07-01] = +* Feature - Added new quiz statistics visualization tools. +* Fix - Resolved an issue with the course progress not updating correctly. +... +``` + +## Development + +For detailed implementation instructions, customization options, and technical details, see the [Implementation Guide](IMPLEMENTATION-GUIDE.md). + +### Key Components + +- **plugin.php**: Main plugin file +- **class-github-api.php**: Handles GitHub API integration and caching +- **class-changelog-parser.php**: Parses changelog text into structured data +- **class-settings.php**: Manages plugin settings and admin interface +- **class-api.php**: Registers REST API endpoints +- **index.js**: Block editor implementation +- **frontend.js**: Frontend interaction handling +- **src/views/changelog.php**: View template for rendering the changelog (uses BEM CSS classes) + +### Building the JavaScript + +This plugin uses @wordpress/scripts for building: + +1. Install dependencies: `npm install` +2. Development build with watch mode: `npm start` +3. Production build: `npm run build` + +## Customization + +### CSS Customization + +The plugin includes styling that should work with most themes. The CSS uses BEM methodology with the `stellar-changelog-embed` block. If you need to customize the appearance, you can: + +1. Add custom CSS to your theme targeting the BEM classes: + ```css + .stellar-changelog-embed__version-header { + /* Custom styles for version headers */ + } + + .stellar-changelog-embed__section[data-type="Feature"] { + /* Custom styles for feature sections */ + } + ``` +2. Or edit the `assets/css/changelog-viewer.css` file directly + +### Template Customization + +To customize the HTML output: + +1. Copy `src/views/changelog.php` to your theme folder under `my-theme/stellar-changelog-embed/changelog.php` +2. Edit the template as needed. The template uses BEM CSS classes for styling: + - Main container: `stellar-changelog-embed` + - Elements: `stellar-changelog-embed__element` + - Modifiers: `stellar-changelog-embed__element--modifier` + +## Troubleshooting + +- **GitHub API Rate Limit**: If you see errors about rate limits, add a GitHub Personal Access Token in the plugin settings +- **No Changelog Data**: Verify your repository details and ensure the changelog file exists at the specified path +- **Parse Errors**: Check that your changelog follows the expected format + +## Credits + +- Built by Your Name +- Inspired by the React component provided as a reference diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..62d149d --- /dev/null +++ b/changelog.txt @@ -0,0 +1,9 @@ += [2.0.0] 2025-08-11 = + +* Feature - Breaking change! The changelog is now fetched from Github repositories. No need to update the files anymore. If you previously had the block configured, it will need to be reconfigured to be pointed to GitHub. +* Feature - A GitHub Personal Access Token can be entered under Settings -> StellarWP Changelog Viewer to increase rate limits. +* Tweak - Added filters: `stellar_changelog_embed_cache_duration`, `stellar_changelog_embed_supported_file_types`, `stellar_changelog_embed_type_plurals`. + += [1.0.0] 2024-10-03 = + +* Feature - Initial release. diff --git a/composer.json b/composer.json index 4180bc3..33965bd 100644 --- a/composer.json +++ b/composer.json @@ -8,11 +8,19 @@ "StellarWP\\ChangelogEmbed\\": "src/ChangelogEmbed/" } }, - "require": {}, "scripts": { "pup": [ "sh -c 'test -f ./bin/pup.phar || curl -o bin/pup.phar -L -C - https://github.com/stellarwp/pup/releases/download/1.3.7/pup.phar'", "@php ./bin/pup.phar" ] + }, + "require-dev": { + "slevomat/coding-standard": "^8.20", + "wp-coding-standards/wpcs": "^3.1" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/composer.lock b/composer.lock index 6a1bbc9..5765b1c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,543 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a3cf4163eac284c9afa9239eb7f1ee98", + "content-hash": "b59bb3a4305885cd4a2d569bd8280cec", "packages": [], - "packages-dev": [], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^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", + "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": "2025-07-17T20:45:56+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/fa4b8d051e278072928e32d817456a7fdb57b6ca", + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.1.6", + "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": "2025-06-14T07:40:39+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad", + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad", + "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.0 || ^4.0" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.1.6", + "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": "2025-06-12T04:32:33+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" + }, + "time": "2025-07-13T07:04:09+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "8.20.0", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "b4f9f02edd4e6a586777f0cabe8d05574323f3eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b4f9f02edd4e6a586777f0cabe8d05574323f3eb", + "reference": "b4f9f02edd4e6a586777f0cabe8d05574323f3eb", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.1.2", + "php": "^7.4 || ^8.0", + "phpstan/phpdoc-parser": "^2.2.0", + "squizlabs/php_codesniffer": "^3.13.2" + }, + "require-dev": { + "phing/phing": "3.0.1|3.1.0", + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/phpstan": "2.1.19", + "phpstan/phpstan-deprecation-rules": "2.0.3", + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "2.0.6", + "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.27|12.2.7" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.20.0" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2025-07-26T15:35:10+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "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", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "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": "2025-06-17T22:17:01+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "d2421de7cec3274ae622c22c744de9a62c7925af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/d2421de7cec3274ae622c22c744de9a62c7925af", + "reference": "d2421de7cec3274ae622c22c744de9a62c7925af", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=5.4", + "phpcsstandards/phpcsextra": "^1.4.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^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": "2025-07-24T20:08:31+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -14,5 +548,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..62332aa --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,69 @@ + + + The custom ruleset for the StellarWP Changelog Embed plugin. + + + + + + . + + vendor/ + vendor-prefixed/ + tests/_data + build/modules/blocks/**/*.asset.php + node_modules/ + + + + + + + + + + + + + + + + + + + + + + + + + + + src/ChangelogEmbed/* + + + + + + + + + + + + + + + + + + + + + src/views/* + + + + diff --git a/plugin.php b/plugin.php index 6dfb091..38e0a00 100644 --- a/plugin.php +++ b/plugin.php @@ -1,10 +1,12 @@ register(); -} ); +add_action( + 'init', + static function () { + Plugin::instance()->register(); + + Settings::instance()->hooks(); + + API::instance()->hooks(); + } +); diff --git a/src/ChangelogEmbed/API.php b/src/ChangelogEmbed/API.php new file mode 100644 index 0000000..e3200a0 --- /dev/null +++ b/src/ChangelogEmbed/API.php @@ -0,0 +1,151 @@ + \WP_REST_Server::READABLE, + 'callback' => [ __CLASS__, 'get_changelog' ], + 'permission_callback' => '__return_true', + 'args' => [ + 'owner' => [ + 'required' => true, + 'description' => __( 'GitHub repository owner', 'stellar-changelog-embed' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'repo' => [ + 'required' => true, + 'description' => __( 'GitHub repository name', 'stellar-changelog-embed' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'path' => [ + 'required' => false, + 'default' => 'changelog.txt', + 'description' => __( 'Path to changelog file in repository', 'stellar-changelog-embed' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'branch' => [ + 'required' => false, + 'default' => 'main', + 'description' => __( 'Repository branch', 'stellar-changelog-embed' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'max_versions' => [ + 'required' => false, + 'default' => 5, + 'description' => __( 'Maximum versions to return', 'stellar-changelog-embed' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ], + ], + ] + ); + } + + /** + * Gets changelog data. + * + * @since 2.0.0 + * + * @param \WP_REST_Request $request REST API request. + * + * @return \WP_REST_Response|\WP_Error Changelog data or error. + */ + public static function get_changelog( \WP_REST_Request $request ) { + // Get request parameters. + $owner = $request->get_param( 'owner' ); + $repo = $request->get_param( 'repo' ); + $path = $request->get_param( 'path' ); + $branch = $request->get_param( 'branch' ); + $max_versions = $request->get_param( 'max_versions' ); + + $changelog_parser = new Changelog_Parser(); + $cache = new Cache(); + + $cache_key = $owner . '_' . $repo . '_' . $path . '_' . $branch . '_' . $max_versions; + $cached_data = $cache->get( $cache_key ); + + if ( $cached_data ) { + if ( is_wp_error( $cached_data ) ) { + return rest_ensure_response( $cached_data ); + } + + $changelog_data = $changelog_parser->parse( $cached_data, $max_versions ); + + return rest_ensure_response( $changelog_data ); + } + + // Get GitHub API instance. + $github_api = new GitHub_API(); + + // Fetch changelog content. + $changelog_content = $github_api->get_file_content( $owner, $repo, $path, $branch ); + + $cache->set( $cache_key, $changelog_content ); + + if ( is_wp_error( $changelog_content ) ) { + return rest_ensure_response( $changelog_content ); + } + + // Parse changelog. + $changelog_parser = new Changelog_Parser(); + $changelog_data = $changelog_parser->parse( $changelog_content, $max_versions ); + + return rest_ensure_response( $changelog_data ); + } +} diff --git a/src/ChangelogEmbed/Cache.php b/src/ChangelogEmbed/Cache.php new file mode 100644 index 0000000..49a54e3 --- /dev/null +++ b/src/ChangelogEmbed/Cache.php @@ -0,0 +1,194 @@ +get_cache_key( $key ); + + return get_transient( $cache_key ); + } + + /** + * Sets cached content. + * + * @since 2.0.0 + * + * @param string $key Cache key. + * @param mixed $content Content to cache. + * @param int|null $duration Cache duration in seconds. Default is null (uses default duration). + * + * @return bool True on success, false on failure. + */ + public function set( string $key, $content, ?int $duration = null ): bool { + $cache_key = $this->get_cache_key( $key ); + $duration = $duration ?? $this->get_cache_duration(); + + $result = set_transient( $cache_key, $content, $duration ); + + // Always maintain a list of our cache keys for clearing. + if ( $result ) { + $this->add_to_cache_keys_list( $cache_key ); + } + + return $result; + } + + /** + * Deletes cached content. + * + * @since 2.0.0 + * + * @param string $key Cache key. + * + * @return bool True on success, false on failure. + */ + public function delete( string $key ): bool { + $cache_key = $this->get_cache_key( $key ); + + $result = delete_transient( $cache_key ); + + // Always remove from our keys list. + if ( $result ) { + $this->remove_from_cache_keys_list( $cache_key ); + } + + return $result; + } + + /** + * Clears all plugin cache. + * + * @since 2.0.0 + * + * @return int Number of cache items cleared. + */ + public function clear_all(): int { + $count = 0; + + // Get the list of all our cache keys. + $cache_keys_list = get_transient( $this->cache_prefix . 'keys_list' ); + + if ( is_array( $cache_keys_list ) ) { + foreach ( $cache_keys_list as $key ) { + if ( delete_transient( $key ) ) { + ++$count; + } + } + } + + // Clear the keys list itself. + delete_transient( $this->cache_prefix . 'keys_list' ); + + return $count; + } + + /** + * Gets the cache key with prefix. + * + * @since 2.0.0 + * + * @param string $key Cache key. + * + * @return string Full cache key with prefix. + */ + private function get_cache_key( string $key ): string { + return $this->cache_prefix . md5( $key ); + } + + /** + * Gets the current cache duration. + * + * @since 2.0.0 + * + * @return int Cache duration in seconds. + */ + public function get_cache_duration(): int { + /** + * Filters the cache duration. + * + * @since 2.0.0 + * + * @param int $default_cache_duration The default cache duration in seconds. Default is 1 hour. + * + * @return int Cache duration in seconds. + */ + return apply_filters( 'stellar_changelog_embed_cache_duration', HOUR_IN_SECONDS ); + } + + /** + * Adds a cache key to the list for cache clearing. + * + * @since 2.0.0 + * + * @param string $cache_key The cache key to add. + * + * @return void + */ + private function add_to_cache_keys_list( string $cache_key ): void { + $keys_list_key = $this->cache_prefix . 'keys_list'; + $keys_list = get_transient( $keys_list_key ); + + if ( ! is_array( $keys_list ) ) { + $keys_list = []; + } + + // Add the key if it's not already in the list. + if ( ! in_array( $cache_key, $keys_list, true ) ) { + $keys_list[] = $cache_key; + + // Store the list with the same duration as individual cache items. + set_transient( $keys_list_key, $keys_list, $this->get_cache_duration() ); + } + } + + /** + * Removes a cache key from the list for cache clearing. + * + * @since 2.0.0 + * + * @param string $cache_key The cache key to remove. + * + * @return void + */ + private function remove_from_cache_keys_list( string $cache_key ): void { + $keys_list_key = $this->cache_prefix . 'keys_list'; + $keys_list = get_transient( $keys_list_key ); + + if ( is_array( $keys_list ) ) { + $keys_list = array_diff( $keys_list, [ $cache_key ] ); + + set_transient( $keys_list_key, $keys_list, $this->get_cache_duration() ); + } + } +} diff --git a/src/ChangelogEmbed/Changelog_Parser.php b/src/ChangelogEmbed/Changelog_Parser.php new file mode 100644 index 0000000..157ec97 --- /dev/null +++ b/src/ChangelogEmbed/Changelog_Parser.php @@ -0,0 +1,180 @@ += $max_versions ) { + break; + } + + $version = $matches[1]; + // Extract date if available (sometimes it's added after the version). + $date = isset( $matches[2] ) ? trim( $matches[2] ) : ''; + + $current_version = [ + 'version' => $version, + 'date' => $date, + 'isLatest' => count( $changelog_data ) === 0, // First one is latest. + 'changes' => [], + ]; + + $changelog_data[] = $current_version; + continue; + } + + // Process change entries (e.g., "* Fix - Fixed an issue..."). + if ( $current_version !== null && + preg_match( '/^(?:[\*|-]\s*)(?:([\w\-]+)\s*-)?\s*(.+)$/i', $line, $matches ) ) { + + $type = ! empty( trim( $matches[1] ) ) ? trim( $matches[1] ) : __( 'Change', 'stellar-changelog-embed' ); + $content = trim( $matches[2] ); + + $content = $this->escape_shortcodes( $content ); + $content = $this->escape_code_blocks( $content ); + $content = $this->convert_bold_text( $content ); + $content = $this->convert_italic_text( $content ); + + $change = [ + 'type' => $type, + 'content' => $content, + ]; + + // Add to current version's changes. + $index = count( $changelog_data ) - 1; + $changelog_data[ $index ]['changes'][] = $change; + } + } + + return $changelog_data; + } + + /** + * Escapes registered shortcodes that are used in the content. + * + * @since 2.0.0 + * + * @param string $content Content to escape. + * + * @return string Escaped content. + */ + private function escape_shortcodes( string $content ): string { + global $shortcode_tags; + + $registered_shortcodes = array_map( + function ( $tag ) { + return preg_quote( $tag, '/' ); + }, + array_keys( $shortcode_tags ) + ); + + return (string) preg_replace( + '/\[(' . implode( '|', $registered_shortcodes ) . ')\]/', + '[[$1]]', + $content + ); + } + + /** + * Escapes code blocks in the content and wraps them in tags. + * + * @since 2.0.0 + * + * @param string $content Content to escape. + * + * @return string Escaped content. + */ + private function escape_code_blocks( string $content ): string { + return (string) preg_replace_callback( + '/`{1,3}([^`]+)`{1,3}/', + function ( $matches ) { + $is_multiline = strpos( $matches[0], '```' ) !== false; + $code = esc_html( trim( $matches[1] ) ); + + if ( $is_multiline ) { + $code = preg_replace( '/\n/', '
', $code ); + + return "\n\n" . '' . $code . '' . "\n\n"; + } + + return '' . $code . ''; + }, + $content + ); + } + + /** + * Converts bold text in the content to tags. + * + * @since 2.0.0 + * + * @param string $content Content to convert. + * + * @return string Converted content. + */ + private function convert_bold_text( string $content ): string { + return (string) preg_replace( + '/\*\*([^\*]+)\*\*/', + '$1', + $content + ); + } + + /** + * Converts italic text styled as *italic* in the content to tags. + * _italic_ text is not converted, as attempting to account for this would be too complex as that pattern would often have false positives with things like action and filter names. + * + * @since 2.0.0 + * + * @param string $content Content to convert. + * + * @return string Converted content. + */ + private function convert_italic_text( string $content ): string { + return (string) preg_replace( + '/(?$1', + $content + ); + } +} diff --git a/src/ChangelogEmbed/GitHub_API.php b/src/ChangelogEmbed/GitHub_API.php new file mode 100644 index 0000000..1eea1a8 --- /dev/null +++ b/src/ChangelogEmbed/GitHub_API.php @@ -0,0 +1,138 @@ +api_base_url, + rawurlencode( $owner ), + rawurlencode( $repo ), + rawurlencode( $file_path ), + rawurlencode( $branch ) + ); + + // Set up request arguments. + $args = [ + 'headers' => [ + 'Accept' => 'application/vnd.github.v4.raw', + 'User-Agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ), + ], + 'timeout' => 30, + ]; + + // Add authentication if token is set. + $github_token = get_option( 'stellar_changelog_embed_github_token', '' ); + + if ( ! empty( $github_token ) ) { + $args['headers']['Authorization'] = 'token ' . $github_token; + } + + // Make the request. + $response = wp_remote_get( $url, $args ); + + // Check for errors. + if ( is_wp_error( $response ) ) { + return $response; + } + + // Check response code. + $response_code = wp_remote_retrieve_response_code( $response ); + + if ( 200 !== $response_code ) { + return new WP_Error( + 'github_api_error', + sprintf( + /* translators: %1$d: HTTP response code, %2$s: Response message */ + __( 'GitHub API error (HTTP %1$d): %2$s', 'stellar-changelog-embed' ), + $response_code, + wp_remote_retrieve_response_message( $response ) + ) + ); + } + + // Return the response body. + return wp_remote_retrieve_body( $response ); + } +} diff --git a/src/ChangelogEmbed/Helper.php b/src/ChangelogEmbed/Helper.php new file mode 100644 index 0000000..7ee01a6 --- /dev/null +++ b/src/ChangelogEmbed/Helper.php @@ -0,0 +1,60 @@ + __( 'Additions', 'stellar-changelog-embed' ), + __( 'Change', 'stellar-changelog-embed' ) => __( 'Changes', 'stellar-changelog-embed' ), + __( 'Deprecation', 'stellar-changelog-embed' ) => __( 'Deprecations', 'stellar-changelog-embed' ), + __( 'Enhancement', 'stellar-changelog-embed' ) => __( 'Enhancements', 'stellar-changelog-embed' ), + __( 'Feature', 'stellar-changelog-embed' ) => __( 'Features', 'stellar-changelog-embed' ), + __( 'Fix', 'stellar-changelog-embed' ) => __( 'Fixes', 'stellar-changelog-embed' ), + __( 'Improvement', 'stellar-changelog-embed' ) => __( 'Improvements', 'stellar-changelog-embed' ), + __( 'Performance', 'stellar-changelog-embed' ) => __( 'Performance', 'stellar-changelog-embed' ), + __( 'Removal', 'stellar-changelog-embed' ) => __( 'Removals', 'stellar-changelog-embed' ), + __( 'Security', 'stellar-changelog-embed' ) => __( 'Security', 'stellar-changelog-embed' ), + __( 'Tweak', 'stellar-changelog-embed' ) => __( 'Tweaks', 'stellar-changelog-embed' ), + __( 'Update', 'stellar-changelog-embed' ) => __( 'Updates', 'stellar-changelog-embed' ), + ]; + + /** + * Filters the plural forms for change types. + * + * @since 2.0.0 + * + * @param array $plurals Array of singular => plural mappings. + * @param string $type The change type being pluralized. + * + * @return array Modified array of plural forms. + */ + $plurals = apply_filters( 'stellar_changelog_embed_type_plurals', $plurals, $type ); + + // Return the predefined plural if it exists, otherwise add 's'. + return isset( $plurals[ $type ] ) + ? $plurals[ $type ] + : $type . 's'; + } +} diff --git a/src/ChangelogEmbed/Plugin.php b/src/ChangelogEmbed/Plugin.php index 4db36c4..77cd2e2 100644 --- a/src/ChangelogEmbed/Plugin.php +++ b/src/ChangelogEmbed/Plugin.php @@ -1,7 +1,21 @@ $value ) { + $request->set_param( $key, $value ); } - // Fetch the contents of the text file - $response = wp_remote_get( $changelog_url ); + $api = new API(); + $response = $api->get_changelog( $request ); if ( is_wp_error( $response ) ) { - return ''; + $error_message = sprintf( + /* translators: %s is the error message. */ + __( 'Error fetching changelog data: %s', 'stellar-changelog-embed' ), + $response->get_error_message() + ); + + ob_start(); + include STELLAR_CHANGELOG_EMBED_DIR . '/src/views/error.php'; + $template = (string) ob_get_clean(); + + return wp_kses_post( $template ); } - $body = wp_remote_retrieve_body( $response ); + $changelog_data = $response->get_data(); + $versions_per_page = intval( $attributes['per_page'] ?? 5 ); - // Replace each backtick-enclosed text with a tag. - $body = preg_replace('/`([^`]+)`/', '$1', $body ); + ob_start(); + include STELLAR_CHANGELOG_EMBED_DIR . '/src/views/changelog.php'; + $template = (string) ob_get_clean(); - // Make headings

tags. - $body = preg_replace('/= \[(\d+\.\d+\.\d+)\] =/', "\n

$1

", $body); + // Return the contents of the text file. - // Replace each asterisk at the beginning of a line with a list item
  • . - $body = preg_replace('/^\* (.+)/m', '
  • $1
  • ', $body); + $allowed_html = wp_kses_allowed_html( 'post' ); - // Wrap all groups of list items in an unordered list