Compare commits
17
Commits
915ca4cb5d
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0480abe292 | ||
|
|
981c29ab39 | ||
|
|
5913c8799f | ||
|
|
d250f6b89f | ||
|
|
62a049b2c2 | ||
|
|
e25431a25d | ||
|
|
9803b497ab | ||
|
|
d8ce719433 | ||
|
|
e60a9eafb3 | ||
|
|
925ef184b9 | ||
|
|
40793cd4bb | ||
|
|
e3b27e8974 | ||
|
|
62a20ed514 | ||
|
|
28789f6295 | ||
|
|
4a9f43c850 | ||
|
|
730893c1f0 | ||
|
|
e924b4833a |
@@ -2,3 +2,6 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
docs/
|
docs/
|
||||||
notes/
|
notes/
|
||||||
|
vendor/
|
||||||
|
phpunit.xml
|
||||||
|
.phpunit.result.cache
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ Create a beautifully structured, developer-first showcase directory for your Wor
|
|||||||
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, paste the GitHub Repository URL (e.g., `https://github.com/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`).
|
||||||
5. Publish.
|
5. For Gitea projects, optionally set a **Gitea Base URL** to override the global default.
|
||||||
|
6. Publish.
|
||||||
|
|
||||||
### Enable Download Link
|
### Enable Download Link
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ Each project automatically gets a unique download endpoint:
|
|||||||
Example:
|
Example:
|
||||||
`https://example.com/download/123`
|
`https://example.com/download/123`
|
||||||
|
|
||||||
This URL fetches the latest `.zip` release from GitHub and increments the download count.
|
This URL fetches the latest `.zip` release from the project's host (GitHub or Gitea) and increments the download count.
|
||||||
|
|
||||||
## Settings Overview
|
## Settings Overview
|
||||||
|
|
||||||
@@ -64,6 +65,8 @@ Navigate to **Projects → Settings** to configure:
|
|||||||
### General Settings
|
### General Settings
|
||||||
|
|
||||||
- **GitHub API Token** – Recommended for authenticated requests (avoids GitHub rate limits).
|
- **GitHub API Token** – Recommended for authenticated requests (avoids GitHub rate limits).
|
||||||
|
- **Default Gitea Base URL** – Used when a Gitea project doesn't specify one. Defaults to `https://codeberg.org`.
|
||||||
|
- **Gitea API Token** – Recommended for authenticated Gitea requests.
|
||||||
- **Telemetry** – Toggle to help improve the plugin.
|
- **Telemetry** – Toggle to help improve the plugin.
|
||||||
|
|
||||||
### Templates Settings
|
### Templates Settings
|
||||||
@@ -102,6 +105,22 @@ To avoid hitting rate limits or improve reliability:
|
|||||||
2. Generate a token (no scopes required).
|
2. Generate a token (no scopes required).
|
||||||
3. Paste it into the settings screen under **GitHub API Token**.
|
3. Paste it into the settings screen under **GitHub API Token**.
|
||||||
|
|
||||||
|
## Connect Your Gitea Repo
|
||||||
|
|
||||||
|
To connect a project to a Gitea instance (self-hosted or public):
|
||||||
|
|
||||||
|
1. Set **Provider** to **Gitea** in the project editor.
|
||||||
|
2. Paste the full repository URL into **Repository URL** (e.g., `https://codeberg.org/username/repo`).
|
||||||
|
3. Optionally override the Gitea base URL for this project (leave blank to use the global default).
|
||||||
|
4. Make sure the repo has a release with a `.zip` asset, or any tag — the plugin will fall back to the source archive URL.
|
||||||
|
|
||||||
|
### Optional: Add a Gitea API Token
|
||||||
|
|
||||||
|
To avoid rate limits or improve reliability:
|
||||||
|
|
||||||
|
1. In your Gitea instance, go to **Settings → Applications** and generate a token.
|
||||||
|
2. Paste it into the plugin settings screen under **Gitea API Token**.
|
||||||
|
|
||||||
## How to Zip the Plugin for GitHub Releases
|
## How to Zip the Plugin for GitHub Releases
|
||||||
|
|
||||||
When attaching the plugin to a GitHub release:
|
When attaching the plugin to a GitHub release:
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ function projects_portfolio_render_settings_page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve saved settings.
|
// Retrieve saved settings.
|
||||||
$api_token = get_option( 'projects_portfolio_github_api_token', '' );
|
$api_token = get_option( 'projects_portfolio_github_api_token', '' );
|
||||||
$share_telemetry = get_option( 'projects_portfolio_share_telemetry', '0' );
|
$gitea_api_token = get_option( 'projects_portfolio_gitea_api_token', '' );
|
||||||
|
$default_gitea_base_url = get_option( 'projects_portfolio_default_gitea_base_url', 'https://codeberg.org' );
|
||||||
|
$share_telemetry = get_option( 'projects_portfolio_share_telemetry', '0' );
|
||||||
|
|
||||||
// Templates Settings.
|
// Templates Settings.
|
||||||
$templates_settings = [
|
$templates_settings = [
|
||||||
@@ -83,6 +85,27 @@ function projects_portfolio_render_settings_page() {
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Gitea Settings', 'projects-wp' ); ?></h2>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="projects_portfolio_default_gitea_base_url"><?php esc_html_e( 'Default Gitea Base URL', 'projects-wp' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" id="projects_portfolio_default_gitea_base_url" name="projects_portfolio_default_gitea_base_url" value="<?php echo esc_attr( $default_gitea_base_url ); ?>" class="regular-text" placeholder="https://codeberg.org" />
|
||||||
|
<p class="description"><?php esc_html_e( 'Used when a Gitea project has no per-project base URL override.', 'projects-wp' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="projects_portfolio_gitea_api_token"><?php esc_html_e( 'Gitea API Token', 'projects-wp' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="password" id="projects_portfolio_gitea_api_token" name="projects_portfolio_gitea_api_token" value="<?php echo esc_attr( $gitea_api_token ); ?>" class="regular-text" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<h2><?php esc_html_e( 'Templates Settings', 'projects-wp' ); ?></h2>
|
<h2><?php esc_html_e( 'Templates Settings', 'projects-wp' ); ?></h2>
|
||||||
<table class="form-table">
|
<table class="form-table">
|
||||||
<?php foreach ( $templates_settings as $key => $label ) :
|
<?php foreach ( $templates_settings as $key => $label ) :
|
||||||
@@ -134,6 +157,8 @@ function projects_portfolio_save_settings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update_option( 'projects_portfolio_github_api_token', sanitize_text_field( $_POST['projects_portfolio_github_api_token'] ?? '' ) );
|
update_option( 'projects_portfolio_github_api_token', sanitize_text_field( $_POST['projects_portfolio_github_api_token'] ?? '' ) );
|
||||||
|
update_option( 'projects_portfolio_gitea_api_token', sanitize_text_field( $_POST['projects_portfolio_gitea_api_token'] ?? '' ) );
|
||||||
|
update_option( 'projects_portfolio_default_gitea_base_url', esc_url_raw( $_POST['projects_portfolio_default_gitea_base_url'] ?? 'https://codeberg.org' ) );
|
||||||
update_option( 'projects_portfolio_share_telemetry', isset( $_POST['projects_portfolio_share_telemetry'] ) ? '1' : '0' );
|
update_option( 'projects_portfolio_share_telemetry', isset( $_POST['projects_portfolio_share_telemetry'] ) ? '1' : '0' );
|
||||||
|
|
||||||
// Templates Settings Keys.
|
// Templates Settings Keys.
|
||||||
|
|||||||
+87
-15
@@ -6,15 +6,15 @@ if ( ! defined( 'WPINC' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add GitHub Repository URL meta box.
|
* Add Repository meta box (provider-aware).
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function projects_portfolio_add_meta_boxes() {
|
function projects_portfolio_add_meta_boxes() {
|
||||||
add_meta_box(
|
add_meta_box(
|
||||||
'projects_portfolio_github_url',
|
'projects_portfolio_repo',
|
||||||
esc_html__( 'GitHub URL', 'projects-wp' ),
|
esc_html__( 'Repository', 'projects-wp' ),
|
||||||
'projects_portfolio_render_meta_box',
|
'projects_portfolio_render_meta_box',
|
||||||
'projects',
|
'projects',
|
||||||
'side'
|
'side'
|
||||||
@@ -23,27 +23,64 @@ function projects_portfolio_add_meta_boxes() {
|
|||||||
add_action( 'add_meta_boxes', 'projects_portfolio_add_meta_boxes' );
|
add_action( 'add_meta_boxes', 'projects_portfolio_add_meta_boxes' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the GitHub Repository URL meta box for the Projects WP plugin.
|
* Render the Repository meta box.
|
||||||
*
|
*
|
||||||
* Outputs a label and input field for setting the GitHub repository URL,
|
* @param WP_Post $post
|
||||||
* as well as a nonce for security.
|
|
||||||
*
|
|
||||||
* @param WP_Post $post The post object currently being edited.
|
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function projects_portfolio_render_meta_box( $post ) {
|
function projects_portfolio_render_meta_box( $post ) {
|
||||||
wp_nonce_field( 'projects_portfolio_save_meta_box', 'projects_portfolio_meta_box_nonce' );
|
wp_nonce_field( 'projects_portfolio_save_meta_box', 'projects_portfolio_meta_box_nonce' );
|
||||||
$github_url = get_post_meta( $post->ID, '_projects_portfolio_github_url', true );
|
|
||||||
echo '<label for="projects_portfolio_github_url">' . __( 'GitHub Repository URL:', 'projects-wp' ) . '</label>';
|
$provider = get_post_meta( $post->ID, '_projects_portfolio_provider', true );
|
||||||
echo '<input type="url" id="projects_portfolio_github_url" name="projects_portfolio_github_url" value="' . esc_attr( $github_url ) . '" style="width: 100%;" />';
|
if ( '' === $provider ) {
|
||||||
|
$provider = 'github';
|
||||||
|
}
|
||||||
|
$repo_url = get_post_meta( $post->ID, '_projects_portfolio_repo_url', true );
|
||||||
|
if ( '' === $repo_url ) {
|
||||||
|
$repo_url = get_post_meta( $post->ID, '_projects_portfolio_github_url', true );
|
||||||
|
}
|
||||||
|
$gitea_base_url = get_post_meta( $post->ID, '_projects_portfolio_gitea_base_url', true );
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="projects_portfolio_provider"><?php esc_html_e( 'Provider', 'projects-wp' ); ?></label>
|
||||||
|
<select id="projects_portfolio_provider" name="projects_portfolio_provider" style="width: 100%;">
|
||||||
|
<option value="github" <?php selected( $provider, 'github' ); ?>><?php esc_html_e( 'GitHub', 'projects-wp' ); ?></option>
|
||||||
|
<option value="gitea" <?php selected( $provider, 'gitea' ); ?>><?php esc_html_e( 'Gitea', 'projects-wp' ); ?></option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="projects_portfolio_repo_url"><?php esc_html_e( 'Repository URL', 'projects-wp' ); ?></label>
|
||||||
|
<input type="url" id="projects_portfolio_repo_url" name="projects_portfolio_repo_url" value="<?php echo esc_attr( $repo_url ); ?>" style="width: 100%;" placeholder="https://github.com/owner/repo" />
|
||||||
|
</p>
|
||||||
|
<p class="projects-portfolio-gitea-only" <?php echo 'gitea' !== $provider ? 'style="display:none;"' : ''; ?>>
|
||||||
|
<label for="projects_portfolio_gitea_base_url"><?php esc_html_e( 'Gitea Base URL (optional override)', 'projects-wp' ); ?></label>
|
||||||
|
<input type="url" id="projects_portfolio_gitea_base_url" name="projects_portfolio_gitea_base_url" value="<?php echo esc_attr( $gitea_base_url ); ?>" style="width: 100%;" placeholder="https://codeberg.org" />
|
||||||
|
<span class="description"><?php esc_html_e( 'Leave blank to use the default from Settings.', 'projects-wp' ); ?></span>
|
||||||
|
</p>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var sel = document.getElementById('projects_portfolio_provider');
|
||||||
|
if (!sel) return;
|
||||||
|
var gitea = document.querySelector('.projects-portfolio-gitea-only');
|
||||||
|
function toggle(){
|
||||||
|
if (!gitea) return;
|
||||||
|
gitea.style.display = sel.value === 'gitea' ? '' : 'none';
|
||||||
|
}
|
||||||
|
sel.addEventListener('change', toggle);
|
||||||
|
toggle();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the GitHub repository URL meta box data for the Projects WP plugin.
|
* Save the Repository meta box.
|
||||||
*
|
*
|
||||||
* @param int $post_id The ID of the post currently being saved.
|
* Performs a lazy migration from legacy `_projects_portfolio_github_url`.
|
||||||
|
*
|
||||||
|
* @param int $post_id
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @return void
|
* @return void
|
||||||
@@ -55,8 +92,43 @@ function projects_portfolio_save_meta_box( $post_id ) {
|
|||||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ( isset( $_POST['projects_portfolio_github_url'] ) ) {
|
|
||||||
update_post_meta( $post_id, '_projects_portfolio_github_url', esc_url_raw( $_POST['projects_portfolio_github_url'] ) );
|
$existing_provider = get_post_meta( $post_id, '_projects_portfolio_provider', true );
|
||||||
|
$existing_repo_url = get_post_meta( $post_id, '_projects_portfolio_repo_url', true );
|
||||||
|
$legacy_github = get_post_meta( $post_id, '_projects_portfolio_github_url', true );
|
||||||
|
|
||||||
|
// Lazy migration: legacy URL present, no explicit choice.
|
||||||
|
if ( '' === $existing_provider && '' === $existing_repo_url && '' !== $legacy_github ) {
|
||||||
|
update_post_meta( $post_id, '_projects_portfolio_provider', 'github' );
|
||||||
|
update_post_meta( $post_id, '_projects_portfolio_repo_url', $legacy_github );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_POST['projects_portfolio_provider'] ) ) {
|
||||||
|
$provider = ( 'gitea' === $_POST['projects_portfolio_provider'] ) ? 'gitea' : 'github';
|
||||||
|
update_post_meta( $post_id, '_projects_portfolio_provider', $provider );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_POST['projects_portfolio_repo_url'] ) ) {
|
||||||
|
$repo_url = esc_url_raw( wp_unslash( $_POST['projects_portfolio_repo_url'] ) );
|
||||||
|
update_post_meta( $post_id, '_projects_portfolio_repo_url', $repo_url );
|
||||||
|
// Maintain the legacy key for backward-compat reads (only when this is a GitHub project).
|
||||||
|
$provider_now = get_post_meta( $post_id, '_projects_portfolio_provider', true );
|
||||||
|
if ( 'github' === $provider_now ) {
|
||||||
|
if ( '' === $repo_url ) {
|
||||||
|
// Clearing the field on a GitHub project should clear the legacy key too.
|
||||||
|
delete_post_meta( $post_id, '_projects_portfolio_github_url' );
|
||||||
|
} else {
|
||||||
|
update_post_meta( $post_id, '_projects_portfolio_github_url', $repo_url );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Provider is gitea (or any non-github): ensure no stale GitHub URL can
|
||||||
|
// resurrect via the legacy fallback chain.
|
||||||
|
delete_post_meta( $post_id, '_projects_portfolio_github_url' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_POST['projects_portfolio_gitea_base_url'] ) ) {
|
||||||
|
update_post_meta( $post_id, '_projects_portfolio_gitea_base_url', esc_url_raw( wp_unslash( $_POST['projects_portfolio_gitea_base_url'] ) ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
add_action( 'save_post', 'projects_portfolio_save_meta_box' );
|
add_action( 'save_post', 'projects_portfolio_save_meta_box' );
|
||||||
|
|||||||
+20
-16
@@ -27,9 +27,12 @@ function projects_portfolio_get_projects_data( $request ) {
|
|||||||
while ( $query->have_posts() ) {
|
while ( $query->have_posts() ) {
|
||||||
$query->the_post();
|
$query->the_post();
|
||||||
|
|
||||||
$project_id = get_the_ID();
|
$project_id = get_the_ID();
|
||||||
$github_url = get_post_meta( $project_id, '_projects_portfolio_github_url', true );
|
$repo_url = get_post_meta( $project_id, '_projects_portfolio_repo_url', true );
|
||||||
$github_data = projects_portfolio_get_github_data( $github_url );
|
if ( '' === $repo_url ) {
|
||||||
|
$repo_url = get_post_meta( $project_id, '_projects_portfolio_github_url', true );
|
||||||
|
}
|
||||||
|
$repo_data = projects_portfolio_get_repo_data( $project_id );
|
||||||
|
|
||||||
// Fetch project-type taxonomy terms (names).
|
// Fetch project-type taxonomy terms (names).
|
||||||
$project_types = wp_get_post_terms( $project_id, 'project-type', [
|
$project_types = wp_get_post_terms( $project_id, 'project-type', [
|
||||||
@@ -44,23 +47,24 @@ function projects_portfolio_get_projects_data( $request ) {
|
|||||||
'thumbnail' => get_the_post_thumbnail_url( $project_id, 'large' ),
|
'thumbnail' => get_the_post_thumbnail_url( $project_id, 'large' ),
|
||||||
'download_count' => (int) get_post_meta( $project_id, '_projects_portfolio_download_count', true ),
|
'download_count' => (int) get_post_meta( $project_id, '_projects_portfolio_download_count', true ),
|
||||||
'download_url' => esc_url( site_url( '/download/' . $project_id ) ),
|
'download_url' => esc_url( site_url( '/download/' . $project_id ) ),
|
||||||
'github_url' => $github_url,
|
'github_url' => $repo_url,
|
||||||
|
'provider' => get_post_meta( $project_id, '_projects_portfolio_provider', true ) ?: 'github',
|
||||||
'github_data' => [
|
'github_data' => [
|
||||||
'owner' => [
|
'owner' => [
|
||||||
'avatar_url' => $github_data['owner']['avatar_url'] ?? '',
|
'avatar_url' => $repo_data['owner']['avatar_url'] ?? '',
|
||||||
'name' => $github_data['owner']['login'] ?? '',
|
'name' => $repo_data['owner']['login'] ?? '',
|
||||||
'profile' => $github_data['owner']['html_url'] ?? '',
|
'profile' => $repo_data['owner']['html_url'] ?? '',
|
||||||
],
|
],
|
||||||
'last_updated' => ! empty( $github_data['updated_at'] )
|
'last_updated' => ! empty( $repo_data['updated_at'] )
|
||||||
? date_i18n( get_option( 'date_format' ), strtotime( $github_data['updated_at'] ) )
|
? date_i18n( get_option( 'date_format' ), strtotime( $repo_data['updated_at'] ) )
|
||||||
: '',
|
: '',
|
||||||
'language' => $github_data['language'] ?? '',
|
'language' => $repo_data['language'] ?? '',
|
||||||
'license' => $github_data['license']['name'] ?? 'None',
|
'license' => $repo_data['license']['name'] ?? 'None',
|
||||||
'stars' => $github_data['stargazers_count'] ?? 0,
|
'stars' => $repo_data['stargazers_count'] ?? 0,
|
||||||
'forks' => $github_data['forks_count'] ?? 0,
|
'forks' => $repo_data['forks_count'] ?? 0,
|
||||||
'issues' => $github_data['open_issues_count'] ?? 0,
|
'issues' => $repo_data['open_issues_count'] ?? 0,
|
||||||
],
|
],
|
||||||
'version' => projects_portfolio_get_version_from_github( $github_url ),
|
'version' => projects_portfolio_get_version( $project_id ),
|
||||||
'project_types' => $project_types,
|
'project_types' => $project_types,
|
||||||
'content' => get_the_content(),
|
'content' => get_the_content(),
|
||||||
];
|
];
|
||||||
@@ -121,7 +125,7 @@ function projects_portfolio_get_popular_projects( $data ) {
|
|||||||
'title' => get_the_title(),
|
'title' => get_the_title(),
|
||||||
'download_count' => (int) get_post_meta( get_the_ID(), '_projects_portfolio_download_count', true ),
|
'download_count' => (int) get_post_meta( get_the_ID(), '_projects_portfolio_download_count', true ),
|
||||||
'download_url' => esc_url( site_url( '/download/' . get_the_ID() ) ),
|
'download_url' => esc_url( site_url( '/download/' . get_the_ID() ) ),
|
||||||
'github_url' => get_post_meta( get_the_ID(), '_projects_portfolio_github_url', true ),
|
'github_url' => ( $url = get_post_meta( get_the_ID(), '_projects_portfolio_repo_url', true ) ?: get_post_meta( get_the_ID(), '_projects_portfolio_github_url', true ) ),
|
||||||
'permalink' => get_permalink(),
|
'permalink' => get_permalink(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "keithsolomon/projects-portfolio",
|
||||||
|
"description": "WordPress plugin — dev-only test dependencies.",
|
||||||
|
"type": "wordpress-plugin",
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.6",
|
||||||
|
"brain/monkey": "^2.6",
|
||||||
|
"yoast/phpunit-polyfills": "^2.0"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"php-http/discovery": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2116
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,10 @@ function projects_portfolio_settings() {
|
|||||||
|
|
||||||
if ( $settings === null ) {
|
if ( $settings === null ) {
|
||||||
$settings = [
|
$settings = [
|
||||||
'github_api_token' => get_option( 'projects_portfolio_github_api_token', '' ),
|
'github_api_token' => get_option( 'projects_portfolio_github_api_token', '' ),
|
||||||
'share_telemetry' => get_option( 'projects_portfolio_share_telemetry', '0' ),
|
'gitea_api_token' => get_option( 'projects_portfolio_gitea_api_token', '' ),
|
||||||
|
'default_gitea_base_url' => get_option( 'projects_portfolio_default_gitea_base_url', 'https://codeberg.org' ),
|
||||||
|
'share_telemetry' => get_option( 'projects_portfolio_share_telemetry', '0' ),
|
||||||
'templates' => [
|
'templates' => [
|
||||||
'version' => get_option( 'projects_portfolio_templates_version', '0' ),
|
'version' => get_option( 'projects_portfolio_templates_version', '0' ),
|
||||||
'last_updated' => get_option( 'projects_portfolio_templates_last_updated', '0' ),
|
'last_updated' => get_option( 'projects_portfolio_templates_last_updated', '0' ),
|
||||||
@@ -41,6 +43,67 @@ function projects_portfolio_settings() {
|
|||||||
return $settings;
|
return $settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch normalized repository data for a project.
|
||||||
|
*
|
||||||
|
* @since 1.1.0
|
||||||
|
* @param int $post_id
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
function projects_portfolio_get_repo_data( int $post_id ): ?array {
|
||||||
|
return projects_portfolio_get_provider( $post_id )->get_repo_data();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the latest release ZIP download URL for a project.
|
||||||
|
*
|
||||||
|
* @since 1.1.0
|
||||||
|
* @param int $post_id
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
function projects_portfolio_get_release_url( int $post_id ): ?string {
|
||||||
|
return projects_portfolio_get_provider( $post_id )->get_release_url();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the latest tag name for a project.
|
||||||
|
*
|
||||||
|
* @since 1.1.0
|
||||||
|
* @param int $post_id
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function projects_portfolio_get_version( int $post_id ): string {
|
||||||
|
return projects_portfolio_get_provider( $post_id )->get_latest_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the repo owner profile data for a project.
|
||||||
|
*
|
||||||
|
* @since 1.1.0
|
||||||
|
* @param int $post_id
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
function projects_portfolio_get_owner( int $post_id ): ?array {
|
||||||
|
$provider = projects_portfolio_get_provider( $post_id );
|
||||||
|
$repo = $provider->get_repo_data();
|
||||||
|
$login = $repo['owner']['login'] ?? '';
|
||||||
|
if ( '' === $login ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $provider->get_owner_data( $login );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public browse URL for the project's repo.
|
||||||
|
*
|
||||||
|
* @since 1.1.0
|
||||||
|
* @param int $post_id
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function projects_portfolio_get_repo_browse_url( int $post_id ): string {
|
||||||
|
return projects_portfolio_get_provider( $post_id )->get_repo_browse_url();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GitHub API call for owner data
|
* GitHub API call for owner data
|
||||||
*
|
*
|
||||||
@@ -50,6 +113,8 @@ function projects_portfolio_settings() {
|
|||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
function projects_portfolio_github_owner( $owner_name = NULL ) {
|
function projects_portfolio_github_owner( $owner_name = NULL ) {
|
||||||
|
_deprecated_function( __FUNCTION__, '1.1.0', 'projects_portfolio_get_owner( $post_id )' );
|
||||||
|
|
||||||
if ( ! $owner_name ) { return; }
|
if ( ! $owner_name ) { return; }
|
||||||
|
|
||||||
$owner = 'https://api.github.com/users/' . $owner_name;
|
$owner = 'https://api.github.com/users/' . $owner_name;
|
||||||
@@ -87,6 +152,8 @@ function projects_portfolio_github_owner( $owner_name = NULL ) {
|
|||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
function projects_portfolio_get_github_release_url( $github_url ) {
|
function projects_portfolio_get_github_release_url( $github_url ) {
|
||||||
|
_deprecated_function( __FUNCTION__, '1.1.0', 'projects_portfolio_get_release_url( $post_id )' );
|
||||||
|
|
||||||
$api_token = get_option( 'projects_portfolio_github_api_token', '' );
|
$api_token = get_option( 'projects_portfolio_github_api_token', '' );
|
||||||
$api_url = str_replace( 'https://github.com/', 'https://api.github.com/repos/', rtrim( $github_url, '/' ) ) . '/releases/latest';
|
$api_url = str_replace( 'https://github.com/', 'https://api.github.com/repos/', rtrim( $github_url, '/' ) ) . '/releases/latest';
|
||||||
|
|
||||||
@@ -126,6 +193,8 @@ function projects_portfolio_get_github_release_url( $github_url ) {
|
|||||||
* @return array|null Associative array of GitHub repo data on success, or null on failure.
|
* @return array|null Associative array of GitHub repo data on success, or null on failure.
|
||||||
*/
|
*/
|
||||||
function projects_portfolio_get_github_data( $github_url ) {
|
function projects_portfolio_get_github_data( $github_url ) {
|
||||||
|
_deprecated_function( __FUNCTION__, '1.1.0', 'projects_portfolio_get_repo_data( $post_id )' );
|
||||||
|
|
||||||
if ( empty( $github_url ) ) {
|
if ( empty( $github_url ) ) {
|
||||||
error_log( 'GitHub URL is empty.' );
|
error_log( 'GitHub URL is empty.' );
|
||||||
return null;
|
return null;
|
||||||
@@ -186,6 +255,8 @@ function projects_portfolio_get_github_data( $github_url ) {
|
|||||||
* @return string The version number or 'Unknown'.
|
* @return string The version number or 'Unknown'.
|
||||||
*/
|
*/
|
||||||
function projects_portfolio_get_version_from_github( $github_url ) {
|
function projects_portfolio_get_version_from_github( $github_url ) {
|
||||||
|
_deprecated_function( __FUNCTION__, '1.1.0', 'projects_portfolio_get_version( $post_id )' );
|
||||||
|
|
||||||
if ( empty( $github_url ) ) {
|
if ( empty( $github_url ) ) {
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
<?php
|
||||||
|
// If this file is called directly, abort.
|
||||||
|
if ( ! defined( 'WPINC' ) ) {
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitea adapter for the Repository_Provider interface.
|
||||||
|
*
|
||||||
|
* Endpoint base: {base_url}/api/v1
|
||||||
|
* - repo: {base_url}/api/v1/repos/{owner}/{repo}
|
||||||
|
* - release: {base_url}/api/v1/repos/{owner}/{repo}/releases/latest (returns array)
|
||||||
|
* - user: {base_url}/api/v1/users/{login}
|
||||||
|
* - archive: {base_url}/{owner}/{repo}/archive/refs/tags/{tag}.zip
|
||||||
|
*
|
||||||
|
* @since 1.1.0
|
||||||
|
*/
|
||||||
|
class Gitea_Provider implements Repository_Provider {
|
||||||
|
|
||||||
|
private string $repo_url;
|
||||||
|
private string $base_url;
|
||||||
|
private string $api_token;
|
||||||
|
|
||||||
|
public function __construct( string $repo_url, string $base_url, string $api_token = '' ) {
|
||||||
|
$this->repo_url = $repo_url;
|
||||||
|
$this->base_url = rtrim( $base_url, '/' );
|
||||||
|
$this->api_token = $api_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_id(): string {
|
||||||
|
return 'gitea';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_label(): string {
|
||||||
|
return 'Gitea';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_repo_data(): ?array {
|
||||||
|
$api_url = $this->base_url . '/api/v1/repos/' . $this->owner_repo_path();
|
||||||
|
if ( empty( $api_url ) ) {
|
||||||
|
error_log( 'Projects Portfolio: Gitea repo URL is empty.' );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache_key = 'projects_portfolio_provider_data_' . md5( $api_url );
|
||||||
|
$cached_data = get_transient( $cache_key );
|
||||||
|
if ( $cached_data ) {
|
||||||
|
return $cached_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [ 'Accept' => 'application/json' ];
|
||||||
|
if ( ! empty( $this->api_token ) ) {
|
||||||
|
$headers['Authorization'] = 'token ' . $this->api_token;
|
||||||
|
} else {
|
||||||
|
error_log( 'Projects Portfolio: Gitea API token is missing. Using unauthenticated requests.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get( $api_url, [ 'headers' => $headers ] );
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
error_log( 'Projects Portfolio: Gitea API error: ' . $response->get_error_message() );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code( $response );
|
||||||
|
if ( 200 !== $code ) {
|
||||||
|
error_log( 'Projects Portfolio: Gitea API error: Received status ' . $code );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
|
if ( empty( $raw ) || ! is_array( $raw ) ) {
|
||||||
|
error_log( 'Projects Portfolio: Gitea API error: Invalid data received.' );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$owner = $raw['owner'] ?? [];
|
||||||
|
$owner_login = $owner['login'] ?? $this->owner_login_from_path();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'owner' => [
|
||||||
|
'avatar_url' => $owner['avatar_url'] ?? '',
|
||||||
|
'login' => $owner_login,
|
||||||
|
'html_url' => $owner['html_url'] ?? ( $this->base_url . '/' . $owner_login ),
|
||||||
|
],
|
||||||
|
'updated_at' => $raw['updated_at'] ?? '',
|
||||||
|
'language' => $raw['language'] ?? '',
|
||||||
|
'license' => $this->normalize_license( $raw['license'] ?? null ),
|
||||||
|
'stargazers_count' => (int) ( $raw['stars_count'] ?? 0 ),
|
||||||
|
'forks_count' => (int) ( $raw['forks_count'] ?? 0 ),
|
||||||
|
'open_issues_count' => (int) ( $raw['open_issues_count'] ?? 0 ),
|
||||||
|
];
|
||||||
|
|
||||||
|
set_transient( $cache_key, $data, HOUR_IN_SECONDS );
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_release_url(): ?string {
|
||||||
|
$release = $this->fetch_latest_release();
|
||||||
|
if ( null === $release ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assets = $release['assets'] ?? [];
|
||||||
|
if ( is_array( $assets ) ) {
|
||||||
|
foreach ( $assets as $asset ) {
|
||||||
|
if ( isset( $asset['name'] ) && 'zip' === pathinfo( $asset['name'], PATHINFO_EXTENSION ) ) {
|
||||||
|
$url = $asset['browser_download_url'] ?? null;
|
||||||
|
if ( $url ) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = $release['tag_name'] ?? '';
|
||||||
|
if ( '' === $tag ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->base_url . '/' . $this->owner_repo_path() . '/archive/refs/tags/' . $tag . '.zip';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_latest_version(): string {
|
||||||
|
$release = $this->fetch_latest_release();
|
||||||
|
if ( null === $release ) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
return isset( $release['tag_name'] ) ? (string) $release['tag_name'] : 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_repo_browse_url(): string {
|
||||||
|
return $this->base_url . '/' . $this->owner_repo_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_owner_data( string $owner_login ): ?array {
|
||||||
|
if ( empty( $owner_login ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_url = $this->base_url . '/api/v1/users/' . rawurlencode( $owner_login );
|
||||||
|
$response = wp_remote_get( $api_url, [ 'headers' => $this->auth_headers() ] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
|
if ( ! is_array( $data ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'avatar_url' => $data['avatar_url'] ?? '',
|
||||||
|
'login' => $data['login'] ?? $owner_login,
|
||||||
|
'html_url' => $data['html_url'] ?? ( $this->base_url . '/' . $owner_login ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first release object or null on failure / empty response.
|
||||||
|
* Gitea's /releases/latest returns an array.
|
||||||
|
*/
|
||||||
|
private function fetch_latest_release(): ?array {
|
||||||
|
$api_url = $this->base_url . '/api/v1/repos/' . $this->owner_repo_path() . '/releases/latest';
|
||||||
|
$cache_key = 'projects_portfolio_provider_release_' . md5( $api_url );
|
||||||
|
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
if ( $cached ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get( $api_url, [ 'headers' => $this->auth_headers() ] );
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
error_log( 'Projects Portfolio: Gitea API error: ' . $response->get_error_message() );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code( $response );
|
||||||
|
if ( 200 !== $code ) {
|
||||||
|
error_log( 'Projects Portfolio: Gitea releases/latest returned status ' . $code );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
|
if ( ! is_array( $body ) || empty( $body ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$release = $body[0];
|
||||||
|
set_transient( $cache_key, $release, HOUR_IN_SECONDS );
|
||||||
|
return $release;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function auth_headers(): array {
|
||||||
|
$headers = [ 'Accept' => 'application/json' ];
|
||||||
|
if ( ! empty( $this->api_token ) ) {
|
||||||
|
$headers['Authorization'] = 'token ' . $this->api_token;
|
||||||
|
}
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function owner_repo_path(): string {
|
||||||
|
// Strip the base URL prefix if present, return "owner/repo".
|
||||||
|
$path = $this->repo_url;
|
||||||
|
if ( '' !== $this->base_url && 0 === strpos( $path, $this->base_url ) ) {
|
||||||
|
$path = substr( $path, strlen( $this->base_url ) );
|
||||||
|
}
|
||||||
|
$path = ltrim( $path, '/' );
|
||||||
|
// Drop trailing slashes and any "/archive/...", "/releases/...", etc.
|
||||||
|
$path = preg_replace( '#/(archive|releases|tree|blob|issues|pulls|src|commits|wiki)(/.*)?$#', '', $path );
|
||||||
|
return trim( $path, '/' );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function owner_login_from_path(): string {
|
||||||
|
$parts = explode( '/', $this->owner_repo_path() );
|
||||||
|
return $parts[0] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalize_license( $license ): array {
|
||||||
|
if ( is_array( $license ) ) {
|
||||||
|
$name = $license['name'] ?? '';
|
||||||
|
return [ 'name' => '' !== $name ? $name : 'None' ];
|
||||||
|
}
|
||||||
|
if ( is_string( $license ) && '' !== $license ) {
|
||||||
|
return [ 'name' => $license ];
|
||||||
|
}
|
||||||
|
return [ 'name' => 'None' ];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
// If this file is called directly, abort.
|
||||||
|
if ( ! defined( 'WPINC' ) ) {
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub adapter for the Repository_Provider interface.
|
||||||
|
*
|
||||||
|
* Preserves the GitHub URL/header/asset behavior from the original
|
||||||
|
* helper-functions.php verbatim.
|
||||||
|
*
|
||||||
|
* @since 1.1.0
|
||||||
|
*/
|
||||||
|
class GitHub_Provider implements Repository_Provider {
|
||||||
|
|
||||||
|
private string $repo_url;
|
||||||
|
private string $api_token;
|
||||||
|
|
||||||
|
public function __construct( string $repo_url, string $api_token = '' ) {
|
||||||
|
$this->repo_url = rtrim( $repo_url, '/' );
|
||||||
|
$this->api_token = $api_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_id(): string {
|
||||||
|
return 'github';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_label(): string {
|
||||||
|
return 'GitHub';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_repo_data(): ?array {
|
||||||
|
if ( empty( $this->repo_url ) ) {
|
||||||
|
error_log( 'Projects Portfolio: GitHub URL is empty.' );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_url = str_replace( 'https://github.com/', 'https://api.github.com/repos/', $this->repo_url );
|
||||||
|
|
||||||
|
$cache_key = 'projects_portfolio_provider_data_' . md5( $api_url );
|
||||||
|
$cached_data = get_transient( $cache_key );
|
||||||
|
if ( $cached_data ) {
|
||||||
|
return $cached_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [ 'Accept' => 'application/vnd.github.v3+json' ];
|
||||||
|
if ( ! empty( $this->api_token ) ) {
|
||||||
|
$headers['Authorization'] = 'token ' . $this->api_token;
|
||||||
|
} else {
|
||||||
|
error_log( 'Projects Portfolio: GitHub API token is missing. Using unauthenticated requests.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get( $api_url, [ 'headers' => $headers ] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
error_log( 'Projects Portfolio: GitHub API error: ' . $response->get_error_message() );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code( $response );
|
||||||
|
if ( 403 === $code ) {
|
||||||
|
$remaining = wp_remote_retrieve_header( $response, 'x-ratelimit-remaining' );
|
||||||
|
$reset = wp_remote_retrieve_header( $response, 'x-ratelimit-reset' );
|
||||||
|
error_log( 'Projects Portfolio: GitHub API 403: Rate limit exceeded. Remaining: ' . $remaining . ' Reset at: ' . date( 'Y-m-d H:i:s', (int) $reset ) );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ( 200 !== $code ) {
|
||||||
|
error_log( 'Projects Portfolio: GitHub API error: Received status ' . $code );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
|
if ( empty( $data ) || ! is_array( $data ) ) {
|
||||||
|
error_log( 'Projects Portfolio: GitHub API error: Invalid data received.' );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_transient( $cache_key, $data, HOUR_IN_SECONDS );
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_release_url(): ?string {
|
||||||
|
$api_url = $this->release_api_url();
|
||||||
|
if ( null === $api_url ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache_key = 'projects_portfolio_provider_release_' . md5( $api_url );
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
if ( $cached ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [ 'Accept' => 'application/vnd.github.v3+json' ];
|
||||||
|
if ( ! empty( $this->api_token ) ) {
|
||||||
|
$headers['Authorization'] = 'token ' . $this->api_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get( $api_url, [ 'headers' => $headers ] );
|
||||||
|
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
|
if ( ! is_array( $data ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $data['assets'] ) && is_array( $data['assets'] ) ) {
|
||||||
|
foreach ( $data['assets'] as $asset ) {
|
||||||
|
if ( isset( $asset['name'] ) && 'zip' === pathinfo( $asset['name'], PATHINFO_EXTENSION ) ) {
|
||||||
|
$url = $asset['browser_download_url'] ?? null;
|
||||||
|
if ( $url ) {
|
||||||
|
set_transient( $cache_key, $url, HOUR_IN_SECONDS );
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zipball = $data['zipball_url'] ?? null;
|
||||||
|
if ( $zipball ) {
|
||||||
|
set_transient( $cache_key, $zipball, HOUR_IN_SECONDS );
|
||||||
|
}
|
||||||
|
return $zipball;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_latest_version(): string {
|
||||||
|
$api_url = $this->release_api_url();
|
||||||
|
if ( null === $api_url ) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache_key = 'projects_portfolio_provider_release_version_' . md5( $api_url );
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
if ( $cached ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get( $api_url, [] );
|
||||||
|
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
|
$tag = is_array( $data ) && isset( $data['tag_name'] ) ? (string) $data['tag_name'] : 'Unknown';
|
||||||
|
|
||||||
|
set_transient( $cache_key, $tag, HOUR_IN_SECONDS );
|
||||||
|
return $tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_repo_browse_url(): string {
|
||||||
|
return $this->repo_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_owner_data( string $owner_login ): ?array {
|
||||||
|
if ( empty( $owner_login ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_url = 'https://api.github.com/users/' . $owner_login;
|
||||||
|
$ctx = stream_context_create( [
|
||||||
|
'http' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'header' => "User-Agent: WORDPRESS\r\n",
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
$body = @file_get_contents( $api_url, false, $ctx );
|
||||||
|
|
||||||
|
if ( false === $body ) {
|
||||||
|
error_log( 'Projects Portfolio: Error fetching data from GitHub API.' );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode( $body, true );
|
||||||
|
if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
|
||||||
|
error_log( 'Projects Portfolio: Error decoding JSON data: ' . json_last_error_msg() );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'avatar_url' => $data['avatar_url'] ?? '',
|
||||||
|
'login' => $data['login'] ?? $owner_login,
|
||||||
|
'html_url' => $data['html_url'] ?? ( 'https://github.com/' . $owner_login ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function release_api_url(): ?string {
|
||||||
|
if ( empty( $this->repo_url ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return str_replace( 'https://github.com/', 'https://api.github.com/repos/', $this->repo_url ) . '/releases/latest';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
// If this file is called directly, abort.
|
||||||
|
if ( ! defined( 'WPINC' ) ) {
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider factory: selects the right adapter for a project post.
|
||||||
|
*
|
||||||
|
* @since 1.1.0
|
||||||
|
*
|
||||||
|
* @param int $post_id
|
||||||
|
* @return Repository_Provider
|
||||||
|
*/
|
||||||
|
function projects_portfolio_get_provider( int $post_id ): Repository_Provider {
|
||||||
|
$provider_meta = (string) get_post_meta( $post_id, '_projects_portfolio_provider', true );
|
||||||
|
$provider_id = '' !== $provider_meta ? $provider_meta : 'github';
|
||||||
|
|
||||||
|
$repo_url = (string) get_post_meta( $post_id, '_projects_portfolio_repo_url', true );
|
||||||
|
if ( '' === $repo_url ) {
|
||||||
|
// Lazy fallback to legacy key.
|
||||||
|
$repo_url = (string) get_post_meta( $post_id, '_projects_portfolio_github_url', true );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'gitea' === $provider_id ) {
|
||||||
|
$base_url = (string) get_post_meta( $post_id, '_projects_portfolio_gitea_base_url', true );
|
||||||
|
if ( '' === $base_url ) {
|
||||||
|
$base_url = (string) get_option( 'projects_portfolio_default_gitea_base_url', 'https://codeberg.org' );
|
||||||
|
}
|
||||||
|
$token = (string) get_option( 'projects_portfolio_gitea_api_token', '' );
|
||||||
|
return new Gitea_Provider( $repo_url, $base_url, $token );
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = (string) get_option( 'projects_portfolio_github_api_token', '' );
|
||||||
|
return new GitHub_Provider( $repo_url, $token );
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
// If this file is called directly, abort.
|
||||||
|
if ( ! defined( 'WPINC' ) ) {
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract for a repository host adapter (GitHub, Gitea, ...).
|
||||||
|
*
|
||||||
|
* @since 1.1.0
|
||||||
|
*/
|
||||||
|
interface Repository_Provider {
|
||||||
|
public function get_id(): string;
|
||||||
|
|
||||||
|
public function get_label(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalized repo metadata. Returns null on any failure.
|
||||||
|
*
|
||||||
|
* Shape:
|
||||||
|
* [
|
||||||
|
* 'owner' => [ 'avatar_url' => string, 'login' => string, 'html_url' => string ],
|
||||||
|
* 'updated_at' => string (ISO 8601),
|
||||||
|
* 'language' => string,
|
||||||
|
* 'license' => [ 'name' => string ],
|
||||||
|
* 'stargazers_count' => int,
|
||||||
|
* 'forks_count' => int,
|
||||||
|
* 'open_issues_count' => int,
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
public function get_repo_data(): ?array;
|
||||||
|
|
||||||
|
/** Direct URL to the latest release zip asset, or null. */
|
||||||
|
public function get_release_url(): ?string;
|
||||||
|
|
||||||
|
/** Tag name of the latest release, or 'Unknown'. */
|
||||||
|
public function get_latest_version(): string;
|
||||||
|
|
||||||
|
/** Public URL of the repo, for the 'View Repo' button. */
|
||||||
|
public function get_repo_browse_url(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owner profile data: [ 'avatar_url', 'login', 'html_url' ].
|
||||||
|
* Returns null on failure.
|
||||||
|
*/
|
||||||
|
public function get_owner_data( string $owner_login ): ?array;
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<?php
|
<?php
|
||||||
// generated by Poedit from projects-portfolio-es_MX.po, do not edit directly
|
// generated by Poedit from projects-portfolio-es_MX.po, do not edit directly
|
||||||
return ['domain'=>NULL,'plural-forms'=>'nplurals=2; plural=(n != 1);','language'=>'es_MX','pot-creation-date'=>'2025-04-04 10:12-0400','po-revision-date'=>'2025-04-04 10:13-0400','translation-revision-date'=>'2025-04-04 10:13-0400','project-id-version'=>'Projects for WordPress®','x-generator'=>'Poedit 3.6','messages'=>['Settings'=>'Ajustes','Version'=>'Versión','Last Updated'=>'Última Actualización','License'=>'Licencia','Language'=>'Idioma','Downloads'=>'Descargas','Forks'=>'Horquillas','Stars'=>'Estrellas','Issues'=>'Problemas','GitHub Owner'=>'Propietario de GitHub','Archive Title'=>'Título del archivo','Project Title'=>'Título del Proyecto','Project Excerpt'=>'Estracto de proyecto','Project Buttons'=>'Botones de proyecto','Projects for WordPress Settings'=>'Proyectos para la configuración de WordPress','General Settings'=>'Ajustes Generales','GitHub API Token'=>'Token de la API de GitHub','Templates Settings'=>'Configuración de plantillas','Archives Settings'=>'Ajustes de los archivos','Save Settings'=>'Guardar Ajustes','Settings saved successfully.'=>'Ajustes guardados exitosamente.','Projects'=>'Proyectos','Project'=>'Proyecto','Add New Project'=>'Agregar Proyecto','Project Types'=>'Proyectos','Project Type'=>'Tipo de Proyecto','Plugin'=>'Plugin','Theme'=>'Tema','Pattern'=>'Patron','GitHub URL'=>'GitHub URL','GitHub Repository URL:'=>'URL del repositorio GitHub:','View details'=>'Ver Detalles','More information about %s'=>'Más información sobre %s','Check for updates'=>'Checar por Actualizaciones','the plugin title' . "\4" . 'The %s plugin is up to date.'=>'El plugin %s está actualizado.','the plugin title' . "\4" . 'A new version of the %s plugin is available.'=>'Una nueva versión del plugin %s está disponible.','the plugin title' . "\4" . 'Could not determine if updates are available for %s.'=>'No se pudo determinar si las actualizaciones están disponibles para %s.','Unknown update checker status "%s"'=>'Status de actualización desconocido "%s"','There is no changelog available.'=>'No está disponible la lista de cambios.','Invalid download URL. Please check the GitHub repository.'=>'URL de descarga no válida. Compruebe el repositorio de GitHub.','Download Error'=>'Error','Download Count'=>'Descargar el recuento','0'=>'0','Download'=>'Descargar','View Project'=>'Ver Proyecto','« Previous'=>'« Anterior','Next »'=>'Siguiente »','No projects found.'=>'No se han encontrado proyectos.','Project Banner'=>'Proyecto Banner','Download Now'=>'Descargar Ahora','View on GitHub'=>'Ver en GitHub','GitHub Repository:'=>'Repositorio GitHub:','No repository linked.'=>'Ningún repositorio vinculado.','Version:'=>'Versión:','Updated:'=>'Actualizado:','License:'=>'Licencia:','Language:'=>'Idioma:','Downloads:'=>'Descargas:','Stars:'=>'Estrellas:','Forks:'=>'Horquillas:','Issues:'=>'Problemas:','Follow'=>'Seguir','Projects for WordPress®'=>'Proyectos para WordPress','https://github.com/robertdevore/projects-portfolio/'=>'https://github.com/robertdevore/projects-portfolio/','Create a showcase directory for projects (plugins, themes, patterns) with custom post types, taxonomies, and download functionality.'=>'Crear un directorio de escaparate para proyectos (plugins, temas, patrones) con tipos de post personalizados, taxonomías y funcionalidad de descarga.','Robert DeVore'=>'Robert DeVore','https://www.robertdevore.com'=>'https://www.robertdevore.com']];
|
return ['domain'=>NULL,'plural-forms'=>'nplurals=2; plural=(n != 1);','language'=>'es_MX','pot-creation-date'=>'2025-04-04 10:12-0400','po-revision-date'=>'2025-04-04 10:13-0400','translation-revision-date'=>'2025-04-04 10:13-0400','project-id-version'=>'Projects for WordPress®','x-generator'=>'Poedit 3.6','messages'=>['Settings'=>'Ajustes','Version'=>'Versión','Last Updated'=>'Última Actualización','License'=>'Licencia','Language'=>'Idioma','Downloads'=>'Descargas','Forks'=>'Horquillas','Stars'=>'Estrellas','Issues'=>'Problemas','GitHub Owner'=>'Propietario de GitHub','Archive Title'=>'Título del archivo','Project Title'=>'Título del Proyecto','Project Excerpt'=>'Estracto de proyecto','Project Buttons'=>'Botones de proyecto','Projects for WordPress Settings'=>'Proyectos para la configuración de WordPress','General Settings'=>'Ajustes Generales','GitHub API Token'=>'Token de la API de GitHub','Templates Settings'=>'Configuración de plantillas','Archives Settings'=>'Ajustes de los archivos','Save Settings'=>'Guardar Ajustes','Settings saved successfully.'=>'Ajustes guardados exitosamente.','Projects'=>'Proyectos','Project'=>'Proyecto','Add New Project'=>'Agregar Proyecto','Project Types'=>'Proyectos','Project Type'=>'Tipo de Proyecto','Plugin'=>'Plugin','Theme'=>'Tema','Pattern'=>'Patron','GitHub URL'=>'GitHub URL','GitHub Repository URL:'=>'URL del repositorio GitHub:','View details'=>'Ver Detalles','More information about %s'=>'Más información sobre %s','Check for updates'=>'Checar por Actualizaciones','the plugin title' . "\4" . 'The %s plugin is up to date.'=>'El plugin %s está actualizado.','the plugin title' . "\4" . 'A new version of the %s plugin is available.'=>'Una nueva versión del plugin %s está disponible.','the plugin title' . "\4" . 'Could not determine if updates are available for %s.'=>'No se pudo determinar si las actualizaciones están disponibles para %s.','Unknown update checker status "%s"'=>'Status de actualización desconocido "%s"','There is no changelog available.'=>'No está disponible la lista de cambios.','Invalid download URL. Please check the GitHub repository.'=>'URL de descarga no válida. Compruebe el repositorio de GitHub.','Invalid download URL. Please check the repository URL.'=>'URL de descarga no válida. Compruebe el repositorio.','Repository'=>'','Provider'=>'','Gitea Base URL (optional override)'=>'','Leave blank to use the default from Settings.'=>'','View Repo'=>'','Gitea Settings'=>'','Default Gitea Base URL'=>'','Used when a Gitea project has no per-project base URL override.'=>'','Gitea API Token'=>'','Download Error'=>'Error','Download Count'=>'Descargar el recuento','0'=>'0','Download'=>'Descargar','View Project'=>'Ver Proyecto','« Previous'=>'« Anterior','Next »'=>'Siguiente »','No projects found.'=>'No se han encontrado proyectos.','Project Banner'=>'Proyecto Banner','Download Now'=>'Descargar Ahora','View on GitHub'=>'Ver en GitHub','GitHub Repository:'=>'Repositorio GitHub:','No repository linked.'=>'Ningún repositorio vinculado.','Version:'=>'Versión:','Updated:'=>'Actualizado:','License:'=>'Licencia:','Language:'=>'Idioma:','Downloads:'=>'Descargas:','Stars:'=>'Estrellas:','Forks:'=>'Horquillas:','Issues:'=>'Problemas:','Follow'=>'Seguir','Projects for WordPress®'=>'Proyectos para WordPress','https://github.com/robertdevore/projects-portfolio/'=>'https://github.com/robertdevore/projects-portfolio/','Create a showcase directory for projects (plugins, themes, patterns) with custom post types, taxonomies, and download functionality.'=>'Crear un directorio de escaparate para proyectos (plugins, temas, patrones) con tipos de post personalizados, taxonomías y funcionalidad de descarga.','Robert DeVore'=>'Robert DeVore','https://www.robertdevore.com'=>'https://www.robertdevore.com']];
|
||||||
|
|||||||
@@ -88,6 +88,42 @@ msgstr "Ajustes Generales"
|
|||||||
msgid "GitHub API Token"
|
msgid "GitHub API Token"
|
||||||
msgstr "Token de la API de GitHub"
|
msgstr "Token de la API de GitHub"
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:88
|
||||||
|
msgid "Gitea Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:92
|
||||||
|
msgid "Default Gitea Base URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:96
|
||||||
|
msgid "Used when a Gitea project has no per-project base URL override."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:101
|
||||||
|
msgid "Gitea API Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:17
|
||||||
|
msgid "Repository"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:47
|
||||||
|
msgid "Provider"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:58
|
||||||
|
msgid "Gitea Base URL (optional override)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:60
|
||||||
|
msgid "Leave blank to use the default from Settings."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/single-projects.php:73
|
||||||
|
msgid "View Repo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: admin/admin-settings.php:86
|
#: admin/admin-settings.php:86
|
||||||
msgid "Templates Settings"
|
msgid "Templates Settings"
|
||||||
msgstr "Configuración de plantillas"
|
msgstr "Configuración de plantillas"
|
||||||
@@ -189,6 +225,10 @@ msgstr "No está disponible la lista de cambios."
|
|||||||
msgid "Invalid download URL. Please check the GitHub repository."
|
msgid "Invalid download URL. Please check the GitHub repository."
|
||||||
msgstr "URL de descarga no válida. Compruebe el repositorio de GitHub."
|
msgstr "URL de descarga no válida. Compruebe el repositorio de GitHub."
|
||||||
|
|
||||||
|
#: projects-portfolio.php:119
|
||||||
|
msgid "Invalid download URL. Please check the repository URL."
|
||||||
|
msgstr "URL de descarga no válida. Compruebe el repositorio."
|
||||||
|
|
||||||
#: projects-portfolio.php:118
|
#: projects-portfolio.php:118
|
||||||
msgid "Download Error"
|
msgid "Download Error"
|
||||||
msgstr "Error"
|
msgstr "Error"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<?php
|
<?php
|
||||||
// generated by Poedit from projects-wp-fr_FR.po, do not edit directly
|
// generated by Poedit from projects-wp-fr_FR.po, do not edit directly
|
||||||
return ['domain'=>NULL,'plural-forms'=>'nplurals=2; plural=(n != 1);','language'=>'fr_FR','pot-creation-date'=>'2025-04-04 10:13-0400','po-revision-date'=>'2025-04-04 10:14-0400','translation-revision-date'=>'2025-04-04 10:14-0400','project-id-version'=>'Projects for WordPress®','x-generator'=>'Poedit 3.6','messages'=>['Settings'=>'Paramètres','Version'=>'Version','Last Updated'=>'Dernière mise à jour','License'=>'Licence','Language'=>'Langue','Downloads'=>'Téléchargements','Forks'=>'Fourchettes','Stars'=>'Étoiles','Issues'=>'Problèmes','GitHub Owner'=>'Propriétaire de GitHub','Archive Title'=>'Archive Titre','Project Title'=>'Titre du projet','Project Excerpt'=>'Extrait du projet','Project Buttons'=>'Boutons de projet','Projects for WordPress Settings'=>'Projets pour les réglages de WordPress','General Settings'=>'Réglages généraux','GitHub API Token'=>'Token API GitHub','Templates Settings'=>'¨Paramètres des modèles','Archives Settings'=>'Paramètres des archives','Save Settings'=>'Enregistrer les paramètres','Settings saved successfully.'=>'Réglages bien enregistrés.','Projects'=>'Projets','Project'=>'Projet','Add New Project'=>'Ajouter un nouveau projet','Project Types'=>'Types de projets','Project Type'=>'Type de projet','Plugin'=>'Extension','Theme'=>'Thème','Pattern'=>'Motif','GitHub URL'=>'URL GitHub','GitHub Repository URL:'=>'URL du dépôt GitHub :','View details'=>'Voir les détails','More information about %s'=>'Plus d’informations sur %s','Check for updates'=>'Vérifier les mises à jour','the plugin title' . "\4" . 'The %s plugin is up to date.'=>'Le plugin %s est à jour.','the plugin title' . "\4" . 'A new version of the %s plugin is available.'=>'Une nouvelle version de l\'extension %s est disponible.','the plugin title' . "\4" . 'Could not determine if updates are available for %s.'=>'Impossible de déterminer si des mises à jour sont disponibles pour %s.','Unknown update checker status "%s"'=>'Mise à jour inconnue statut "%s"','There is no changelog available.'=>'Aucun registre de modifications disponible.','Invalid download URL. Please check the GitHub repository.'=>'URL de téléchargement invalide. Veuillez vérifier le dépôt GitHub.','Download Error'=>'Télécharger Erreur','Download Count'=>'Nombre de Téléchargements','0'=>'0','Download'=>'Télécharger','View Project'=>'Voir le projet','« Previous'=>'« Précédent','Next »'=>'Suivant »','No projects found.'=>'Aucun projet trouvé.','Project Banner'=>'Bannière du projet','Download Now'=>'Télécharger maintenant','View on GitHub'=>'Voir sur GitHub','GitHub Repository:'=>'Dépôt GitHub:','No repository linked.'=>'Aucun dépôt n\'est lié.','Version:'=>'Version:','Updated:'=>'Mis à jour :','License:'=>'Licence:','Language:'=>'Langue:','Downloads:'=>'Téléchargements:','Stars:'=>'Étoiles:','Forks:'=>'Forks:','Issues:'=>'Problèmes:','Follow'=>'Suivre','Projects for WordPress®'=>'Projets pour WordPress®','https://github.com/robertdevore/projects-portfolio/'=>'https://github.com/robertdevore/projects-portfolio/','Create a showcase directory for projects (plugins, themes, patterns) with custom post types, taxonomies, and download functionality.'=>'Créer un répertoire de présentation pour les projets (plugins, thèmes, modèles) avec des types d\'articles personnalisés, des taxonomies et des fonctionnalités de téléchargement.','Robert DeVore'=>'Robert DeVore','https://www.robertdevore.com'=>'https://www.robertdevore.com']];
|
return ['domain'=>NULL,'plural-forms'=>'nplurals=2; plural=(n != 1);','language'=>'fr_FR','pot-creation-date'=>'2025-04-04 10:13-0400','po-revision-date'=>'2025-04-04 10:14-0400','translation-revision-date'=>'2025-04-04 10:14-0400','project-id-version'=>'Projects for WordPress®','x-generator'=>'Poedit 3.6','messages'=>['Settings'=>'Paramètres','Version'=>'Version','Last Updated'=>'Dernière mise à jour','License'=>'Licence','Language'=>'Langue','Downloads'=>'Téléchargements','Forks'=>'Fourchettes','Stars'=>'Étoiles','Issues'=>'Problèmes','GitHub Owner'=>'Propriétaire de GitHub','Archive Title'=>'Archive Titre','Project Title'=>'Titre du projet','Project Excerpt'=>'Extrait du projet','Project Buttons'=>'Boutons de projet','Projects for WordPress Settings'=>'Projets pour les réglages de WordPress','General Settings'=>'Réglages généraux','GitHub API Token'=>'Token API GitHub','Templates Settings'=>'¨Paramètres des modèles','Archives Settings'=>'Paramètres des archives','Save Settings'=>'Enregistrer les paramètres','Settings saved successfully.'=>'Réglages bien enregistrés.','Projects'=>'Projets','Project'=>'Projet','Add New Project'=>'Ajouter un nouveau projet','Project Types'=>'Types de projets','Project Type'=>'Type de projet','Plugin'=>'Extension','Theme'=>'Thème','Pattern'=>'Motif','GitHub URL'=>'URL GitHub','GitHub Repository URL:'=>'URL du dépôt GitHub :','View details'=>'Voir les détails','More information about %s'=>'Plus d’informations sur %s','Check for updates'=>'Vérifier les mises à jour','the plugin title' . "\4" . 'The %s plugin is up to date.'=>'Le plugin %s est à jour.','the plugin title' . "\4" . 'A new version of the %s plugin is available.'=>'Une nouvelle version de l\'extension %s est disponible.','the plugin title' . "\4" . 'Could not determine if updates are available for %s.'=>'Impossible de déterminer si des mises à jour sont disponibles pour %s.','Unknown update checker status "%s"'=>'Mise à jour inconnue statut "%s"','There is no changelog available.'=>'Aucun registre de modifications disponible.','Invalid download URL. Please check the GitHub repository.'=>'URL de téléchargement invalide. Veuillez vérifier le dépôt GitHub.','Invalid download URL. Please check the repository URL.'=>'URL de téléchargement invalide. Veuillez vérifier le dépôt.','Repository'=>'','Provider'=>'','Gitea Base URL (optional override)'=>'','Leave blank to use the default from Settings.'=>'','View Repo'=>'','Gitea Settings'=>'','Default Gitea Base URL'=>'','Used when a Gitea project has no per-project base URL override.'=>'','Gitea API Token'=>'','Download Error'=>'Télécharger Erreur','Download Count'=>'Nombre de Téléchargements','0'=>'0','Download'=>'Télécharger','View Project'=>'Voir le projet','« Previous'=>'« Précédent','Next »'=>'Suivant »','No projects found.'=>'Aucun projet trouvé.','Project Banner'=>'Bannière du projet','Download Now'=>'Télécharger maintenant','View on GitHub'=>'Voir sur GitHub','GitHub Repository:'=>'Dépôt GitHub:','No repository linked.'=>'Aucun dépôt n\'est lié.','Version:'=>'Version:','Updated:'=>'Mis à jour :','License:'=>'Licence:','Language:'=>'Langue:','Downloads:'=>'Téléchargements:','Stars:'=>'Étoiles:','Forks:'=>'Forks:','Issues:'=>'Problèmes:','Follow'=>'Suivre','Projects for WordPress®'=>'Projets pour WordPress®','https://github.com/robertdevore/projects-portfolio/'=>'https://github.com/robertdevore/projects-portfolio/','Create a showcase directory for projects (plugins, themes, patterns) with custom post types, taxonomies, and download functionality.'=>'Créer un répertoire de présentation pour les projets (plugins, thèmes, modèles) avec des types d\'articles personnalisés, des taxonomies et des fonctionnalités de téléchargement.','Robert DeVore'=>'Robert DeVore','https://www.robertdevore.com'=>'https://www.robertdevore.com']];
|
||||||
|
|||||||
@@ -88,6 +88,42 @@ msgstr "Réglages généraux"
|
|||||||
msgid "GitHub API Token"
|
msgid "GitHub API Token"
|
||||||
msgstr "Token API GitHub"
|
msgstr "Token API GitHub"
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:88
|
||||||
|
msgid "Gitea Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:92
|
||||||
|
msgid "Default Gitea Base URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:96
|
||||||
|
msgid "Used when a Gitea project has no per-project base URL override."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:101
|
||||||
|
msgid "Gitea API Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:17
|
||||||
|
msgid "Repository"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:47
|
||||||
|
msgid "Provider"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:58
|
||||||
|
msgid "Gitea Base URL (optional override)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:60
|
||||||
|
msgid "Leave blank to use the default from Settings."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/single-projects.php:73
|
||||||
|
msgid "View Repo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: admin/admin-settings.php:86
|
#: admin/admin-settings.php:86
|
||||||
msgid "Templates Settings"
|
msgid "Templates Settings"
|
||||||
msgstr "¨Paramètres des modèles"
|
msgstr "¨Paramètres des modèles"
|
||||||
@@ -189,6 +225,10 @@ msgstr "Aucun registre de modifications disponible."
|
|||||||
msgid "Invalid download URL. Please check the GitHub repository."
|
msgid "Invalid download URL. Please check the GitHub repository."
|
||||||
msgstr "URL de téléchargement invalide. Veuillez vérifier le dépôt GitHub."
|
msgstr "URL de téléchargement invalide. Veuillez vérifier le dépôt GitHub."
|
||||||
|
|
||||||
|
#: projects-portfolio.php:119
|
||||||
|
msgid "Invalid download URL. Please check the repository URL."
|
||||||
|
msgstr "URL de téléchargement invalide. Veuillez vérifier le dépôt."
|
||||||
|
|
||||||
#: projects-portfolio.php:118
|
#: projects-portfolio.php:118
|
||||||
msgid "Download Error"
|
msgid "Download Error"
|
||||||
msgstr "Télécharger Erreur"
|
msgstr "Télécharger Erreur"
|
||||||
|
|||||||
@@ -189,10 +189,50 @@ msgstr ""
|
|||||||
msgid "Invalid download URL. Please check the GitHub repository."
|
msgid "Invalid download URL. Please check the GitHub repository."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: projects-portfolio.php:119
|
||||||
|
msgid "Invalid download URL. Please check the repository URL."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: projects-portfolio.php:118
|
#: projects-portfolio.php:118
|
||||||
msgid "Download Error"
|
msgid "Download Error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:17
|
||||||
|
msgid "Repository"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:47
|
||||||
|
msgid "Provider"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:58
|
||||||
|
msgid "Gitea Base URL (optional override)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/metabox.php:60
|
||||||
|
msgid "Leave blank to use the default from Settings."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/single-projects.php:73
|
||||||
|
msgid "View Repo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:88
|
||||||
|
msgid "Gitea Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:92
|
||||||
|
msgid "Default Gitea Base URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:96
|
||||||
|
msgid "Used when a Gitea project has no per-project base URL override."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: admin/admin-settings.php:101
|
||||||
|
msgid "Gitea API Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: projects-portfolio.php:133
|
#: projects-portfolio.php:133
|
||||||
msgid "Download Count"
|
msgid "Download Count"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
colors="true"
|
||||||
|
convertErrorsToExceptions="true"
|
||||||
|
convertNoticesToExceptions="true"
|
||||||
|
convertWarningsToExceptions="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="projects-portfolio">
|
||||||
|
<directory suffix=".php">tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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.0.0
|
* Version: 1.1.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+
|
||||||
@@ -39,13 +39,17 @@ $myUpdateChecker = PucFactory::buildUpdateChecker(
|
|||||||
$myUpdateChecker->setBranch( 'main' );
|
$myUpdateChecker->setBranch( 'main' );
|
||||||
|
|
||||||
// Current plugin version.
|
// Current plugin version.
|
||||||
define( 'PROJECTS_PORTFOLIO_VERSION', time() );
|
define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.0' );
|
||||||
|
|
||||||
// Add the required files.
|
// Add the required files.
|
||||||
require 'admin/admin-settings.php';
|
require 'admin/admin-settings.php';
|
||||||
require 'admin/cpt-taxonomy.php';
|
require 'admin/cpt-taxonomy.php';
|
||||||
require 'admin/metabox.php';
|
require 'admin/metabox.php';
|
||||||
require 'includes/helper-functions.php';
|
require 'includes/helper-functions.php';
|
||||||
|
require 'includes/providers/interface-repository-provider.php';
|
||||||
|
require 'includes/providers/class-github-provider.php';
|
||||||
|
require 'includes/providers/class-gitea-provider.php';
|
||||||
|
require 'includes/providers/class-provider-factory.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flush rewrite rules on activation.
|
* Flush rewrite rules on activation.
|
||||||
@@ -99,8 +103,7 @@ function projects_portfolio_handle_download_redirect() {
|
|||||||
$project_id = get_query_var( 'project_download_id' );
|
$project_id = get_query_var( 'project_download_id' );
|
||||||
|
|
||||||
if ( $project_id ) {
|
if ( $project_id ) {
|
||||||
$github_url = get_post_meta( $project_id, '_projects_portfolio_github_url', true );
|
$download_url = projects_portfolio_get_release_url( (int) $project_id );
|
||||||
$download_url = projects_portfolio_get_github_release_url( $github_url );
|
|
||||||
|
|
||||||
if ( $download_url ) {
|
if ( $download_url ) {
|
||||||
// Increment download count.
|
// Increment download count.
|
||||||
@@ -108,12 +111,12 @@ function projects_portfolio_handle_download_redirect() {
|
|||||||
++$download_count;
|
++$download_count;
|
||||||
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 GitHub ZIP file.
|
// Redirect to the release ZIP on the project's host.
|
||||||
wp_safe_redirect( esc_url_raw( $download_url ) );
|
wp_safe_redirect( esc_url_raw( $download_url ) );
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
wp_die(
|
wp_die(
|
||||||
esc_html__( 'Invalid download URL. Please check the GitHub repository.', 'projects-wp' ),
|
esc_html__( 'Invalid download URL. Please check the repository URL.', 'projects-wp' ),
|
||||||
esc_html__( 'Download Error', 'projects-wp' ),
|
esc_html__( 'Download Error', 'projects-wp' ),
|
||||||
array( 'response' => 404 )
|
array( 'response' => 404 )
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
# Gitea Support for Projects Portfolio
|
||||||
|
|
||||||
|
- **Date:** 2026-08-09
|
||||||
|
- **Plugin:** Projects Portfolio (`projects-wp`)
|
||||||
|
- **Status:** Approved design, awaiting implementation plan
|
||||||
|
|
||||||
|
## 1. Goals
|
||||||
|
|
||||||
|
- Per-project support for either GitHub OR Gitea (self-hosted, configurable base URL).
|
||||||
|
- Feature parity between providers: repo metadata, latest version, release-ZIP download redirect, repo link button.
|
||||||
|
- Existing GitHub behavior preserved bit-for-bit — no regressions on current installs.
|
||||||
|
- Provider interface designed so adding a third provider later (e.g. GitLab) is additive and does not require template branching.
|
||||||
|
- WP PHPUnit tests cover the new adapters, the provider factory, and the metabox/settings surface.
|
||||||
|
|
||||||
|
## 2. Non-goals
|
||||||
|
|
||||||
|
- GitLab, Bitbucket, or any non-GitHub/non-Gitea provider (interface permits it; implementation deferred).
|
||||||
|
- Auto-migration on activation (existing posts keep their GitHub association; admin manually switches a project to Gitea when desired).
|
||||||
|
- Public-Codeberg-only shortcut — Gitea support always treats the base URL as configurable per project and globally.
|
||||||
|
- New CPT or taxonomy fields beyond what's listed in this spec.
|
||||||
|
- UI changes outside the project metabox and the plugin settings page.
|
||||||
|
|
||||||
|
## 3. Architecture
|
||||||
|
|
||||||
|
The plugin introduces a small `Repository_Provider` interface with two adapters and a factory. All call sites consume the interface; only the factory knows which adapter to use.
|
||||||
|
|
||||||
|
```
|
||||||
|
Templates + REST API + Download Redirect
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
projects_portfolio_get_provider( $post_id ) ← factory
|
||||||
|
│
|
||||||
|
┌────────┴────────┐
|
||||||
|
▼ ▼
|
||||||
|
GitHub_Provider Gitea_Provider
|
||||||
|
│ │
|
||||||
|
└────────┬────────┘
|
||||||
|
▼
|
||||||
|
WP HTTP API + transient cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 Files added
|
||||||
|
|
||||||
|
- `includes/providers/interface-repository-provider.php` — `Repository_Provider` interface.
|
||||||
|
- `includes/providers/class-github-provider.php` — current GitHub logic, repackaged.
|
||||||
|
- `includes/providers/class-gitea-provider.php` — new adapter.
|
||||||
|
- `includes/providers/class-provider-factory.php` — `projects_portfolio_get_provider()`.
|
||||||
|
- `tests/bootstrap.php`, `tests/test-*.php` — WP PHPUnit suite.
|
||||||
|
- `composer.json` (dev-only) — pulls in `phpunit/phpunit` and `brain/monkey`.
|
||||||
|
|
||||||
|
### 3.2 Files modified
|
||||||
|
|
||||||
|
- `includes/helper-functions.php` — adds provider-aware wrappers; legacy GitHub-named wrappers retained verbatim with deprecation PHPDoc.
|
||||||
|
- `admin/metabox.php` — replaces single GitHub URL field with provider dropdown + repo URL + optional Gitea base URL.
|
||||||
|
- `admin/admin-settings.php` — adds "Gitea Settings" section (default base URL + token).
|
||||||
|
- `templates/single-projects.php` — routes data fetches through the provider; no per-provider branching.
|
||||||
|
- `admin/rest-api.php` — same swap for the REST response builder.
|
||||||
|
- `projects-portfolio.php` — `handle_download_redirect()` routes through the provider.
|
||||||
|
- `README.md` — documents the new metabox fields, Gitea settings, and a "Connecting to a Gitea repo" subsection.
|
||||||
|
- `languages/projects-wp.pot` (and `.po`/`.l10n.php` siblings) — re-generated for new strings.
|
||||||
|
|
||||||
|
## 4. Provider interface
|
||||||
|
|
||||||
|
```php
|
||||||
|
interface Repository_Provider {
|
||||||
|
public function get_id(): string; // 'github' | 'gitea'
|
||||||
|
public function get_label(): string; // 'GitHub' | 'Gitea'
|
||||||
|
public function get_repo_data(): ?array; // normalized shape (see §6)
|
||||||
|
public function get_release_url(): ?string; // direct asset zip URL or null
|
||||||
|
public function get_latest_version(): string; // tag_name or 'Unknown'
|
||||||
|
public function get_repo_browse_url(): string; // for the 'View Repo' button
|
||||||
|
public function get_owner_data( string $owner_login ): ?array; // avatar/login/html_url
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Per-project storage
|
||||||
|
|
||||||
|
Post meta keys:
|
||||||
|
|
||||||
|
| Key | Type | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `_projects_portfolio_provider` | `'github'` \| `'gitea'` | Provider selection. Defaults to `github` if missing. |
|
||||||
|
| `_projects_portfolio_repo_url` | string (URL) | Canonical repo URL. On read, falls back to `_projects_portfolio_github_url` for backward compatibility. |
|
||||||
|
| `_projects_portfolio_gitea_base_url` | string (URL) | Optional per-project Gitea base URL override. Empty → uses global default. |
|
||||||
|
| `_projects_portfolio_github_url` | string | **Deprecated, read-only.** Retained so legacy posts don't lose data. |
|
||||||
|
| `_projects_portfolio_download_count` | int | Unchanged. |
|
||||||
|
|
||||||
|
**Lazy migration:** in `projects_portfolio_save_meta_box()`, when a project is saved and `_projects_portfolio_provider` is unset, `_projects_portfolio_repo_url` is empty, but the legacy `_projects_portfolio_github_url` is non-empty, copy the legacy value into `_projects_portfolio_repo_url` and set provider to `github`. No batch update is required.
|
||||||
|
|
||||||
|
## 6. Normalized repo data shape
|
||||||
|
|
||||||
|
Templates and the REST API read this shape regardless of provider. The GitHub adapter returns it natively; the Gitea adapter maps from Gitea's native response.
|
||||||
|
|
||||||
|
```php
|
||||||
|
[
|
||||||
|
'owner' => [
|
||||||
|
'avatar_url' => string,
|
||||||
|
'login' => string,
|
||||||
|
'html_url' => string,
|
||||||
|
],
|
||||||
|
'updated_at' => string, // ISO 8601
|
||||||
|
'language' => string,
|
||||||
|
'license' => [ 'name' => string ],
|
||||||
|
'stargazers_count' => int,
|
||||||
|
'forks_count' => int,
|
||||||
|
'open_issues_count' => int,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.1 Gitea → normalized mapping
|
||||||
|
|
||||||
|
| Gitea field | Normalized field |
|
||||||
|
|---|---|
|
||||||
|
| `stars_count` | `stargazers_count` |
|
||||||
|
| `forks_count` | `forks_count` |
|
||||||
|
| `open_issues_count` | `open_issues_count` |
|
||||||
|
| `updated_at` | `updated_at` |
|
||||||
|
| `language` | `language` |
|
||||||
|
| `license` (string\|null\|object) | `['name' => string\|'None']` |
|
||||||
|
| `owner.avatar_url` | `owner.avatar_url` |
|
||||||
|
| `owner.login` | `owner.login` |
|
||||||
|
| `owner.html_url` (or fallback `{base}/{owner}`) | `owner.html_url` |
|
||||||
|
|
||||||
|
## 7. Endpoints
|
||||||
|
|
||||||
|
| Purpose | GitHub | Gitea |
|
||||||
|
|---|---|---|
|
||||||
|
| Repo metadata | `https://api.github.com/repos/{owner}/{repo}` | `{base}/api/v1/repos/{owner}/{repo}` |
|
||||||
|
| Latest release | `…/{owner}/{repo}/releases/latest` | `{base}/api/v1/repos/{owner}/{repo}/releases/latest` |
|
||||||
|
| Owner data | `https://api.github.com/users/{login}` | `{base}/api/v1/users/{login}` |
|
||||||
|
| Repo browse | the project URL itself | `{base}/{owner}/{repo}` |
|
||||||
|
|
||||||
|
Gitea uses header `Authorization: token <PAT>` against the `projects_portfolio_gitea_api_token` option. GitHub uses the same header scheme against the existing `projects_portfolio_github_api_token` option.
|
||||||
|
|
||||||
|
## 8. Release & download logic
|
||||||
|
|
||||||
|
**GitHub adapter** (preserves current behavior):
|
||||||
|
1. Fetch release at `…/releases/latest`.
|
||||||
|
2. Iterate `assets[]` and return the first entry whose filename ends in `.zip` (field `browser_download_url`).
|
||||||
|
3. Fallback: return `zipball_url`.
|
||||||
|
4. `null` if neither is present.
|
||||||
|
|
||||||
|
**Gitea adapter**:
|
||||||
|
1. Fetch the latest release endpoint. Gitea returns an **array** of releases (not a single object), so the adapter takes `releases[0]`.
|
||||||
|
2. Iterate that release's `assets[]` for the first `.zip` entry.
|
||||||
|
3. Fallback: build `{base}/{owner}/{repo}/archive/refs/tags/{tag_name}.zip` (Gitea serves this even when no assets are attached).
|
||||||
|
4. `null` if no tag is present.
|
||||||
|
5. `get_latest_version()` reads `tag_name` from the same response.
|
||||||
|
|
||||||
|
## 9. Global settings
|
||||||
|
|
||||||
|
Added to the settings page (between General and Templates sections):
|
||||||
|
|
||||||
|
| Option | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `projects_portfolio_default_gitea_base_url` | `https://codeberg.org` | Used when a Gitea project has no per-project base URL override. |
|
||||||
|
| `projects_portfolio_gitea_api_token` | `''` | Bearer token for authenticated Gitea requests. |
|
||||||
|
|
||||||
|
Existing `projects_portfolio_github_api_token` is unchanged.
|
||||||
|
|
||||||
|
`projects_portfolio_settings()` returns the new keys so templates can read them.
|
||||||
|
|
||||||
|
## 10. Metabox
|
||||||
|
|
||||||
|
The "GitHub URL" metabox becomes a "Repository" metabox with:
|
||||||
|
|
||||||
|
1. **Provider** — `<select>` with options `github` (GitHub) and `gitea` (Gitea). Defaults to `github`.
|
||||||
|
2. **Repository URL** — `<input type="url">`. Helper text: "Example: `https://github.com/owner/repo` or `https://codeberg.org/owner/repo`."
|
||||||
|
3. **Gitea Base URL (optional override)** — `<input type="url">`. Visible only when Provider = Gitea. Helper text: "Leave blank to use the default from Settings."
|
||||||
|
|
||||||
|
Save behavior writes `_projects_portfolio_provider`, `_projects_portfolio_repo_url`, and `_projects_portfolio_gitea_base_url`. The legacy `_projects_portfolio_github_url` is written **only** during the one-time lazy migration (§5); it is not written on every save.
|
||||||
|
|
||||||
|
## 11. Helper-function surface
|
||||||
|
|
||||||
|
New provider-aware wrappers (in `includes/helper-functions.php`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
projects_portfolio_get_provider( $post_id ): Repository_Provider
|
||||||
|
projects_portfolio_get_repo_data( $post_id ): ?array
|
||||||
|
projects_portfolio_get_release_url( $post_id ): ?string
|
||||||
|
projects_portfolio_get_version( $post_id ): string
|
||||||
|
projects_portfolio_get_owner( $post_id ): ?array
|
||||||
|
projects_portfolio_get_repo_browse_url( $post_id ): string
|
||||||
|
```
|
||||||
|
|
||||||
|
Legacy GitHub-named wrappers are retained **verbatim** with a `_deprecated_function()` PHPDoc note pointing to the new wrappers:
|
||||||
|
|
||||||
|
```php
|
||||||
|
projects_portfolio_get_github_data( $github_url ): ?array
|
||||||
|
projects_portfolio_get_github_release_url( $github_url ): ?string
|
||||||
|
projects_portfolio_get_version_from_github( $github_url ): string
|
||||||
|
projects_portfolio_github_owner( $owner_name ): ?array
|
||||||
|
```
|
||||||
|
|
||||||
|
These wrappers still take a GitHub URL and never route through the provider interface, preserving any external code that called them directly.
|
||||||
|
|
||||||
|
## 12. Call-site swaps
|
||||||
|
|
||||||
|
1. **`projects-portfolio.php` → `projects_portfolio_handle_download_redirect()`**
|
||||||
|
- Read `_projects_portfolio_repo_url` (with legacy fallback) and `_projects_portfolio_provider`.
|
||||||
|
- Call `projects_portfolio_get_release_url( $post_id )`.
|
||||||
|
- Error message: "Invalid download URL. Please check the repository URL."
|
||||||
|
|
||||||
|
2. **`templates/single-projects.php`**
|
||||||
|
- `$repo_data = projects_portfolio_get_repo_data( $post_id );`
|
||||||
|
- `$version = projects_portfolio_get_version( $post_id );`
|
||||||
|
- `$owner = projects_portfolio_get_owner( $post_id );`
|
||||||
|
- "View on GitHub" → "View Repo", href = `projects_portfolio_get_repo_browse_url( $post_id )`.
|
||||||
|
- Field reads (`$repo_data['stargazers_count']` etc.) unchanged because adapters normalize.
|
||||||
|
|
||||||
|
3. **`admin/rest-api.php`**
|
||||||
|
- `projects_portfolio_get_projects_data()` uses `projects_portfolio_get_repo_data()` and `projects_portfolio_get_version()`. Response shape is unchanged.
|
||||||
|
|
||||||
|
## 13. Error handling
|
||||||
|
|
||||||
|
| Scenario | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| Provider meta missing | Default to `github`; fall back to legacy `_projects_portfolio_github_url`. |
|
||||||
|
| Repo URL invalid/empty | Provider returns `null`; templates omit provider-only rows; download redirect shows wp_die. |
|
||||||
|
| API 401/403 | Logged, treated as `null`. |
|
||||||
|
| API 429 | Logged with reset time, treated as `null`. |
|
||||||
|
| Gitea release array empty | `get_release_url()` returns `null`. |
|
||||||
|
| Gitea release with no zip asset | Falls back to archive URL pattern. |
|
||||||
|
| Gitea license = string | Adapter wraps to `['name' => $string]`. |
|
||||||
|
| Gitea license = null | Adapter returns `['name' => 'None']`. |
|
||||||
|
| Cache transient corruption | Treated as a miss; re-fetched. |
|
||||||
|
| Owner fetch failure | Returns `null`; owner avatar block is omitted. |
|
||||||
|
|
||||||
|
All errors are logged via `error_log()` with a `Projects Portfolio:` prefix. **No `die()` calls in adapter code** — the legacy wrappers retain their existing `die()` behavior for backward compatibility.
|
||||||
|
|
||||||
|
## 14. Testing
|
||||||
|
|
||||||
|
WP PHPUnit suite under `tests/`:
|
||||||
|
|
||||||
|
- `test-github-provider.php` — correct URL construction, header behavior, asset/zipball fallback, multi-asset selection.
|
||||||
|
- `test-gitea-provider.php` — correct URL construction, Gitea→normalized mapping, releases-as-array handling, archive-URL fallback, license normalization.
|
||||||
|
- `test-provider-factory.php` — provider selection from meta; legacy URL fallback; token wiring per provider.
|
||||||
|
- `test-metabox.php` — save persists new meta keys; legacy key preserved; lazy migration from legacy → new repo URL.
|
||||||
|
- `test-settings.php` — new options register, save, and sanitize correctly.
|
||||||
|
|
||||||
|
Local run: `composer install && vendor/bin/phpunit`.
|
||||||
|
|
||||||
|
## 15. Rollout
|
||||||
|
|
||||||
|
1. Implement on a feature branch.
|
||||||
|
2. Manual smoke tests:
|
||||||
|
- GitHub project on fresh install — output matches current plugin exactly.
|
||||||
|
- Gitea project on codeberg.org.
|
||||||
|
- Self-hosted Gitea with custom base URL.
|
||||||
|
- Project with empty repo URL — download endpoint shows wp_die gracefully.
|
||||||
|
3. Run the WP PHPUnit suite.
|
||||||
|
4. Update README per §3.2.
|
||||||
|
5. Bump `PROJECTS_PORTFOLIO_VERSION` and plugin header Version `1.0.0` → `1.1.0`.
|
||||||
|
6. Release as a backward-compatible minor version.
|
||||||
|
|
||||||
|
## 16. New translatable strings
|
||||||
|
|
||||||
|
- "Repository" (metabox title)
|
||||||
|
- "Provider"
|
||||||
|
- "GitHub" (option label)
|
||||||
|
- "Gitea" (option label)
|
||||||
|
- "Repository URL"
|
||||||
|
- "Gitea Base URL (optional override)"
|
||||||
|
- "Leave blank to use the default from Settings."
|
||||||
|
- "View Repo"
|
||||||
|
- "Invalid download URL. Please check the repository URL."
|
||||||
|
- "Gitea Settings" (settings section heading)
|
||||||
|
- "Default Gitea Base URL"
|
||||||
|
- "Gitea API Token"
|
||||||
|
|
||||||
|
All strings use the existing `projects-wp` text domain. POT regeneration picks them up.
|
||||||
@@ -12,73 +12,24 @@ if ( $is_fse_theme ) {
|
|||||||
$settings = projects_portfolio_settings();
|
$settings = projects_portfolio_settings();
|
||||||
$project_id = get_the_ID();
|
$project_id = get_the_ID();
|
||||||
$download_count = (int) get_post_meta( $project_id, '_projects_portfolio_download_count', true );
|
$download_count = (int) get_post_meta( $project_id, '_projects_portfolio_download_count', true );
|
||||||
$github_url = get_post_meta( $project_id, '_projects_portfolio_github_url', true );
|
|
||||||
$version = 'Unknown';
|
|
||||||
$github_data = projects_portfolio_get_github_data( $github_url );
|
|
||||||
|
|
||||||
// Extract specific GitHub data.
|
$repo_url = get_post_meta( $project_id, '_projects_portfolio_repo_url', true );
|
||||||
$owner_avatar = $github_data['owner']['avatar_url'] ?? '';
|
if ( '' === $repo_url ) {
|
||||||
$owner_name = $github_data['owner']['login'] ?? '';
|
$repo_url = get_post_meta( $project_id, '_projects_portfolio_github_url', true );
|
||||||
$owner_url = $github_data['owner']['html_url'] ?? '';
|
|
||||||
$last_updated = $github_data['updated_at'] ?? '';
|
|
||||||
$language = $github_data['language'] ?? '';
|
|
||||||
$license = $github_data['license']['name'] ?? 'None';
|
|
||||||
|
|
||||||
$owner = projects_portfolio_github_owner( $owner_name );
|
|
||||||
|
|
||||||
// Fetch the version number from the GitHub API if the URL is set.
|
|
||||||
if ( $github_url ) {
|
|
||||||
// Retrieve the GitHub API token from the plugin settings.
|
|
||||||
$github_token = get_option( 'projects_portfolio_github_api_token', '' );
|
|
||||||
|
|
||||||
// Prepare the API URL.
|
|
||||||
$api_url = str_replace( 'https://github.com/', 'https://api.github.com/repos/', rtrim( $github_url, '/' ) ) . '/releases/latest';
|
|
||||||
|
|
||||||
// Check if the response is cached.
|
|
||||||
$transient_key = 'github_api_response_' . md5( $api_url );
|
|
||||||
$response = get_transient( $transient_key );
|
|
||||||
|
|
||||||
if ( false === $response ) {
|
|
||||||
// Prepare the request arguments.
|
|
||||||
$args = [];
|
|
||||||
if ( ! empty( $github_token ) ) {
|
|
||||||
$args['headers'] = [
|
|
||||||
'Authorization' => 'token ' . $github_token,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the API request.
|
|
||||||
$response = wp_remote_get( $api_url, $args );
|
|
||||||
|
|
||||||
// Cache the response for 1 hour if the request is successful.
|
|
||||||
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
|
|
||||||
set_transient( $transient_key, $response, HOUR_IN_SECONDS );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the response.
|
|
||||||
if ( is_wp_error( $response ) ) {
|
|
||||||
error_log( 'GitHub API error: ' . $response->get_error_message() );
|
|
||||||
} else {
|
|
||||||
$response_code = wp_remote_retrieve_response_code( $response );
|
|
||||||
$response_body = wp_remote_retrieve_body( $response );
|
|
||||||
error_log( 'GitHub API response code: ' . $response_code );
|
|
||||||
error_log( 'GitHub API response body: ' . $response_body );
|
|
||||||
|
|
||||||
if ( $response_code === 200 ) {
|
|
||||||
$data = json_decode( $response_body, true );
|
|
||||||
|
|
||||||
// Check if tag_name exists in the response.
|
|
||||||
if ( isset( $data['tag_name'] ) ) {
|
|
||||||
$version = esc_html( $data['tag_name'] );
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error_log( "GitHub API error: HTTP $response_code for URL $api_url" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format last updated date.
|
$repo_data = projects_portfolio_get_repo_data( $project_id );
|
||||||
|
$version = projects_portfolio_get_version( $project_id );
|
||||||
|
$owner = projects_portfolio_get_owner( $project_id );
|
||||||
|
|
||||||
|
$owner_avatar = $repo_data['owner']['avatar_url'] ?? '';
|
||||||
|
$owner_name = $repo_data['owner']['login'] ?? '';
|
||||||
|
$owner_url = $repo_data['owner']['html_url'] ?? '';
|
||||||
|
$last_updated = $repo_data['updated_at'] ?? '';
|
||||||
|
$language = $repo_data['language'] ?? '';
|
||||||
|
$license = $repo_data['license']['name'] ?? 'None';
|
||||||
|
$browse_url = $repo_url ? projects_portfolio_get_repo_browse_url( $project_id ) : '';
|
||||||
|
|
||||||
if ( $last_updated ) {
|
if ( $last_updated ) {
|
||||||
$last_updated = date_i18n( get_option( 'date_format' ), strtotime( $last_updated ) );
|
$last_updated = date_i18n( get_option( 'date_format' ), strtotime( $last_updated ) );
|
||||||
}
|
}
|
||||||
@@ -117,12 +68,12 @@ if ( $last_updated ) {
|
|||||||
<a href="<?php echo esc_url( site_url( '/download/' . $project_id ) ); ?>" class="button project-download-button">
|
<a href="<?php echo esc_url( site_url( '/download/' . $project_id ) ); ?>" class="button project-download-button">
|
||||||
<?php esc_html_e( 'Download Now', 'projects-wp' ); ?>
|
<?php esc_html_e( 'Download Now', 'projects-wp' ); ?>
|
||||||
</a>
|
</a>
|
||||||
<?php if ( $github_url ) : ?>
|
<?php if ( $repo_url ) : ?>
|
||||||
<a href="<?php echo esc_url( $github_url ); ?>" class="button project-github-button" target="_blank" rel="noopener noreferrer">
|
<a href="<?php echo esc_url( $browse_url ); ?>" class="button project-github-button" target="_blank" rel="noopener noreferrer">
|
||||||
<?php esc_html_e( 'View on GitHub', 'projects-wp' ); ?>
|
<?php esc_html_e( 'View Repo', 'projects-wp' ); ?>
|
||||||
</a>
|
</a>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<p><?php esc_html_e( 'GitHub 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; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -163,23 +114,23 @@ if ( $last_updated ) {
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php
|
<?php
|
||||||
if ( ! empty( $github_data ) ) :
|
if ( ! empty( $repo_data ) ) :
|
||||||
if ( isset( $github_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( $github_data['stargazers_count'] ); ?></td>
|
<td><?php esc_html_e( $repo_data['stargazers_count'] ); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif;
|
<?php endif;
|
||||||
if ( isset( $github_data['forks_count'] ) && $settings['templates']['forks'] ) : ?>
|
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( $github_data['forks_count'] ); ?></td>
|
<td><?php esc_html_e( $repo_data['forks_count'] ); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif;
|
<?php endif;
|
||||||
if ( isset( $github_data['open_issues_count'] ) && $settings['templates']['open_issues_count'] ) : ?>
|
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( $github_data['open_issues_count'] ); ?></td>
|
<td><?php esc_html_e( $repo_data['open_issues_count'] ); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif;
|
<?php endif;
|
||||||
endif;
|
endif;
|
||||||
@@ -189,7 +140,7 @@ 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 esc_html_e( $github_url ); ?>">
|
<a href="<?php echo esc_url( $browse_url ); ?>">
|
||||||
<div class="project-owner">
|
<div class="project-owner">
|
||||||
<span>
|
<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%;" />
|
<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%;" />
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
// Load Composer dev autoload (PHPUnit + Brain\Monkey + polyfills).
|
||||||
|
$autoload = dirname( __DIR__ ) . '/vendor/autoload.php';
|
||||||
|
if ( ! file_exists( $autoload ) ) {
|
||||||
|
fwrite( STDERR, "Run `composer install` before running tests.\n" );
|
||||||
|
exit( 1 );
|
||||||
|
}
|
||||||
|
require_once $autoload;
|
||||||
|
|
||||||
|
// Brain\Monkey sets up minimal function stubs (apply_filters, plugin_dir_path, etc.).
|
||||||
|
// Must run BEFORE the WP function stubs in wp-stubs.php are loaded, because Patchwork
|
||||||
|
// (used by Brain\Monkey) cannot redefine functions defined in the same file/import
|
||||||
|
// chain that loaded Patchwork itself. Loading the stubs in a separate file imported
|
||||||
|
// after Brain\Monkey\setUp() lets Patchwork track them properly.
|
||||||
|
\Brain\Monkey\setUp();
|
||||||
|
|
||||||
|
require_once __DIR__ . '/wp-stubs.php';
|
||||||
|
|
||||||
|
register_shutdown_function( function () {
|
||||||
|
\Brain\Monkey\tearDown();
|
||||||
|
} );
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/providers/interface-repository-provider.php';
|
||||||
|
require_once __DIR__ . '/../includes/providers/class-gitea-provider.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \Gitea_Provider
|
||||||
|
*/
|
||||||
|
class Gitea_Provider_Test extends \PHPUnit\Framework\TestCase {
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
\Brain\Monkey\setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void {
|
||||||
|
\Brain\Monkey\tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function make( string $base = 'https://codeberg.org', string $token = '' ): Gitea_Provider {
|
||||||
|
return new Gitea_Provider( 'https://codeberg.org/owner/repo', $base, $token );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_id_and_label(): void {
|
||||||
|
$p = $this->make();
|
||||||
|
$this->assertSame( 'gitea', $p->get_id() );
|
||||||
|
$this->assertSame( 'Gitea', $p->get_label() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_repo_data_normalizes_gitea_fields(): void {
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->once()
|
||||||
|
->with( 'https://codeberg.org/api/v1/repos/owner/repo', \Brain\Monkey\Actions\anyArgs() )
|
||||||
|
->andReturn( [
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
'headers' => [],
|
||||||
|
'body' => json_encode( [
|
||||||
|
'owner' => [
|
||||||
|
'avatar_url' => 'https://codeberg.org/avatars/owner',
|
||||||
|
'login' => 'owner',
|
||||||
|
'html_url' => 'https://codeberg.org/owner',
|
||||||
|
],
|
||||||
|
'updated_at' => '2026-02-03T04:05:06Z',
|
||||||
|
'language' => 'PHP',
|
||||||
|
'license' => [ 'name' => 'MIT' ],
|
||||||
|
'stars_count' => 100,
|
||||||
|
'forks_count' => 10,
|
||||||
|
'open_issues_count' => 5,
|
||||||
|
] ),
|
||||||
|
] );
|
||||||
|
|
||||||
|
$data = $this->make()->get_repo_data();
|
||||||
|
|
||||||
|
$this->assertSame( 100, $data['stargazers_count'] );
|
||||||
|
$this->assertSame( 10, $data['forks_count'] );
|
||||||
|
$this->assertSame( 5, $data['open_issues_count'] );
|
||||||
|
$this->assertSame( 'MIT', $data['license']['name'] );
|
||||||
|
$this->assertSame( 'owner', $data['owner']['login'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_repo_data_handles_string_license(): void {
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->once()
|
||||||
|
->andReturn( [
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
'headers' => [],
|
||||||
|
'body' => json_encode( [
|
||||||
|
'owner' => [],
|
||||||
|
'stars_count' => 0,
|
||||||
|
'forks_count' => 0,
|
||||||
|
'open_issues_count' => 0,
|
||||||
|
'license' => 'Apache-2.0',
|
||||||
|
] ),
|
||||||
|
] );
|
||||||
|
|
||||||
|
$data = $this->make()->get_repo_data();
|
||||||
|
$this->assertSame( 'Apache-2.0', $data['license']['name'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_repo_data_handles_null_license(): void {
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->once()
|
||||||
|
->andReturn( [
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
'headers' => [],
|
||||||
|
'body' => json_encode( [
|
||||||
|
'owner' => [],
|
||||||
|
'stars_count' => 0,
|
||||||
|
'forks_count' => 0,
|
||||||
|
'open_issues_count' => 0,
|
||||||
|
'license' => null,
|
||||||
|
] ),
|
||||||
|
] );
|
||||||
|
|
||||||
|
$data = $this->make()->get_repo_data();
|
||||||
|
$this->assertSame( 'None', $data['license']['name'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_release_url_picks_zip_asset_from_first_release(): void {
|
||||||
|
$body = json_encode( [
|
||||||
|
[
|
||||||
|
'tag_name' => 'v2.0.0',
|
||||||
|
'assets' => [
|
||||||
|
[ 'name' => 'source.tar.gz', 'browser_download_url' => 'https://example/x.tar.gz' ],
|
||||||
|
[ 'name' => 'release.zip', 'browser_download_url' => 'https://example/release.zip' ],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->twice()
|
||||||
|
->andReturn( [
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
'headers' => [],
|
||||||
|
'body' => $body,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$this->assertSame( 'https://example/release.zip', $this->make()->get_release_url() );
|
||||||
|
$this->assertSame( 'v2.0.0', $this->make()->get_latest_version() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_release_url_falls_back_to_archive_url(): void {
|
||||||
|
$body = json_encode( [
|
||||||
|
[ 'tag_name' => 'v0.1.0', 'assets' => [] ],
|
||||||
|
] );
|
||||||
|
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->once()
|
||||||
|
->andReturn( [
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
'headers' => [],
|
||||||
|
'body' => $body,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'https://codeberg.org/owner/repo/archive/refs/tags/v0.1.0.zip',
|
||||||
|
$this->make()->get_release_url()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_release_url_empty_array_returns_null(): void {
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->once()
|
||||||
|
->andReturn( [
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
'headers' => [],
|
||||||
|
'body' => '[]',
|
||||||
|
] );
|
||||||
|
|
||||||
|
$this->assertNull( $this->make()->get_release_url() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_repo_browse_url_is_built_from_base(): void {
|
||||||
|
$this->assertSame(
|
||||||
|
'https://codeberg.org/owner/repo',
|
||||||
|
$this->make()->get_repo_browse_url()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_http_failure_returns_null(): void {
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->once()
|
||||||
|
->andReturn( new \WP_Error( 'http_error', 'boom' ) );
|
||||||
|
|
||||||
|
$this->assertNull( $this->make()->get_repo_data() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_self_hosted_base_url_is_used(): void {
|
||||||
|
$provider = new Gitea_Provider(
|
||||||
|
'https://git.example.org/owner/repo',
|
||||||
|
'https://git.example.org',
|
||||||
|
''
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://git.example.org/owner/repo',
|
||||||
|
$provider->get_repo_browse_url()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/providers/interface-repository-provider.php';
|
||||||
|
require_once __DIR__ . '/../includes/providers/class-github-provider.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \GitHub_Provider
|
||||||
|
*/
|
||||||
|
class GitHub_Provider_Test extends \PHPUnit\Framework\TestCase {
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
\Brain\Monkey\setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void {
|
||||||
|
\Brain\Monkey\tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function make( string $token = '' ): GitHub_Provider {
|
||||||
|
return new GitHub_Provider( 'https://github.com/owner/repo', $token );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_id_and_label(): void {
|
||||||
|
$p = $this->make();
|
||||||
|
$this->assertSame( 'github', $p->get_id() );
|
||||||
|
$this->assertSame( 'GitHub', $p->get_label() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_repo_data_normalizes_response(): void {
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->once()
|
||||||
|
->with( 'https://api.github.com/repos/owner/repo', \Brain\Monkey\Actions\anyArgs() )
|
||||||
|
->andReturn( [
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
'headers' => [],
|
||||||
|
'body' => json_encode( [
|
||||||
|
'owner' => [
|
||||||
|
'avatar_url' => 'https://avatars.example/owner.png',
|
||||||
|
'login' => 'owner',
|
||||||
|
'html_url' => 'https://github.com/owner',
|
||||||
|
],
|
||||||
|
'updated_at' => '2026-01-02T03:04:05Z',
|
||||||
|
'language' => 'PHP',
|
||||||
|
'license' => [ 'name' => 'MIT' ],
|
||||||
|
'stargazers_count' => 42,
|
||||||
|
'forks_count' => 7,
|
||||||
|
'open_issues_count' => 3,
|
||||||
|
] ),
|
||||||
|
] );
|
||||||
|
|
||||||
|
$data = $this->make()->get_repo_data();
|
||||||
|
|
||||||
|
$this->assertSame( 'owner', $data['owner']['login'] );
|
||||||
|
$this->assertSame( 42, $data['stargazers_count'] );
|
||||||
|
$this->assertSame( 'MIT', $data['license']['name'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_release_url_prefers_zip_asset(): void {
|
||||||
|
$release_body = json_encode( [
|
||||||
|
'tag_name' => 'v1.2.3',
|
||||||
|
'assets' => [
|
||||||
|
[ 'name' => 'project.tar.gz', 'browser_download_url' => 'https://example/x.tar.gz' ],
|
||||||
|
[ 'name' => 'project.zip', 'browser_download_url' => 'https://example/x.zip' ],
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->twice()
|
||||||
|
->with( 'https://api.github.com/repos/owner/repo/releases/latest', \Brain\Monkey\Actions\anyArgs() )
|
||||||
|
->andReturn( [
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
'headers' => [],
|
||||||
|
'body' => $release_body,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$this->assertSame( 'https://example/x.zip', $this->make()->get_release_url() );
|
||||||
|
$this->assertSame( 'v1.2.3', $this->make()->get_latest_version() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_release_url_falls_back_to_zipball(): void {
|
||||||
|
$release_body = json_encode( [
|
||||||
|
'tag_name' => 'v0.0.1',
|
||||||
|
'zipball_url' => 'https://api.github.com/repos/owner/repo/zipball/v0.0.1',
|
||||||
|
'assets' => [],
|
||||||
|
] );
|
||||||
|
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->once()
|
||||||
|
->andReturn( [
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
'headers' => [],
|
||||||
|
'body' => $release_body,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'https://api.github.com/repos/owner/repo/zipball/v0.0.1',
|
||||||
|
$this->make()->get_release_url()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_repo_browse_url_returns_input(): void {
|
||||||
|
$this->assertSame(
|
||||||
|
'https://github.com/owner/repo',
|
||||||
|
$this->make()->get_repo_browse_url()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_http_failure_returns_null(): void {
|
||||||
|
\Brain\Monkey\Functions\expect( 'wp_remote_get' )
|
||||||
|
->once()
|
||||||
|
->andReturn( new \WP_Error( 'http_error', 'boom' ) );
|
||||||
|
|
||||||
|
$this->assertNull( $this->make()->get_repo_data() );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../admin/metabox.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \projects_portfolio_save_meta_box
|
||||||
|
*/
|
||||||
|
class Metabox_Test extends \PHPUnit\Framework\TestCase {
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
\Brain\Monkey\setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void {
|
||||||
|
$_POST = [];
|
||||||
|
\Brain\Monkey\tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var array<string,string> */
|
||||||
|
private array $saved = [];
|
||||||
|
|
||||||
|
protected function setUpSaveStubs(): void {
|
||||||
|
$this->saved = [];
|
||||||
|
\Brain\Monkey\Functions\stubs( [
|
||||||
|
'update_post_meta' => function ( $post_id, $key, $value ) {
|
||||||
|
$this->saved[ $key ] = $value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'wp_verify_nonce' => function () { return true; },
|
||||||
|
'wp_nonce_field' => function () { /* noop */ },
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_save_persists_provider_and_repo_url(): void {
|
||||||
|
$this->setUpSaveStubs();
|
||||||
|
$_POST['projects_portfolio_meta_box_nonce'] = 'nonce';
|
||||||
|
$_POST['projects_portfolio_provider'] = 'gitea';
|
||||||
|
$_POST['projects_portfolio_repo_url'] = 'https://codeberg.org/owner/repo';
|
||||||
|
$_POST['projects_portfolio_gitea_base_url'] = 'https://codeberg.org';
|
||||||
|
|
||||||
|
projects_portfolio_save_meta_box( 42 );
|
||||||
|
|
||||||
|
$this->assertSame( 'gitea', $this->saved['_projects_portfolio_provider'] );
|
||||||
|
$this->assertSame( 'https://codeberg.org/owner/repo', $this->saved['_projects_portfolio_repo_url'] );
|
||||||
|
$this->assertSame( 'https://codeberg.org', $this->saved['_projects_portfolio_gitea_base_url'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_lazy_migration_from_legacy_url(): void {
|
||||||
|
$existing = [
|
||||||
|
'_projects_portfolio_provider' => '',
|
||||||
|
'_projects_portfolio_repo_url' => '',
|
||||||
|
'_projects_portfolio_github_url' => 'https://github.com/owner/repo',
|
||||||
|
];
|
||||||
|
\Brain\Monkey\Functions\stubs( [
|
||||||
|
'get_post_meta' => function ( $post_id, $key, $single = false ) use ( &$existing ) {
|
||||||
|
return $existing[ $key ] ?? '';
|
||||||
|
},
|
||||||
|
'update_post_meta' => function ( $post_id, $key, $value ) use ( &$existing ) {
|
||||||
|
$existing[ $key ] = $value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'wp_verify_nonce' => function () { return true; },
|
||||||
|
'wp_nonce_field' => function () { /* noop */ },
|
||||||
|
] );
|
||||||
|
|
||||||
|
// No $_POST provider/repo_url. Lazy migration should fire.
|
||||||
|
$_POST['projects_portfolio_meta_box_nonce'] = 'nonce';
|
||||||
|
|
||||||
|
projects_portfolio_save_meta_box( 1 );
|
||||||
|
|
||||||
|
$this->assertSame( 'github', $existing['_projects_portfolio_provider'] );
|
||||||
|
$this->assertSame( 'https://github.com/owner/repo', $existing['_projects_portfolio_repo_url'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_switching_to_gitea_deletes_legacy_github_url(): void {
|
||||||
|
$existing = [
|
||||||
|
'_projects_portfolio_provider' => 'github',
|
||||||
|
'_projects_portfolio_repo_url' => 'https://github.com/owner/repo',
|
||||||
|
'_projects_portfolio_github_url' => 'https://github.com/owner/repo',
|
||||||
|
];
|
||||||
|
$deleted = [];
|
||||||
|
\Brain\Monkey\Functions\stubs( [
|
||||||
|
'get_post_meta' => function ( $post_id, $key, $single = false ) use ( &$existing ) {
|
||||||
|
return $existing[ $key ] ?? '';
|
||||||
|
},
|
||||||
|
'update_post_meta' => function ( $post_id, $key, $value ) use ( &$existing ) {
|
||||||
|
$existing[ $key ] = $value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'delete_post_meta' => function ( $post_id, $key ) use ( &$existing, &$deleted ) {
|
||||||
|
unset( $existing[ $key ] );
|
||||||
|
$deleted[] = $key;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'wp_verify_nonce' => function () { return true; },
|
||||||
|
'wp_nonce_field' => function () { /* noop */ },
|
||||||
|
] );
|
||||||
|
|
||||||
|
$_POST['projects_portfolio_meta_box_nonce'] = 'nonce';
|
||||||
|
$_POST['projects_portfolio_provider'] = 'gitea';
|
||||||
|
$_POST['projects_portfolio_repo_url'] = 'https://codeberg.org/owner/repo';
|
||||||
|
|
||||||
|
projects_portfolio_save_meta_box( 7 );
|
||||||
|
|
||||||
|
$this->assertSame( 'gitea', $existing['_projects_portfolio_provider'] );
|
||||||
|
$this->assertSame( 'https://codeberg.org/owner/repo', $existing['_projects_portfolio_repo_url'] );
|
||||||
|
$this->assertArrayNotHasKey( '_projects_portfolio_github_url', $existing );
|
||||||
|
$this->assertContains( '_projects_portfolio_github_url', $deleted );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_github_with_empty_repo_url_deletes_legacy_github_url(): void {
|
||||||
|
$existing = [
|
||||||
|
'_projects_portfolio_provider' => 'github',
|
||||||
|
'_projects_portfolio_repo_url' => 'https://github.com/owner/repo',
|
||||||
|
'_projects_portfolio_github_url' => 'https://github.com/owner/repo',
|
||||||
|
];
|
||||||
|
$deleted = [];
|
||||||
|
\Brain\Monkey\Functions\stubs( [
|
||||||
|
'get_post_meta' => function ( $post_id, $key, $single = false ) use ( &$existing ) {
|
||||||
|
return $existing[ $key ] ?? '';
|
||||||
|
},
|
||||||
|
'update_post_meta' => function ( $post_id, $key, $value ) use ( &$existing ) {
|
||||||
|
$existing[ $key ] = $value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'delete_post_meta' => function ( $post_id, $key ) use ( &$existing, &$deleted ) {
|
||||||
|
unset( $existing[ $key ] );
|
||||||
|
$deleted[] = $key;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'wp_verify_nonce' => function () { return true; },
|
||||||
|
'wp_nonce_field' => function () { /* noop */ },
|
||||||
|
] );
|
||||||
|
|
||||||
|
$_POST['projects_portfolio_meta_box_nonce'] = 'nonce';
|
||||||
|
$_POST['projects_portfolio_provider'] = 'github';
|
||||||
|
$_POST['projects_portfolio_repo_url'] = '';
|
||||||
|
|
||||||
|
projects_portfolio_save_meta_box( 9 );
|
||||||
|
|
||||||
|
$this->assertSame( 'github', $existing['_projects_portfolio_provider'] );
|
||||||
|
$this->assertSame( '', $existing['_projects_portfolio_repo_url'] );
|
||||||
|
$this->assertArrayNotHasKey( '_projects_portfolio_github_url', $existing );
|
||||||
|
$this->assertContains( '_projects_portfolio_github_url', $deleted );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/providers/interface-repository-provider.php';
|
||||||
|
require_once __DIR__ . '/../includes/providers/class-github-provider.php';
|
||||||
|
require_once __DIR__ . '/../includes/providers/class-gitea-provider.php';
|
||||||
|
require_once __DIR__ . '/../includes/providers/class-provider-factory.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \projects_portfolio_get_provider
|
||||||
|
*/
|
||||||
|
class Provider_Factory_Test extends \PHPUnit\Framework\TestCase {
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
\Brain\Monkey\setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void {
|
||||||
|
\Brain\Monkey\tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stub_meta( array $provider_meta, array $options = [] ): void {
|
||||||
|
\Brain\Monkey\Functions\stubs( [
|
||||||
|
'get_post_meta' => function ( $post_id, $key, $single = false ) use ( $provider_meta ) {
|
||||||
|
return $provider_meta[ $key ] ?? '';
|
||||||
|
},
|
||||||
|
'get_option' => function ( $key, $default = '' ) use ( $options ) {
|
||||||
|
return $options[ $key ] ?? $default;
|
||||||
|
},
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_defaults_to_github_when_provider_meta_missing(): void {
|
||||||
|
$this->stub_meta( [
|
||||||
|
'_projects_portfolio_github_url' => 'https://github.com/owner/repo',
|
||||||
|
] );
|
||||||
|
|
||||||
|
$provider = projects_portfolio_get_provider( 42 );
|
||||||
|
$this->assertSame( 'github', $provider->get_id() );
|
||||||
|
$this->assertSame( 'https://github.com/owner/repo', $provider->get_repo_browse_url() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_uses_repo_url_when_provider_explicit(): void {
|
||||||
|
$this->stub_meta( [
|
||||||
|
'_projects_portfolio_provider' => 'gitea',
|
||||||
|
'_projects_portfolio_repo_url' => 'https://codeberg.org/owner/repo',
|
||||||
|
], [
|
||||||
|
'projects_portfolio_default_gitea_base_url' => 'https://codeberg.org',
|
||||||
|
'projects_portfolio_gitea_api_token' => 'gitea-token',
|
||||||
|
] );
|
||||||
|
|
||||||
|
$provider = projects_portfolio_get_provider( 7 );
|
||||||
|
$this->assertSame( 'gitea', $provider->get_id() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_per_project_gitea_base_overrides_global(): void {
|
||||||
|
$this->stub_meta( [
|
||||||
|
'_projects_portfolio_provider' => 'gitea',
|
||||||
|
'_projects_portfolio_repo_url' => 'https://git.example.org/owner/repo',
|
||||||
|
'_projects_portfolio_gitea_base_url' => 'https://git.example.org',
|
||||||
|
], [
|
||||||
|
'projects_portfolio_default_gitea_base_url' => 'https://codeberg.org',
|
||||||
|
] );
|
||||||
|
|
||||||
|
$provider = projects_portfolio_get_provider( 7 );
|
||||||
|
$this->assertSame( 'https://git.example.org/owner/repo', $provider->get_repo_browse_url() );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/helper-functions.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \projects_portfolio_settings
|
||||||
|
*/
|
||||||
|
class Settings_Test extends \PHPUnit\Framework\TestCase {
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
\Brain\Monkey\setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void {
|
||||||
|
\Brain\Monkey\tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_settings_aggregator_includes_gitea_keys(): void {
|
||||||
|
\Brain\Monkey\Functions\stubs( [
|
||||||
|
'get_option' => function ( $key, $default = '' ) {
|
||||||
|
$values = [
|
||||||
|
'projects_portfolio_default_gitea_base_url' => 'https://git.example.org',
|
||||||
|
'projects_portfolio_gitea_api_token' => 'secret',
|
||||||
|
];
|
||||||
|
return $values[ $key ] ?? $default;
|
||||||
|
},
|
||||||
|
] );
|
||||||
|
|
||||||
|
$settings = projects_portfolio_settings();
|
||||||
|
$this->assertSame( 'https://git.example.org', $settings['default_gitea_base_url'] );
|
||||||
|
$this->assertSame( 'secret', $settings['gitea_api_token'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
namespace Brain\Monkey\Actions {
|
||||||
|
if ( ! function_exists( __NAMESPACE__ . '\\anyArgs' ) && function_exists( 'anyArgs' ) ) {
|
||||||
|
function anyArgs( ...$args ) { return \anyArgs( ...$args ); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Shim: ensure Mockery's anyArgs() helper is loaded.
|
||||||
|
if ( ! function_exists( 'anyArgs' ) ) {
|
||||||
|
require_once dirname( __DIR__ ) . '/vendor/mockery/mockery/library/helpers.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define WP constants the helpers rely on.
|
||||||
|
if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
|
||||||
|
define( 'HOUR_IN_SECONDS', 3600 );
|
||||||
|
}
|
||||||
|
if ( ! defined( 'WPINC' ) ) {
|
||||||
|
define( 'WPINC', 'wp-includes' );
|
||||||
|
}
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
define( 'ABSPATH', __DIR__ . '/' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide minimal WordPress function stubs used by the providers under test.
|
||||||
|
if ( ! function_exists( 'get_option' ) ) {
|
||||||
|
function get_option( $key, $default = false ) { return $default; }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'get_transient' ) ) {
|
||||||
|
function get_transient( $key ) { return false; }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'set_transient' ) ) {
|
||||||
|
function set_transient( $key, $value, $expiration ) { return true; }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'delete_transient' ) ) {
|
||||||
|
function delete_transient( $key ) { return true; }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'wp_remote_get' ) ) {
|
||||||
|
function wp_remote_get( $url, $args = [] ) { return new \WP_Error( 'no_http', 'no http' ); }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'is_wp_error' ) ) {
|
||||||
|
function is_wp_error( $thing ) { return $thing instanceof \WP_Error; }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'wp_remote_retrieve_response_code' ) ) {
|
||||||
|
function wp_remote_retrieve_response_code( $response ) {
|
||||||
|
if ( $response instanceof \WP_Error ) { return 0; }
|
||||||
|
return $response['response']['code'] ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'wp_remote_retrieve_body' ) ) {
|
||||||
|
function wp_remote_retrieve_body( $response ) {
|
||||||
|
if ( $response instanceof \WP_Error ) { return ''; }
|
||||||
|
return $response['body'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'wp_remote_retrieve_header' ) ) {
|
||||||
|
function wp_remote_retrieve_header( $response, $header ) {
|
||||||
|
if ( $response instanceof \WP_Error ) { return ''; }
|
||||||
|
$headers = $response['headers'] ?? [];
|
||||||
|
$key = strtolower( $header );
|
||||||
|
return $headers[ $key ] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'error_log' ) ) {
|
||||||
|
function error_log( $message ) { /* swallow during tests */ }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'get_post_meta' ) ) {
|
||||||
|
function get_post_meta( $post_id, $key = '', $single = false ) { return ''; }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'update_post_meta' ) ) {
|
||||||
|
function update_post_meta( $post_id, $key, $value ) { return true; }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'delete_post_meta' ) ) {
|
||||||
|
function delete_post_meta( $post_id, $key ) { return true; }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'wp_unslash' ) ) {
|
||||||
|
function wp_unslash( $value ) { return $value; }
|
||||||
|
}
|
||||||
|
if ( ! function_exists( 'esc_url_raw' ) ) {
|
||||||
|
function esc_url_raw( $url ) { return $url; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal WP_Error stub if Brain\Monkey doesn't supply one.
|
||||||
|
if ( ! class_exists( 'WP_Error' ) ) {
|
||||||
|
class WP_Error {
|
||||||
|
private $code;
|
||||||
|
private $message;
|
||||||
|
public function __construct( $code = '', $message = '' ) {
|
||||||
|
$this->code = $code;
|
||||||
|
$this->message = $message;
|
||||||
|
}
|
||||||
|
public function get_error_code() { return $this->code; }
|
||||||
|
public function get_error_message() { return $this->message; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user