Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin Directory: Add a low-level upload-zip-to-svn api, basis for a github action #343

Draft
wants to merge 22 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e9dc4fc
Initial WIP on adding a ZIP to SVN endpoint.
dd32 Jul 5, 2024
87d4f8e
Remove unused imports
dd32 Jul 5, 2024
fe2351b
Add a note about older versions.
dd32 Jul 5, 2024
008363f
Merge branch 'trunk' into plugins/add/upload-by-zip
dd32 Sep 10, 2024
b1db982
Clarify Auth options to be 2FA'd or SVN Password.
dd32 Sep 10, 2024
1a41399
Simplify logic by always overwriting trunk and copying to a tag.
dd32 Sep 10, 2024
2352c6c
Update it to always auth against SVN passwords.
dd32 Sep 10, 2024
18faed3
Check the 2FA status of the logged in user.
dd32 Sep 10, 2024
f92fad0
Add a TODO about the revalidation state..
dd32 Sep 10, 2024
d6a5660
SVN: Avoid a warning in PHP8 when $output is nullish.
dd32 Nov 29, 2024
f53d1dd
Only skip commits from the plugin management user when it's adding a …
dd32 Nov 29, 2024
2758f4c
Merge branch 'WordPress:trunk' into plugins/add/upload-by-zip
dd32 Nov 29, 2024
e125b23
Add SVN::copy() and SVN::add_remove().
dd32 Nov 29, 2024
6f352f3
Add a SVN_Automations class to automate some SVN tasks.
dd32 Nov 29, 2024
bcf2007
SVN Automations: Add a method to create a tag from trunk.
dd32 Nov 29, 2024
74bb5d5
SVN Automations: When creating the tag, if the Stable Tag line isn't …
dd32 Nov 29, 2024
aef697b
Update the API to only accept a 2FA'd user and use the SVN_Automation…
dd32 Nov 29, 2024
41f3394
Add an async job to import a ZIP into a plugin repo.
dd32 Nov 29, 2024
bef65f5
Allow queueing a job to import a ZIP from a remote URL.
dd32 Nov 29, 2024
a7036b9
Correct the commit message format.
dd32 Nov 29, 2024
db9b0dd
Merge SVN::copy() and SVN::rename().
dd32 Nov 29, 2024
b398712
Merge branch 'trunk' into plugins/add/upload-by-zip
dd32 Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public static function load_routes() {
new Routes\Plugin_Release_Confirmation();
new Routes\Plugin_Categorization();
new Routes\Plugin_Upload();
new Routes\Plugin_Upload_to_SVN();
new Routes\Plugin_Blueprint();
new Routes\Plugin_Review();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php
namespace WordPressdotorg\Plugin_Directory\API\Routes;

use WordPressdotorg\Plugin_Directory\Plugin_Directory;
use WordPressdotorg\Plugin_Directory\Tools;
use WordPressdotorg\Plugin_Directory\Tools\SVN_Automation;
use WordPressdotorg\Plugin_Directory\API\Base;
use WP_REST_Server;
use WP_Error;
use WP_User;
use function WordPressdotorg\Two_Factor\{ get_revalidation_status, get_revalidate_url }; // PR https://github.com/WordPress/wporg-two-factor/pull/283

/**
* An API Endpoint to upload a new version of a plugin to SVN.
*
* NOTE: This endpoint currently does not have strings translated, this is intentional.
* This endpoint is intended on being used as an internal endpoint / by automated tools,
* via the WordPress.org domain only, as a result, the strings will always be output in english.
*
* This is intended on being a low-level API that's used by other endpoints, such as a GitHub action.
*
* @package WordPressdotorg_Plugin_Directory
*/
class Plugin_Upload_to_SVN extends Base {

/**
* Plugin constructor.
*/
function __construct() {
register_rest_route( 'plugins/v1', '/plugin/(?P<plugin_slug>[^/]+)/?', array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'upload' ),
'permission_callback' => array( $this, 'permission_check' ),
'args' => [
'plugin_slug' => [
'type' => 'string',
'required' => true,
'validate_callback' => array( $this, 'validate_plugin_slug_callback' ),
],
'file' => [
// This field won't actually be used, this is just a placeholder to encourage including a file.
'required' => false,
],
'set_as_stable' => [
'type' => 'boolean',
'required' => false,
'default' => true,
]
],
) );
}

public function permission_check( $request ) {
/**
* Auth should be a 2FA'd user.
*/
if ( ! is_user_logged_in() ) {
return false;
}

// Check the current user is 2FA'd.
$status = get_revalidation_status();
if ( ! $status->last_validated ) {
return new WP_Error( 'not_2fa', 'The authorized user does not have 2FA enabled.', 403 );
}

// TODO: This API endpoint should not be interactive, it should be a async job creator.
if ( $status->needs_revalidate ) {
// TODO Uhhhh... We kinda need to revalidate, yet we need the ZIP file that they've submitted.. Store it somewhere?
wp_redirect( get_revalidate_url( /* TODO, current rest-api-endpoint url here... */ ) );
die();
}

// User must have confirmed 2FA to get here.
$user = wp_get_current_user();

// If no user, bail.
if ( ! $user || ! $user->exists() ) {
return false;
}

// Check if the user is a committer.
$committers = Tools::get_plugin_committers( $request['plugin_slug'], false );
if ( $user && in_array( $user->user_login, $committers, true ) ) {
return true;
}

return new WP_Error( 'not_a_committer', 'The authorized user is not a committer.', 403 );
}

/**
* Process a ZIP upload and commit it to SVN.
*
* @param \WP_REST_Request $request The request object.
* @return bool|WP_Error True on success, WP_Error on failure.
*/
public function upload( $request ) {
global $post;
$post = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );

// Validate that we expected a ZIP to be uploaded.
$file = reset( $_FILES );
if ( ! $file ) {
return new WP_Error( 'no_file', 'No file was uploaded.', 400 );
}

// Start the automated SVN process.
$svn_automations = new SVN_Automations( $post );

// Import the ZIP to the SVN repositories trunk folder.
$result = $svn_automations->import_zip_to_trunk( $file['tmp_name'] );
if ( ! $result || is_wp_error( $result ) ) {
return $result;
}

// Tag it, and set as stable.
if ( $request['set_as_stable'] ) {
$svn_automations->create_tag_from_trunk( true );
}

// Commit the new version.
$result = $svn_automations->commit();
if ( ! $result ) {
return new WP_Error( 'commit_failed', 'An error occured during the SVN commit.', 500 );
}

return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php
namespace WordPressdotorg\Plugin_Directory\Jobs;

use Exception;
use WordPressdotorg\Plugin_Directory\Tools\SVN_Automation;

/**
* Import a ZIP into plugins.svn.
*
* @package WordPressdotorg\Plugin_Directory\Jobs
*/
class Plugin_ZIP_Import {

public static function queue( $plugin_slug, $zip_reference, $set_as_stable = true, $author_id = null ) {
// If there's another ZIP import already scheduled, abort.
if ( Manager::get_scheduled_time( "import_zip:{$plugin_slug}", 'last' ) ) {
return false;
}

$author_id ??= get_current_user_id();

wp_schedule_single_event(
time() + 5,
"import_zip:{$plugin_slug}",
array(
$plugin_slug,
$zip_reference,
$set_as_stable,
$author_id,
)
);
}

/**
* The cron trigger for the import job.
*
* @param string $plugin_slug The plugin slug.
* @param int|string $zip_reference The ZIP post ID, or URL to ZIP.
* @param bool $set_as_stable Whether to set the imported ZIP as the stable version.
* @param int $author_id The author ID for the import.
*/
public static function cron_trigger( $plugin_slug, $zip_reference, $set_as_stable, $author_id ) {
$plugin = Plugin_Directory::get_plugin_post( $plugin_slug );

if ( is_numeric( $zip_reference ) ) {
// Fetch the ZIP details.
$zip = get_post( $zip_post_id );
if ( ! $zip ) {
fwrite( STDERR, "[{$plugin_slug}] ZIP Import Failed: ZIP post not found.\n" );
return false;
}

// Use the ZIP post author if no author ID is provided.
if ( ! $author_id ) {
$author_id = $zip->post_author;
}

// Local path to the ZIP.
$zip_filepath = get_attached_file( $zip->ID );
} elseif ( $zip_reference && preg_match( '/^https?:\/\//', $zip_reference ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';

// Download the ZIP.
$zip_filepath = download_url( $zip_reference );
if ( is_wp_error( $zip_filepath ) ) {
fwrite( STDERR, "[{$plugin_slug}] ZIP Import Failed: " . $zip_filepath->get_error_message() . "\n" );
return false;
}

// Cleanup the ZIP on shutdown.
add_action( 'shutdown', function() use ( $zip_filepath ) {
unlink( $zip_filepath );
} );

} else {
fwrite( STDERR, "[{$plugin_slug}] ZIP Import Failed: Invalid ZIP reference.\n" );
return false;
}

// Start the automated SVN process.
$svn_automations = new SVN_Automations( $plugin );

// Import the ZIP to the SVN repositories trunk folder.
$result = $svn_automations->import_zip_to_trunk( $zip_filepath );
if ( is_wp_error( $result ) ) {
fwrite( STDERR, "[{$plugin_slug}] ZIP Import Failed: " . $result->get_error_message() . "\n" );
return false;
}

// Tag it, and set as stable.
if ( $set_as_stable ) {
$result = $svn_automations->create_tag_from_trunk( true );
if ( is_wp_error( $result ) ) {
fwrite( STDERR, "[{$plugin_slug}] ZIP Import Failed: " . $result->get_error_message() . "\n" );
return false;
}
}

// Commit the new version.
$result = $svn_automations->commit();
if ( ! $result ) {
fwrite( STDERR, "[{$plugin_slug}] ZIP Import Failed: An error occured during the SVN commit.\n" );
return false;
}

return true;
}

}
Loading