• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to implement custom Filesystem API endpoints with token authentication in Gutenberg blocks

How to implement custom Filesystem API endpoints with token authentication in Gutenberg blocks

Understanding the Need for Custom Filesystem API Endpoints

WordPress’s built-in Filesystem API, while powerful, often exposes more than necessary for specific use cases, especially when integrating with custom Gutenberg blocks. For instance, a block might only need to upload images to a designated, secure directory, or retrieve specific configuration files. Directly exposing the entire filesystem via the REST API or standard WordPress functions can be a significant security risk. This post details how to create custom, token-authenticated API endpoints that provide granular control over filesystem operations, enhancing both security and functionality for your Gutenberg blocks.

Setting Up Token Authentication

Before we dive into filesystem operations, robust authentication is paramount. We’ll implement a simple token-based authentication mechanism. This involves generating a unique token for each user or application and requiring it in the request headers. For this example, we’ll store the token in user meta. In a production environment, consider more sophisticated methods like JWT or OAuth.

Generating and Storing User Tokens

We’ll create a function to generate a secure token and store it in the user’s meta data. This can be triggered manually or via a user profile setting.

function my_generate_secure_token( $user_id ) {
    if ( ! current_user_can( 'edit_user', $user_id ) ) {
        return new WP_Error( 'rest_forbidden', esc_html__( 'You do not have permission to generate tokens for this user.', 'your-text-domain' ), array( 'status' => 403 ) );
    }

    $token = wp_generate_password( 64, false ); // Generate a 64-character random string
    update_user_meta( $user_id, 'my_filesystem_api_token', $token );
    return $token;
}

// Example usage:
// $user_id = get_current_user_id();
// $token = my_generate_secure_token( $user_id );
// if ( ! is_wp_error( $token ) ) {
//     echo "Your API Token: " . esc_html( $token );
// }

Validating the Token

We need a callback function to check for the token in the request header and verify its validity against the stored user meta.

function my_authenticate_api_request( WP_REST_Request $request ) {
    $token = $request->get_header( 'X-API-Token' ); // Expecting token in 'X-API-Token' header

    if ( empty( $token ) ) {
        return new WP_Error( 'rest_not_authenticated', esc_html__( 'Authentication token is missing.', 'your-text-domain' ), array( 'status' => 401 ) );
    }

    // Find the user associated with this token
    $user_id = null;
    $users = get_users( array(
        'meta_key' => 'my_filesystem_api_token',
        'meta_value' => $token,
    ) );

    if ( ! empty( $users ) ) {
        $user_id = $users[0]->ID;
    }

    if ( empty( $user_id ) ) {
        return new WP_Error( 'rest_invalid_token', esc_html__( 'Invalid authentication token.', 'your-text-domain' ), array( 'status' => 403 ) );
    }

    // Optionally, you can set the current user for WordPress functions
    wp_set_current_user( $user_id );

    return true; // Authentication successful
}

Registering Custom REST API Endpoints

We’ll use the `rest_api_init` action hook to register our custom endpoints. These endpoints will handle file uploads and retrieval.

File Upload Endpoint

This endpoint will allow authenticated users to upload files to a specific, predefined directory. We’ll enforce file type and size restrictions.

add_action( 'rest_api_init', function () {
    register_rest_route( 'my-filesystem/v1', '/upload', array(
        'methods' => WP_REST_Server::CREATABLE, // Equivalent to POST
        'callback' => 'my_handle_file_upload',
        'permission_callback' => 'my_authenticate_api_request',
        'args' => array(
            'file' => array(
                'required' => true,
                'type' => 'string',
                'description' =>'The base64 encoded file content.',
                'sanitize_callback' => 'my_sanitize_base64_file',
                'validate_callback' => 'my_validate_uploaded_file',
            ),
            'filename' => array(
                'required' => true,
                'type' => 'string',
                'description' =>'The name of the file to be uploaded.',
                'sanitize_callback' => 'sanitize_file_name',
                'validate_callback' => 'my_validate_filename',
            ),
            'target_dir' => array(
                'required' => false,
                'type' => 'string',
                'description' =>'Optional subdirectory within the designated upload path.',
                'sanitize_callback' => 'sanitize_text_field',
            ),
        ),
    ) );
} );

// Helper functions for validation and sanitization
function my_sanitize_base64_file( $data ) {
    // Basic check to ensure it looks like base64
    if ( ! preg_match( '/^[a-zA-Z0-9\/\r\n+]*={0,2}/', $data ) ) {
        return false;
    }
    return $data;
}

function my_validate_uploaded_file( $data, $request, $param ) {
    $decoded_data = base64_decode( $data );
    if ( $decoded_data === false ) {
        return new WP_Error( 'rest_invalid_file_data', esc_html__( 'File data is not valid base64.', 'your-text-domain' ), array( 'status' => 400 ) );
    }

    // File size limit (e.g., 5MB)
    $max_file_size = 5 * 1024 * 1024;
    if ( strlen( $decoded_data ) > $max_file_size ) {
        return new WP_Error( 'rest_file_too_large', sprintf( esc_html__( 'File is too large. Maximum size is %s.', 'your-text-domain' ), size_format( $max_file_size ) ), array( 'status' => 413 ) );
    }

    // File type validation (example: allow only images)
    $finfo = finfo_open( FILEINFO_MIME_TYPE );
    $mime_type = finfo_buffer( $finfo, $decoded_data );
    finfo_close( $finfo );

    $allowed_mime_types = array( 'image/jpeg', 'image/png', 'image/gif' );
    if ( ! in_array( $mime_type, $allowed_mime_types ) ) {
        return new WP_Error( 'rest_invalid_file_type', esc_html__( 'Invalid file type. Only JPEG, PNG, and GIF are allowed.', 'your-text-domain' ), array( 'status' => 415 ) );
    }

    return true;
}

function my_validate_filename( $filename, $request, $param ) {
    // Prevent directory traversal and other malicious filenames
    $sanitized_filename = sanitize_file_name( $filename );
    if ( $filename !== $sanitized_filename || strpos( $filename, '..' ) !== false || strpos( $filename, '/' ) !== false || strpos( $filename, '\\' ) !== false ) {
        return new WP_Error( 'rest_invalid_filename', esc_html__( 'Invalid filename.', 'your-text-domain' ), array( 'status' => 400 ) );
    }
    return true;
}

function my_handle_file_upload( WP_REST_Request $request ) {
    $file_data_base64 = $request->get_param( 'file' );
    $filename = $request->get_param( 'filename' );
    $target_dir = $request->get_param( 'target_dir' );

    $decoded_file_data = base64_decode( $file_data_base64 );

    // Define the base upload directory. This should be a secure, non-web-accessible location if possible.
    // For simplicity, we'll use wp-content/uploads/custom-uploads, but ensure this is configured securely.
    $upload_dir = wp_upload_dir();
    $base_upload_path = trailingslashit( $upload_dir['basedir'] ) . 'custom-uploads/';

    // Create the base directory if it doesn't exist
    if ( ! wp_mkdir_p( $base_upload_path ) ) {
        return new WP_Error( 'rest_directory_creation_failed', esc_html__( 'Failed to create upload directory.', 'your-text-domain' ), array( 'status' => 500 ) );
    }

    // Append target directory if provided
    if ( ! empty( $target_dir ) ) {
        $target_dir_path = trailingslashit( $base_upload_path ) . sanitize_text_field( $target_dir );
        if ( ! wp_mkdir_p( $target_dir_path ) ) {
            return new WP_Error( 'rest_directory_creation_failed', esc_html__( 'Failed to create target subdirectory.', 'your-text-domain' ), array( 'status' => 500 ) );
        }
        $destination_path = trailingslashit( $target_dir_path ) . sanitize_file_name( $filename );
    } else {
        $destination_path = trailingslashit( $base_upload_path ) . sanitize_file_name( $filename );
    }

    // Ensure the file doesn't already exist to prevent overwrites unless intended
    if ( file_exists( $destination_path ) ) {
        return new WP_Error( 'rest_file_exists', esc_html__( 'A file with this name already exists.', 'your-text-domain' ), array( 'status' => 409 ) );
    }

    // Write the file
    if ( file_put_contents( $destination_path, $decoded_file_data ) === false ) {
        return new WP_Error( 'rest_file_write_failed', esc_html__( 'Failed to write file to disk.', 'your-text-domain' ), array( 'status' => 500 ) );
    }

    // Return success response
    return new WP_REST_Response( array(
        'success' => true,
        'message' => esc_html__( 'File uploaded successfully.', 'your-text-domain' ),
        'file_path' => str_replace( ABSPATH, '/', $destination_path ), // Relative path from WordPress root
    ), 201 );
}

File Retrieval Endpoint

This endpoint allows authenticated users to retrieve files from a specific directory. It’s crucial to restrict access to only intended files.

add_action( 'rest_api_init', function () {
    register_rest_route( 'my-filesystem/v1', '/get/(?P<filepath>.*)', array(
        'methods' => WP_REST_Server::READABLE, // Equivalent to GET
        'callback' => 'my_handle_file_get',
        'permission_callback' => 'my_authenticate_api_request',
        'args' => array(
            'filepath' => array(
                'required' => true,
                'description' =>'The relative path of the file to retrieve.',
                'validate_callback' => 'my_validate_retrieval_filepath',
            ),
        ),
    ) );
} );

function my_validate_retrieval_filepath( $filepath, $request, $param ) {
    // Sanitize and validate the filepath to prevent directory traversal
    $sanitized_filepath = sanitize_text_field( $filepath );
    if ( $filepath !== $sanitized_filepath || strpos( $filepath, '..' ) !== false || strpos( $filepath, '/' ) === 0 || strpos( $filepath, '\\' ) !== false ) {
        return new WP_Error( 'rest_invalid_filepath', esc_html__( 'Invalid file path.', 'your-text-domain' ), array( 'status' => 400 ) );
    }

    // Define the base upload directory. This must match the upload endpoint's base.
    $upload_dir = wp_upload_dir();
    $base_upload_path = trailingslashit( $upload_dir['basedir'] ) . 'custom-uploads/';

    $full_path = trailingslashit( $base_upload_path ) . $sanitized_filepath;

    // Ensure the file exists and is within the allowed directory
    if ( ! file_exists( $full_path ) || ! is_readable( $full_path ) ) {
        return new WP_Error( 'rest_file_not_found', esc_html__( 'File not found or not readable.', 'your-text-domain' ), array( 'status' => 404 ) );
    }

    // Further security: Ensure the resolved path is still within our intended base directory
    if ( realpath( $full_path ) === false || strpos( realpath( $full_path ), realpath( $base_upload_path ) ) !== 0 ) {
        return new WP_Error( 'rest_access_denied', esc_html__( 'Access denied.', 'your-text-domain' ), array( 'status' => 403 ) );
    }

    return true;
}

function my_handle_file_get( WP_REST_Request $request ) {
    $filepath = $request->get_param( 'filepath' );

    $upload_dir = wp_upload_dir();
    $base_upload_path = trailingslashit( $upload_dir['basedir'] ) . 'custom-uploads/';
    $full_path = trailingslashit( $base_upload_path ) . sanitize_text_field( $filepath );

    // Double-check existence and readability (already validated, but good practice)
    if ( ! file_exists( $full_path ) || ! is_readable( $full_path ) ) {
        return new WP_Error( 'rest_file_not_found', esc_html__( 'File not found or not readable.', 'your-text-domain' ), array( 'status' => 404 ) );
    }

    // Determine MIME type for correct Content-Type header
    $finfo = finfo_open( FILEINFO_MIME_TYPE );
    $mime_type = finfo_file( $finfo, $full_path );
    finfo_close( $finfo );

    $response = new WP_REST_Response();
    $response->set_content( file_get_contents( $full_path ) );
    $response->set_status( 200 );
    $response->set_headers( array(
        'Content-Type' => $mime_type,
        'Content-Disposition' => 'attachment; filename="' . basename( $full_path ) . '"',
    ) );

    return $response;
}

Integrating with Gutenberg Blocks

In your Gutenberg block’s JavaScript, you’ll need to make authenticated requests to these endpoints. This typically involves using the `wp.apiFetch` utility, which can be configured to include custom headers.

Client-Side JavaScript Example

Here’s a conceptual example of how you might handle file uploads from a Gutenberg block’s edit function. You’ll need to retrieve the user’s token (e.g., from a REST API endpoint that exposes it securely, or passed via `wp_localize_script`).

// Assuming you have the user's token available in a variable `userApiToken`
// and the REST API URL is available in `restApiUrl`

async function uploadFileToGutenbergBlock( file, filename, targetDir = '' ) {
    const formData = new FormData();
    formData.append( 'file', file ); // The actual File object
    formData.append( 'filename', filename );
    if ( targetDir ) {
        formData.append( 'target_dir', targetDir );
    }

    // Convert File object to Base64 for the API endpoint
    const reader = new FileReader();
    reader.readAsDataURL( file );

    return new Promise( ( resolve, reject ) => {
        reader.onload = async () => {
            const base64File = reader.result.split( ',' )[1]; // Remove the data URI prefix

            try {
                const response = await wp.apiFetch( {
                    path: '/my-filesystem/v1/upload',
                    method: 'POST',
                    headers: {
                        'X-API-Token': userApiToken, // Your authenticated token
                        'Content-Type': 'application/json', // Sending JSON payload
                    },
                    body: JSON.stringify( {
                        file: base64File,
                        filename: filename,
                        target_dir: targetDir,
                    } ),
                } );
                resolve( response );
            } catch ( error ) {
                console.error( 'File upload failed:', error );
                reject( error );
            }
        };
        reader.onerror = ( error ) => reject( error );
    } );
}

// Example usage within a block's edit component:
// const handleFileChange = async ( event ) => {
//     const file = event.target.files[0];
//     if ( ! file ) return;
//
//     try {
//         const result = await uploadFileToGutenbergBlock( file, file.name, 'block-assets' );
//         console.log( 'Upload successful:', result );
//         // Update block attributes with the file path or URL
//     } catch ( error ) {
//         console.error( 'Upload failed:', error );
//         // Display error message to the user
//     }
// };

// To retrieve a file:
async function getFileFromGutenbergBlock( filePath ) {
    try {
        const response = await wp.apiFetch( {
            path: `/my-filesystem/v1/get/${encodeURIComponent( filePath )}`,
            method: 'GET',
            headers: {
                'X-API-Token': userApiToken,
            },
            // For binary data, you might need to handle response type differently
            // wp.apiFetch might not directly support binary responses easily.
            // Consider a direct fetch or a custom wrapper if needed.
        } );
        // The response here might be an object if JSON, or raw data if handled differently.
        // For binary files, you'd typically use a standard fetch API and handle Blob.
        console.log( 'File retrieved:', response );
        return response;
    } catch ( error ) {
        console.error( 'File retrieval failed:', error );
        throw error;
    }
}

Security Considerations and Best Practices

Implementing custom filesystem endpoints requires a strong focus on security:

  • Directory Traversal Prevention: Always sanitize and validate file paths rigorously to prevent users from accessing files outside the intended directories. Use functions like `sanitize_file_name`, `realpath`, and explicit path checks.
  • File Type and Size Restrictions: Enforce strict limits on allowed file types (using MIME types) and maximum file sizes to prevent denial-of-service attacks and malicious uploads.
  • Secure Storage: Store uploaded files in a directory that is not directly web-accessible if possible. If files need to be served, use a dedicated, secure serving mechanism or ensure proper `.htaccess` or Nginx configurations prevent direct execution of uploaded files.
  • Token Management: Implement secure token generation, storage, and revocation. Avoid storing tokens in client-side JavaScript directly; use `wp_localize_script` or fetch them securely. Consider token expiration and refresh mechanisms.
  • HTTPS: Always use HTTPS to encrypt communication, protecting tokens and file data in transit.
  • Error Handling: Provide informative but not overly revealing error messages. Avoid exposing internal server details.
  • Rate Limiting: Consider implementing rate limiting on your API endpoints to prevent brute-force attacks on tokens or excessive resource consumption.

Conclusion

By creating custom, token-authenticated REST API endpoints, you can provide Gutenberg blocks with secure and controlled access to filesystem operations. This approach significantly enhances the security posture of your WordPress site compared to exposing broader filesystem functionalities. Remember to adapt the token management and storage strategies to your specific application’s security requirements and always prioritize robust validation and sanitization.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Optimizing p99 database query response latency in multi-site Singleton Registry Pattern custom tables
  • Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using React components
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in customer support tickets
  • Optimizing p99 database query response latency in multi-site Domain-driven architecture (DDD) blocks custom tables
  • How to design a modular Action-hook Event Mediator architecture for enterprise-level custom plugins

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (41)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (68)
  • WordPress Plugin Development (73)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Optimizing p99 database query response latency in multi-site Singleton Registry Pattern custom tables
  • Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using React components
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in customer support tickets

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala