Files
aurora-wordpress-admin/admin/src/Rest/MediaData.php
T

238 lines
8.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace AuroraAdmin\Rest;
use AuroraAdmin\Media\MediaFolders;
defined("ABSPATH") || exit();
/**
* REST endpoints for Aurora's custom Media Library page — same list-table
* pattern as Posts/Pages/Comments. Deliberately doesn't rebuild the
* upload flow (drag-drop, chunked uploads, the grid picker used inside
* post editors) — "Add Media File" still opens the native media-new.php,
* same way Posts'/Pages' "Add" buttons still open the native, already
* themed block editor. This replaces the *library/browse* experience,
* not uploading.
*/
class MediaData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/media", [
"methods" => "GET",
"callback" => [self::class, "list_media"],
"permission_callback" => function () {
return current_user_can("upload_files");
},
"args" => [
"type" => ["type" => "string", "required" => false],
"search" => ["type" => "string", "required" => false],
"page" => ["type" => "integer", "required" => false],
"perPage" => ["type" => "integer", "required" => false],
"folderId" => ["type" => "integer", "required" => false],
],
]);
register_rest_route("aurora-admin/v1", "/media/(?P<id>\d+)", [
"methods" => "DELETE",
"callback" => [self::class, "delete_media"],
"permission_callback" => [self::class, "can_delete_route_media"],
"args" => [
"id" => ["type" => "integer", "required" => true],
],
]);
}
public static function can_delete_route_media($request)
{
$id = (int) $request["id"];
return $id && current_user_can("delete_post", $id);
}
public static function list_media($request)
{
$type = self::query_mime_type($request->get_param("type"));
$page = max(1, (int) ($request->get_param("page") ?: 1));
$per_page = min(100, max(10, (int) ($request->get_param("perPage") ?: 24)));
$search = sanitize_text_field((string) $request->get_param("search"));
$args = [
"post_type" => "attachment",
"post_status" => "inherit",
"posts_per_page" => $per_page,
"paged" => $page,
"orderby" => "date",
"order" => "DESC",
];
if ($type === "unattached") {
$args["post_parent"] = 0;
} elseif ($type !== "") {
$args["post_mime_type"] = $type;
}
if ($search !== "") {
$args["s"] = $search;
}
$folder_param = $request->get_param("folderId");
if ($folder_param !== null && $folder_param !== "") {
$folder_id = (int) $folder_param;
if ($folder_id === MediaFolders::UNCATEGORIZED) {
$in_any_folder = MediaFolders::all_assigned_attachment_ids();
// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- a WordPress VIP-scale performance advisory, not a WP.org Plugin Directory requirement; "show media not assigned to any folder" has no other query shape without a real inverse-membership index, and Media Folders is bounded by how many attachments a site actually has, not VIP-scale traffic.
$args["post__not_in"] = !empty($in_any_folder) ? $in_any_folder : [0];
} elseif ($folder_id !== MediaFolders::ALL_MEDIA) {
$ids = MediaFolders::attachment_ids_in_folder($folder_id);
$args["post__in"] = !empty($ids) ? $ids : [0];
}
}
$query = new \WP_Query($args);
$folder_map = MediaFolders::folder_ids_for_attachments(wp_list_pluck($query->posts, "ID"));
return new \WP_REST_Response([
"items" => array_map(
function ($post) use ($folder_map) {
return self::serialize_attachment($post, $folder_map[$post->ID] ?? MediaFolders::UNCATEGORIZED);
},
$query->posts
),
"stats" => self::stats(),
"pagination" => [
"page" => $page,
"perPage" => $per_page,
"total" => (int) $query->found_posts,
"totalPages" => max(1, (int) $query->max_num_pages),
],
], 200);
}
public static function delete_media($request)
{
$id = (int) $request["id"];
$post = get_post($id);
if (!$post || $post->post_type !== "attachment") {
return new \WP_Error("aurora_media_invalid", __("Invalid media item.", "aurora-admin"), ["status" => 400]);
}
$result = wp_delete_attachment($id, true);
if (!$result) {
return new \WP_Error("aurora_media_delete_failed", __("Could not delete the media item.", "aurora-admin"), ["status" => 500]);
}
return new \WP_REST_Response(["success" => true, "message" => __("Media item deleted.", "aurora-admin")], 200);
}
private static function serialize_attachment($post, $folder_id = null)
{
$author = get_userdata($post->post_author);
$mime = $post->post_mime_type;
$is_image = wp_attachment_is_image($post->ID);
$meta = wp_get_attachment_metadata($post->ID);
$file_path = get_attached_file($post->ID);
$parent_title = "";
$parent_edit_url = "";
if ($post->post_parent) {
$parent_title = get_the_title($post->post_parent);
$parent_edit_url = get_edit_post_link($post->post_parent, "raw") ?: "";
}
return [
"id" => (int) $post->ID,
"title" => get_the_title($post) ?: basename($file_path),
"filename" => basename($file_path),
"mimeType" => $mime,
"typeLabel" => self::type_label($mime),
"isImage" => $is_image,
"thumbnailUrl" => $is_image ? (wp_get_attachment_image_url($post->ID, "thumbnail") ?: "") : "",
"fileUrl" => wp_get_attachment_url($post->ID) ?: "",
"dimensions" => $is_image && !empty($meta["width"]) ? "{$meta['width']} × {$meta['height']}" : "",
"fileSize" => !empty($meta["filesize"]) ? size_format($meta["filesize"]) : "",
"author" => $author ? $author->display_name : __("Unknown", "aurora-admin"),
"attachedToTitle" => $parent_title,
"attachedToEditUrl" => $parent_edit_url,
"date" => get_the_date("M j, Y", $post),
"editUrl" => get_edit_post_link($post->ID, "raw") ?: "",
"canDelete" => current_user_can("delete_post", $post->ID),
"folderId" => $folder_id,
];
}
private static function stats()
{
global $wpdb;
$counts = [
"all" => (int) wp_count_posts("attachment")->inherit,
"image" => 0,
"audio" => 0,
"video" => 0,
"application" => 0,
"unattached" => 0,
];
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- {$wpdb->posts} is WordPress core's own table, no user-supplied value in the query; there's no WP_Query/get_posts() shape for "counts grouped by mime-type prefix", which is exactly what this powers (the Media Library's type-filter tab counts).
$rows = $wpdb->get_results(
"SELECT post_mime_type, COUNT(*) AS c FROM {$wpdb->posts}
WHERE post_type = 'attachment' AND post_status = 'inherit'
GROUP BY post_mime_type"
);
foreach ($rows as $row) {
$group = strtok((string) $row->post_mime_type, "/");
if (isset($counts[$group])) {
$counts[$group] += (int) $row->c;
} else {
$counts["application"] += (int) $row->c;
}
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- {$wpdb->posts} is WordPress core's own table, no user-supplied value in the query.
$counts["unattached"] = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->posts}
WHERE post_type = 'attachment' AND post_status = 'inherit' AND post_parent = 0"
);
return $counts;
}
private static function query_mime_type($type)
{
switch ($type) {
case "image":
case "audio":
case "video":
return $type;
case "application":
return "application";
case "unattached":
return "unattached";
case "all":
default:
return "";
}
}
private static function type_label($mime)
{
$group = strtok((string) $mime, "/");
switch ($group) {
case "image":
return __("Image", "aurora-admin");
case "audio":
return __("Audio", "aurora-admin");
case "video":
return __("Video", "aurora-admin");
default:
return __("Document", "aurora-admin");
}
}
}