$conf, $runtime; function_exists('chdir') AND chdir(APP_PATH); $r = 'mysql' == $conf['cache']['type'] ? website_set('runtime', $runtime) : cache_set('runtime', $runtime); } function runtime_truncate() { global $conf; 'mysql' == $conf['cache']['type'] ? website_set('runtime', '') : cache_delete('runtime'); } register_shutdown_function('runtime_save'); ?>Memory Leak in plugin action|Programmer puzzle solving
最新消息:Welcome to the puzzle paradise for programmers! Here, a well-designed puzzle awaits you. From code logic puzzles to algorithmic challenges, each level is closely centered on the programmer's expertise and skills. Whether you're a novice programmer or an experienced tech guru, you'll find your own challenges on this site. In the process of solving puzzles, you can not only exercise your thinking skills, but also deepen your understanding and application of programming knowledge. Come to start this puzzle journey full of wisdom and challenges, with many programmers to compete with each other and show your programming wisdom! Translated with DeepL.com (free version)

Memory Leak in plugin action

matteradmin9PV0评论

I made a plugin that basically reads a CSV file and imports data to relevant tables.

However, the action seems to create an error:

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 65015808 bytes) in /var/www/proj/wp-includes/functions.php on line

which led me to this code in functions.php:

function wp_ob_end_flush_all() {
    $levels = ob_get_level();
    for ($i=0; $i<$levels; $i++)
        ob_end_flush();
}

I did a Google and came across two popular solutions, both of which didn't seem to work.

Solution 1: disabling zlib - this is already disabled.
Solution 2: remove_action('shutdown', 'wp_ob_end_flush_all', 1);

Solution 2 still errors but with no message, which, isn't exactly ideal.

This is the script that's causing the error:

<?php
    ini_set('display_startup_errors', 1);
    ini_set('display_errors', 1);
    error_reporting(-1);

    # load core wp fnc
    require_once $_SERVER['DOCUMENT_ROOT']. '/wp-load.php';
    # load db functions
    require_once $_SERVER['DOCUMENT_ROOT']. '/wp-admin/includes/upgrade.php';
    # load admin fnc
    require_once $_SERVER['DOCUMENT_ROOT']. '/wp-content/plugins/vendor-module/admin/inc/admin-functions.php';

    global $wpdb;

    $admin_functions = new vendor_module_admin_functions();

    # get csv
    $file = $_FILES['csv'];

    $name = $file['name'];
    $dir = $file['tmp_name'];

    # rm spaces, replace with _
    $name = str_replace(' ', '_', $name);

    $file_location = $_SERVER['DOCUMENT_ROOT']. '/wp-content/plugins/vendor-module/uploads/import/'. $name;

    # if successfully moved, carry on, else return
    $moved = ($file['error'] == 0 ? true : false);

    $error = false;

    if (!$moved) {
        echo 'Error! CSV file may be incorrectly formatted or there was an issue in reading the file. Please try again.';
    } else {
        move_uploaded_file($dir, $file_location);

        $id = $_POST['val'];
        $type = $_POST['type'];

        $table = ($type == 1 ? 'vendor_module_type_one' : 'vendor_module_type_two');

        $handle = fopen($file_location, 'r');

        $parts = array();
        $components = array();

        $i = 0;

        while (($data = fgetcsv($handle, 1000, ',')) !== false)
        {
            if (is_array($data)) {
                if (empty($data[0])) {
                    echo 'Error! Reference can\'t be empty. Please ensure all rows are using a ref no.';
                    $error = true;

                    continue;
                }

                # get data
                $get_for_sql = 'SELECT `id` FROM `'. $wpdb->prefix. $table .'` WHERE `ref` = %s';
                $get_for_res = $wpdb->get_results($wpdb->prepare($get_for_sql, array($data[0])));

                if (count($get_for_res) <= 0) {
                    echo 'Error! Reference has no match. Please ensure the CSV is using the correct ref no.';
                    $error = true;

                    exit();
                }

                $get_for_id = $get_for_res[0]->id;

                # create data arrays
                $parts[$i]['name'] = $data[1];
                $parts[$i]['ref'] = $data[2];
                $parts[$i]['for'] = $get_for_id;

                $components[$i]['part_ref'] = $data[2];
                $components[$i]['component_ref'] = $data[3];
                $components[$i]['sku'] = $data[4];
                $components[$i]['desc'] = utf8_decode($data[5]);
                $components[$i]['req'] = $data[6];
                $components[$i]['price'] = $data[7];

                unset($get_for_id);
                unset($get_for_res);
                unset($get_for_sql);

                $i++;
            }
        }

        fclose($handle);
        unlink($file_location);

        # get unique parts only
        $parts = array_unique($parts, SORT_REGULAR);

        # check to see if part already exists, if so delete
        $search_field = ($type == 1 ? 'id_field_one' : 'id_field_two');

        $check_sql = 'SELECT `id` FROM `'. $wpdb->prefix .'vendor_module_parts` WHERE `'. $search_field .'` = %d';
        $delete_parts_sql = 'DELETE FROM `'. $wpdb->prefix .'vendor_module_parts` WHERE `'. $search_field .'` = %d';
        $delete_components_sql = 'DELETE FROM `'. $wpdb->prefix .'vendor_module_components` WHERE `part_id` = %d';

        $check_res = $wpdb->get_results($wpdb->prepare($check_sql, array($id)));

        if ($check_res) {
            $wpdb->query($wpdb->prepare($delete_parts_sql, array($id)));
        }

        $part_ids = $admin_functions->insert_parts($parts, $type);

        unset($parts);
        unset($delete_parts_sql);
        unset($search_field);
        unset($check_sql);
        unset($check_res);
        unset($i);

        # should never be empty, but just as a precaution ...
        if (!empty($part_ids)) {
            foreach ($components as $key => $component)
            {
                $components[$key]['part_id'] = $part_ids[$component['part_ref']];
            }

            # rm components from assoc part id
            foreach ($part_ids as $id)
            {
                $wpdb->query($wpdb->prepare($delete_components_sql, array($id)));
            }

            # insert components
            $admin_functions->insert_components($components);
        } else {
            echo 'Error!';
        }

        echo (!$error ? 'Success! File Successfully Imported.' : 'There be something wrong with the import. Please try again.');
    }

it's triggered through a button press and uses AJAX to handle it etc.

I'm not sure why a memory leak is occurring or why WordPress doesn't offer more useful error messages. I don't call that function.. so I'm guessing it's something WordPress is doing in the background when things are run.

My info:

PHP 7.2.10
Apache 2.4
Linux Mint 19

Also happens on my server:

PHP 7.1.25
Apache 2.4
CentOS 7.6.1810

WordPress running version: 4.9.8

I made a plugin that basically reads a CSV file and imports data to relevant tables.

However, the action seems to create an error:

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 65015808 bytes) in /var/www/proj/wp-includes/functions.php on line

which led me to this code in functions.php:

function wp_ob_end_flush_all() {
    $levels = ob_get_level();
    for ($i=0; $i<$levels; $i++)
        ob_end_flush();
}

I did a Google and came across two popular solutions, both of which didn't seem to work.

Solution 1: disabling zlib - this is already disabled.
Solution 2: remove_action('shutdown', 'wp_ob_end_flush_all', 1);

Solution 2 still errors but with no message, which, isn't exactly ideal.

This is the script that's causing the error:

<?php
    ini_set('display_startup_errors', 1);
    ini_set('display_errors', 1);
    error_reporting(-1);

    # load core wp fnc
    require_once $_SERVER['DOCUMENT_ROOT']. '/wp-load.php';
    # load db functions
    require_once $_SERVER['DOCUMENT_ROOT']. '/wp-admin/includes/upgrade.php';
    # load admin fnc
    require_once $_SERVER['DOCUMENT_ROOT']. '/wp-content/plugins/vendor-module/admin/inc/admin-functions.php';

    global $wpdb;

    $admin_functions = new vendor_module_admin_functions();

    # get csv
    $file = $_FILES['csv'];

    $name = $file['name'];
    $dir = $file['tmp_name'];

    # rm spaces, replace with _
    $name = str_replace(' ', '_', $name);

    $file_location = $_SERVER['DOCUMENT_ROOT']. '/wp-content/plugins/vendor-module/uploads/import/'. $name;

    # if successfully moved, carry on, else return
    $moved = ($file['error'] == 0 ? true : false);

    $error = false;

    if (!$moved) {
        echo 'Error! CSV file may be incorrectly formatted or there was an issue in reading the file. Please try again.';
    } else {
        move_uploaded_file($dir, $file_location);

        $id = $_POST['val'];
        $type = $_POST['type'];

        $table = ($type == 1 ? 'vendor_module_type_one' : 'vendor_module_type_two');

        $handle = fopen($file_location, 'r');

        $parts = array();
        $components = array();

        $i = 0;

        while (($data = fgetcsv($handle, 1000, ',')) !== false)
        {
            if (is_array($data)) {
                if (empty($data[0])) {
                    echo 'Error! Reference can\'t be empty. Please ensure all rows are using a ref no.';
                    $error = true;

                    continue;
                }

                # get data
                $get_for_sql = 'SELECT `id` FROM `'. $wpdb->prefix. $table .'` WHERE `ref` = %s';
                $get_for_res = $wpdb->get_results($wpdb->prepare($get_for_sql, array($data[0])));

                if (count($get_for_res) <= 0) {
                    echo 'Error! Reference has no match. Please ensure the CSV is using the correct ref no.';
                    $error = true;

                    exit();
                }

                $get_for_id = $get_for_res[0]->id;

                # create data arrays
                $parts[$i]['name'] = $data[1];
                $parts[$i]['ref'] = $data[2];
                $parts[$i]['for'] = $get_for_id;

                $components[$i]['part_ref'] = $data[2];
                $components[$i]['component_ref'] = $data[3];
                $components[$i]['sku'] = $data[4];
                $components[$i]['desc'] = utf8_decode($data[5]);
                $components[$i]['req'] = $data[6];
                $components[$i]['price'] = $data[7];

                unset($get_for_id);
                unset($get_for_res);
                unset($get_for_sql);

                $i++;
            }
        }

        fclose($handle);
        unlink($file_location);

        # get unique parts only
        $parts = array_unique($parts, SORT_REGULAR);

        # check to see if part already exists, if so delete
        $search_field = ($type == 1 ? 'id_field_one' : 'id_field_two');

        $check_sql = 'SELECT `id` FROM `'. $wpdb->prefix .'vendor_module_parts` WHERE `'. $search_field .'` = %d';
        $delete_parts_sql = 'DELETE FROM `'. $wpdb->prefix .'vendor_module_parts` WHERE `'. $search_field .'` = %d';
        $delete_components_sql = 'DELETE FROM `'. $wpdb->prefix .'vendor_module_components` WHERE `part_id` = %d';

        $check_res = $wpdb->get_results($wpdb->prepare($check_sql, array($id)));

        if ($check_res) {
            $wpdb->query($wpdb->prepare($delete_parts_sql, array($id)));
        }

        $part_ids = $admin_functions->insert_parts($parts, $type);

        unset($parts);
        unset($delete_parts_sql);
        unset($search_field);
        unset($check_sql);
        unset($check_res);
        unset($i);

        # should never be empty, but just as a precaution ...
        if (!empty($part_ids)) {
            foreach ($components as $key => $component)
            {
                $components[$key]['part_id'] = $part_ids[$component['part_ref']];
            }

            # rm components from assoc part id
            foreach ($part_ids as $id)
            {
                $wpdb->query($wpdb->prepare($delete_components_sql, array($id)));
            }

            # insert components
            $admin_functions->insert_components($components);
        } else {
            echo 'Error!';
        }

        echo (!$error ? 'Success! File Successfully Imported.' : 'There be something wrong with the import. Please try again.');
    }

it's triggered through a button press and uses AJAX to handle it etc.

I'm not sure why a memory leak is occurring or why WordPress doesn't offer more useful error messages. I don't call that function.. so I'm guessing it's something WordPress is doing in the background when things are run.

My info:

PHP 7.2.10
Apache 2.4
Linux Mint 19

Also happens on my server:

PHP 7.1.25
Apache 2.4
CentOS 7.6.1810

WordPress running version: 4.9.8

Share Improve this question edited Dec 11, 2018 at 16:10 treyBake asked Dec 11, 2018 at 15:57 treyBaketreyBake 12712 bronze badges 16
  • 1 Why are you using a custom php page and not defining your own AJAX hook or (even better) custom REST endpoint? If the csv is too large, can't you split this operation into multiple operations? – kero Commented Dec 11, 2018 at 16:10
  • @kero what do you mean? :) All I've done is in my script.js file added an ajax function to a .click which points to this page ... never really done WP development - more of a Symfony/Magento dev - used to this way, so just assumed it would be ok haha – treyBake Commented Dec 11, 2018 at 16:11
  • 2 You require some WP files. This is not the WordPress way, the documentation describes how to do AJAX or using the more modern REST API. I haven't looked too deep in your code but the thing is: you break WP flow by including some files, so it is possible that default WP behaviour is broken. Instead, if you do it the WP way, you can be sure, that things like shutdown handlers work as intended – kero Commented Dec 11, 2018 at 16:16
  • 1 @treyBake Create a custom REST endpoint - it is really easy. How is this a backwards way? You wouldn't go create a custom file and require 'vendor/symfony/process/InputStream.php';, but create a class, make use auf autoloading, etc. -> you would use symfony architecture. And here, you should use WP architecture – kero Commented Dec 11, 2018 at 16:24
  • 1 A WP CLI command would have been a much better way of doing this, avoiding both the memory and the PHP time execution limits. Also, WP Admin AJAX is an ancient API, use a REST API endpoint as kero suggests. Also consider that your CSV may be too large to fit in the available memory, there won't be a config option you can change to just fix it unless you start cleaning up variables after you're finished with them, or you modify php.ini to give the script more memory – Tom J Nowell Commented Dec 11, 2018 at 16:33
 |  Show 11 more comments

2 Answers 2

Reset to default 1

Depending on what exactly you're trying to achieve, I agree with Tom's comment, that a WP-CLI command might be better. The advantage is that the command runs from php directly on the server (usually has no max execution time, loads different php.ini, etc.) and you don't need to involve the webserver.


If that is not possible, the next best way is probably to create a custom REST endpoint. WordPress has a class WP_REST_Controller, usually I write classes that extend this and work from there. For simplicity the following example is not using inheritence, but I try to keep to the same lingo.

1. Register new route

New/custom routes are registered via register_rest_route() like so

$version = 1;
$namespace = sprintf('acme/v%u', $version);
$base = '/import';

\register_rest_route(
    $namespace,
    $base,
    [
        [
            'methods' => \WP_REST_Server::CREATABLE,
                         // equals ['POST','PUT','PATCH']

            'callback' => [$this, 'import_csv'],
            'permission_callback' => [$this, 'get_import_permissions_check'],
            'args' => [],
            // used for OPTIONS calls, left out for simplicity's sake
        ],
    ]
);

This will create a new route that you can call via

http://www.example/wp-json/acme/v1/import/
   default REST start-^       ^       ^
       namespace with version-|       |-base

2. Define permissions check

Maybe you need authentication? Use nonces?

public function get_import_permissions_check($request)
{
    //TODO: implement
    return true;
}

3. Create your actual endpoint callback

The method previously defined gets passed a WP_REST_Request object, use that to access request body, etc. To stay consistent, it is usually best to return a WP_REST_Response instead of custom printing of JSON or similar.

public function import_csv($request)
{
    $data = [];
    // do stuff
    return new \WP_REST_Response($data, 200);
}

If you do all of this in OOP style, you'll get the following class

class Import_CSV
{

    /**
     * register routes for this controller
     */
    public function register_routes()
    {
        $version = 1;
        $namespace = sprintf('acme/v%u', $version);
        $base = '/import';

        \register_rest_route(
            $namespace,
            $base,
            [
                [
                    'methods' => \WP_REST_Server::CREATABLE,
                    'callback' => [$this, 'import_csv'],
                    'permission_callback' => [$this, 'get_import_permissions_check'],
                    'args' => [],
                ],
            ]
        );
    }

    /**
     * endpoint for POST|PUT|PATCH /acme/v1/import
     */
    public function import_csv($request)
    {
        $data = [];
        // do stuff
        return new \WP_REST_Response($data, 200);
    }

    /**
     * check if user is permitted to access the import route
     */
    public function get_import_permissions_check($request)
    {
        //TODO: implement
        return true;
    }

}

But .. still 404? Yes, simply defining the class sadly doesn't work (no autoloading by default :( ), so we need to run register_routes() like so (in your plugin file)

require_once 'Import_CSV.php';
add_action('rest_api_init', function(){
    $import_csv = new \Import_CSV;
    $import_csv->register_routes();
});

In your functions file do something like this:

add_action('wp_ajax_import_parts_abc', 'import_parts_abc');
function import_parts_abc() {
    include_once('/admin/inc/scripts/import-parts.php');
    exit();

}

Then in your js file something like this:

jQuery(document).ready(function() {
     jQuery('.my-button').click(function() {
jQuery.ajax({  
            type: 'GET',  
            url: ajaxurl,  
            data: { 
                action : 'import_parts_abc'
            },  
            success: function(textStatus){
                alert('processing complete')
            },
            error: function(MLHttpRequest, textStatus, errorThrown){  
                console.log(errorThrown);  
            }  
      }); 
});

});

Articles related to this article

Post a comment

comment list (0)

  1. No comments so far