16 Commits
Author SHA1 Message Date
Keith Solomon 9b6aa8a6e8 🔵 other: Bump version
Release / Build & publish plugin zip (push) Successful in 7s
2026-08-12 19:20:32 -05:00
Keith Solomon 9249a1e0a1 Refactor social sharing buttons and add Twitter SVG icon 2026-08-12 18:14:36 -05:00
Keith Solomon b7dfda1f99 Switch social-share icons to Lucide v0.460.0; shrink avatar to 40x40
Release / Build & publish plugin zip (push) Successful in 7s
Lucide (https://lucide.dev) replaces the hand-rolled Tabler paths. Lucide
is more compact and consistent, and ships under the ISC license (more
permissive than Tabler's MIT and equally fine for plugin redistribution).

Lucide has no brand icons for Reddit/WhatsApp/Pinterest, so we use
generic substitutes that preserve the visual intent:
- reddit.svg   <- lucide 'share-2'  (community share metaphor)
- whatsapp.svg <- lucide 'message-circle'
- pinterest.svg<- lucide 'pin'

Avatar: shrunk to 40x40 per user testing — sizes above 41x41 were being
clipped inside the round border on this host.
2026-08-12 17:52:44 -05:00
Keith Solomon 93c81d0fa6 Use explicit background-size 50px 50px instead of 'cover'
Release / Build & publish plugin zip (push) Successful in 7s
The 'cover' keyword was producing fallback 'auto auto' rendering in some
environments (likely a kses/CSS shorthand quirk). Switching to explicit
pixel values matching the container size has the same visual effect
for the 270x270 source image and works reliably across all browsers.
2026-08-12 17:31:51 -05:00
Keith Solomon e9b55bb0e1 Force avatar dimensions with !important on inline styles
Release / Build & publish plugin zip (push) Successful in 7s
2026-08-12 17:17:43 -05:00
Keith Solomon 426d60b692 Fix avatar and social icons with bulletproof rendering
Release / Build & publish plugin zip (push) Successful in 7s
Root cause of social icons rendering as alt text: wp_kses() stripped
the 'data:' prefix from data: URIs in img.src, leaving a relative URL
that 404s. Switched to real .svg files under assets/icons/ referenced
via plugins_url() — kses passes img tags with relative URLs cleanly,
and the icons load as standard images.

Root cause of avatar still being cropped: layout-side issues with
inline-styled <img> in a flex layout. Switched to a <span> with
inline background-image, background-size: cover, background-position:
center — no <img> element, no object-fit reliance, no way to crop wrong.

Also removed the outer <a> wrapper around the .project-owner div
(block-level element inside inline <a> is invalid HTML and was likely
contributing to layout weirdness). The owner name is now an explicit
<a> inside its own span.

Reverted kses to the default wp_kses_post() since we're back to
standard HTML elements only — no SVG, no data URIs, no inline styles
that need a custom allowed list.
2026-08-12 17:04:15 -05:00
Keith Solomon 60db82e459 Render social-share icons as <img> SVG data URIs
Release / Build & publish plugin zip (push) Successful in 7s
Inline <svg> elements render with their default width/height in browsers
even when 'style' and width/height attributes are set (a known quirk
in mixed HTML5/SVG content with some CSS specificity scenarios). Switched
to <img src='data:image/svg+xml,...'> instead. This is universally well-
supported, sizes via standard <img> CSS rules (no namespace/parser edge
cases), and the inline 'width=20 height=20 style=width:20px;height:20px'
on <img> reliably applies.

Added 'img' and 'style' to the kses allowed list so wp_kses() preserves
the data: URI src and the inline styles.
2026-08-12 12:08:24 -05:00
Keith Solomon c10866bffd Inline icon SVG dimensions + style + cache-bust plugin version
Release / Build & publish plugin zip (push) Successful in 7s
- Add inline width/height attributes AND style='width:20px;height:20px;
  display:block' to each social-share SVG. Inline styles have higher
  specificity than any CSS rule, so the icons should now size correctly
  regardless of theme CSS or specificity quirks.
- Bump PROJECTS_PORTFOLIO_VERSION to 1.1.9 (and plugin header) so
  WordPress invalidates the cached stylesheet URL.
2026-08-12 11:40:06 -05:00
Keith Solomon c127a1d065 Fix icon and avatar rendering: remove SVG viewport rects + inline avatar styles
Release / Build & publish plugin zip (push) Successful in 6s
- Removed the <rect x='0' y='0' width='24' height='24' fill='none'> viewport
  markers from each social-share SVG. With 'fill=none' the rect was
  still visible if any inherited stroke wasn't suppressed (which turned
  out to be unreliable across themes/CSS specificity).
- Removed redundant width='24' height='24' attributes from each SVG so
  the CSS .project-social-sharing svg { width:20px; height:20px } can
  size them unambiguously.
- Inlined avatar <img> styles (width:50px; height:50px; border-radius:50%;
  object-fit:cover; flex-shrink:0; display:block) so the avatar is
  guaranteed to render as a 50x50 circle regardless of theme CSS.
- Switched .project-owner-avatar to inline-flex so the span shrinks to
  the image's natural size within the flex parent.
2026-08-12 10:44:35 -05:00
Keith Solomon cd5405fe62 Allow SVG through kses in social-share buttons
Release / Build & publish plugin zip (push) Successful in 7s
wp_kses_post() strips <svg> tags by default (the 'post' context doesn't
allow them), so the icons were being silently removed from the rendered
HTML. Extend the allowed list to include svg/path/rect/circle/line with
the attributes the Tabler-style icons actually use.
2026-08-12 09:05:58 -05:00
Keith Solomon 98fd2fba5a Fix View Repo outline, social-share icon stroke, and owner avatar sizing
Release / Build & publish plugin zip (push) Successful in 7s
- .project-github-button renamed to .project-repo-button (added 1px border
  for proper outline style; the old class is gone so the button was rendering
  as unstyled <a>).
- Suppress inherited stroke on the Tabler SVG <rect> viewport marker — without
  this it draws a square outline inside the round social-share button.
- Move owner avatar inline styles into CSS with object-fit: cover, explicit
  width/height, and border-radius: 50%; add flex shrink/grow rules for the
  owner row's three spans so the avatar isn't squashed by the flex layout.
2026-08-12 08:49:56 -05:00
Keith Solomon 5c843dc76f Wire social-share hook + make Follow button provider-aware
Release / Build & publish plugin zip (push) Successful in 7s
The social-share buttons function was hooked to
projects_after_download_button, but the template never fired that action —
the buttons never rendered. Added a do_action() call after the View Repo
button.

The Follow button hardcoded a GitHub URL and used the GitHub-specific
github-button class (which only acts on github.com). Replaced with a
plain styled link pointing at the owner's actual URL (works for any
provider). Removed the assets/js/buttons.js enqueue since nothing uses
the github-button class anymore.

Renamed the project-github-button CSS class on the View Repo link to
project-repo-button since it's provider-neutral now.
2026-08-11 20:18:06 -05:00
Keith Solomon 37832b72da Fix download redirect: use wp_redirect() for external URLs
Release / Build & publish plugin zip (push) Successful in 8s
wp_safe_redirect() rejects external hosts not in WP's allowed list and
silently falls back to admin_url() — which is why /download/{id}/ was
redirecting to /wp-admin/ on a clean install. Switch to wp_redirect()
since the URL is explicitly user-configured.
2026-08-11 19:09:04 -05:00
Keith Solomon aab39f66af Fix Gitea release fetch: /releases/latest returns a single object, not an array
Release / Build & publish plugin zip (push) Successful in 7s
2026-08-11 16:52:27 -05:00
Keith Solomon 365a3898b9 Track PUC's bundled vendor/ in git (formerly ignored)
Release / Build & publish plugin zip (push) Successful in 7s
2026-08-11 16:10:19 -05:00
Keith Solomon 2bbd2dc7c8 Update bundled plugin-update-checker to v5.7 and remove broken setBranch() call
The bundled PUC was missing the setBranch() method on the main UpdateChecker
class (it's only on Vcs\PluginUpdateChecker via the VcsCheckerMethods trait).
When called with a non-VCS URL like the Gitea update URL, this caused a fatal
error at plugin activation.

Updating to PUC v5.7 (May 2026) which is the latest upstream release. The
setBranch() call is removed because PUC doesn't recognize Gitea as a VCS host;
the plugin falls back to PUC's plain JSON metadata mode, which is fine for a
plugin hosted on a self-managed repo.
2026-08-11 16:08:20 -05:00
68 changed files with 977 additions and 369 deletions
+2
View File
@@ -5,3 +5,5 @@ notes/
vendor/ vendor/
phpunit.xml phpunit.xml
.phpunit.result.cache .phpunit.result.cache
# But DO track the vendor/ that ships with the bundled plugin-update-checker library.
!/includes/plugin-update-checker/vendor/
+13 -13
View File
@@ -1,13 +1,13 @@
# Projects Portfolio # Projects Portfolio
Create a beautifully structured, developer-first showcase directory for your WordPress® plugins, themes, and patterns - powered by [GitHub](https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio). Create a beautifully structured, developer-first showcase directory for your WordPress plugins, themes, and patterns - powered by git.
## What It Does ## What It Does
**Projects Portfolio** adds a custom post type (`projects`) and taxonomy (`project-type`) to your WordPress® admin, allowing you to: **Projects Portfolio** adds a custom post type (`projects`) and taxonomy (`project-type`) to your WordPress admin, allowing you to:
- Add plugins, themes, or patterns as projects. - Add plugins, themes, or patterns as projects.
- Connect each project to a GitHub repository. - Connect each project to a git repository.
- Automatically fetch and display data like: - Automatically fetch and display data like:
- Latest version - Latest version
- Stars, forks, issues - Stars, forks, issues
@@ -27,14 +27,14 @@ Create a beautifully structured, developer-first showcase directory for your Wor
### From GitHub ### From GitHub
1. **Download the latest ZIP** from the [GitHub Releases page](https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio/releases). 1. **Download the latest ZIP** from the [GitHub Releases page](https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio/releases).
2. Go to your WordPress® Dashboard → Plugins → Add New → Upload Plugin. 2. Go to your WordPress Dashboard → Plugins → Add New → Upload Plugin.
3. Upload the ZIP and activate the plugin. 3. Upload the ZIP and activate the plugin.
### From [keithsolomon.net](https://keithsolomon.net) ### From [keithsolomon.net](https://keithsolomon.net)
1. Visit the official plugin page: [Projects Portfolio](https://keithsolomon.net/projects/projects-portfolio). 1. Visit the official plugin page: [Projects Portfolio](https://keithsolomon.net/projects/projects-portfolio).
2. Download the ZIP. 2. Download the ZIP.
3. Install it via the WordPress® dashboard like above. 3. Install it via the WordPress dashboard like above.
--- ---
@@ -42,7 +42,7 @@ Create a beautifully structured, developer-first showcase directory for your Wor
### Add a New Project ### Add a New Project
1. In the WordPress® admin, go to **Projects → Add New**. 1. In the WordPress admin, go to **Projects → Add New**.
2. Add your project title, description, and featured image. 2. Add your project title, description, and featured image.
3. Select a **Project Type**: Plugin, Theme, or Pattern. 3. Select a **Project Type**: Plugin, Theme, or Pattern.
4. In the sidebar, choose **Provider** (GitHub or Gitea) and paste the **Repository URL** (e.g., `https://github.com/username/repo` or `https://codeberg.org/username/repo`). 4. In the sidebar, choose **Provider** (GitHub or Gitea) and paste the **Repository URL** (e.g., `https://github.com/username/repo` or `https://codeberg.org/username/repo`).
@@ -79,7 +79,7 @@ Enable/disable the following GitHub data in your project views:
- Language - Language
- Downloads - Downloads
- Stars / Forks / Issues - Stars / Forks / Issues
- GitHub Owner - Repo Owner
### Archive Settings ### Archive Settings
@@ -94,8 +94,8 @@ Toggle components for the project archive view:
To connect a project to GitHub: To connect a project to GitHub:
1. Paste the full repo URL into the **GitHub URL** field when editing a Project. 1. Paste the full repo URL into the **Repository URL** field when editing a Project.
2. Make sure your GitHub repo has a release with a `.zip` asset or a valid `zipball_url`. 2. Make sure your repo has a release with a `.zip` asset or a valid `zipball_url`.
### Optional: Add a GitHub API Token ### Optional: Add a GitHub API Token
@@ -194,14 +194,14 @@ Each project includes social sharing links for:
## Support & Contributions ## Support & Contributions
Have a feature request or bug report? Open an issue on [GitHub](https://github.com/robertdevore/projects-portfolio/issues) or contribute via pull request. Have a feature request or bug report? Open an issue on [the repo](https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio/issues) or contribute via pull request.
## License ## License
Projects for WordPress® is licensed under the [GNU GPL v2+](http://www.gnu.org/licenses/gpl-2.0.txt). Projects for WordPress is licensed under the [GNU GPL v2+](http://www.gnu.org/licenses/gpl-2.0.txt).
## Author ## Author
Built and maintained by [Robert DeVore](https://www.robertdevore.com). Built and maintained by [Keith Solomon](https://keithsolomon.net).
Follow on [Twitter](https://twitter.com/deviorobert) or sponsor development on [GitHub Sponsors](https://github.com/sponsors/robertdevore). Follow on [Twitter](https://twitter.com/KeithInWPG).
+32 -10
View File
@@ -21,6 +21,7 @@
border-radius: 50%; border-radius: 50%;
text-decoration: none; text-decoration: none;
transition: background-color 0.3s; transition: background-color 0.3s;
overflow: hidden;
} }
.project-social-sharing a:hover { .project-social-sharing a:hover {
@@ -35,6 +36,14 @@
transition: stroke 0.3s; transition: stroke 0.3s;
} }
/* Tabler-style icons include a viewport <rect> with no fill; without
`stroke="none"` it inherits the parent's stroke color and renders as
a square outline inside the round button. Suppress that stroke on
the rect alone. */
.project-social-sharing svg rect {
stroke: none;
}
.project-social-sharing a:hover svg { .project-social-sharing a:hover svg {
stroke: #ffffff; stroke: #ffffff;
} }
@@ -82,16 +91,28 @@
.project-owner img { .project-owner img {
display: block; display: block;
margin: 0 auto 0; margin: 0;
max-width: 50px; width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
} }
.project-owner span:last-of-type { .project-owner-avatar {
display: flex; flex: 0 0 auto;
justify-content: space-between; display: inline-flex;
flex-direction: column; align-items: center;
flex: 1; justify-content: center;
align-items: end; }
.project-owner-name {
flex: 1 1 auto;
min-width: 0;
}
.project-owner-follow {
flex: 0 0 auto;
} }
.project-meta-table th { .project-meta-table th {
@@ -119,7 +140,7 @@
color: #FFF; color: #FFF;
} }
.project-buttons a.button.project-github-button { .project-buttons a.button.project-repo-button {
display: block; display: block;
background-color: #e0e0e0; background-color: #e0e0e0;
color: #333; color: #333;
@@ -128,9 +149,10 @@
margin: 12px 0; margin: 12px 0;
border-radius: 4px; border-radius: 4px;
text-decoration: none; text-decoration: none;
border: 1px solid #c9c9c9;
} }
.project-buttons a.button.project-github-button:hover { .project-buttons a.button.project-repo-button:hover {
background-color: #c9c9c9; background-color: #c9c9c9;
color: #333; color: #333;
} }
+15
View File
@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.460.0 - ISC -->
<svg
class="lucide lucide-facebook"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" />
</svg>

After

Width:  |  Height:  |  Size: 369 B

+17
View File
@@ -0,0 +1,17 @@
<!-- @license lucide-static v0.460.0 - ISC -->
<svg
class="lucide lucide-linkedin"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
<rect width="4" height="12" x="2" y="9" />
<circle cx="4" cy="4" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 460 B

+16
View File
@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.460.0 - ISC -->
<svg
class="lucide lucide-mail"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="20" height="16" x="2" y="4" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>

After

Width:  |  Height:  |  Size: 394 B

+16
View File
@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.460.0 - ISC -->
<svg
class="lucide lucide-pin"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 17v5" />
<path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" />
</svg>

After

Width:  |  Height:  |  Size: 525 B

+19
View File
@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.460.0 - ISC -->
<svg
class="lucide lucide-share-2"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="18" cy="5" r="3" />
<circle cx="6" cy="12" r="3" />
<circle cx="18" cy="19" r="3" />
<line x1="8.59" x2="15.42" y1="13.51" y2="17.49" />
<line x1="15.41" x2="8.59" y1="6.51" y2="10.49" />
</svg>

After

Width:  |  Height:  |  Size: 497 B

+15
View File
@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.460.0 - ISC -->
<svg
class="lucide lucide-twitter"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z" />
</svg>

After

Width:  |  Height:  |  Size: 438 B

+15
View File
@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.460.0 - ISC -->
<svg
class="lucide lucide-message-circle"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" />
</svg>

After

Width:  |  Height:  |  Size: 340 B

+15
View File
@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.460.0 - ISC -->
<svg
class="lucide lucide-twitter"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z" />
</svg>

After

Width:  |  Height:  |  Size: 438 B

@@ -4,7 +4,7 @@ namespace YahnisElsts\PluginUpdateChecker\v5;
if ( !class_exists(PucFactory::class, false) ): if ( !class_exists(PucFactory::class, false) ):
class PucFactory extends \YahnisElsts\PluginUpdateChecker\v5p4\PucFactory { class PucFactory extends \YahnisElsts\PluginUpdateChecker\v5p7\PucFactory {
} }
endif; endif;
@@ -1,6 +1,6 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
if ( !class_exists(Autoloader::class, false) ): if ( !class_exists(Autoloader::class, false) ):
@@ -1,8 +1,8 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar; namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
use YahnisElsts\PluginUpdateChecker\v5p4\PucFactory; use YahnisElsts\PluginUpdateChecker\v5p7\PucFactory;
use YahnisElsts\PluginUpdateChecker\v5p4\UpdateChecker; use YahnisElsts\PluginUpdateChecker\v5p7\UpdateChecker;
if ( !class_exists(Extension::class, false) ): if ( !class_exists(Extension::class, false) ):
@@ -1,7 +1,7 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar; namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
use YahnisElsts\PluginUpdateChecker\v5p4\UpdateChecker; use YahnisElsts\PluginUpdateChecker\v5p7\UpdateChecker;
if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false) ): if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false) ):
@@ -41,11 +41,11 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
echo '<h3>Configuration</h3>'; echo '<h3>Configuration</h3>';
echo '<table class="puc-debug-data">'; echo '<table class="puc-debug-data">';
$this->displayConfigHeader(); $this->displayConfigHeader();
$this->row('Slug', htmlentities($this->updateChecker->slug)); $this->row('Slug', esc_html($this->updateChecker->slug));
$this->row('DB option', htmlentities($this->updateChecker->optionName)); $this->row('DB option', esc_html($this->updateChecker->optionName));
$requestInfoButton = $this->getMetadataButton(); $requestInfoButton = $this->getMetadataButton();
$this->row('Metadata URL', htmlentities($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox); $this->row('Metadata URL', esc_html($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox);
$scheduler = $this->updateChecker->scheduler; $scheduler = $this->updateChecker->scheduler;
if ( $scheduler->checkPeriod > 0 ) { if ( $scheduler->checkPeriod > 0 ) {
@@ -86,14 +86,22 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
echo '<h3>Status</h3>'; echo '<h3>Status</h3>';
echo '<table class="puc-debug-data">'; echo '<table class="puc-debug-data">';
$state = $this->updateChecker->getUpdateState(); $state = $this->updateChecker->getUpdateState();
$checkNowButton = ''; $checkButtonId = $this->updateChecker->getUniqueName('check-now-button');
if ( function_exists('get_submit_button') ) { if ( function_exists('get_submit_button') ) {
$checkNowButton = get_submit_button( $checkNowButton = get_submit_button(
'Check Now', 'Check Now',
'secondary', 'secondary',
'puc-check-now-button', 'puc-check-now-button',
false, false,
array('id' => $this->updateChecker->getUniqueName('check-now-button')) array('id' => $checkButtonId)
);
} else {
//get_submit_button() is not available in the frontend. Make a button directly.
//It won't look the same without admin styles, but it should still work.
$checkNowButton = sprintf(
'<input type="button" id="%1$s" name="puc-check-now-button" value="%2$s" class="button button-secondary" />',
esc_attr($checkButtonId),
esc_attr('Check Now')
); );
} }
@@ -107,10 +115,10 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
$this->row('Next automatic check', $this->formatTimeWithDelta($nextCheck)); $this->row('Next automatic check', $this->formatTimeWithDelta($nextCheck));
if ( $state->getCheckedVersion() !== '' ) { if ( $state->getCheckedVersion() !== '' ) {
$this->row('Checked version', htmlentities($state->getCheckedVersion())); $this->row('Checked version', esc_html($state->getCheckedVersion()));
$this->row('Cached update', $state->getUpdate()); $this->row('Cached update', $state->getUpdate());
} }
$this->row('Update checker class', htmlentities(get_class($this->updateChecker))); $this->row('Update checker class', esc_html(get_class($this->updateChecker)));
echo '</table>'; echo '</table>';
} }
@@ -124,7 +132,7 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
if ( property_exists($update, $field) ) { if ( property_exists($update, $field) ) {
$this->row( $this->row(
ucwords(str_replace('_', ' ', $field)), ucwords(str_replace('_', ' ', $field)),
isset($update->$field) ? htmlentities($update->$field) : null isset($update->$field) ? esc_html($update->$field) : null
); );
} }
} }
@@ -162,7 +170,7 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
if ( is_object($value) || is_array($value) ) { if ( is_object($value) || is_array($value) ) {
//This is specifically for debugging, so print_r() is fine. //This is specifically for debugging, so print_r() is fine.
//phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
$value = '<pre>' . htmlentities(print_r($value, true)) . '</pre>'; $value = '<pre>' . esc_html(print_r($value, true)) . '</pre>';
} else if ($value === null) { } else if ($value === null) {
$value = '<code>null</code>'; $value = '<code>null</code>';
} }
@@ -1,8 +1,8 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar; namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
use YahnisElsts\PluginUpdateChecker\v5p4\Plugin\UpdateChecker; use YahnisElsts\PluginUpdateChecker\v5p7\Plugin\UpdateChecker;
if ( !class_exists(PluginExtension::class, false) ): if ( !class_exists(PluginExtension::class, false) ):
@@ -1,7 +1,7 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar; namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
use YahnisElsts\PluginUpdateChecker\v5p4\Plugin\UpdateChecker; use YahnisElsts\PluginUpdateChecker\v5p7\Plugin\UpdateChecker;
if ( !class_exists(PluginPanel::class, false) ): if ( !class_exists(PluginPanel::class, false) ):
@@ -12,19 +12,25 @@ if ( !class_exists(PluginPanel::class, false) ):
protected $updateChecker; protected $updateChecker;
protected function displayConfigHeader() { protected function displayConfigHeader() {
$this->row('Plugin file', htmlentities($this->updateChecker->pluginFile)); $this->row('Plugin file', esc_html($this->updateChecker->pluginFile));
parent::displayConfigHeader(); parent::displayConfigHeader();
} }
protected function getMetadataButton() { protected function getMetadataButton() {
$requestInfoButton = ''; $buttonId = $this->updateChecker->getUniqueName('request-info-button');
if ( function_exists('get_submit_button') ) { if ( function_exists('get_submit_button') ) {
$requestInfoButton = get_submit_button( $requestInfoButton = get_submit_button(
'Request Info', 'Request Info',
'secondary', 'secondary',
'puc-request-info-button', 'puc-request-info-button',
false, false,
array('id' => $this->updateChecker->getUniqueName('request-info-button')) array('id' => $buttonId)
);
} else {
$requestInfoButton = sprintf(
'<input type="button" name="puc-request-info-button" id="%1$s" value="%2$s" class="button button-secondary" />',
esc_attr($buttonId),
esc_attr('Request Info')
); );
} }
return $requestInfoButton; return $requestInfoButton;
@@ -1,8 +1,8 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar; namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
use YahnisElsts\PluginUpdateChecker\v5p4\Theme\UpdateChecker; use YahnisElsts\PluginUpdateChecker\v5p7\Theme\UpdateChecker;
if ( !class_exists(ThemePanel::class, false) ): if ( !class_exists(ThemePanel::class, false) ):
@@ -13,7 +13,7 @@ if ( !class_exists(ThemePanel::class, false) ):
protected $updateChecker; protected $updateChecker;
protected function displayConfigHeader() { protected function displayConfigHeader() {
$this->row('Theme directory', htmlentities($this->updateChecker->directoryName)); $this->row('Theme directory', esc_html($this->updateChecker->directoryName));
parent::displayConfigHeader(); parent::displayConfigHeader();
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
if ( !class_exists(InstalledPackage::class, false) ): if ( !class_exists(InstalledPackage::class, false) ):
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
use LogicException; use LogicException;
use stdClass; use stdClass;
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
if ( !class_exists(OAuthSignature::class, false) ): if ( !class_exists(OAuthSignature::class, false) ):
@@ -1,8 +1,8 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin; namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
use YahnisElsts\PluginUpdateChecker\v5p4\InstalledPackage; use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
use YahnisElsts\PluginUpdateChecker\v5p4\PucFactory; use YahnisElsts\PluginUpdateChecker\v5p7\PucFactory;
if ( !class_exists(Package::class, false) ): if ( !class_exists(Package::class, false) ):
@@ -1,7 +1,7 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin; namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
use YahnisElsts\PluginUpdateChecker\v5p4\Metadata; use YahnisElsts\PluginUpdateChecker\v5p7\Metadata;
if ( !class_exists(PluginInfo::class, false) ): if ( !class_exists(PluginInfo::class, false) ):
@@ -39,6 +39,7 @@ if ( !class_exists(PluginInfo::class, false) ):
public $downloaded; public $downloaded;
public $active_installs; public $active_installs;
public $last_updated; public $last_updated;
public $autoupdate = false;
public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything. public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything.
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin; namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
if ( !class_exists(Ui::class, false) ): if ( !class_exists(Ui::class, false) ):
/** /**
@@ -187,7 +187,7 @@ if ( !class_exists(Ui::class, false) ):
} }
} }
wp_redirect(add_query_arg( wp_safe_redirect(add_query_arg(
array( array(
'puc_update_check_result' => $status, 'puc_update_check_result' => $status,
'puc_slug' => $this->updateChecker->slug, 'puc_slug' => $this->updateChecker->slug,
@@ -1,7 +1,7 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin; namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
use YahnisElsts\PluginUpdateChecker\v5p4\Update as BaseUpdate; use YahnisElsts\PluginUpdateChecker\v5p7\Update as BaseUpdate;
if ( !class_exists(Update::class, false) ): if ( !class_exists(Update::class, false) ):
@@ -20,9 +20,10 @@ if ( !class_exists(Update::class, false) ):
public $requires_php = false; public $requires_php = false;
public $icons = array(); public $icons = array();
public $filename; //Plugin filename relative to the plugins directory. public $filename; //Plugin filename relative to the plugins directory.
public $autoupdate = false;
protected static $extraFields = array( protected static $extraFields = array(
'id', 'homepage', 'tested', 'requires_php', 'upgrade_notice', 'icons', 'filename', 'id', 'homepage', 'tested', 'requires_php', 'upgrade_notice', 'icons', 'filename', 'autoupdate',
); );
/** /**
@@ -86,6 +87,7 @@ if ( !class_exists(Update::class, false) ):
$update->tested = $this->tested; $update->tested = $this->tested;
$update->requires_php = $this->requires_php; $update->requires_php = $this->requires_php;
$update->plugin = $this->filename; $update->plugin = $this->filename;
$update->autoupdate = $this->autoupdate;
if ( !empty($this->upgrade_notice) ) { if ( !empty($this->upgrade_notice) ) {
$update->upgrade_notice = $this->upgrade_notice; $update->upgrade_notice = $this->upgrade_notice;
@@ -102,7 +104,7 @@ if ( !class_exists(Update::class, false) ):
$update->icons = $icons; $update->icons = $icons;
//It appears that the 'default' icon isn't used anywhere in WordPress 4.9, //It appears that the 'default' icon isn't used anywhere in WordPress 4.9,
//but lets set it just in case a future release needs it. //but let's set it just in case a future release needs it.
if ( !isset($update->icons['default']) ) { if ( !isset($update->icons['default']) ) {
$update->icons['default'] = current($update->icons); $update->icons['default'] = current($update->icons);
} }
@@ -1,10 +1,10 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin; namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
use YahnisElsts\PluginUpdateChecker\v5p4\InstalledPackage; use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
use YahnisElsts\PluginUpdateChecker\v5p4\UpdateChecker as BaseUpdateChecker; use YahnisElsts\PluginUpdateChecker\v5p7\UpdateChecker as BaseUpdateChecker;
use YahnisElsts\PluginUpdateChecker\v5p4\Scheduler; use YahnisElsts\PluginUpdateChecker\v5p7\Scheduler;
use YahnisElsts\PluginUpdateChecker\v5p4\DebugBar; use YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
if ( !class_exists(UpdateChecker::class, false) ): if ( !class_exists(UpdateChecker::class, false) ):
@@ -1,10 +1,10 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
use YahnisElsts\PluginUpdateChecker\v5p4\Plugin; use YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
use YahnisElsts\PluginUpdateChecker\v5p4\Theme; use YahnisElsts\PluginUpdateChecker\v5p7\Theme;
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs; use YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
if ( !class_exists(PucFactory::class, false) ): if ( !class_exists(PucFactory::class, false) ):
@@ -86,7 +86,7 @@ if ( !class_exists(PucFactory::class, false) ):
throw new \RuntimeException(sprintf( throw new \RuntimeException(sprintf(
'The update checker cannot determine if "%s" is a plugin or a theme. ' . 'The update checker cannot determine if "%s" is a plugin or a theme. ' .
'This is a bug. Please contact the PUC developer.', 'This is a bug. Please contact the PUC developer.',
htmlentities($fullPath) esc_html($fullPath)
)); ));
} }
@@ -147,7 +147,7 @@ if ( !class_exists(PucFactory::class, false) ):
* *
* Normalize a filesystem path. Introduced in WP 3.9. * Normalize a filesystem path. Introduced in WP 3.9.
* Copying here allows use of the class on earlier versions. * Copying here allows use of the class on earlier versions.
* This version adapted from WP 4.8.2 (unchanged since 4.5.4) * This version adapted from WP 4.8.2 (unchanged since 4.5.7)
* *
* @param string $path Path to normalize. * @param string $path Path to normalize.
* @return string Normalized path. * @return string Normalized path.
@@ -239,7 +239,7 @@ if ( !class_exists(PucFactory::class, false) ):
//URI was not found so throw an error. //URI was not found so throw an error.
throw new \RuntimeException( throw new \RuntimeException(
sprintf('Unable to locate URI in header of "%s"', htmlentities($fullPath)) sprintf('Unable to locate URI in header of "%s"', esc_html($fullPath))
); );
} }
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
if ( !class_exists(Scheduler::class, false) ): if ( !class_exists(Scheduler::class, false) ):
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
if ( !class_exists(StateStore::class, false) ): if ( !class_exists(StateStore::class, false) ):
@@ -77,7 +77,7 @@ if ( !class_exists(StateStore::class, false) ):
* @param Update|null $update * @param Update|null $update
* @return $this * @return $this
*/ */
public function setUpdate(Update $update = null) { public function setUpdate($update = null) {
$this->lazyLoad(); $this->lazyLoad();
$this->update = $update; $this->update = $update;
return $this; return $this;
@@ -1,7 +1,7 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Theme; namespace YahnisElsts\PluginUpdateChecker\v5p7\Theme;
use YahnisElsts\PluginUpdateChecker\v5p4\InstalledPackage; use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
if ( !class_exists(Package::class, false) ): if ( !class_exists(Package::class, false) ):
@@ -1,8 +1,8 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Theme; namespace YahnisElsts\PluginUpdateChecker\v5p7\Theme;
use YahnisElsts\PluginUpdateChecker\v5p4\Update as BaseUpdate; use YahnisElsts\PluginUpdateChecker\v5p7\Update as BaseUpdate;
if ( !class_exists(Update::class, false) ): if ( !class_exists(Update::class, false) ):
@@ -1,11 +1,11 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Theme; namespace YahnisElsts\PluginUpdateChecker\v5p7\Theme;
use YahnisElsts\PluginUpdateChecker\v5p4\UpdateChecker as BaseUpdateChecker; use YahnisElsts\PluginUpdateChecker\v5p7\UpdateChecker as BaseUpdateChecker;
use YahnisElsts\PluginUpdateChecker\v5p4\InstalledPackage; use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
use YahnisElsts\PluginUpdateChecker\v5p4\Scheduler; use YahnisElsts\PluginUpdateChecker\v5p7\Scheduler;
use YahnisElsts\PluginUpdateChecker\v5p4\DebugBar; use YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
if ( !class_exists(UpdateChecker::class, false) ): if ( !class_exists(UpdateChecker::class, false) ):
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
use stdClass; use stdClass;
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
use stdClass; use stdClass;
use WP_Error; use WP_Error;
@@ -171,6 +171,10 @@ if ( !class_exists(UpdateChecker::class, false) ):
//Allow HTTP requests to the metadata URL even if it's on a local host. //Allow HTTP requests to the metadata URL even if it's on a local host.
add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2); add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);
//Potentially exclude information about this entity from core update check requests to api.wordpress.org.
//phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- Doesn't modify timeouts.
add_filter('http_request_args', array($this, 'excludeEntityFromWordPressAPI'), 10, 2);
//DebugBar integration. //DebugBar integration.
if ( did_action('plugins_loaded') ) { if ( did_action('plugins_loaded') ) {
$this->maybeInitDebugBar(); $this->maybeInitDebugBar();
@@ -192,6 +196,7 @@ if ( !class_exists(UpdateChecker::class, false) ):
remove_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10); remove_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10);
remove_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10); remove_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10);
remove_filter('http_request_args', array($this, 'excludeEntityFromWordPressAPI'));
remove_action('plugins_loaded', array($this, 'maybeInitDebugBar')); remove_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
remove_action('init', array($this, 'loadTextDomain')); remove_action('init', array($this, 'loadTextDomain'));
@@ -266,6 +271,76 @@ if ( !class_exists(UpdateChecker::class, false) ):
*/ */
abstract protected function createScheduler($checkPeriod); abstract protected function createScheduler($checkPeriod);
/**
* Remove information about this plugin or theme from the requests that WordPress core sends
* to api.wordpress.org when checking for updates.
*
* @param array $args
* @param string $url
* @return array
*/
public function excludeEntityFromWordPressAPI($args, $url) {
//Is this an api.wordpress.org update check request?
$parsedUrl = wp_parse_url($url);
if ( !isset($parsedUrl['host']) || (strtolower($parsedUrl['host']) !== 'api.wordpress.org') ) {
return $args;
}
$typePluralised = $this->componentType . 's';
$expectedPathPrefix = '/' . $typePluralised . '/update-check/1.'; //e.g. "/plugins/update-check/1.1/"
if ( !isset($parsedUrl['path']) || !Utils::startsWith($parsedUrl['path'], $expectedPathPrefix) ) {
return $args;
}
//Plugins and themes can disable this feature by using the filter below.
if ( !apply_filters(
$this->getUniqueName('remove_from_default_update_checks'),
true, $this, $args, $url
) ) {
return $args;
}
if ( empty($args['body'][$typePluralised]) ) {
return $args;
}
$reportingItems = json_decode($args['body'][$typePluralised], true);
if ( $reportingItems === null ) {
return $args;
}
//The list of installed items uses different key formats for plugins and themes.
//Luckily, we can reuse the getUpdateListKey() method here.
$updateListKey = $this->getUpdateListKey();
if ( isset($reportingItems[$typePluralised][$updateListKey]) ) {
unset($reportingItems[$typePluralised][$updateListKey]);
}
if ( !empty($reportingItems['active']) ) {
if ( is_array($reportingItems['active']) ) {
foreach ($reportingItems['active'] as $index => $relativePath) {
if ( $relativePath === $updateListKey ) {
unset($reportingItems['active'][$index]);
}
}
//Re-index the array.
$reportingItems['active'] = array_values($reportingItems['active']);
} else if ( $reportingItems['active'] === $updateListKey ) {
//For themes, the "active" field is a string that contains the theme's directory name.
//Pretend that the default theme is active so that we don't reveal the actual theme.
if ( defined('WP_DEFAULT_THEME') ) {
$reportingItems['active'] = WP_DEFAULT_THEME;
}
//Unfortunately, it doesn't seem to be documented if we can safely remove the "active"
//key. So when we don't know the default theme, we'll just leave it as is.
}
}
$args['body'][$typePluralised] = wp_json_encode($reportingItems);
return $args;
}
/** /**
* Check for updates. The results are stored in the DB option specified in $optionName. * Check for updates. The results are stored in the DB option specified in $optionName.
* *
@@ -384,7 +459,7 @@ if ( !class_exists(UpdateChecker::class, false) ):
* *
* @param Metadata|null $update * @param Metadata|null $update
*/ */
protected function fixSupportedWordpressVersion(Metadata $update = null) { protected function fixSupportedWordpressVersion($update = null) {
if ( !isset($update->tested) || !preg_match('/^\d++\.\d++$/', $update->tested) ) { if ( !isset($update->tested) || !preg_match('/^\d++\.\d++$/', $update->tested) ) {
return; return;
} }
@@ -557,7 +632,15 @@ if ( !class_exists(UpdateChecker::class, false) ):
if ( !empty($update) ) { if ( !empty($update) ) {
//Let plugins filter the update info before it's passed on to WordPress. //Let plugins filter the update info before it's passed on to WordPress.
$update = apply_filters($this->getUniqueName('pre_inject_update'), $update); $update = apply_filters($this->getUniqueName('pre_inject_update'), $update);
$updates = $this->addUpdateToList($updates, $update->toWpFormat()); //Convert the update into the format used by WordPress core.
$wpUpdate = $update->toWpFormat();
//Disable the "autoupdate" flag unless explicitly allowed. This is a safety precaution;
//untrusted or compromised update sources could otherwise set this flag to true even
//if the plugin/theme developer didn't intend to allow automatic update installation.
if ( isset($wpUpdate->autoupdate) && !$this->isAutoupdateFieldAllowed($update) ) {
$wpUpdate->autoupdate = false;
}
$updates = $this->addUpdateToList($updates, $wpUpdate);
} else { } else {
//Clean up any stale update info. //Clean up any stale update info.
$updates = $this->removeUpdateFromList($updates); $updates = $this->removeUpdateFromList($updates);
@@ -655,6 +738,38 @@ if ( !class_exists(UpdateChecker::class, false) ):
return true; return true;
} }
/**
* @var bool
*/
protected $autoupdateFieldAllowed = false;
/**
* Allow the "autoupdate" field in incoming plugin updates to be set to `true`.
*
* By default, the update checker will parse the field (so it will be available in the update
* object), but will set it to `false` before passing the update to WordPress.
*
* @return $this
*/
public function allowAutoupdateField() {
$this->autoupdateFieldAllowed = true;
return $this;
}
/**
* Is the "autoupdate" field for injected updates allowed to be set to true?
*
* @param object $incomingUpdate
* @return bool
*/
protected function isAutoupdateFieldAllowed($incomingUpdate) {
return apply_filters(
$this->getUniqueName('autoupdate_field_allowed'),
$this->autoupdateFieldAllowed,
$incomingUpdate
);
}
/* ------------------------------------------------------------------- /* -------------------------------------------------------------------
* JSON-based update API * JSON-based update API
* ------------------------------------------------------------------- * -------------------------------------------------------------------
@@ -698,7 +813,7 @@ if ( !class_exists(UpdateChecker::class, false) ):
$result = wp_remote_get($url, $options); $result = wp_remote_get($url, $options);
$result = apply_filters($this->getUniqueName('request_metadata_http_result'), $result, $url, $options); $result = apply_filters($this->getUniqueName('request_metadata_http_result'), $result, $url, $options);
//Try to parse the response //Try to parse the response
$status = $this->validateApiResponse($result); $status = $this->validateApiResponse($result);
$metadata = null; $metadata = null;
@@ -924,25 +1039,62 @@ if ( !class_exists(UpdateChecker::class, false) ):
return $source; return $source;
} }
//Fix the remote source structure if necessary.
//The update archive should contain a single directory that contains the rest of plugin/theme files.
//Otherwise, WordPress will try to copy the entire working directory ($source == $remoteSource).
//We can't rename $remoteSource because that would break WordPress code that cleans up temporary files
//after update.
if ( $this->isBadDirectoryStructure($remoteSource) ) {
//Create a new directory using the plugin slug.
$newDirectory = trailingslashit($remoteSource) . $this->slug . '/';
if ( !$wp_filesystem->is_dir($newDirectory) ) {
$wp_filesystem->mkdir($newDirectory);
//Move all files to the newly created directory.
$sourceFiles = $wp_filesystem->dirlist($remoteSource);
if ( is_array($sourceFiles) ) {
$sourceFiles = array_keys($sourceFiles);
$allMoved = true;
foreach ($sourceFiles as $filename) {
//Skip our newly created folder.
if ( $filename === $this->slug ) {
continue;
}
$previousSource = trailingslashit($remoteSource) . $filename;
$newSource = trailingslashit($newDirectory) . $filename;
if ( !$wp_filesystem->move($previousSource, $newSource, true) ) {
$allMoved = false;
break;
}
}
if ( $allMoved ) {
//Rename source.
$source = $newDirectory;
} else {
//Delete our newly created folder including all files in it.
$wp_filesystem->rmdir($newDirectory, true);
//And return a relevant error.
return new WP_Error(
'puc-incorrect-directory-structure',
sprintf(
'The directory structure of the update was incorrect. All files should be inside ' .
'a directory named <span class="code">%s</span>, not at the root of the ZIP archive. Plugin Update Checker tried to fix the directory structure, but failed.',
esc_html($this->slug)
)
);
}
}
}
}
//Rename the source to match the existing directory. //Rename the source to match the existing directory.
$correctedSource = trailingslashit($remoteSource) . $this->directoryName . '/'; $correctedSource = trailingslashit($remoteSource) . $this->directoryName . '/';
if ( $source !== $correctedSource ) { if ( $source !== $correctedSource ) {
//The update archive should contain a single directory that contains the rest of plugin/theme files.
//Otherwise, WordPress will try to copy the entire working directory ($source == $remoteSource).
//We can't rename $remoteSource because that would break WordPress code that cleans up temporary files
//after update.
if ( $this->isBadDirectoryStructure($remoteSource) ) {
return new WP_Error(
'puc-incorrect-directory-structure',
sprintf(
'The directory structure of the update is incorrect. All files should be inside ' .
'a directory named <span class="code">%s</span>, not at the root of the ZIP archive.',
htmlentities($this->slug)
)
);
}
/** @var \WP_Upgrader_Skin $upgrader ->skin */
$upgrader->skin->feedback(sprintf( $upgrader->skin->feedback(sprintf(
'Renaming %s to %s&#8230;', 'Renaming %s to %s&#8230;',
'<span class="code">' . basename($source) . '</span>', '<span class="code">' . basename($source) . '</span>',
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
if ( !class_exists(UpgraderStatus::class, false) ): if ( !class_exists(UpgraderStatus::class, false) ):
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
if ( !class_exists(Utils::class, false) ): if ( !class_exists(Utils::class, false) ):
@@ -1,6 +1,6 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
use Parsedown; use Parsedown;
use PucReadmeParser; use PucReadmeParser;
@@ -51,6 +51,16 @@ if ( !class_exists(Api::class, false) ):
*/ */
protected $credentials = null; protected $credentials = null;
/**
* @var string|null The value of the "Authorization" header for API requests.
*/
private $authorizationHeader = null;
/**
* @var string|null If set, add the "Authorization" header to update downloads that start with this prefix.
*/
private $downloadUrlPrefixRequiringAuth = null;
/** /**
* @var string The filter tag that's used to filter options passed to wp_remote_get. * @var string The filter tag that's used to filter options passed to wp_remote_get.
* For example, "puc_request_info_options-slug" or "puc_request_update_options_theme-slug". * For example, "puc_request_info_options-slug" or "puc_request_update_options_theme-slug".
@@ -322,6 +332,22 @@ if ( !class_exists(Api::class, false) ):
return null; return null;
} }
/**
* @return array
*/
protected function getApiRequestHttpOptions() {
$options = ['timeout' => wp_doing_cron() ? 10 : 3];
if ( $this->isAuthenticationEnabled() && !empty($this->authorizationHeader) ) {
$options['headers'] = ['Authorization' => $this->authorizationHeader];
}
if ( !empty($this->httpFilterName) ) {
$options = apply_filters($this->httpFilterName, $options);
}
return $options;
}
/** /**
* Set authentication credentials. * Set authentication credentials.
* *
@@ -332,7 +358,130 @@ if ( !class_exists(Api::class, false) ):
} }
public function isAuthenticationEnabled() { public function isAuthenticationEnabled() {
return !empty($this->credentials); return !empty($this->credentials) || !empty($this->authorizationHeader);
}
/**
* Get the value of the "Authorization" header for API requests, if any.
*
* @return string
*/
protected function getAuthorizationHeader() {
return $this->authorizationHeader ?: '';
}
/**
* Enable basic access authentication with the specified username and password.
*
* @param string $username
* @param string $password
* @param string|null $downloadUrlPrefix Optionally, add the same Authorization header to update
* downloads where the download URL starts with this prefix.
*/
protected function enableBasicAuth($username, $password, $downloadUrlPrefix = null) {
$this->authorizationHeader = 'Basic ' . base64_encode($username . ':' . $password);
$this->downloadUrlPrefixRequiringAuth = $downloadUrlPrefix;
if ( !empty($this->downloadUrlPrefixRequiringAuth) ) {
$this->enableDownloadRequestFilter();
}
}
/**
* @var bool Whether the hook that registers download request filter(s) has already been added.
*/
private $preDownloadHookAdded = false;
/**
* Enable the hooks that let you filter HTTP requests for update downloads.
*/
protected function enableDownloadRequestFilter() {
if ( $this->preDownloadHookAdded ) {
return;
}
$this->preDownloadHookAdded = true;
//Optimization: Instead of filtering all HTTP requests, let's do it only when
//WordPress is about to download an update. So this is a two-step process;
//the actual request arg filter is added in the following hook.
add_filter('upgrader_pre_download', [$this, 'addDownloadRequestFilters']); //WP 3.7+
}
/**
* @var bool
*/
private $downloadFiltersAdded = false;
/**
* @internal
* @param bool $result Pass-through value for the "upgrader_pre_download" filter. Ignored by this callback.
* @return bool
*/
public function addDownloadRequestFilters($result) {
if ( !$this->downloadFiltersAdded ) {
$this->downloadFiltersAdded = true;
//phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- The callback doesn't change the timeout.
add_filter('http_request_args', [$this, 'filterUpdateDownloadRequestArgs'], 10, 2);
$authorizationHeader = $this->getAuthorizationHeader();
if ( $this->isAuthenticationEnabled() && !empty($authorizationHeader) ) {
add_action('requests-requests.before_redirect', [$this, 'removeAuthHeaderFromRedirects'], 10, 2);
}
}
return $result;
}
/**
* Filter request arguments/options for update downloads.
*
* Note that this callback can potentially be called for *any* update download. You still
* need to verify that the URL is one of yours.
*
* @internal
* @param array $requestArgs
* @param string $url
* @return array
*/
public function filterUpdateDownloadRequestArgs($requestArgs, $url = '') {
//Add an authorization header to our downloads if needed.
$authHeader = $this->getAuthorizationHeader();
if (
$this->isAuthenticationEnabled()
&& !empty($authHeader)
&& !empty($this->downloadUrlPrefixRequiringAuth)
&& ((strpos($url, $this->downloadUrlPrefixRequiringAuth)) === 0)
) {
$requestArgs['headers']['Authorization'] = $authHeader;
}
return $requestArgs;
}
/**
* At least in older WP versions, when following a redirect, the Requests library will
* automatically forward the Authorization header to other hosts. We don't want that
* because it breaks AWS downloads and can leak authorization information.
*
* @param string $location
* @param array $headers
* @internal
*/
public function removeAuthHeaderFromRedirects(&$location, &$headers) {
if (
//If there's no download URL prefix configured, we would not have added an auth header,
//and there's also no way to check if this URL needs auth or not.
empty($this->downloadUrlPrefixRequiringAuth)
//If this request goes to our download URL, we can leave the header.
|| ((strpos($location, $this->downloadUrlPrefixRequiringAuth)) === 0)
) {
return;
}
//Remove the header.
$authorizationHeader = $this->getAuthorizationHeader();
if ( isset($headers['Authorization']) && ($headers['Authorization'] === $authorizationHeader) ) {
unset($headers['Authorization']);
}
} }
/** /**
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
if ( !interface_exists(BaseChecker::class, false) ): if ( !interface_exists(BaseChecker::class, false) ):
@@ -1,18 +1,12 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
use YahnisElsts\PluginUpdateChecker\v5p4\OAuthSignature; use YahnisElsts\PluginUpdateChecker\v5p7\Utils;
use YahnisElsts\PluginUpdateChecker\v5p4\Utils;
if ( !class_exists(BitBucketApi::class, false) ): if ( !class_exists(BitBucketApi::class, false) ):
class BitBucketApi extends Api { class BitBucketApi extends Api {
/**
* @var OAuthSignature
*/
private $oauth = null;
/** /**
* @var string * @var string
*/ */
@@ -148,11 +142,19 @@ if ( !class_exists(BitBucketApi::class, false) ):
* @return string * @return string
*/ */
protected function getDownloadUrl($ref) { protected function getDownloadUrl($ref) {
return $this->getDownloadBaseUrl() . $ref . '.zip';
}
/**
* Get the base URL for ZIP downloads from our repo. Includes the trailing slash.
*
* @return string
*/
protected function getDownloadBaseUrl() {
return sprintf( return sprintf(
'https://bitbucket.org/%s/%s/get/%s.zip', 'https://bitbucket.org/%s/%s/get/',
$this->username, $this->username,
$this->repository, $this->repository
$ref
); );
} }
@@ -206,15 +208,7 @@ if ( !class_exists(BitBucketApi::class, false) ):
)); ));
$baseUrl = $url; $baseUrl = $url;
if ( $this->oauth ) { $response = wp_remote_get($url, $this->getApiRequestHttpOptions());
$url = $this->oauth->sign($url,'GET');
}
$options = array('timeout' => wp_doing_cron() ? 10 : 3);
if ( !empty($this->httpFilterName) ) {
$options = apply_filters($this->httpFilterName, $options);
}
$response = wp_remote_get($url, $options);
if ( is_wp_error($response) ) { if ( is_wp_error($response) ) {
do_action('puc_api_error', $response, null, $url, $this->slug); do_action('puc_api_error', $response, null, $url, $this->slug);
return $response; return $response;
@@ -248,25 +242,18 @@ if ( !class_exists(BitBucketApi::class, false) ):
public function setAuthentication($credentials) { public function setAuthentication($credentials) {
parent::setAuthentication($credentials); parent::setAuthentication($credentials);
if ( !empty($credentials) && !empty($credentials['consumer_key']) ) { if (
$this->oauth = new OAuthSignature( is_array($credentials)
$credentials['consumer_key'], && !empty($credentials['username'])
$credentials['consumer_secret'] && !empty($credentials['api_token'])
) {
$this->enableBasicAuth(
$credentials['username'],
$credentials['api_token'],
$this->getDownloadBaseUrl()
); );
} else {
$this->oauth = null;
} }
} }
public function signDownloadUrl($url) {
//Add authentication data to download URLs. Since OAuth signatures incorporate
//timestamps, we have to do this immediately before inserting the update. Otherwise,
//authentication could fail due to a stale timestamp.
if ( $this->oauth ) {
$url = $this->oauth->sign($url);
}
return $url;
}
} }
endif; endif;
@@ -1,6 +1,6 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
use Parsedown; use Parsedown;
@@ -29,11 +29,6 @@ if ( !class_exists(GitHubApi::class, false) ):
*/ */
protected $accessToken; protected $accessToken;
/**
* @var bool
*/
private $downloadFilterAdded = false;
public function __construct($repositoryUrl, $accessToken = null) { public function __construct($repositoryUrl, $accessToken = null) {
$path = wp_parse_url($repositoryUrl, PHP_URL_PATH); $path = wp_parse_url($repositoryUrl, PHP_URL_PATH);
if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) { if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
@@ -248,15 +243,7 @@ if ( !class_exists(GitHubApi::class, false) ):
$baseUrl = $url; $baseUrl = $url;
$url = $this->buildApiUrl($url, $queryParams); $url = $this->buildApiUrl($url, $queryParams);
$options = array('timeout' => wp_doing_cron() ? 10 : 3); $response = wp_remote_get($url, $this->getApiRequestHttpOptions());
if ( $this->isAuthenticationEnabled() ) {
$options['headers'] = array('Authorization' => $this->getAuthorizationHeader());
}
if ( !empty($this->httpFilterName) ) {
$options = apply_filters($this->httpFilterName, $options);
}
$response = wp_remote_get($url, $options);
if ( is_wp_error($response) ) { if ( is_wp_error($response) ) {
do_action('puc_api_error', $response, null, $url, $this->slug); do_action('puc_api_error', $response, null, $url, $this->slug);
return $response; return $response;
@@ -348,11 +335,17 @@ if ( !class_exists(GitHubApi::class, false) ):
public function setAuthentication($credentials) { public function setAuthentication($credentials) {
parent::setAuthentication($credentials); parent::setAuthentication($credentials);
//This property is no longer used internally, but is kept for backwards compatibility.
$this->accessToken = is_string($credentials) ? $credentials : null; $this->accessToken = is_string($credentials) ? $credentials : null;
//Optimization: Instead of filtering all HTTP requests, let's do it only when if ( is_string($credentials) && !empty($credentials) ) {
//WordPress is about to download an update. $repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', []);
add_filter('upgrader_pre_download', array($this, 'addHttpRequestFilter'), 10, 1); //WP 3.7+ $this->enableBasicAuth($this->userName, $credentials, $repoApiBaseUrl);
}
//Assets sometimes need an Accept header, so we add a request filter even if basic
//authentication isn't enabled.
$this->enableDownloadRequestFilter();
} }
protected function getUpdateDetectionStrategies($configBranch) { protected function getUpdateDetectionStrategies($configBranch) {
@@ -393,74 +386,15 @@ if ( !class_exists(GitHubApi::class, false) ):
return null; return null;
} }
/** public function filterUpdateDownloadRequestArgs($requestArgs, $url = '') {
* @param bool $result //Release assets need an "Accept" header. See GitHub Docs:
* @return bool //https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
* @internal
*/
public function addHttpRequestFilter($result) {
if ( !$this->downloadFilterAdded && $this->isAuthenticationEnabled() ) {
//phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- The callback doesn't change the timeout.
add_filter('http_request_args', array($this, 'setUpdateDownloadHeaders'), 10, 2);
add_action('requests-requests.before_redirect', array($this, 'removeAuthHeaderFromRedirects'), 10, 4);
$this->downloadFilterAdded = true;
}
return $result;
}
/**
* Set the HTTP headers that are necessary to download updates from private repositories.
*
* See GitHub docs:
*
* @link https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
* @link https://developer.github.com/v3/auth/#basic-authentication
*
* @internal
* @param array $requestArgs
* @param string $url
* @return array
*/
public function setUpdateDownloadHeaders($requestArgs, $url = '') {
//Is WordPress trying to download one of our release assets? //Is WordPress trying to download one of our release assets?
if ( $this->releaseAssetsEnabled && (strpos($url, $this->getAssetApiBaseUrl()) !== false) ) { if ( $this->releaseAssetsEnabled && (strpos($url, $this->getAssetApiBaseUrl()) !== false) ) {
$requestArgs['headers']['Accept'] = 'application/octet-stream'; $requestArgs['headers']['Accept'] = 'application/octet-stream';
} }
//Use Basic authentication, but only if the download is from our repository.
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
if ( $this->isAuthenticationEnabled() && (strpos($url, $repoApiBaseUrl)) === 0 ) {
$requestArgs['headers']['Authorization'] = $this->getAuthorizationHeader();
}
return $requestArgs;
}
/** return parent::filterUpdateDownloadRequestArgs($requestArgs, $url);
* When following a redirect, the Requests library will automatically forward
* the authorization header to other hosts. We don't want that because it breaks
* AWS downloads and can leak authorization information.
*
* @param string $location
* @param array $headers
* @internal
*/
public function removeAuthHeaderFromRedirects(&$location, &$headers) {
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
if ( strpos($location, $repoApiBaseUrl) === 0 ) {
return; //This request is going to GitHub, so it's fine.
}
//Remove the header.
if ( isset($headers['Authorization']) ) {
unset($headers['Authorization']);
}
}
/**
* Generate the value of the "Authorization" header.
*
* @return string
*/
protected function getAuthorizationHeader() {
return 'Basic ' . base64_encode($this->userName . ':' . $this->accessToken);
} }
} }
@@ -1,6 +1,6 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
if ( !class_exists(GitLabApi::class, false) ): if ( !class_exists(GitLabApi::class, false) ):
@@ -260,12 +260,7 @@ if ( !class_exists(GitLabApi::class, false) ):
$baseUrl = $url; $baseUrl = $url;
$url = $this->buildApiUrl($url, $queryParams); $url = $this->buildApiUrl($url, $queryParams);
$options = array('timeout' => wp_doing_cron() ? 10 : 3); $response = wp_remote_get($url, $this->getApiRequestHttpOptions());
if ( !empty($this->httpFilterName) ) {
$options = apply_filters($this->httpFilterName, $options);
}
$response = wp_remote_get($url, $options);
if ( is_wp_error($response) ) { if ( is_wp_error($response) ) {
do_action('puc_api_error', $response, null, $url, $this->slug); do_action('puc_api_error', $response, null, $url, $this->slug);
return $response; return $response;
@@ -1,8 +1,8 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
use YahnisElsts\PluginUpdateChecker\v5p4\Plugin; use YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
if ( !class_exists(PluginUpdateChecker::class, false) ): if ( !class_exists(PluginUpdateChecker::class, false) ):
@@ -1,5 +1,5 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
if ( !class_exists(Reference::class, false) ): if ( !class_exists(Reference::class, false) ):
@@ -1,6 +1,6 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
if ( !trait_exists(ReleaseAssetSupport::class, false) ) : if ( !trait_exists(ReleaseAssetSupport::class, false) ) :
@@ -1,6 +1,6 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
if ( !trait_exists(ReleaseFilteringFeature::class, false) ) : if ( !trait_exists(ReleaseFilteringFeature::class, false) ) :
@@ -1,9 +1,9 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
use YahnisElsts\PluginUpdateChecker\v5p4\Theme; use YahnisElsts\PluginUpdateChecker\v5p7\Theme;
use YahnisElsts\PluginUpdateChecker\v5p4\Utils; use YahnisElsts\PluginUpdateChecker\v5p7\Utils;
if ( !class_exists(ThemeUpdateChecker::class, false) ): if ( !class_exists(ThemeUpdateChecker::class, false) ):
@@ -1,6 +1,6 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs; namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
if ( !trait_exists(VcsCheckerMethods::class, false) ) : if ( !trait_exists(VcsCheckerMethods::class, false) ) :
@@ -1,6 +1,6 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
use WP_CLI; use WP_CLI;
+23 -20
View File
@@ -29,7 +29,7 @@ From the users' perspective, it works just like with plugins and themes hosted o
Getting Started Getting Started
--------------- ---------------
*Note:* In each of the below examples, part of the instructions are to create an instance of the update checker class. It's recommended to do this either during the `plugins_loaded` action or outside of any hooks. If you do it only during an `admin_*` action, then updates will not be visible to a wide variety of WordPress maanagement tools; they will only be visible to logged-in users on dashboard pages. *Note:* In each of the below examples, part of the instructions is to create an instance of the update checker class. It's recommended to do this either during the `plugins_loaded` action or outside of any hooks. If you do it only during an `admin_*` action, then updates will not be visible to a wide variety of WordPress management tools; they will only be visible to logged-in users on dashboard pages.
### Self-hosted Plugins and Themes ### Self-hosted Plugins and Themes
@@ -128,6 +128,11 @@ This library supports a couple of different ways to release updates on GitHub. P
$myUpdateChecker->getVcsApi()->enableReleaseAssets(); $myUpdateChecker->getVcsApi()->enableReleaseAssets();
``` ```
By default, PUC will simply use the first available asset. You can pass a regular expression to `enableReleaseAssets()` to filter assets by name. For example:
```php
$myUpdateChecker->getVcsApi()->enableReleaseAssets('/\.zip($|[?&#])/i');
```
- **Tags** - **Tags**
To release version 1.2.3, create a new Git tag named `v1.2.3` or `1.2.3`. That's it. To release version 1.2.3, create a new Git tag named `v1.2.3` or `1.2.3`. That's it.
@@ -188,14 +193,12 @@ The library will pull update details from the following parts of a release/tag/b
'unique-plugin-or-theme-slug' 'unique-plugin-or-theme-slug'
); );
//Optional: If you're using a private repository, create an OAuth consumer //Optional: If you're using a private repository, create an API token
//and set the authentication credentials like this: //with the "read:repository:bitbucket" scope and set the authentication
//Note: For now you need to check "This is a private consumer" when //credentials like this:
//creating the consumer to work around #134:
// https://github.com/YahnisElsts/plugin-update-checker/issues/134
$myUpdateChecker->setAuthentication(array( $myUpdateChecker->setAuthentication(array(
'consumer_key' => '...', 'username' => 'example@example.com', //Your BitBucket email address.
'consumer_secret' => '...', 'api_token' => '...',
)); ));
//Optional: Set the branch that contains the stable release. //Optional: Set the branch that contains the stable release.
@@ -252,8 +255,8 @@ BitBucket doesn't have an equivalent to GitHub's releases, so the process is sli
Alternatively, if you're using a self-hosted GitLab instance, initialize the update checker like this: Alternatively, if you're using a self-hosted GitLab instance, initialize the update checker like this:
```php ```php
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\PluginUpdateChecker; use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\PluginUpdateChecker;
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitLabApi; use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitLabApi;
$myUpdateChecker = new PluginUpdateChecker( $myUpdateChecker = new PluginUpdateChecker(
new GitLabApi('https://myserver.com/user-name/repo-name/'), new GitLabApi('https://myserver.com/user-name/repo-name/'),
@@ -264,8 +267,8 @@ BitBucket doesn't have an equivalent to GitHub's releases, so the process is sli
``` ```
If you're using a self-hosted GitLab instance and [subgroups or nested groups](https://docs.gitlab.com/ce/user/group/subgroups/index.html), you have to tell the update checker which parts of the URL are subgroups: If you're using a self-hosted GitLab instance and [subgroups or nested groups](https://docs.gitlab.com/ce/user/group/subgroups/index.html), you have to tell the update checker which parts of the URL are subgroups:
```php ```php
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\PluginUpdateChecker; use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\PluginUpdateChecker;
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitLabApi; use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitLabApi;
$myUpdateChecker = new PluginUpdateChecker( $myUpdateChecker = new PluginUpdateChecker(
new GitLabApi( new GitLabApi(
@@ -347,14 +350,14 @@ Other classes have also been renamed, usually by simply removing the `Puc_vXpY_`
| Old class name | New class name | | Old class name | New class name |
|-------------------------------------|----------------------------------------------------------------| |-------------------------------------|----------------------------------------------------------------|
| `Puc_v4_Factory` | `YahnisElsts\PluginUpdateChecker\v5\PucFactory` | | `Puc_v4_Factory` | `YahnisElsts\PluginUpdateChecker\v5\PucFactory` |
| `Puc_v4p13_Factory` | `YahnisElsts\PluginUpdateChecker\v5p4\PucFactory` | | `Puc_v4p13_Factory` | `YahnisElsts\PluginUpdateChecker\v5p7\PucFactory` |
| `Puc_v4p13_Plugin_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Plugin\UpdateChecker` | | `Puc_v4p13_Plugin_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Plugin\UpdateChecker` |
| `Puc_v4p13_Theme_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Theme\UpdateChecker` | | `Puc_v4p13_Theme_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Theme\UpdateChecker` |
| `Puc_v4p13_Vcs_PluginUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\PluginUpdateChecker` | | `Puc_v4p13_Vcs_PluginUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\PluginUpdateChecker` |
| `Puc_v4p13_Vcs_ThemeUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\ThemeUpdateChecker` | | `Puc_v4p13_Vcs_ThemeUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\ThemeUpdateChecker` |
| `Puc_v4p13_Vcs_GitHubApi` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitHubApi` | | `Puc_v4p13_Vcs_GitHubApi` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitHubApi` |
| `Puc_v4p13_Vcs_GitLabApi` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitLabApi` | | `Puc_v4p13_Vcs_GitLabApi` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitLabApi` |
| `Puc_v4p13_Vcs_BitBucketApi` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\BitBucketApi` | | `Puc_v4p13_Vcs_BitBucketApi` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\BitBucketApi` |
License Management License Management
------------------ ------------------
+1 -1
View File
@@ -18,6 +18,6 @@
"ext-json": "*" "ext-json": "*"
}, },
"autoload": { "autoload": {
"files": ["load-v5p4.php"] "files": ["load-v5p7.php"]
} }
} }
@@ -0,0 +1,52 @@
msgid ""
msgstr ""
"Project-Id-Version: plugin-update-checker\n"
"POT-Creation-Date: 2022-07-29 15:34+0300\n"
"PO-Revision-Date: 2024-05-09 22:22+0000\n"
"Last-Translator: theogk\n"
"Language-Team: Ελληνικά\n"
"Language: el\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Poedit-Basepath: ..\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
"X-Poedit-SearchPath-0: .\n"
"Report-Msgid-Bugs-To: \n"
"X-Loco-Version: 2.6.9; wp-6.5.3"
#: Puc/v5p4/Plugin/Ui.php:128
msgid "Check for updates"
msgstr "Έλεγχος για ενημερώσεις"
#: Puc/v5p4/Plugin/Ui.php:214
#, php-format
msgctxt "the plugin title"
msgid "The %s plugin is up to date."
msgstr "Το πρόσθετο %s είναι ενημερωμένο."
#: Puc/v5p4/Plugin/Ui.php:216
#, php-format
msgctxt "the plugin title"
msgid "A new version of the %s plugin is available."
msgstr "Μία νέα έκδοση είναι διαθέσιμη για το πρόσθετο %s."
#: Puc/v5p4/Plugin/Ui.php:218
#, php-format
msgctxt "the plugin title"
msgid "Could not determine if updates are available for %s."
msgstr ""
"Δεν ήταν εφικτό να εκτελεστεί ο έλεγχος για νέες ενημερώσεις για το πρόσθετο "
"%s."
#: Puc/v5p4/Plugin/Ui.php:224
#, php-format
msgid "Unknown update checker status \"%s\""
msgstr "Άγνωστο πρόβλημα του ενημερωτή προσθέτων \"%s\""
#: Puc/v5p4/Vcs/PluginUpdateChecker.php:100
msgid "There is no changelog available."
msgstr "Δεν υπάρχει διαθέσιμο αρχείο αλλαγών."
@@ -1,38 +1,50 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: plugin-update-checker\n" "Project-Id-Version: plugin-update-checker\n"
"POT-Creation-Date: 2016-02-17 14:21+0100\n" "POT-Creation-Date: 2025-06-12 23:40+0100\n"
"PO-Revision-Date: 2016-10-28 14:30+0330\n" "PO-Revision-Date: 2025-06-12 23:49+0100\n"
"Last-Translator: studio RVOLA <hello@rvola.com>\n" "Last-Translator: Pro Style <info@prostyle.ir>\n"
"Language-Team: Pro Style <info@prostyle.ir>\n" "Language-Team: Alex Javadi <alex@aljm.org>\n"
"Language: fa_IR\n" "Language: fa_IR\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.8\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 3.6\n"
"X-Poedit-Basepath: ..\n"
"X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e\n" "X-Poedit-KeywordsList: __;_e\n"
"X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPath-0: .\n"
#: github-checker.php:120 #: Puc/v5p6/Plugin/Ui.php:56
msgid "There is no changelog available." msgid "View details"
msgstr "شرحی برای تغییرات یافت نشد" msgstr "مشاهده جزئیات"
#: plugin-update-checker.php:637 #: Puc/v5p6/Plugin/Ui.php:79
#, php-format
msgid "More information about %s"
msgstr "اطلاعات بیشتر درباره %s"
# It had some “potential” grammar issues and also didnt sound native.
# P.S. I know the current translation is literally “Checking for new updates”, however, I thought it might sound more natural and known to others.
#: Puc/v5p6/Plugin/Ui.php:130
msgid "Check for updates" msgid "Check for updates"
msgstr "بررسی برای بروزرسانی " msgstr "بررسی بروزرسانی جدید"
#: plugin-update-checker.php:681 # The word “ناشناخته” is seems to be translated directly from the word (Un-known), rather than checking for the context.
msgid "This plugin is up to date." # I think “نامشخص” (unknown) might be a suitable version in this scenario.
msgstr "شما از آخرین نسخه استفاده میکنید . به‌روز باشید" #: Puc/v5p6/Plugin/Ui.php:227
#: plugin-update-checker.php:683
msgid "A new version of this plugin is available."
msgstr "نسخه جدیدی برای افزونه ارائه شده است ."
#: plugin-update-checker.php:685
#, php-format #, php-format
msgid "Unknown update checker status \"%s\"" msgid "Unknown update checker status \"%s\""
msgstr "وضعیت ناشناخته برای بروزرسانی \"%s\"" msgstr "وضعیت نامشخص برای بروزرسانی \"%s\""
# The previous translation was okay, however, it didnt sound native to me.
#: Puc/v5p6/Vcs/PluginUpdateChecker.php:113
msgid "There is no changelog available."
msgstr "آخرین تغییراتی یافت نشد."
#~ msgid "This plugin is up to date."
#~ msgstr "شما از آخرین نسخه استفاده میکنید . به‌روز باشید"
#~ msgid "A new version of this plugin is available."
#~ msgstr "نسخه جدیدی برای افزونه ارائه شده است ."
@@ -0,0 +1,57 @@
msgid ""
msgstr ""
"Project-Id-Version: plugin-update-checker\n"
"POT-Creation-Date: 2024-12-09 11:45+0100\n"
"PO-Revision-Date: 2024-12-09 12:20+0100\n"
"Last-Translator: Aleksandar Urošević <urke.kg@gmail.com>\n"
"Language-Team: \n"
"Language: sr_RS\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.4.3\n"
"X-Poedit-Basepath: ..\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
"X-Poedit-SearchPath-0: .\n"
#: Puc/v5p5/Plugin/Ui.php:56
msgid "View details"
msgstr "Види детаље"
#: Puc/v5p5/Plugin/Ui.php:79
#, php-format
msgid "More information about %s"
msgstr "Више информација о %s"
#: Puc/v5p5/Plugin/Ui.php:130
msgid "Check for updates"
msgstr "Провера ажурирања"
#: Puc/v5p5/Plugin/Ui.php:217
#, php-format
msgctxt "the plugin title"
msgid "The %s plugin is up to date."
msgstr "Додатак %s је у најновијем издању."
#: Puc/v5p5/Plugin/Ui.php:219
#, php-format
msgctxt "the plugin title"
msgid "A new version of the %s plugin is available."
msgstr "Доступно је ново издање за %s."
#: Puc/v5p5/Plugin/Ui.php:221
#, php-format
msgctxt "the plugin title"
msgid "Could not determine if updates are available for %s."
msgstr "Није могуће утврдити да ли су доступне исправке за %s."
#: Puc/v5p5/Plugin/Ui.php:227
#, php-format
msgid "Unknown update checker status \"%s\""
msgstr "Непознат статус провере ажурирања \"%s\""
#: Puc/v5p5/Vcs/PluginUpdateChecker.php:113
msgid "There is no changelog available."
msgstr "Белешке о изменама нису доступне."
@@ -1,57 +1,57 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: plugin-update-checker\n" "Project-Id-Version: plugin-update-checker\n"
"POT-Creation-Date: 2022-01-29 12:09+0800\n" "POT-Creation-Date: 2025-11-21 10:40+0800\n"
"PO-Revision-Date: 2022-01-29 12:10+0800\n" "PO-Revision-Date: 2025-11-21 10:40+0800\n"
"Last-Translator: Seaton Jiang <hi@seatonjiang.com>\n" "Last-Translator: Seaton Jiang <hi@seatonjiang.com>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: zh_CN\n" "Language: zh_CN\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.4.3\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.8\n"
"X-Poedit-Basepath: ..\n"
"X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n" "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
"X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPath-0: .\n"
#: Puc/v4p11/Plugin/Ui.php:54 #: Puc/v5p6/Plugin/Ui.php:56
msgid "View details" msgid "View details"
msgstr "查看详情" msgstr "查看详情"
#: Puc/v4p11/Plugin/Ui.php:77 #: Puc/v5p6/Plugin/Ui.php:79
#, php-format #, php-format
msgid "More information about %s" msgid "More information about %s"
msgstr "%s 的更多信息" msgstr "%s 的更多信息"
#: Puc/v4p11/Plugin/Ui.php:128 #: Puc/v5p6/Plugin/Ui.php:130
msgid "Check for updates" msgid "Check for updates"
msgstr "检查更新" msgstr "检查更新"
#: Puc/v4p11/Plugin/Ui.php:214 #: Puc/v5p6/Plugin/Ui.php:217
#, php-format #, php-format
msgctxt "the plugin title" msgctxt "the plugin title"
msgid "The %s plugin is up to date." msgid "The %s plugin is up to date."
msgstr "%s 目前是最新版本。" msgstr "%s 目前是最新版本。"
#: Puc/v4p11/Plugin/Ui.php:216 #: Puc/v5p6/Plugin/Ui.php:219
#, php-format #, php-format
msgctxt "the plugin title" msgctxt "the plugin title"
msgid "A new version of the %s plugin is available." msgid "A new version of the %s plugin is available."
msgstr "%s 当前有可用的更新。" msgstr "%s 当前有可用的更新。"
#: Puc/v4p11/Plugin/Ui.php:218 #: Puc/v5p6/Plugin/Ui.php:221
#, php-format #, php-format
msgctxt "the plugin title" msgctxt "the plugin title"
msgid "Could not determine if updates are available for %s." msgid "Could not determine if updates are available for %s."
msgstr "%s 无法确定是否有可用的更新。" msgstr "%s 无法确定是否有可用的更新。"
#: Puc/v4p11/Plugin/Ui.php:224 #: Puc/v5p6/Plugin/Ui.php:227
#, php-format #, php-format
msgid "Unknown update checker status \"%s\"" msgid "Unknown update checker status \"%s\""
msgstr "未知的更新检查状态:%s" msgstr "未知的更新检查状态:%s"
#: Puc/v4p11/Vcs/PluginUpdateChecker.php:100 #: Puc/v5p6/Vcs/PluginUpdateChecker.php:113
msgid "There is no changelog available." msgid "There is no changelog available."
msgstr "没有可用的更新日志。" msgstr "没有可用的更新日志。"
@@ -0,0 +1,66 @@
# Blank Plugin POT Template
# Copyright 2025 ...
# This file is distributed under the GNU General Public License v3 or later.
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Plugin Update Checker\n"
"Report-Msgid-Bugs-To: Alex Lion <learnwithalex@gmail.com>\n"
"POT-Creation-Date: 2025-09-19 14:05-0700\n"
"PO-Revision-Date: \n"
"Last-Translator: Alex Lion <learnwithalex@gmail.com>\n"
"Language-Team: Alex Lion <learnwithalex@gmail.com>\n"
"Language: zh_TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Poedit-WPHeader: plugin-update-checker.php\n"
"X-Textdomain-Support: yesX-Generator: Poedit 1.6.4\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: "
"__;_e;esc_html_e;esc_html_x:1,2c;esc_html__;esc_attr_e;esc_attr_x:1,2c;esc_attr__;_ex:1,2c;_nx:4c,1,2;_nx_noop:4c,1,2;_x:1,2c;_n:1,2;_n_noop:1,2;__ngettext:1,2;__ngettext_noop:1,2;_c,_nc:4c,1,2\n"
"X-Poedit-Basepath: ..\n"
"X-Poedit-Bookmarks: \n"
"X-Generator: Poedit 3.7\n"
"X-Poedit-SearchPath-0: .\n"
#: Puc/v5p6/Plugin/Ui.php:56
msgid "View details"
msgstr "檢視詳細資料"
#: Puc/v5p6/Plugin/Ui.php:79
#, php-format
msgid "More information about %s"
msgstr "進一步了解 %s 的相關資訊"
#: Puc/v5p6/Plugin/Ui.php:130
msgid "Check for updates"
msgstr "檢查更新"
#: Puc/v5p6/Plugin/Ui.php:217
#, php-format
msgctxt "the plugin title"
msgid "The %s plugin is up to date."
msgstr "%s 外掛已為最新版本。"
#: Puc/v5p6/Plugin/Ui.php:219
#, php-format
msgctxt "the plugin title"
msgid "A new version of the %s plugin is available."
msgstr "%s 外掛已有新版本可供更新。"
#: Puc/v5p6/Plugin/Ui.php:221
#, php-format
msgctxt "the plugin title"
msgid "Could not determine if updates are available for %s."
msgstr "無法確定 %s 是否有可用的更新。"
#: Puc/v5p6/Plugin/Ui.php:227
#, php-format
msgid "Unknown update checker status \"%s\""
msgstr "未知的更新檢查程式狀態: %s"
#: Puc/v5p6/Vcs/PluginUpdateChecker.php:113
msgid "There is no changelog available."
msgstr "目前沒有可供檢閱的變更記錄。"
@@ -2,7 +2,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: plugin-update-checker\n" "Project-Id-Version: plugin-update-checker\n"
"POT-Creation-Date: 2022-07-29 15:34+0300\n" "POT-Creation-Date: 2025-05-20 15:27+0300\n"
"PO-Revision-Date: 2016-01-10 20:59+0100\n" "PO-Revision-Date: 2016-01-10 20:59+0100\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
@@ -11,39 +11,39 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.1.1\n" "X-Generator: Poedit 3.6\n"
"X-Poedit-Basepath: ..\n" "X-Poedit-Basepath: ..\n"
"X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n" "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
"X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPath-0: .\n"
#: Puc/v5p4/Plugin/Ui.php:128 #: Puc/v5p7/Plugin/Ui.php:130
msgid "Check for updates" msgid "Check for updates"
msgstr "" msgstr ""
#: Puc/v5p4/Plugin/Ui.php:214 #: Puc/v5p7/Plugin/Ui.php:217
#, php-format #, php-format
msgctxt "the plugin title" msgctxt "the plugin title"
msgid "The %s plugin is up to date." msgid "The %s plugin is up to date."
msgstr "" msgstr ""
#: Puc/v5p4/Plugin/Ui.php:216 #: Puc/v5p7/Plugin/Ui.php:219
#, php-format #, php-format
msgctxt "the plugin title" msgctxt "the plugin title"
msgid "A new version of the %s plugin is available." msgid "A new version of the %s plugin is available."
msgstr "" msgstr ""
#: Puc/v5p4/Plugin/Ui.php:218 #: Puc/v5p7/Plugin/Ui.php:221
#, php-format #, php-format
msgctxt "the plugin title" msgctxt "the plugin title"
msgid "Could not determine if updates are available for %s." msgid "Could not determine if updates are available for %s."
msgstr "" msgstr ""
#: Puc/v5p4/Plugin/Ui.php:224 #: Puc/v5p7/Plugin/Ui.php:227
#, php-format #, php-format
msgid "Unknown update checker status \"%s\"" msgid "Unknown update checker status \"%s\""
msgstr "" msgstr ""
#: Puc/v5p4/Vcs/PluginUpdateChecker.php:100 #: Puc/v5p7/Vcs/PluginUpdateChecker.php:113
msgid "There is no changelog available." msgid "There is no changelog available."
msgstr "" msgstr ""
@@ -1,14 +1,14 @@
<?php <?php
namespace YahnisElsts\PluginUpdateChecker\v5p4; namespace YahnisElsts\PluginUpdateChecker\v5p7;
use YahnisElsts\PluginUpdateChecker\v5\PucFactory as MajorFactory; use YahnisElsts\PluginUpdateChecker\v5\PucFactory as MajorFactory;
use YahnisElsts\PluginUpdateChecker\v5p4\PucFactory as MinorFactory; use YahnisElsts\PluginUpdateChecker\v5p7\PucFactory as MinorFactory;
require __DIR__ . '/Puc/v5p4/Autoloader.php'; require __DIR__ . '/Puc/v5p7/Autoloader.php';
new Autoloader(); new Autoloader();
require __DIR__ . '/Puc/v5p4/PucFactory.php'; require __DIR__ . '/Puc/v5p7/PucFactory.php';
require __DIR__ . '/Puc/v5/PucFactory.php'; require __DIR__ . '/Puc/v5/PucFactory.php';
//Register classes defined in this version with the factory. //Register classes defined in this version with the factory.
@@ -26,9 +26,9 @@ foreach (
) )
as $pucGeneralClass => $pucVersionedClass as $pucGeneralClass => $pucVersionedClass
) { ) {
MajorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.4'); MajorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.7');
//Also add it to the minor-version factory in case the major-version factory //Also add it to the minor-version factory in case the major-version factory
//was already defined by another, older version of the update checker. //was already defined by another, older version of the update checker.
MinorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.4'); MinorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.7');
} }
@@ -1,10 +1,10 @@
<?php <?php
/** /**
* Plugin Update Checker Library 5.4 * Plugin Update Checker Library 5.7
* http://w-shadow.com/ * http://w-shadow.com/
* *
* Copyright 2024 Janis Elsts * Copyright 2026 Janis Elsts
* Released under the MIT license. See license.txt for details. * Released under the MIT license. See license.txt for details.
*/ */
require dirname(__FILE__) . '/load-v5p4.php'; require dirname(__FILE__) . '/load-v5p7.php';
+2 -2
View File
@@ -648,7 +648,7 @@ class Parsedown
# #
# Setext # Setext
protected function blockSetextHeader($Line, array $Block = null) protected function blockSetextHeader($Line, $Block = null)
{ {
if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
{ {
@@ -786,7 +786,7 @@ class Parsedown
# #
# Table # Table
protected function blockTable($Line, array $Block = null) protected function blockTable($Line, $Block = null)
{ {
if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
{ {
+7 -1
View File
@@ -186,7 +186,13 @@ class Gitea_Provider implements Repository_Provider {
return null; return null;
} }
$release = $body[0]; // Gitea's /releases/latest returns a single release OBJECT (associative array),
// not a list. Some endpoints / older Gitea versions may return a list; handle both.
$release = isset( $body['tag_name'] ) ? $body : ( $body[0] ?? null );
if ( ! is_array( $release ) || empty( $release['tag_name'] ?? '' ) ) {
return null;
}
set_transient( $cache_key, $release, HOUR_IN_SECONDS ); set_transient( $cache_key, $release, HOUR_IN_SECONDS );
return $release; return $release;
} }
+25 -24
View File
@@ -11,7 +11,7 @@
* Plugin Name: Projects Portfolio * Plugin Name: Projects Portfolio
* Description: Create a showcase directory for projects (plugins, themes, patterns) with custom post types, taxonomies, and download functionality. * Description: Create a showcase directory for projects (plugins, themes, patterns) with custom post types, taxonomies, and download functionality.
* Plugin URI: https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio * Plugin URI: https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio
* Version: 1.1.1 * Version: 1.2.0
* Author: Keith Solomon * Author: Keith Solomon
* Author URI: https://keithsolomon.net * Author URI: https://keithsolomon.net
* License: GPL-2.0+ * License: GPL-2.0+
@@ -34,12 +34,12 @@ $myUpdateChecker = PucFactory::buildUpdateChecker(
__FILE__, __FILE__,
'projects-portfolio' 'projects-portfolio'
); );
// Note: PUC's setBranch() is only available when the host is recognized as a
// Set the branch that contains the stable release. // VCS provider (GitHub, GitLab, Bitbucket). The Gitea URL here falls back to
$myUpdateChecker->setBranch( 'main' ); // PUC's plain JSON metadata mode, so we skip the call rather than throw.
// Current plugin version. // Current plugin version.
define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.1' ); define( 'PROJECTS_PORTFOLIO_VERSION', '1.2.0' );
// Add the required files. // Add the required files.
require 'admin/admin-settings.php'; require 'admin/admin-settings.php';
@@ -112,7 +112,10 @@ function projects_portfolio_handle_download_redirect() {
update_post_meta( $project_id, '_projects_portfolio_download_count', $download_count ); update_post_meta( $project_id, '_projects_portfolio_download_count', $download_count );
// Redirect to the release ZIP on the project's host. // Redirect to the release ZIP on the project's host.
wp_safe_redirect( esc_url_raw( $download_url ) ); // Use wp_redirect() (not wp_safe_redirect()) because the destination
// is an external host the user has explicitly configured — wp_safe_redirect
// would reject it and fall back to admin_url().
wp_redirect( esc_url_raw( $download_url ), 302 ); // phpcs:ignore
exit; exit;
} else { } else {
wp_die( wp_die(
@@ -268,16 +271,6 @@ function projects_portfolio_enqueue_project_styles() {
wp_enqueue_style( 'projects-wp-styles' ); wp_enqueue_style( 'projects-wp-styles' );
} }
} }
if ( is_singular( 'projects' ) ) {
wp_enqueue_script(
'projects-wp-buttons',
plugin_dir_url( __FILE__ ) . 'assets/js/buttons.js',
array(),
PROJECTS_PORTFOLIO_VERSION,
true
);
}
} }
add_action( 'wp_enqueue_scripts', 'projects_portfolio_enqueue_project_styles' ); add_action( 'wp_enqueue_scripts', 'projects_portfolio_enqueue_project_styles' );
@@ -351,39 +344,47 @@ function projects_portfolio_social_sharing_buttons( $project_id ) {
$html = '<div class="project-social-sharing">'; $html = '<div class="project-social-sharing">';
$svg_icon = function ( string $name, string $file ) use ( &$html ) {
// Render each social icon as an <img> referencing a real .svg file
// shipped under assets/icons/. This bypasses any kses data: URI stripping
// and ensures the icon loads as a normal image via standard img rules.
$src = plugins_url( 'assets/icons/' . $file, __FILE__ );
$html .= '<img src="' . esc_url( $src ) . '" alt="' . esc_attr( $name ) . '" width="20" height="20" style="width:20px;height:20px;display:inline-block;vertical-align:middle;" />';
};
// Facebook. // Facebook.
$html .= '<a href="https://www.facebook.com/sharer/sharer.php?u=' . $url . '" target="_blank" rel="noopener noreferrer">'; $html .= '<a href="https://www.facebook.com/sharer/sharer.php?u=' . $url . '" target="_blank" rel="noopener noreferrer">';
$html .= '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-facebook" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#0073aa" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="0" y="0" width="24" height="24" fill="none"/><path d="M7 10v-3a1 1 0 0 1 1 -1h3v-4h4v4h3a1 1 0 0 1 1 1v3h-4v10h-4v-10h-3" /></svg>'; $svg_icon( 'Facebook', 'facebook.svg' );
$html .= '</a>'; $html .= '</a>';
// X (Twitter). // X (Twitter).
$html .= '<a href="https://twitter.com/intent/tweet?text=' . $title . '&url=' . $url . '" target="_blank" rel="noopener noreferrer">'; $html .= '<a href="https://twitter.com/intent/tweet?text=' . $title . '&url=' . $url . '" target="_blank" rel="noopener noreferrer">';
$html .= '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#0073aa" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 4l10 16m0 -16l-10 16" /></svg>'; $svg_icon( 'X', 'x.svg' );
$html .= '</a>'; $html .= '</a>';
// LinkedIn. // LinkedIn.
$html .= '<a href="https://www.linkedin.com/sharing/share-offsite/?url=' . $url . '" target="_blank" rel="noopener noreferrer">'; $html .= '<a href="https://www.linkedin.com/sharing/share-offsite/?url=' . $url . '" target="_blank" rel="noopener noreferrer">';
$html .= '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-linkedin" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#0073aa" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><rect x="4" y="4" width="16" height="16" rx="2" /><line x1="8" y1="11" x2="8" y2="16" /><line x1="8" y1="8" x2="8" y2="8.01" /><line x1="12" y1="16" x2="12" y2="11" /><path d="M16 16v-3a2 2 0 0 0 -4 0" /></svg>'; $svg_icon( 'LinkedIn', 'linkedin.svg' );
$html .= '</a>'; $html .= '</a>';
// Reddit. // Reddit.
$html .= '<a href="https://www.reddit.com/submit?url=' . $url . '&title=' . $title . '" target="_blank" rel="noopener noreferrer">'; $html .= '<a href="https://www.reddit.com/submit?url=' . $url . '&title=' . $title . '" target="_blank" rel="noopener noreferrer">';
$html .= '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-reddit" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#0073aa" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><circle cx="12" cy="12" r="9" /><path d="M12 12m-6 0a6 6 0 1 0 12 0a6 6 0 1 0 -12 0" /><path d="M12 12h0" /><path d="M8.5 13c.667 .667 1.333 1 2 1s1.333 -.333 2 -1" /></svg>'; $svg_icon( 'Reddit', 'reddit.svg' );
$html .= '</a>'; $html .= '</a>';
// WhatsApp. // WhatsApp.
$html .= '<a href="https://wa.me/?text=' . $title . '%20' . $url . '" target="_blank" rel="noopener noreferrer">'; $html .= '<a href="https://www.whatsapp.com/send?text=' . $title . '%20' . $url . '" target="_blank" rel="noopener noreferrer">';
$html .= '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-whatsapp" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#0073aa" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 21l1.65 -4.75a9 9 0 1 1 3.85 3.85z" /><path d="M9 10c1 2 3 3 5 4" /><path d="M9 13c1 1 2 2 4 3" /></svg>'; $svg_icon( 'WhatsApp', 'whatsapp.svg' );
$html .= '</a>'; $html .= '</a>';
// Pinterest. // Pinterest.
$html .= '<a href="https://pinterest.com/pin/create/button/?url=' . $url . '&description=' . $title . '" target="_blank" rel="noopener noreferrer">'; $html .= '<a href="https://pinterest.com/pin/create/button/?url=' . $url . '&description=' . $title . '" target="_blank" rel="noopener noreferrer">';
$html .= '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-pinterest" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#0073aa" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><circle cx="12" cy="12" r="9" /><path d="M8 17c1 -2 1.5 -4 .5 -6s-.5 -4 .5 -6" /><path d="M15 11c.667 -1 1.667 -2.333 3 -4" /></svg>'; $svg_icon( 'Pinterest', 'pinterest.svg' );
$html .= '</a>'; $html .= '</a>';
// Email. // Email.
$html .= '<a href="mailto:?subject=' . $title . '&body=' . $url . '" target="_blank" rel="noopener noreferrer">'; $html .= '<a href="mailto:?subject=' . $title . '&body=' . $url . '" target="_blank" rel="noopener noreferrer">';
$html .= '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#0073aa" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7l9 6l9 -6" /><rect x="3" y="5" width="18" height="14" rx="2" /></svg>'; $svg_icon( 'Email', 'mail.svg' );
$html .= '</a>'; $html .= '</a>';
$html .= '</div>'; $html .= '</div>';
+26 -24
View File
@@ -4,7 +4,7 @@ $is_fse_theme = wp_theme_has_theme_json() && function_exists( 'wp_is_block_theme
if ( $is_fse_theme ) { if ( $is_fse_theme ) {
wp_head(); wp_head();
echo do_blocks( '<!-- wp:template-part {"slug":"header"} /-->' ); echo do_blocks( '<!-- wp:template-part {"slug":"header"} /-->' ); // phpcs:ignore
} else { } else {
get_header(); get_header();
} }
@@ -23,11 +23,11 @@ $version = projects_portfolio_get_version( $project_id );
$owner = projects_portfolio_get_owner( $project_id ); $owner = projects_portfolio_get_owner( $project_id );
$owner_avatar = $repo_data['owner']['avatar_url'] ?? ''; $owner_avatar = $repo_data['owner']['avatar_url'] ?? '';
$owner_name = $repo_data['owner']['login'] ?? ''; $owner_name = $repo_data['owner']['login'] ?? '';
$owner_url = $repo_data['owner']['html_url'] ?? ''; $owner_url = $repo_data['owner']['html_url'] ?? '';
$last_updated = $repo_data['updated_at'] ?? ''; $last_updated = $repo_data['updated_at'] ?? '';
$language = $repo_data['language'] ?? ''; $language = $repo_data['language'] ?? '';
$license = $repo_data['license']['name'] ?? 'None'; $license = $repo_data['license']['name'] ?? 'None';
$browse_url = $repo_url ? projects_portfolio_get_repo_browse_url( $project_id ) : ''; $browse_url = $repo_url ? projects_portfolio_get_repo_browse_url( $project_id ) : '';
if ( $last_updated ) { if ( $last_updated ) {
@@ -69,12 +69,13 @@ if ( $last_updated ) {
<?php esc_html_e( 'Download Now', 'projects-wp' ); ?> <?php esc_html_e( 'Download Now', 'projects-wp' ); ?>
</a> </a>
<?php if ( $repo_url ) : ?> <?php if ( $repo_url ) : ?>
<a href="<?php echo esc_url( $browse_url ); ?>" class="button project-github-button" target="_blank" rel="noopener noreferrer"> <a href="<?php echo esc_url( $browse_url ); ?>" class="button project-repo-button" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'View Repo', 'projects-wp' ); ?> <?php esc_html_e( 'View Repo', 'projects-wp' ); ?>
</a> </a>
<?php else : ?> <?php else : ?>
<p><?php esc_html_e( 'Repository:', 'projects-wp' ); ?> <?php esc_html_e( 'No repository linked.', 'projects-wp' ); ?></p> <p><?php esc_html_e( 'Repository:', 'projects-wp' ); ?> <?php esc_html_e( 'No repository linked.', 'projects-wp' ); ?></p>
<?php endif; ?> <?php endif; ?>
<?php do_action( 'projects_after_download_button', $project_id ); ?>
</div> </div>
<div class="project-meta"> <div class="project-meta">
@@ -115,24 +116,30 @@ if ( $last_updated ) {
<?php endif; ?> <?php endif; ?>
<?php <?php
if ( ! empty( $repo_data ) ) : if ( ! empty( $repo_data ) ) :
if ( isset( $repo_data['stargazers_count'] ) && $settings['templates']['stargazers_count'] ) : ?> if ( isset( $repo_data['stargazers_count'] ) && $settings['templates']['stargazers_count'] ) :
?>
<tr> <tr>
<th><?php esc_html_e( 'Stars:', 'projects-wp' ); ?></th> <th><?php esc_html_e( 'Stars:', 'projects-wp' ); ?></th>
<td><?php esc_html_e( $repo_data['stargazers_count'] ); ?></td> <td><?php esc_html_e( $repo_data['stargazers_count'] ); ?></td>
</tr> </tr>
<?php endif; <?php
if ( isset( $repo_data['forks_count'] ) && $settings['templates']['forks'] ) : ?> endif;
if ( isset( $repo_data['forks_count'] ) && $settings['templates']['forks'] ) :
?>
<tr> <tr>
<th><?php esc_html_e( 'Forks:', 'projects-wp' ); ?></th> <th><?php esc_html_e( 'Forks:', 'projects-wp' ); ?></th>
<td><?php esc_html_e( $repo_data['forks_count'] ); ?></td> <td><?php esc_html_e( $repo_data['forks_count'] ); ?></td>
</tr> </tr>
<?php endif; <?php
if ( isset( $repo_data['open_issues_count'] ) && $settings['templates']['open_issues_count'] ) : ?> endif;
if ( isset( $repo_data['open_issues_count'] ) && $settings['templates']['open_issues_count'] ) :
?>
<tr> <tr>
<th><?php esc_html_e( 'Issues:', 'projects-wp' ); ?></th> <th><?php esc_html_e( 'Issues:', 'projects-wp' ); ?></th>
<td><?php esc_html_e( $repo_data['open_issues_count'] ); ?></td> <td><?php esc_html_e( $repo_data['open_issues_count'] ); ?></td>
</tr> </tr>
<?php endif; <?php
endif;
endif; endif;
?> ?>
</tbody> </tbody>
@@ -140,18 +147,13 @@ if ( $last_updated ) {
</div> </div>
<?php if ( $settings['templates']['github_owner'] && $owner_avatar && $owner_name ) : ?> <?php if ( $settings['templates']['github_owner'] && $owner_avatar && $owner_name ) : ?>
<a href="<?php echo esc_url( $browse_url ); ?>">
<div class="project-owner"> <div class="project-owner">
<span> <span class="project-owner-avatar" style="display:block !important;width:50px !important;height:50px !important;min-width:50px !important;min-height:50px !important;max-width:50px !important;max-height:50px !important;border-radius:50% !important;background-image:url('<?php echo esc_url( $owner_avatar ); ?>') !important;background-size:40px 40px !important;background-position:center center !important;background-repeat:no-repeat !important;flex-shrink:0 !important;flex-grow:0 !important;" aria-label="<?php echo esc_attr( $owner_name ); ?>" role="img"></span>
<img src="<?php echo esc_url( $owner_avatar ); ?>" alt="<?php esc_attr_e( $owner_name ); ?>" class="owner-avatar" style="width: 50px; height: 50px; border-radius: 50%;" /> <span class="project-owner-name">
<a href="<?php echo esc_url( $owner_url ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $owner_name ); ?></a>
</span> </span>
<span> <span class="project-owner-follow">
<strong> <a class="button project-follow-button" href="<?php echo esc_url( $owner_url ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Follow', 'projects-wp' ); ?></a>
<a href="<?php echo esc_url( $owner_url ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( $owner_name ); ?></a>
</strong>
</span>
<span>
<a class="github-button" href="https://github.com/<?php esc_html_e( $owner_name ); ?>" data-show-count="true" data-color-scheme="no-preference: light; light: light; dark: dark;" data-size="large" aria-label="Follow @<?php esc_html_e( $owner_name ); ?> on GitHub"><?php esc_attr_e( 'Follow', 'projects-wp' ) ?></a>
</span> </span>
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -165,7 +167,7 @@ if ( $last_updated ) {
<?php <?php
if ( $is_fse_theme ) { if ( $is_fse_theme ) {
wp_footer(); wp_footer();
echo do_blocks( '<!-- wp:template-part {"slug":"footer"} /-->' ); echo do_blocks( '<!-- wp:template-part {"slug":"footer"} /-->' ); // phpcs:ignore
} else { } else {
get_footer(); get_footer();
} }
+23
View File
@@ -136,6 +136,29 @@ class Gitea_Provider_Test extends \PHPUnit\Framework\TestCase {
); );
} }
public function test_release_object_shape_picks_first_tag_from_object(): void {
// Gitea's /releases/latest returns a single release OBJECT (associative array),
// not a list. The implementation must accept either shape.
$body = json_encode( [
'tag_name' => 'v2.1.0',
'name' => 'v2.1.0',
'assets' => [
[ 'name' => 'release.zip', 'browser_download_url' => 'https://example/release.zip' ],
],
] );
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
->twice() // get_release_url and get_latest_version both fetch
->andReturn( [
'response' => [ 'code' => 200 ],
'headers' => [],
'body' => $body,
] );
$provider = $this->make();
$this->assertSame( 'https://example/release.zip', $provider->get_release_url() );
$this->assertSame( 'v2.1.0', $provider->get_latest_version() );
}
public function test_release_url_empty_array_returns_null(): void { public function test_release_url_empty_array_returns_null(): void {
\Brain\Monkey\Functions\expect( 'wp_remote_get' ) \Brain\Monkey\Functions\expect( 'wp_remote_get' )
->once() ->once()