سال ۱۳۹۵ بود که اولین بار با یک سایت هک‌شده مواجه شدم که ریشه مشکل، یک افزونه اختصاصی بود که با تابع file_put_contents ساده، فایل‌های کش را در مسیر wp-content/uploads ذخیره می‌کرد. مهاجم از همین نقطه وارد شده بود و با نوشتن یک فایل PHP ساده در پوشه آپلود، دسترسی کامل به سایت پیدا کرده بود. آن روز فهمیدم که کار با فایل‌ها در وردپرس، هرگز نباید با توابع پایه PHP انجام شود. وردپرس یک لایه اختصاصی به نام WP_Filesystem دارد که هم امنیت را بالا می‌برد، هم سازگاری با محیط‌های مختلف (FTP، SSH، Direct) را تضمین می‌کند. از آن پروژه به بعد، در هیچ افزونه‌ای از توابع پایه PHP برای کار با فایل استفاده نکرده‌ام. این مقاله، تجربه‌ام از کار با توابع فایل در وردپرس است.

اگر با مفاهیم پایه آشنا نیستید، وردپرس چیست و چگونه شروع کنیم و نحوه استفاده از توابع وردپرس در پروژه‌ها پیش‌نیاز این مقاله است. مکمل این مقاله توابع مدیریت رسانه‌ها و ساختار فایل‌های افزونه استاندارد است.

چرا WP_Filesystem و نه توابع پایه PHP؟

بسیاری از توسعه‌دهندگان تازه‌کار، برای خواندن یا نوشتن فایل، از file_get_contents یا file_put_contents استفاده می‌کنند. این انتخاب در نگاه اول ساده است، ولی سه مشکل بنیادین دارد:

  • مشکل مجوز: در بعضی سرورها، مجوز مستقیم فایل غیرفعال است. اگر از توابع پایه استفاده کنید، افزونه شما در این سرورها کار نمی‌کند. WP_Filesystem، روش مناسب را خودش انتخاب می‌کند.
  • مشکل امنیت: توابع پایه PHP، هیچ لایه امنیتی روی مسیر فایل اعمال نمی‌کنند. اگر مسیر از ورودی کاربر بیاید، می‌تواند به حمله Path Traversal منجر شود. WP_Filesystem این لایه را اضافه می‌کند.
  • مشکل سازگاری: بعضی هاست‌ها، دسترسی مستقیم فایل را محدود می‌کنند و انتظار دارند از FTP یا SSH استفاده شود. WP_Filesystem این تفاوت‌ها را پنهان می‌کند.

راهنمای تکمیلی در PHP امن در وردپرس و امنیت وردپرس برای مبتدیان.

هر بار که در وردپرس فایلی می‌نویسید، یک درِ تازه به سایت باز می‌کنید؛ WP_Filesystem همان درِ استاندارد است که کلیدش را وردپرس نگه می‌دارد.

راه‌اندازی WP_Filesystem

برای استفاده از WP_Filesystem، ابتدا باید آن را راه‌اندازی کنید:

function myplugin_init_filesystem() {
    global $wp_filesystem;
    
    if ( empty( $wp_filesystem ) ) {
        require_once ABSPATH . "wp-admin/includes/file.php";
        WP_Filesystem();
    }
    
    return $wp_filesystem;
}

نکته مهم: این تابع، در هر نقطه‌ای از کد قابل فراخوانی نیست. اگر در front-end استفاده می‌کنید، باید مطمئن شوید که وردپرس در حالت admin-init بارگذاری شده. یک الگوی رایج در پروژه‌های من: در فعال‌سازی افزونه، این تابع را صدا می‌زنم و نتیجه را در یک متغیر ذخیره می‌کنم:

$fs = myplugin_init_filesystem();
if ( ! $fs ) {
    error_log( "WP_Filesystem قابل راه‌اندازی نیست" );
    return;
}

راهنمای توابع مدیریت فایل در توابع کار با فایل و توابع مدیریت رسانه‌ها.

خواندن فایل با WP_Filesystem

خواندن فایل، با متد get_contents انجام می‌شود:

$fs = myplugin_init_filesystem();

$file_path = WP_CONTENT_DIR . "/uploads/myplugin/config.json";
if ( $fs->exists( $file_path ) ) {
    $content = $fs->get_contents( $file_path );
    if ( false !== $content ) {
        $data = json_decode( $content, true );
    }
}

نکته: متد get_contents، در صورت خطا false برمی‌گرداند. همیشه بررسی کنید. تفاوت با file_get_contents پایه PHP، در همین exists است که یک بررسی مجوز انجام می‌دهد قبل از خواندن. راهنمای خواندن داده در توابع متادیتا و کار با Options API.

نوشتن فایل با WP_Filesystem

نوشتن فایل، با متد put_contents انجام می‌شود:

$fs = myplugin_init_filesystem();

$file_path = WP_CONTENT_DIR . "/uploads/myplugin/data.json";
$data = array(
    "version" => "1.0.0",
    "updated" => current_time( "mysql" ),
);

$content = wp_json_encode( $data, JSON_PRETTY_PRINT );
$result = $fs->put_contents( $file_path, $content );

if ( false === $result ) {
    error_log( "خطا در نوشتن فایل: " . $file_path );
}

نکته حیاتی: هرگز فایل PHP در wp-content/uploads ذخیره نکنید. این پوشه باید فقط رسانه باشد و اگر فایل اجرایی در آن باشد، خطرناک است. اگر نیاز به فایل PHP دارید، آن را در پوشه افزونه خودتان بگذارید. راهنمای امنیت در امنیت دیتابیس و امنیت وردپرس.

حذف فایل با WP_Filesystem

حذف فایل، با متد delete انجام می‌شود:

$fs = myplugin_init_filesystem();

$file_path = WP_CONTENT_DIR . "/uploads/myplugin/old-data.json";

if ( $fs->exists( $file_path ) ) {
    $result = $fs->delete( $file_path );
    if ( false === $result ) {
        error_log( "خطا در حذف فایل" );
    }
}

نکته: برای حذف پوشه، باید پارامتر دوم را true بگذارید:

$fs->delete( $dir_path, true );

ولی توجه داشته باشید که این کار، تمام محتویات پوشه را هم حذف می‌کند. همیشه پیش از حذف پوشه، یک بکاپ بگیرید. راهنمای بکاپ در بکاپ سایت وردپرسی و افزونه‌های بکاپ.

لیست فایل‌های یک پوشه

برای دریافت لیست فایل‌های یک پوشه:

$fs = myplugin_init_filesystem();

$dir_path = WP_CONTENT_DIR . "/uploads/myplugin/";
$files = $fs->dirlist( $dir_path, false, false );

if ( false !== $files ) {
    foreach ( $files as $path => $info ) {
        if ( $info["type"] === "f" ) {
            echo "فایل: " . esc_html( $path ) . " - " . $info["size"] . " بایت";
        }
    }
}

پارامترهای dirlist: یک — مسیر پوشه. دو — $include_hidden: شامل فایل‌های مخفی. سه — $recursive: شامل زیرپوشه‌ها. راهنمای کامل در توابع کار با فایل.

آپلود فایل با wp_handle_upload

برای آپلود فایل، وردپرس یک تابع اختصاصی دارد:

function myplugin_handle_file_upload() {
    // بررسی نانس
    if ( ! isset( $_POST["myplugin_nonce"] ) || 
         ! wp_verify_nonce( $_POST["myplugin_nonce"], "myplugin_upload" ) ) {
        wp_die( "درخواست نامعتبر" );
    }
    
    // بررسی دسترسی
    if ( ! current_user_can( "upload_files" ) ) {
        wp_die( "شما اجازه آپلود ندارید" );
    }
    
    // بررسی نوع فایل مجاز
    $allowed = array(
        "jpg|jpeg" => "image/jpeg",
        "png"      => "image/png",
        "webp"     => "image/webp",
        "pdf"      => "application/pdf",
    );
    
    $check = wp_check_filetype( $_FILES["myfile"]["name"], $allowed );
    if ( ! $check["ext"] ) {
        wp_die( "نوع فایل مجاز نیست" );
    }
    
    // آپلود
    $overrides = array(
        "test_form" => false,
        "mimes"     => $allowed,
    );
    
    $upload = wp_handle_upload( $_FILES["myfile"], $overrides );
    
    if ( isset( $upload["error"] ) ) {
        wp_die( "خطا در آپلود: " . esc_html( $upload["error"] ) );
    }
    
    // $upload شامل: file، url، type
    echo "فایل آپلود شد: " . esc_url( $upload["url"] );
}

نکته: wp_handle_upload فقط فایل را آپلود می‌کند، ولی آن را به عنوان ضمیمه وردپرس ثبت نمی‌کند. برای ثبت، از wp_insert_attachment و wp_generate_attachment_metadata استفاده کنید. راهنمای کامل در توابع مدیریت رسانه‌ها و بهینه‌سازی تصویر برای موبایل.

ثبت فایل به‌عنوان ضمیمه رسانه

برای ثبت فایل آپلود‌شده به‌عنوان رسانه وردپرس:

$upload = wp_handle_upload( $_FILES["myfile"], array( "test_form" => false ) );

if ( ! isset( $upload["error"] ) ) {
    $attachment = array(
        "post_mime_type" => $upload["type"],
        "post_title"      => sanitize_file_name( basename( $upload["file"] ) ),
        "post_content"    => "",
        "post_status"     => "inherit",
    );
    
    $attach_id = wp_insert_attachment( $attachment, $upload["file"] );
    
    require_once ABSPATH . "wp-admin/includes/image.php";
    $attach_data = wp_generate_attachment_metadata( $attach_id, $upload["file"] );
    wp_update_attachment_metadata( $attach_id, $attach_data );
}

نکته: wp_generate_attachment_metadata، اندازه‌های تصویر و متادیتا را می‌سازد. برای فایل‌های غیرتصویری، این تابع کاری انجام نمی‌دهد ولی به‌عنوان یک نقطه امن، همیشه آن را صدا بزنید. راهنمای کامل در توابع مدیریت رسانه‌ها و سئوی تصویر چیست.

کار با wp_upload_dir

برای دریافت مسیر پوشه آپلود، از wp_upload_dir استفاده کنید:

$upload_dir = wp_upload_dir();

echo $upload_dir["path"];    // مسیر فیزیکی
echo $upload_dir["url"];      // آدرس URL
echo $upload_dir["basedir"];  // مسیر پوشه uploads
echo $upload_dir["baseurl"];  // URL پوشه uploads

// ساخت زیرپوشه اختصاصی
$my_dir = $upload_dir["path"] . "/myplugin/";
if ( ! file_exists( $my_dir ) ) {
    wp_mkdir_p( $my_dir );
}

نکته: تابع wp_mkdir_p، پوشه را به‌صورت بازگشتی می‌سازد و در سرورهای مختلف سازگار است. راهنمای کامل در توابع کار با فایل.

خواندن و نوشتن در پوشه قالب

در بعضی پروژه‌ها، نیاز به ذخیره داده در پوشه قالب داریم (مثلاً فایل تنظیمات export شده):

$fs = myplugin_init_filesystem();

$theme_dir = get_stylesheet_directory();
$export_file = $theme_dir . "/exports/settings.json";

if ( ! $fs->exists( $theme_dir . "/exports/" ) ) {
    $fs->mkdir( $theme_dir . "/exports/" );
}

$fs->put_contents( $export_file, wp_json_encode( $settings ) );

نکته امنیتی: ذخیره داده در پوشه قالب، برای پروژه‌های چندساله توصیه نمی‌شود چون در انتقال به سرور دیگر ممکن است از دست برود. راه‌حل بهتر: ذخیره در wp-content/uploads/myplugin/ با دسترسی محافظت‌شده. راهنمای مسیرها در ساختار فایل‌های قالب استاندارد.

ذخیره داده در پوشه امن

برای ذخیره داده‌های حساس در پوشه آپلود، باید یک فایل .htaccess محافظت‌کننده اضافه کنید:

$upload_dir = wp_upload_dir();
$secure_dir = $upload_dir["basedir"] . "/myplugin-secure/";

if ( ! file_exists( $secure_dir ) ) {
    wp_mkdir_p( $secure_dir );
    
    // محافظت با .htaccess (برای Apache)
    $htaccess = $secure_dir . ".htaccess";
    $content = "<FilesMatch "\.(json|log|txt)$">
";
    $content .= "    Order allow,deny
";
    $content .= "    Deny from all
";
    $content .= "</FilesMatch>
";
    
    $fs = myplugin_init_filesystem();
    $fs->put_contents( $htaccess, $content );
}

نکته: این روش برای Apache کار می‌کند. برای Nginx، باید در فایل کانفیگ سرور اعمال شود. راهنمای کامل در فایروال نرم‌افزاری روی سرور و هدرهای امنیتی HTTP.

کار با JSON در فایل

پرتکرارترین کاربرد فایل، ذخیره داده‌های JSON است:

class My_Plugin_File_Storage {
    const DIR = "myplugin";
    
    public static function get_file_path( $filename ) {
        $upload = wp_upload_dir();
        return $upload["basedir"] . "/" . self::DIR . "/" . $filename;
    }
    
    public static function read( $filename ) {
        $fs = myplugin_init_filesystem();
        $path = self::get_file_path( $filename );
        
        if ( ! $fs->exists( $path ) ) {
            return null;
        }
        
        $content = $fs->get_contents( $path );
        if ( false === $content ) {
            return null;
        }
        
        $data = json_decode( $content, true );
        return ( json_last_error() === JSON_ERROR_NONE ) ? $data : null;
    }
    
    public static function write( $filename, $data ) {
        $fs = myplugin_init_filesystem();
        $dir = dirname( self::get_file_path( $filename ) );
        
        if ( ! $fs->exists( $dir ) ) {
            $fs->mkdir( $dir );
        }
        
        $content = wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
        return $fs->put_contents( self::get_file_path( $filename ), $content );
    }
    
    public static function delete( $filename ) {
        $fs = myplugin_init_filesystem();
        $path = self::get_file_path( $filename );
        
        if ( $fs->exists( $path ) ) {
            return $fs->delete( $path );
        }
        return true;
    }
}

مزیت این ساختار: یک نقطه دسترسی برای تمام ذخیره‌سازی فایلی، جلوگیری از تکرار کد، و امکان تست. الگوهای مشابه در کدنویسی اختصاصی افزونه و اصول کدنویسی تمیز.

خواندن و نوشتن CSV

برای Export داده به CSV:

function myplugin_export_csv( $data ) {
    $upload = wp_upload_dir();
    $file_path = $upload["basedir"] . "/myplugin-export-" . time() . ".csv";
    
    $handle = fopen( $file_path, "w" );
    if ( ! $handle ) {
        return false;
    }
    
    // افزودن BOM برای Excel فارسی
    fprintf( $handle, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) );
    
    // هدرها
    fputcsv( $handle, array( "ID", "عنوان", "تاریخ" ) );
    
    // داده‌ها
    foreach ( $data as $row ) {
        fputcsv( $handle, $row );
    }
    
    fclose( $handle );
    return $file_path;
}

نکته مهم: BOM برای نمایش صحیح UTF-8 در Excel لازم است. راهنمای کامل در توابع کار با فایل.

حذف فایل‌های قدیمی

پاک‌سازی دوره‌ای فایل‌های قدیمی، بخشی از نگهداری سایت است:

function myplugin_cleanup_old_files() {
    $fs = myplugin_init_filesystem();
    $upload = wp_upload_dir();
    $dir = $upload["basedir"] . "/myplugin-exports/";
    
    if ( ! $fs->exists( $dir ) ) {
        return;
    }
    
    $files = $fs->dirlist( $dir, false, false );
    if ( false === $files ) {
        return;
    }
    
    $threshold = time() - ( 30 * DAY_IN_SECONDS );
    
    foreach ( $files as $path => $info ) {
        if ( $info["type"] === "f" && $info["lastmod"] < $threshold ) {
            $fs->delete( $path );
        }
    }
}
add_action( "myplugin_daily_cleanup", "myplugin_cleanup_old_files" );

نکته: این کار را در cron انجام دهید، نه در هر لود سایت. راهنمای cron در عیب‌یابی cron وردپرس و زمان‌بندی cron وردپرس.

امنیت در کار با فایل

پنج قاعده الزامی در کار با فایل در وردپرس:

  1. همیشه WP_Filesystem، نه توابع پایه PHP. راهنمای PHP امن در PHP امن در وردپرس.
  2. پاک‌سازی نام فایل: sanitize_file_name برای حذف کاراکترهای خطرناک.
  3. اعتبارسنجی مسیر: همیشه با realpath و بررسی پیشوند مسیر، از Path Traversal جلوگیری کنید.
  4. بررسی نوع فایل: wp_check_filetype برای نوع، و بررسی MIME واقعی برای امنیت بیشتر.
  5. نبود فایل اجرایی در uploads: اگر فایل PHP در پوشه آپلود باشد، خطر جدی است.
// الگوی امن اعتبارسنجی مسیر
function myplugin_safe_path( $relative_path ) {
    $upload = wp_upload_dir();
    $base = realpath( $upload["basedir"] );
    $path = realpath( $base . "/" . $relative_path );
    
    if ( false === $path || strpos( $path, $base ) !== 0 ) {
        return false;
    }
    return $path;
}

راهنمای کامل در پاک‌سازی داده‌ها، اعتبارسنجی داده‌ها و امنیت دیتابیس.

اشتباهات رایج

جمع‌بندی

توابع کار با فایل در وردپرس، در چهار گروه اصلی خلاصه می‌شوند: WP_Filesystem برای خواندن و نوشتن، wp_handle_upload برای آپلود، wp_upload_dir برای مسیرها، و توابع کمکی مثل wp_mkdir_p. سه اصل را در پایان تاکید می‌کنم: اول، همیشه از WP_Filesystem استفاده کنید، نه توابع پایه PHP. دوم، نام فایل و مسیر را با sanitize_file_name و realpath اعتبارسنجی کنید. سوم، هیچ فایل PHP در پوشه uploads ذخیره نکنید.

اگر امروز یک کار در این مسیر انجام می‌دهید: به کد افزونه‌های فعلی خود نگاه کنید و ببینید آیا از file_put_contents یا file_get_contents استفاده کرده‌اید. هر مورد، یک نامزد بازبینی است. اگر تجربه‌ای از یک باگ امنیتی در کار با فایل دارید که با WP_Filesystem حل شد، در دیدگاه‌ها بنویسید — همان گزارش‌های واقعی، این راهنما را برای توسعه‌دهنده بعدی دقیق‌تر می‌کند. 📁