Skip to content

feat: Add Virtual Media Registrar and Migration classes for handling virtual media integration#1784

Open
shreyasikhar wants to merge 7 commits intodevelopfrom
feat/virtual-media-mgmt
Open

feat: Add Virtual Media Registrar and Migration classes for handling virtual media integration#1784
shreyasikhar wants to merge 7 commits intodevelopfrom
feat/virtual-media-mgmt

Conversation

@shreyasikhar
Copy link
Copy Markdown
Member

@shreyasikhar shreyasikhar commented Apr 14, 2026

Virtual Media Site Registration and Migration

Summary

This PR introduces two classes that allow a WordPress site to integrate as a virtual media site with GoDAM Central. When a virtual media attachment (identified by _godam_original_id meta) is added to the Media Library, this site is automatically registered with GoDAM Central so it receives transcoder callbacks for that job. A companion REST API endpoint allows GoDAM Central to trigger a one-time migration that reconciles all existing virtual attachments.


Changes

inc/classes/class-virtual-media-registrar.php (new)

Singleton class that hooks into the WordPress attachment lifecycle to keep site registration with GoDAM Central in sync.

Registration (add_virtual_media_site)

  • Fires on added_post_meta / updated_post_meta (priority 10) when _godam_original_id is written, and on add_attachment (priority 21) as a fallback — priority 22 ensures it runs after the transcoding is complete for the new attachment.
  • Guards against duplicate registrations using _godam_virtual_site_registered attachment meta.
  • Requires rtgodam-api-key option and RTGODAM_API_BASE constant; returns WP_Error early if either is missing.
  • Calls godam_core.api.transcoder_job.add_virtual_media_site on GoDAM Central with job_name, site_url, callback_url, and api_key.
  • Persists _godam_virtual_site_registered = 1 on the attachment on success.

De-registration (remove_virtual_media_site)

  • Hooks before_delete_post to detect when a virtual attachment is deleted.
  • Only calls Central if _godam_virtual_site_registered is present (skips attachments that were never successfully registered).
  • Calls godam_core.api.transcoder_job.remove_virtual_media_site on GoDAM Central with job_name, site_url, and api_key.
  • Logs failures via error_log without blocking the delete operation.

inc/classes/rest-api/class-virtual-media-migration.php (new)

REST endpoint for GoDAM Central to trigger a migration pass over all existing virtual attachments on this site.

image
  • Route: POST /godam/v1/virtual-media-migration
  • Auth: api_key body param validated with hash_equals against the stored rtgodam-api-key option.
  • Response: returns site_url, callback_url, and a deduplicated array of job_ids — all _godam_original_id values from attachments that have a non-empty value for that meta key.
  • get_job_ids_for_migration() queries attachments in batches of 100 (paginated WP_Query) to avoid memory issues on large sites, then returns array_values( array_unique( $job_ids ) ).

Meta keys used

Key Purpose
_godam_original_id Stores the GoDAM job name for a virtual attachment (pre-existing)
_godam_virtual_site_registered Flag set after successful registration; prevents duplicate API calls

Testing

  1. Add a virtual attachment (one with _godam_original_id set) — confirm godam_core.api.transcoder_job.add_virtual_media_site is called and _godam_virtual_site_registered is stored.
  2. Add the same attachment again / update its meta — confirm the registration API call is not repeated.
  3. Delete the virtual attachment — confirm godam_core.api.transcoder_job.remove_virtual_media_site is called.
  4. POST /godam/v1/virtual-media-migration with a valid api_key — confirm the response includes site_url, callback_url, and a correct deduplicated job_ids array.
  5. Call the migration endpoint with an invalid/missing api_key — confirm a 403 is returned.

Issue

#958 (comment)
#958 (comment)

Slack thread for reference: https://rtcamp.slack.com/archives/C25QZJ50B/p1776062860628749?thread_ts=1776051004.034789&cid=C25QZJ50B

Copilot AI review requested due to automatic review settings April 14, 2026 10:45
@shreyasikhar shreyasikhar self-assigned this Apr 14, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for registering a WordPress site as a “virtual media site” with GoDAM Central when virtual media attachments are created, and introduces a REST endpoint GoDAM Central can call to reconcile/migrate existing virtual attachments.

Changes:

  • Add Virtual_Media_Registrar to register/de-register the site with GoDAM Central based on attachment lifecycle events and _godam_original_id.
  • Add Virtual_Media_Migration REST endpoint to return callback/site details and a deduplicated list of existing virtual media job IDs.
  • Wire both components into the plugin bootstrap (class-plugin.php).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
inc/classes/rest-api/class-virtual-media-migration.php New REST endpoint for Central-triggered migration (job ID discovery + callback URL response).
inc/classes/class-virtual-media-registrar.php New singleton that hooks attachment/meta lifecycle to add/remove virtual media site registration in Central.
inc/classes/class-plugin.php Bootstraps the new registrar and REST controller.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread inc/classes/rest-api/class-virtual-media-migration.php Outdated
Comment thread inc/classes/class-virtual-media-registrar.php Outdated
Comment thread inc/classes/class-virtual-media-registrar.php
Comment thread inc/classes/class-virtual-media-registrar.php Outdated
Comment thread inc/classes/rest-api/class-virtual-media-migration.php Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 14, 2026

🔍 WordPress Plugin Check Report

⚠️ Status: Passed with warnings

📊 Report

🎯 Total Issues ❌ Errors ⚠️ Warnings
50 0 50

⚠️ Warnings (50)

📁 composer.json (1 warning)
📍 Line 🔖 Check 💬 Message
0 missing_composer_json_file The "/vendor" directory using composer exists, but "composer.json" file is missing.
📁 readme.txt (2 warnings)
📍 Line 🔖 Check 💬 Message
0 mismatched_plugin_name Plugin name "GoDAM - Organize WordPress Media Library & File Manager with Unlimited Folders for Images, Videos & more" is different from the name declared in plugin header "GoDAM".
0 trademarked_term The plugin name includes a restricted term. Your chosen plugin name - "GoDAM - Organize WordPress Media Library & File Manager with Unlimited Folders for Images, Videos & more" - contains the restricted term "wordpress" which cannot be used at all in your plugin name.
📁 assets/build/blocks/godam-gallery-v2/render.php (34 warnings)
📍 Line 🔖 Check 💬 Message
212 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$gallery_mode".
213 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$layout".
214 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$view_ratio".
215 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$allowed_ratios".
216 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$view_ratio".
217 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$item_width".
218 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$show_title".
219 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$enable_more_items".
220 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$more_items_behavior".
223 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$more_items_behavior".
227 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$more_items_behavior".
230 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$infinite_scroll".
231 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$show_load_more_button".
232 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$ratio_class".
233 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$block_gap_raw".
234 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$rest_query_args".
235 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$total_query_items".
238 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$block_gap".
240 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$block_gap".
243 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$inline_styles".
249 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$wrapper_attributes".
260 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$items".
263 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$rest_query_args".
281 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$query".
282 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$total_query_items".
285 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$video_post".
286 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$item".
289 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$items".
296 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$inner_block".
301 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$video_id".
302 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$item".
305 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$items".
332 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$item".
381 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$item".
📁 assets/build/css/main.css (1 warning)
📍 Line 🔖 Check 💬 Message
0 EnqueuedStylesScope This style is being loaded in all contexts.
📁 assets/src/libs/analytics.min.js (6 warnings)
📍 Line 🔖 Check 💬 Message
0 EnqueuedScriptsScope This script is being loaded in all frontend contexts.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880 (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/2026/04/15/hello-world/ (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/sample-page/ (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/demo-attachment-post/ (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/?godam-video=demo-godam-video-post (with handle analytics-library) is loaded in the footer. Consider a defer or async script loading strategy instead.
📁 assets/build/js/main.min.js (6 warnings)
📍 Line 🔖 Check 💬 Message
0 EnqueuedScriptsScope This script is being loaded in all frontend contexts.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880 (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/2026/04/15/hello-world/ (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/sample-page/ (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/demo-attachment-post/ (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead.
0 NonBlockingScripts.NoStrategy This script on http://localhost:8880/?godam-video=demo-godam-video-post (with handle rtgodam-script) is loaded in the footer. Consider a defer or async script loading strategy instead.

🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@shreyasikhar shreyasikhar requested a review from subodhr258 April 14, 2026 11:24
@shreyasikhar shreyasikhar changed the base branch from main to develop April 14, 2026 12:18
Copy link
Copy Markdown
Collaborator

@subodhr258 subodhr258 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the code, added some comments

protected function setup_hooks() {
add_action( 'added_post_meta', array( $this, 'maybe_register_from_meta_change' ), 10, 3 );
add_action( 'updated_post_meta', array( $this, 'maybe_register_from_meta_change' ), 10, 3 );
add_action( 'add_attachment', array( $this, 'maybe_register_from_attachment' ), 22, 1 );
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WordPress fires the add_attachment hook before _godam_original_id is set, so register_site_for_attachment_if_needed will always early return with true here. I think we can remove this hook for maybe_register_from_attachment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are setting the meta here https://github.com/rtCamp/godam/blob/main/inc/classes/rest-api/class-media-library.php#L1581-L1586 on the same add_attachment hook with priority 1.

'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'relation' => 'AND',
array(
'key' => '_godam_original_id',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded key can be replaced with this:
'key' => \RTGODAM\Inc\Virtual_Media_Registrar::META_ORIGINAL_ID,

'compare' => 'EXISTS',
),
array(
'key' => '_godam_original_id',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded key can be replaced with this:
'key' => \RTGODAM\Inc\Virtual_Media_Registrar::META_ORIGINAL_ID,

}

foreach ( $query->posts as $attachment_id ) {
$job_id = get_post_meta( $attachment_id, '_godam_original_id', true );
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above we are getting all the IDs and then we're calling get_post_meta() for every single ID, O(N+1).

You can get the Job ID as well for this, in one single query, something like this:

global $wpdb;

$job_ids = $wpdb->get_col(
    $wpdb->prepare(
        "SELECT DISTINCT meta_value
         FROM {$wpdb->postmeta} pm
         INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
         WHERE pm.meta_key = %s
           AND pm.meta_value != ''
           AND p.post_type = 'attachment'
           AND p.post_status = 'inherit'",
        '_godam_original_id'
    )
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but it won't be cached.

Do you want me to use transients to avoid query hitting the DB directly.

$job_ids = get_transient( 'rtgodam_virtual_media_job_ids' );

if ( false === $job_ids ) {
    global $wpdb;
    $job_ids = $wpdb->get_col( $wpdb->prepare( 
        "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} pm 
         INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id 
         WHERE pm.meta_key = %s AND pm.meta_value != '' 
         AND p.post_type = 'attachment'", 
        '_godam_original_id' 
    ) );
    set_transient( 'rtgodam_virtual_media_job_ids', $job_ids, HOUR_IN_SECONDS );
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants