@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/portfolio/lh-add-media-from-url/>
  a sioc:Post ;
  dc:title "Architecture" ;
  dcterms:identifier 147243 ;
  dc:modified "2026-09-12T11:54:39Z"^^xsd:dateTime ;
  dc:created "2026-09-12T11:18:23Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-add-media-from-url/architecture/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded "<ul class=\"lh_portfolio-meta\"><li><strong>Type:</strong> Doc-section</li><li><strong>Part of:</strong> <a href=\"https://lhero.org/portfolio/lh-add-media-from-url/\">LH Add Media from URL</a></li></ul><h2 class=\"wp-block-heading\">Files</h2><ul class=\"wp-block-list\"><li><code>lh-add-media-from-url.php</code> — main plugin file. Singleton <code>LH_add_media_from_url_plugin</code>, hooked via <code>plugins_loaded</code>. Registers the Media -&gt; Add from URL admin page and handles the upload form submission on <code>admin_init</code>. Thin controller only — no HTML lives in this class.</li><li><code>includes/lh-add-media-from-url-copy-from-url.php</code> — <code>LH_add_media_from_url_copy_from_url</code>, a composed helper class (not a PHP subclass of the main plugin) that does the actual remote fetch and sideload into the media library.</li><li><code>includes/lh-add-media-from-url-abilities.php</code> — <code>LH_add_media_from_url_abilities</code>, registers the plugin&#8217;s MCP ability. Loaded from <code>plugins_init()</code> only on the main site and only if the Abilities API is present.</li><li><code>partials/admin-page.php</code> — the admin page&#8217;s own markup: header, any upload error, the upload form include, and the bookmarklet block.</li><li><code>partials/upload.php</code> — the upload form markup itself, included from <code>admin-page.php</code>.</li></ul><h2 class=\"wp-block-heading\">Key methods</h2><ul class=\"wp-block-list\"><li><code>LH_add_media_from_url_copy_from_url::save_external_file( $url, $post_id, $desc, $check )</code> — downloads <code>$url</code> to a temp file via <code>wp_safe_remote_get()</code>, validates it, guesses its extension with <code>wp_check_filetype_and_ext()</code>, and sideloads it via <code>media_handle_sideload()</code>. Stores the original URL as post meta <code>_lh_copy_from_url-original_file</code>.</li><li><code>guess_file_extension()</code> inspects the already-downloaded local temp file rather than re-fetching the remote URL, so extension/MIME detection stays inside WP core&#8217;s own SSRF-protected fetch path.</li></ul><h2 class=\"wp-block-heading\">MCP ability</h2><ul class=\"wp-block-list\"><li><code>lh-add-media-from-url/add-media-from-url</code> — wraps <code>save_external_file()</code> so an MCP agent can pull a remote file into the media library the same way the manual wp-admin form does. Accepts <code>url</code> (required), <code>post_id</code>, <code>desc</code>. Returns the new attachment <code>id</code> and <code>url</code>.</li><li>Gated to <code>upload_files</code>, matching the manual form&#8217;s own capability check in <code>handle_upload_v2()</code> — deliberately not the <code>edit_posts</code> pattern used by content-operation abilities like lh-portfolio&#8217;s, since a Contributor with <code>edit_posts</code> but not <code>upload_files</code> should not gain upload access through this ability that the wp-admin screen would refuse them.</li></ul><h2 class=\"wp-block-heading\">Hooks</h2><ul class=\"wp-block-list\"><li>Filter <code>lh_add_media_from_url_remote_get_args</code> — filters the args array passed to <code>wp_safe_remote_get()</code> when fetching the remote file.</li><li>Filter <code>lh_add_media_from_url_init_plugin</code> — return <code>false</code> to prevent the plugin&#8217;s <code>admin_menu</code>/<code>admin_init</code> hooks and the MCP ability from registering.</li></ul><h2 class=\"wp-block-heading\">Capability</h2><p class=\"wp-block-paragraph\">Gated to <code>upload_files</code> rather than the LH-standard <code>install_plugins</code> default — deliberate, since this is a media-upload tool for any user who can already upload files, not developer/admin tooling.</p><h2 class=\"wp-block-heading\">Database / cron</h2><p class=\"wp-block-paragraph\">No custom database tables. No cron jobs. The only persistent data is the <code>_lh_copy_from_url-original_file</code> post meta on each created attachment.</p><h2 class=\"wp-block-heading\">Notes</h2><ul class=\"wp-block-list\"><li>The bookmarklet&#8217;s <code>$_GET</code> read in <code>plugin_options()</code> is a read-only display convenience (pre-filling the URL field), not a state-changing action — it can&#8217;t carry a nonce, since a bookmarklet is a static link with no server round-trip to mint one. The actual state-changing step, <code>handle_upload_v2()</code>, does verify a nonce before anything is fetched or written.</li></ul>"^^rdf:XMLLiteral ;
  sioc:content "Type: Doc-sectionPart of: LH Add Media from URLFileslh-add-media-from-url.php — main plugin file. Singleton LH_add_media_from_url_plugin, hooked via plugins_loaded. Registers the Media -&gt; Add from URL admin page and handles the upload form submission on admin_init. Thin controller only — no HTML lives in this class.includes/lh-add-media-from-url-copy-from-url.php — LH_add_media_from_url_copy_from_url, a composed helper class (not a PHP subclass of the main plugin) that does the actual remote fetch and sideload into the media library.includes/lh-add-media-from-url-abilities.php — LH_add_media_from_url_abilities, registers the plugin&#8217;s MCP ability. Loaded from plugins_init() only on the main site and only if the Abilities API is present.partials/admin-page.php — the admin page&#8217;s own markup: header, any upload error, the upload form include, and the bookmarklet block.partials/upload.php — the upload form markup itself, included from admin-page.php.Key methodsLH_add_media_from_url_copy_from_url::save_external_file( $url, $post_id, $desc, $check ) — downloads $url to a temp file via wp_safe_remote_get(), validates it, guesses its extension with wp_check_filetype_and_ext(), and sideloads it via media_handle_sideload(). Stores the original URL as post meta _lh_copy_from_url-original_file.guess_file_extension() inspects the already-downloaded local temp file rather than re-fetching the remote URL, so extension/MIME detection stays inside WP core&#8217;s own SSRF-protected fetch path.MCP abilitylh-add-media-from-url/add-media-from-url — wraps save_external_file() so an MCP agent can pull a remote file into the media library the same way the manual wp-admin form does. Accepts url (required), post_id, desc. Returns the new attachment id and url.Gated to upload_files, matching the manual form&#8217;s own capability check in handle_upload_v2() — deliberately not the edit_posts pattern used by content-operation abilities like lh-portfolio&#8217;s, since a Contributor with edit_posts but not upload_files should not gain upload access through this ability that the wp-admin screen would refuse them.HooksFilter lh_add_media_from_url_remote_get_args — filters the args array passed to wp_safe_remote_get() when fetching the remote file.Filter lh_add_media_from_url_init_plugin — return false to prevent the plugin&#8217;s admin_menu/admin_init hooks and the MCP ability from registering.CapabilityGated to upload_files rather than the LH-standard install_plugins default — deliberate, since this is a media-upload tool for any user who can already upload files, not developer/admin tooling.Database / cronNo custom database tables. No cron jobs. The only persistent data is the _lh_copy_from_url-original_file post meta on each created attachment.NotesThe bookmarklet&#8217;s $_GET read in plugin_options() is a read-only display convenience (pre-filling the URL field), not a state-changing action — it can&#8217;t carry a nonce, since a bookmarklet is a static link with no server round-trip to mint one. The actual state-changing step, handle_upload_v2(), does verify a nonce before anything is fetched or written." ;
  sioc:topic <https://lhero.org/?taxonomy=author&term=cap-1> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
