'Insufficient permissions.' ), 403 );
}
}
/* ------------------------------------------------------------------
ADMIN MENÜ & ASSETS
------------------------------------------------------------------ */
public function add_settings_link( $links ) {
$settings_link = 'Settings';
array_unshift( $links, $settings_link );
return $links;
}
public function create_menu() {
add_menu_page(
'Erdo Image Optimizer',
'Image Optimizer',
'manage_options',
'erdo-image-optimizer',
array( $this, 'render_page' ),
'dashicons-performance',
11
);
}
public function assets( $hook ) {
if ( 'toplevel_page_erdo-image-optimizer' !== $hook ) {
return;
}
wp_enqueue_media();
$ver = ERDO_VERSION;
wp_enqueue_style( 'erdo-css', ERDO_URL . 'assets/css/admin.css', array(), $ver );
wp_enqueue_script( 'erdo-js', ERDO_URL . 'assets/js/admin.js', array( 'jquery' ), $ver, true );
wp_localize_script( 'erdo-js', 'erdo_vars', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'erdo_nonce' ),
) );
}
/* ------------------------------------------------------------------
ANA SAYFA HTML
------------------------------------------------------------------ */
public function render_page() {
$format = strtoupper( get_option( 'erdo_convert_format', 'webp' ) );
?>
verify_capability();
$paged = isset( $_POST['page'] ) ? intval( $_POST['page'] ) : 1;
$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
$query = new WP_Query( array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'post_mime_type' => 'image',
'posts_per_page' => 100,
'paged' => $paged,
's' => $search,
'fields' => 'ids',
) );
$buckets = array( 'red' => '', 'orange' => '', 'green' => '', 'gray' => '' );
$counts = array( 'red' => 0, 'orange' => 0, 'green' => 0, 'gray' => 0 );
if ( ! empty( $query->posts ) ) {
$results = ERDO_Auditor::bulk_analyze( $query->posts );
foreach ( $results as $id => $res ) {
$cat = ( isset( $res['is_used'] ) && false === $res['is_used'] ) ? 'gray' : $res['status'];
$counts[ $cat ]++;
$format_label = strtoupper( str_replace( 'image/', '', $res['mime'] ?? '' ) );
$score_label = isset( $res['score'] ) ? intval( $res['score'] ) . '/6' : '';
// Alt text durumu bulk_analyze() checklist'inde zaten hesaplanmış —
// "Missing Alt" filtresi bunu kart üzerinden okur, kart başına
// ayrı bir AJAX isteği atmasına gerek kalmaz.
$has_alt = true;
foreach ( $res['checklist'] ?? array() as $item ) {
if ( 'Alt Text' === $item['label'] ) {
$has_alt = ! empty( $item['pass'] );
break;
}
}
$html = '';
$html .= '
';
$html .= '
 . ')
';
if ( $format_label ) {
$html .= '
' . esc_html( $format_label ) . '';
}
$html .= '
';
$html .= '';
$html .= '
';
$buckets[ $cat ] .= $html;
}
}
wp_send_json_success( array(
'html' => $buckets,
'counts' => $counts,
'max_pages' => $query->max_num_pages,
'found' => $query->found_posts,
) );
}
/* ------------------------------------------------------------------
AJAX: Dashboard istatistikleri
------------------------------------------------------------------ */
public function ajax_get_stats() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
$stats_cache_key = 'erdo_stats_cache';
$cache_group = 'erdo_image_optimizer';
$cached = wp_cache_get( $stats_cache_key, $cache_group );
if ( false !== $cached ) {
wp_send_json_success( $cached );
return;
}
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$total = (int) $wpdb->get_var(
"SELECT COUNT(ID) FROM {$wpdb->posts}
WHERE post_type = 'attachment' AND post_mime_type LIKE 'image/%'"
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$count_optimized = (int) $wpdb->get_var(
"SELECT COUNT(ID) FROM {$wpdb->posts}
WHERE post_type = 'attachment'
AND post_mime_type IN ('image/webp','image/avif')"
);
$seo_cache_key = 'erdo_count_seo';
$count_seo = wp_cache_get( $seo_cache_key, $cache_group );
if ( false === $count_seo ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$count_seo = (int) $wpdb->get_var(
"SELECT COUNT(p.ID)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = 'attachment'
AND pm.meta_key = '_wp_attachment_image_alt'
AND pm.meta_value != ''"
);
wp_cache_set( $seo_cache_key, $count_seo, $cache_group, 300 );
}
$opt_score = ( $total > 0 ) ? round( ( $count_optimized / $total ) * 100 ) : 0;
$seo_score = ( $total > 0 ) ? round( ( $count_seo / $total ) * 100 ) : 0;
$saved = (int) get_option( 'erdo_total_saved_bytes', 0 );
$result = array(
'total' => $total,
'opt_score' => $opt_score,
'seo_score' => $seo_score,
'total_saved' => size_format( $saved, 2 ),
);
wp_cache_set( $stats_cache_key, $result, $cache_group, 300 );
// library_version cache'e dahil edilmez — her istekte taze okunur
$result['library_version'] = (int) get_option( 'erdo_library_version', 0 );
wp_send_json_success( $result );
}
/* ------------------------------------------------------------------
AJAX: Görsel detay modali (HTML döndürür)
------------------------------------------------------------------ */
public function ajax_get_detail() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
if ( ! isset( $_POST['id'] ) ) {
wp_send_json_error( array( 'message' => 'Missing image ID.' ) );
}
$id = intval( $_POST['id'] );
$data = ERDO_Auditor::analyze( $id );
ob_start();
$help = array(
'Filename' => __( 'Search engines prefer descriptive file names with hyphens (-) as separators. Avoid underscores or spaces.', 'erdo-image-optimizer' ),
'Alt Text' => __( 'Alt text helps search engines understand the image and is read by screen readers for accessibility.', 'erdo-image-optimizer' ),
'Image Title' => __( 'A clean, readable title helps users and search engines. Avoid slug-style formatting.', 'erdo-image-optimizer' ),
'Dimensions' => __( 'Oversized images slow down your site. Resize to the maximum width your theme needs (usually 1920px or less).', 'erdo-image-optimizer' ),
'Format' => __( 'WebP and AVIF offer better compression than JPEG/PNG without quality loss, making your site faster.', 'erdo-image-optimizer' ),
'File Size' => __( 'Large images are the #1 cause of slow websites. Aim to keep images under 250 KB.', 'erdo-image-optimizer' ),
);
$usage_msg = ( isset( $data['is_used'] ) && ! $data['is_used'] )
? '⚠ Unused Image
'
: '';
$format = strtoupper( get_option( 'erdo_convert_format', 'webp' ) );
$show_comparison = false;
if ( ! empty( $data['saved_bytes'] ) && ! empty( $data['original_size'] ) ) {
$show_comparison = true;
$orig_size_fmt = size_format( $data['original_size'], 1 );
$curr_size_fmt = size_format( $data['current_size'], 1 );
$saved_fmt = size_format( $data['saved_bytes'], 1 );
$saved_percent = ( $data['original_size'] > 0 )
? round( ( $data['saved_bytes'] / $data['original_size'] ) * 100 )
: 0;
$bar_curr = ( $data['original_size'] > 0 )
? round( ( $data['current_size'] / $data['original_size'] ) * 100 ) . '%'
: '100%';
}
$score = $data['score'] ?? 0;
$score_class = $score >= 5 ? 'score-high' : ( $score >= 3 ? 'score-mid' : 'score-low' );
?>
verify_capability();
$id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
if ( ! $id ) {
wp_send_json_error( array( 'message' => 'Invalid ID.' ) );
}
ERDO_Auditor::save_image_data( $id, wp_unslash( $_POST ) );
$new_data = ERDO_Auditor::analyze( $id );
wp_send_json_success( $new_data );
}
/* ------------------------------------------------------------------
AJAX: WebP/AVIF dönüşümü — artık bağımsız JSON yanıt döndürüyor.
JS tarafı dönüşüm sonrası ayrı bir erdo_get_detail çağrısı yapar.
------------------------------------------------------------------ */
public function ajax_convert_webp() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
if ( ! isset( $_POST['id'] ) ) {
wp_send_json_error( array( 'message' => 'Missing ID.' ) );
}
$id = intval( wp_unslash( $_POST['id'] ) );
$result = ERDO_Auditor::convert_to_webp( $id );
if ( ERDO_Auditor::ALREADY_OPTIMIZED === $result ) {
wp_send_json_error( array(
'message' => 'Image is already optimized.',
'already_optimized' => true,
) );
} elseif ( $result ) {
wp_send_json_success( array( 'id' => $id ) );
} else {
wp_send_json_error( array( 'message' => 'Conversion failed.' ) );
}
}
/* ------------------------------------------------------------------
AJAX: Toplu dönüşüm için ID listesi
------------------------------------------------------------------ */
public function ajax_get_bulk_list() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
$query = new WP_Query( array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'post_mime_type' => array( 'image/jpeg', 'image/png' ),
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
) );
wp_send_json_success( $query->posts );
}
/* ------------------------------------------------------------------
AJAX: Toplu dönüşüm - tek adım
------------------------------------------------------------------ */
public function ajax_bulk_step() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
if ( ! isset( $_POST['id'] ) ) {
wp_send_json_error();
}
$id = intval( wp_unslash( $_POST['id'] ) );
$res = ERDO_Auditor::convert_to_webp( $id );
if ( ERDO_Auditor::ALREADY_OPTIMIZED === $res || $res ) {
wp_send_json_success();
} else {
wp_send_json_error();
}
}
/* ------------------------------------------------------------------
AJAX: Kullanılmayan görsel listesi
bulk_analyze() ile tüm ID'ler TEK sorguda analiz edilir.
Eski kod: 500 görsel × 6 sorgu = 3000 sorgu → timeout
Yeni kod: 500 görsel → 4-5 sorgu → ~saniyeler içinde
------------------------------------------------------------------ */
public function ajax_get_unused_list() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
$query = new WP_Query( array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'post_mime_type' => 'image',
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
) );
if ( empty( $query->posts ) ) {
wp_send_json_success( array() );
return;
}
// Tüm görselleri TEK seferde analiz et (toplu sorgu)
$all_results = ERDO_Auditor::bulk_analyze( $query->posts );
$unused = array();
foreach ( $all_results as $id => $data ) {
if ( isset( $data['is_used'] ) && ! $data['is_used'] ) {
$unused[] = $id;
}
}
wp_send_json_success( $unused );
}
/* ------------------------------------------------------------------
AJAX: Görsel sil
------------------------------------------------------------------ */
public function ajax_delete_image() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
if ( ! isset( $_POST['id'] ) ) {
wp_send_json_error();
}
$id = intval( wp_unslash( $_POST['id'] ) );
ERDO_Auditor::delete_image( $id );
wp_send_json_success();
}
/* ------------------------------------------------------------------
AJAX: Manuel yeniden boyutlandır
------------------------------------------------------------------ */
public function ajax_manual_resize() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
if ( ! isset( $_POST['id'], $_POST['w'], $_POST['h'] ) ) {
wp_send_json_error();
}
$id = intval( wp_unslash( $_POST['id'] ) );
$w = intval( wp_unslash( $_POST['w'] ) );
$h = intval( wp_unslash( $_POST['h'] ) );
$res = ERDO_Auditor::manual_resize( $id, $w, $h );
if ( $res ) {
wp_send_json_success();
} else {
wp_send_json_error();
}
}
/* ------------------------------------------------------------------
AJAX: Orijinal görseli yedeğinden geri yükle
------------------------------------------------------------------ */
public function ajax_restore_original() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
if ( ! isset( $_POST['id'] ) ) {
wp_send_json_error( array( 'message' => 'Missing ID.' ) );
}
$id = intval( wp_unslash( $_POST['id'] ) );
$result = ERDO_Auditor::restore_original( $id );
if ( $result ) {
wp_send_json_success( array( 'id' => $id ) );
} else {
wp_send_json_error( array( 'message' => 'No backup found for this image.' ) );
}
}
/* ------------------------------------------------------------------
AJAX: Tüm audit cache'i temizle ve yenile
------------------------------------------------------------------ */
public function ajax_refresh_all() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
ERDO_Auditor::clear_all_cache();
wp_send_json_success();
}
/* ------------------------------------------------------------------
AJAX: Eksik featured image olan yazıları tara
------------------------------------------------------------------ */
public function ajax_scan_thumbnails() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
global $wpdb;
$cache_key = 'erdo_missing_thumbnails';
$cache_group = 'erdo_image_optimizer';
$results = wp_cache_get( $cache_key, $cache_group );
if ( false === $results ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$results = $wpdb->get_results(
"SELECT p.ID, p.post_title
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm
ON p.ID = pm.post_id AND pm.meta_key = '_thumbnail_id'
WHERE p.post_type = 'post'
AND p.post_status = 'publish'
AND pm.post_id IS NULL
LIMIT 50",
ARRAY_A
);
wp_cache_set( $cache_key, $results, $cache_group, 300 );
}
$missing = array();
if ( ! empty( $results ) ) {
foreach ( $results as $row ) {
$missing[] = array(
'id' => (int) $row['ID'],
'title' => $row['post_title'],
'link' => get_edit_post_link( $row['ID'] ),
);
}
}
wp_send_json_success( $missing );
}
/* ------------------------------------------------------------------
AJAX: Ayarları kaydet
------------------------------------------------------------------ */
public function ajax_save_settings() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
update_option( 'erdo_auto_webp', isset( $_POST['erdo_auto_webp'] ) ? 1 : 0 );
update_option( 'erdo_auto_sanitize', isset( $_POST['erdo_auto_sanitize'] ) ? 1 : 0 );
update_option( 'erdo_remove_exif', isset( $_POST['erdo_remove_exif'] ) ? 1 : 0 );
update_option( 'erdo_lossless', isset( $_POST['erdo_lossless'] ) ? 1 : 0 );
update_option( 'erdo_keep_original', isset( $_POST['erdo_keep_original'] ) ? 1 : 0 );
update_option( 'erdo_serve_webp', isset( $_POST['erdo_serve_webp'] ) ? 1 : 0 );
update_option( 'erdo_lazy_loading', isset( $_POST['erdo_lazy_loading'] ) ? 1 : 0 );
update_option( 'erdo_smart_quality', isset( $_POST['erdo_smart_quality'] ) ? 1 : 0 );
update_option( 'erdo_allow_svg', isset( $_POST['erdo_allow_svg'] ) ? 1 : 0 );
update_option( 'erdo_append_original_filename', isset( $_POST['erdo_append_original_filename'] ) ? 1 : 0 );
if ( isset( $_POST['erdo_webp_serve_method'] ) ) {
$meth = sanitize_text_field( wp_unslash( $_POST['erdo_webp_serve_method'] ) );
update_option( 'erdo_webp_serve_method', in_array( $meth, array( 'picture', 'htaccess' ), true ) ? $meth : 'picture' );
}
if ( isset( $_POST['erdo_max_width'] ) ) {
update_option( 'erdo_max_width', max( 0, intval( wp_unslash( $_POST['erdo_max_width'] ) ) ) );
}
if ( isset( $_POST['erdo_convert_format'] ) ) {
update_option( 'erdo_convert_format', sanitize_text_field( wp_unslash( $_POST['erdo_convert_format'] ) ) );
}
if ( isset( $_POST['erdo_quality'] ) ) {
$q = intval( wp_unslash( $_POST['erdo_quality'] ) );
$q = max( 10, min( 100, $q ) );
update_option( 'erdo_quality', $q );
}
// BunnyCDN
update_option( 'erdo_bunny_enabled', isset( $_POST['erdo_bunny_enabled'] ) ? 1 : 0 );
update_option( 'erdo_bunny_api_key', sanitize_text_field( wp_unslash( $_POST['erdo_bunny_api_key'] ?? '' ) ) );
update_option( 'erdo_bunny_storage_zone', sanitize_text_field( wp_unslash( $_POST['erdo_bunny_storage_zone'] ?? '' ) ) );
update_option( 'erdo_bunny_hostname', sanitize_text_field( wp_unslash( $_POST['erdo_bunny_hostname'] ?? '' ) ) );
wp_send_json_success();
}
/* ------------------------------------------------------------------
AJAX: Ayarlar modali (HTML döndürür)
------------------------------------------------------------------ */
public function ajax_get_settings() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
$aw = get_option( 'erdo_auto_webp' );
$as = get_option( 'erdo_auto_sanitize' );
$re = get_option( 'erdo_remove_exif' );
$fmt = get_option( 'erdo_convert_format', 'webp' );
$q = get_option( 'erdo_quality', 80 );
$ll = get_option( 'erdo_lossless', 0 );
$ko = get_option( 'erdo_keep_original', 0 );
$aof = get_option( 'erdo_append_original_filename', 0 );
$avif = function_exists( 'imageavif' );
?>
verify_capability();
$ids = get_posts( array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'post_mime_type' => array( 'image/jpeg', 'image/png', 'image/gif' ),
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
) );
if ( empty( $ids ) ) {
wp_send_json_error( array( 'message' => 'No convertible images found.' ) );
}
ERDO_Queue::start( $ids, 'convert' );
wp_send_json_success( ERDO_Queue::get_status() );
}
/* ------------------------------------------------------------------
AJAX: Kuyruğu iptal et
------------------------------------------------------------------ */
public function ajax_cancel_queue() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
ERDO_Queue::cancel();
wp_send_json_success( array( 'status' => 'cancelled' ) );
}
/* ------------------------------------------------------------------
AJAX: Kuyruk durumunu sorgula
------------------------------------------------------------------ */
public function ajax_queue_status() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
wp_send_json_success( ERDO_Queue::get_status() );
}
/* ------------------------------------------------------------------
AJAX: .htaccess kurallarını yaz
------------------------------------------------------------------ */
public function ajax_write_htaccess() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
ERDO_WebP_Server::write_htaccess_rules();
wp_send_json_success( array( 'active' => ERDO_WebP_Server::htaccess_has_rules() ) );
}
/* ------------------------------------------------------------------
AJAX: .htaccess kurallarını kaldır
------------------------------------------------------------------ */
public function ajax_remove_htaccess() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
ERDO_WebP_Server::remove_htaccess_rules();
wp_send_json_success( array( 'active' => false ) );
}
/* ------------------------------------------------------------------
AJAX: Tek görsel — dosya adını yeniden adlandır
------------------------------------------------------------------ */
public function ajax_rename_file() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
$id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
if ( ! $id ) {
wp_send_json_error( array( 'message' => 'Invalid ID.' ) );
}
$result = ERDO_Auditor::rename_file( $id );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
$new_data = ERDO_Auditor::analyze( $id );
wp_send_json_success( array_merge( $result, array( 'audit' => $new_data ) ) );
}
/* ------------------------------------------------------------------
AJAX: Toplu dosya adı yeniden adlandırma — ID listesi işle
------------------------------------------------------------------ */
public function ajax_bulk_rename() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
// ID yoksa tüm kütüphaneyi tara
$ids = isset( $_POST['ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['ids'] ) ) : array();
if ( empty( $ids ) ) {
$ids = get_posts( array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'post_mime_type' => 'image',
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
) );
}
$renamed = 0;
$skipped = 0;
foreach ( $ids as $id ) {
$result = ERDO_Auditor::rename_file( $id );
if ( is_wp_error( $result ) ) {
$skipped++;
} else {
$renamed++;
}
}
wp_send_json_success( array( 'renamed' => $renamed, 'skipped' => $skipped ) );
}
/* ------------------------------------------------------------------
AJAX: SEO audit sonuçlarını CSV olarak indir
------------------------------------------------------------------ */
public function ajax_export_csv() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
$status_filter = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '';
$rows = ERDO_Auditor::get_csv_rows( $status_filter );
$csv = "ID,Filename,Score,Status,Format,Alt Text,Title,Edit URL\n";
foreach ( $rows as $r ) {
$csv .= implode( ',', array_map( function( $v ) {
return '"' . str_replace( '"', '""', $v ) . '"';
}, $r ) ) . "\n";
}
wp_send_json_success( array( 'csv' => $csv, 'count' => count( $rows ) ) );
}
/* ------------------------------------------------------------------
AJAX: Seçili görseller için dosya adından alt text üret
------------------------------------------------------------------ */
public function ajax_bulk_alt_text() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
$ids = isset( $_POST['ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['ids'] ) ) : array();
// ID verilmezse sadece alt text eksik olanları işle
if ( empty( $ids ) ) {
$ids = get_posts( array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'post_mime_type' => 'image',
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_wp_attachment_image_alt',
'compare' => 'NOT EXISTS',
),
),
) );
}
$updated = 0;
foreach ( $ids as $id ) {
$alt = ERDO_Auditor::set_alt_from_filename( $id );
if ( $alt ) {
$updated++;
}
}
wp_send_json_success( array( 'updated' => $updated ) );
}
/* ------------------------------------------------------------------
AJAX: BunnyCDN'e görsel yükle
------------------------------------------------------------------ */
public function ajax_cdn_upload() {
check_ajax_referer( 'erdo_nonce', 'nonce' );
$this->verify_capability();
$id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
if ( ! $id ) {
wp_send_json_error( array( 'message' => 'Invalid ID.' ) );
}
$result = $this->upload_to_bunny( $id );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array( 'cdn_url' => $result ) );
}
/* ------------------------------------------------------------------
BunnyCDN: Tek görsel yükle — CDN URL'sini döndürür
------------------------------------------------------------------ */
private function upload_to_bunny( $post_id ) {
$api_key = get_option( 'erdo_bunny_api_key', '' );
$storage_zone = get_option( 'erdo_bunny_storage_zone', '' );
$cdn_hostname = get_option( 'erdo_bunny_hostname', '' );
if ( ! $api_key || ! $storage_zone ) {
return new WP_Error( 'no_config', 'BunnyCDN API key or storage zone not configured.' );
}
$file_path = get_attached_file( $post_id );
if ( ! $file_path || ! file_exists( $file_path ) ) {
return new WP_Error( 'not_found', 'File not found.' );
}
$upload_dir = wp_upload_dir();
$rel_path = ltrim( str_replace( $upload_dir['basedir'], '', $file_path ), '/' );
$cdn_path = $storage_zone . '/' . $rel_path;
$endpoint = 'https://storage.bunnycdn.com/' . $cdn_path;
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$file_data = file_get_contents( $file_path );
if ( false === $file_data ) {
return new WP_Error( 'read_failed', 'Could not read file.' );
}
$response = wp_remote_request( $endpoint, array(
'method' => 'PUT',
'headers' => array(
'AccessKey' => $api_key,
'Content-Type' => 'application/octet-stream',
),
'body' => $file_data,
'timeout' => 60,
) );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code >= 300 ) {
return new WP_Error( 'upload_failed', 'BunnyCDN upload failed. HTTP ' . $code );
}
$hostname = $cdn_hostname ?: ( $storage_zone . '.b-cdn.net' );
$cdn_url = 'https://' . trailingslashit( $hostname ) . $rel_path;
update_post_meta( $post_id, '_erdo_cdn_url', $cdn_url );
return $cdn_url;
}
/* ------------------------------------------------------------------
DASHBOARD WIDGET
------------------------------------------------------------------ */
public function register_dashboard_widget() {
wp_add_dashboard_widget(
'erdo_image_optimizer_widget',
__( 'Image Optimizer', 'erdo-image-optimizer' ),
array( $this, 'render_dashboard_widget' )
);
}
public function render_dashboard_widget() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$cache_key = 'erdo_stats_cache';
$stats = wp_cache_get( $cache_key, 'erdo_image_optimizer' );
if ( false === $stats ) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$total = (int) $wpdb->get_var( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type='attachment' AND post_mime_type LIKE 'image/%'" );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$opt = (int) $wpdb->get_var( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type='attachment' AND post_mime_type IN ('image/webp','image/avif')" );
$stats = array(
'total' => $total,
'optimized' => $opt,
'opt_score' => $total > 0 ? round( ( $opt / $total ) * 100 ) : 0,
'total_saved' => size_format( (int) get_option( 'erdo_total_saved_bytes', 0 ), 1 ),
);
}
$url = admin_url( 'admin.php?page=erdo-image-optimizer' );
?>
WP_REST_Server::READABLE,
'callback' => array( $this, 'rest_stats' ),
'permission_callback' => array( $this, 'rest_permission' ),
) );
register_rest_route( 'erdo/v1', '/audit', array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'rest_audit' ),
'permission_callback' => array( $this, 'rest_permission' ),
'args' => array(
'page' => array( 'default' => 1, 'sanitize_callback' => 'absint' ),
'status' => array( 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ),
),
) );
}
public function rest_permission() {
return current_user_can( 'manage_options' );
}
public function rest_stats( WP_REST_Request $request ) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$total = (int) $wpdb->get_var( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type='attachment' AND post_mime_type LIKE 'image/%'" );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$opt = (int) $wpdb->get_var( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type='attachment' AND post_mime_type IN ('image/webp','image/avif')" );
return rest_ensure_response( array(
'total' => $total,
'optimized' => $opt,
'opt_rate' => $total > 0 ? round( ( $opt / $total ) * 100 ) : 0,
'total_saved' => (int) get_option( 'erdo_total_saved_bytes', 0 ),
'format' => get_option( 'erdo_convert_format', 'webp' ),
) );
}
public function rest_audit( WP_REST_Request $request ) {
$page = $request->get_param( 'page' );
$status_filter = $request->get_param( 'status' );
$per_page = 50;
$query = new WP_Query( array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'post_mime_type' => 'image',
'posts_per_page' => $per_page,
'paged' => $page,
'fields' => 'ids',
) );
$results = ERDO_Auditor::bulk_analyze( $query->posts );
$items = array();
foreach ( $results as $id => $r ) {
$cat = ( isset( $r['is_used'] ) && false === $r['is_used'] ) ? 'unused' : $r['status'];
if ( $status_filter && $status_filter !== $cat ) {
continue;
}
$items[] = array(
'id' => $id,
'filename' => $r['filename'],
'score' => $r['score'],
'status' => $cat,
'mime' => $r['mime'] ?? '',
'is_used' => $r['is_used'],
'edit_url' => get_edit_post_link( $id, 'raw' ),
);
}
return rest_ensure_response( array(
'items' => $items,
'page' => $page,
'max_pages' => $query->max_num_pages,
'total' => $query->found_posts,
) );
}
}