Skip to content

[WordPress Plugin Directory] Review in Progress: Pronamic Pay doneren met Mollie 🔃 2️⃣ #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
remcotolsma opened this issue Mar 25, 2025 · 1 comment
Assignees

Comments

@remcotolsma
Copy link
Member

👋 pronamic - welcome to your plugin review!

Thanks for uploading your plugin "Pronamic Pay doneren met Mollie", we can begin with the review.

We found issues with your plugin code and/or functionality preventing it from being approved immediately. We have pended your submission in order to help you correct all issues so that it may be approved and published.

Who are we?

We are a group of volunteers who help you identify common issues so that you can make your plugin more secure, compatible, reliable and compliant with the guidelines.

We are devoting our time to reviewing your plugin, we ask that you honor this by reading this email in its entirety, addressing any issues listed, testing your changes, and uploading a corrected version of your code if all is well.

The review process

  1. Your plugin is checked by a volunteer who will send you the issues found in this email. This is the current step.

  2. You will read this email in its entirety, checking each issue as well as the links to the documentation and examples provided. In case of any doubt, you will reply to this email asking for clarification.

  3. Then you will thoroughly fix any issues, test your plugin, upload a corrected version of your plugin and reply to this email.

  4. As soon as the volunteer is able, they/she/he will check your corrected plugin again. Please, be patient waiting for a reply.

    • If there are no further issues, the plugin will be approved 🎉

    • If there are still issues, the process will go back to step 1 until all the issues have been addressed 🫷

⚠️ When you reply we will be reviewing your entire plugin again, so please do not reply until you are sure you have addressed all of the issues listed, otherwise your submission will be delayed and eventually rejected.

Understanding the Review Queue

When you reply to this email, your plugin will re-enter a queue to be reviewed by a volunteer. Here's how the system works:

  • Priority when there is significant progress: Plugins submitted with meaningful improvements will be reviewed faster.

  • Fewer review cycles means faster review: If you've only needed 1 or 2 reviews, your plugin will likely be approved in a matter of days. However, if multiple reviews are needed, the wait time between reviews increases significantly and could take weeks or even months.

⭐ How to Speed Up Approval

  • To get your plugin approved faster, focus on thoroughly fixing all identified issues before resubmitting.

  • You will get faster approvals if you take the time to review and test your plugin instead of rushing to upload an update.

This approach encourages authors to invest time in improving their plugins.

Other details

Remember that in addition to code quality, security and functionality, we require all plugins to adhere to the guidelines that you accepted when submitting this plugin. Please keep them in mind when making changes to your plugin: https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/.

Finally, should you at any time wish to alter your permalink (aka the plugin slug) "pronamic-pay-doneren-met-mollie", you must explicitly tell us what you would like it to be. Just changing the display name is not sufficient, and we require you to clearly state your desired permalink. Remember, permalinks cannot be altered after approval.

List of issues found

## Not permitted files

A plugin typically consists of files related to the plugin functionality (php, js, css, txt, md) and maybe some multimedia files (png, svg, jpg) and / or data files (json, xml).

We have detected files that are not among of the files normally found in a plugin, are they necessary? If not, then those won't be allowed.

Optionally, you can use the wp dist-archive command from WP-CLI in conjunction with a .distignore file. This prevents unwanted files from being included in the distribution archive.

Example(s) from your plugin:

pronamic-pay-doneren-met-mollie/packages/wp-pay/core/.pronamic-build-ignore

## Out of Date Libraries

At least one of the 3rd party libraries you're using is out of date. Please upgrade to the latest stable version for better support and security. We do not recommend you use beta releases.

From your plugin:

automattic/jetpack-autoloader v3.1.3 ~ v5.0.5 Creates a custom autoloader for a plugin or theme.

## PHP libraries that might conflict with the same library loaded by other plugins

Your plugin has included a copy of a library in a way that might conflict with other copies of the same library loaded by other plugins.

Remember that all WordPress plugins share the same execution space, which means that you have to pay special attention to how libraries are loaded, as you might be trying to load a library that another plugin has already loaded.

Fortunately, there are ways to avoid these types of conflicts

  • Recommended: Using composer, a tool to manage PHP dependencies, is one of the most convenient and maintainable ways to load a PHP library, it will avoid this kind of conflicts and you'll easily manage the updates of the library.

  • Checking if the library was already loaded using class_exists or function_exists and avoid loading yourself if that's already loaded.

However, after getting around the conflicts of loading the same library, we come to another point of conflict: Dealing with different versions of the same library.

Your plugin may find that another plugin has loaded a different version of the same library (before your plugin was able to load it) that is incompatible with your code. While this may not be necessary in stable libraries when using well-established functions, there are some ways to handle this and make your plugin resilient:

  • Recommended: If you are using Composer, you can use one of the available tools to automatically assign your namespace to these libraries.

  • There are some libraries that provide tools to give a namespace or prefix to the generated library.

  • You can manually provide your namespace to this library. Since this would require you to modify the library, it may not be the most practical choice.

From your plugin:

\# pronamic-pay-doneren-met-mollie/packages/woocommerce/action-scheduler/action-scheduler.php

## No publicly documented resource for your generated/compressed content

In reviewing your plugin, we cannot find a non-compiled version of your javascript and/or css related source code.

In order to comply with our guidelines of human-readable code, we require you to include the source code and / or a link to the source code, this is true for your own code and for developer libraries you’ve included in your plugin. If you include a link, this may be in your source code, however we require you to also have it in your readme.

https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#4-code-must-be-mostly-human-readable

We strongly feel that one of the strengths of open source is the ability to review, observe, and adapt code. By maintaining a public directory of freely available code, we encourage and welcome future developers to engage with WordPress and push it forward.

That said, with the advent of larger and larger plugins using more complex libraries, people are making good use of build tools (such as composer or npm) to generate their distributed production code. In order to balance the need to keep plugin sizes smaller while still encouraging open source development, we require plugins to make the source code to any compressed files available to the public in an easy to find location, by documenting it in the readme.

For example, if you’ve made a Gutenberg plugin and used npm and webpack to compress and minify it, you must either include the source code within the published plugin or provide access to a public maintained source that can be reviewed, studied, and yes, forked.

We strongly recommend you include directions on the use of any build tools to encourage future developers.

From your plugin:

packages/wp-pay-gateways/mollie/assets/dist/wc-legacy-checkout.js:1  ...(()=>{class e{constructor(e,t,o){this.jQuery=e,this.body=t,this.form=o}setup(){this.jQuery(this.body).on("init\_checkout",(()=>this.initCheckout()))}initCheckout(){const e=this.form.querySelector(".pro... 

## Calling files remotely

Offloading images, js, css, and other scripts to your servers or any remote service (like Google, MaxCDN, jQuery.com etc) is disallowed. When you call remote data you introduce an unnecessary dependency on another site. If the file you're calling isn't a part of WordPress Core, then you should include it -locally- in your plugin, not remotely. If the file IS included in WordPress core, please call that instead.

An exception to this rule is if your plugin is performing a service. We will permit this on a case by case basis. Since this can be confusing we have some examples of what are not permitted:

  • Offloading jquery CSS files to Google - You should include the CSS in your plugin.

  • Inserting an iframe with a help doc - A link, or including the docs in your plugin is preferred.

  • Calling images from your own domain - They should be included in your plugin.

Here are some examples of what we would permit:

  • Calling font families from Google or their approved CDN (if GPL compatible)

  • API calls back to your server to process possible spam comments (like Akismet)

  • Offloading comments to your own servers (like Disqus)

  • oEmbed calls to a service provider (like Twitter or YouTube)

Please remove external dependencies from your plugin and, if possible, include all files within the plugin (that is not called remotely). If instead you feel you are providing a service, please re-write your readme.txt in a manner that explains the service, the servers being called, and if any account is needed to connect.

Example(s) from your plugin:

packages/wp-pay/core/src/Cards.php:208 $path = 'cards/' . $brand . '/card-' . $brand . '-logo-\_x80.svg';
packages/wp-pay/core/src/Core/PaymentMethods.php:569 $path = 'methods/' . $method\_slug . '/method-' . $method\_slug . '-' . $size . '.svg';
# ↳ Detected: methods//method--640x360.svg

## Determine files and directories locations correctly

WordPress provides several functions for easily determining where a given file or directory lives.

We detected that the way your plugin references some files, directories and/or URLs may not work with all WordPress setups. This happens because there are hardcoded references or you are using the WordPress internal constants.

Let's improve it, please check out the following documentation:

https://developer.wordpress.org/plugins/plugin-basics/determining-plugin-and-content-directories/

It contains all the functions available to determine locations correctly.

Most common cases in plugins can be solved using the following functions:

  • For where your plugin is located: plugin_dir_path() , plugin_dir_url() , plugins_url()

  • For the uploads directory: wp_upload_dir() (Note: If you need to write files, please do so in a folder in the uploads directory, not in your plugin directories).

Example(s) from your plugin:

packages/woocommerce/action-scheduler/classes/ActionScheduler\_SystemInformation.php:28 $plugin\_file = trailingslashit( WP\_PLUGIN\_DIR ) . $plugin\_file;
packages/woocommerce/action-scheduler/classes/WP\_CLI/System\_Command.php:156 $path = str\_replace( ABSPATH, '', $path );
packages/woocommerce/action-scheduler/classes/WP\_CLI/System\_Command.php:178 $path = str\_replace( ABSPATH, '', $path );

ℹ️ In order to determine your plugin location, you would need to use the __FILE__ variable for this to work properly.
Note that this variable depends on the location of the file making the call. As this can create confusion, a common practice is to save its value in a define() in the main file of your plugin so that you don't have to worry about this.

Example: Your main plugin file.

define( 'MYPREFIX\_PLUGIN\_FILE', \_\_FILE\_\_ );
define( 'MYPREFIX\_PLUGIN\_DIR', plugin\_dir\_path( \_\_FILE\_\_ ) );
define( 'MYPREFIX\_PLUGIN\_URL', plugin\_dir\_url( \_\_FILE\_\_ ) );

Example: Any file of your plugin.

require\_once MYPREFIX\_PLUGIN\_DIR . 'admin/class-init.php';


function myprefix\_scripts() {
 wp\_enqueue\_script( 'myprefix-script', MYPREFIX\_PLUGIN\_URL . 'js/script.js', array(), MYPREFIX\_VERSION );
 // Or alternatively
 wp\_enqueue\_script( 'myprefix-script', plugins\_url( 'js/script.js', MYPREFIX\_PLUGIN\_FILE ), array(), MYPREFIX\_VERSION );
}
add\_action( 'wp\_enqueue\_scripts', 'myprefix\_scripts' );

Example(s) from your plugin:

packages/woocommerce/action-scheduler/classes/ActionScheduler\_SystemInformation.php:28 $plugin\_file = trailingslashit( WP\_PLUGIN\_DIR ) . $plugin\_file;

## Don't Force Set PHP Limits Globally

While many plugins can need optimal settings for PHP, we ask you please not set them as global defaults.

Having defines like ini_set('memory_limit', '-1'); run globally (like on init or in the __construct() part of your code) means you'll be running that for everything on the site, which may cause your users to fall out of compliance with any limits or restrictions on their host.

If you must use those, you need to limit them specifically to only the exact functions that require them.

Example(s) from your plugin:

packages/woocommerce/action-scheduler/classes/ActionScheduler\_Compatibility.php:74 ini\_set('memory\_limit', $wp\_max\_limit);
packages/woocommerce/action-scheduler/classes/ActionScheduler\_Compatibility.php:68 ini\_set('memory\_limit', $filtered\_limit);
packages/pronamic/wp-number/src/Number.php:413 \\ini\_set($option, (string) $precision);
packages/pronamic/wp-number/src/Number.php:419 \\ini\_set($option, $ini\_serialize\_precision);
packages/woocommerce/action-scheduler/classes/ActionScheduler\_Compatibility.php:108 set\_time\_limit($raise\_by);

## Sanitization for register_setting()

Fields registered through register_setting() should be sanitized properly.

Fortunately, in this case, it is very easy to indicate which function should be used to sanitize the input. ✨

The third argument of this function accepts an array() in which you can add your sanitizing function in the sanitize_callback key.

Just like this:

register\_setting(
    'pluginprefix\_group',
    'pluginprefix\_option\_name',
    array(
        'type'              => 'string',
        'sanitize\_callback' => 'sanitize\_text\_field',
    )
);

Make sure you use a proper sanitization function (WordPress has plenty of them!) and add other options as needed.

Please, check out the register_setting() documentation for more information and code examples.

Example(s) from your plugin:

packages/wp-pay/core/src/Settings.php:103 \\register\_setting('pronamic\_pay', $id, \['type' => 'string'\]);
packages/wp-pay/core/src/Settings.php:77 \\register\_setting('pronamic\_pay', 'pronamic\_pay\_debug\_mode', \['type' => 'boolean', 'description' => 'Setting that can be used to trigger the “debug” mode throughout Pronamic Pay.', 'default' => false\]);
packages/wp-pay/core/src/Settings.php:87 \\register\_setting('pronamic\_pay', 'pronamic\_pay\_subscriptions\_processing\_disabled', \['type' => 'boolean', 'description' => 'Setting that can be used to disable processing of recurring payments.', 'default' => false\]);
packages/wp-pay/core/src/Settings.php:68 register\_setting('pronamic\_pay', 'pronamic\_pay\_uninstall\_clear\_data', \['type' => 'boolean', 'default' => false\]);

## Review: Missing permission_callback in register_rest_route() .

When using register_rest_route() to define custom REST API endpoints, it is crucial to include a proper permission_callback .

🔒 This callback function ensures that only authorized users can access or modify data through your endpoint.

Code example, checking that the user can change options:

register\_rest\_route( 'pronamic-pay-doneren-met-mollie/v1', '/my-endpoint', array(
    'methods' => 'GET',
    'callback' => 'pronamic-pay-doneren-met-mollie\_callback\_function',
    'permission\_callback' => function() {
        return current\_user\_can( 'manage\_options' );
    }
) );

Please check the register_rest_route() documentation and the current_user_can() documentation.

When a permission_callback is NOT Required:

There are valid use cases for public endpoints, such as publicly available data (e.g., posts, public metadata) or endpoints designed for unauthenticated access (e.g., fetching public stats or information).

In these cases, you should use __return_true as the permission_callback to indicate that the endpoint is intentionally public.

🔒 When a permission_callback IS Required:

For endpoints that involve sensitive data or actions (e.g., getting not public data, creating, updating, or deleting content).

In these cases, you should always implement proper permission checks.

Possible cases found on this plugin's code:

packages/wp-pay-gateways/mollie/src/WebhookController.php:83 \\register\_rest\_route(Integration::REST\_ROUTE\_NAMESPACE, '/payments/webhook/(?P<payment\_id>\\\\d+)', \['methods' => 'POST', 'callback' => \[$this, 'rest\_api\_mollie\_webhook\_payment'\], 'args' => \['payment\_id' => \['description' => \\\_\_('Payment ID.', 'pronamic-pay-doneren-met-mollie'), 'type' => 'string', 'required' => true\], 'id' => \['description' => \\\_\_('Mollie transaction ID.', 'pronamic-pay-doneren-met-mollie'), 'type' => 'string', 'required' => true\]\], 'permission\_callback' => '\_\_return\_true'\]);
packages/wp-pay-gateways/mollie/src/WebhookController.php:105 \\register\_rest\_route(Integration::REST\_ROUTE\_NAMESPACE, '/orders/webhook/(?P<payment\_id>\\\\d+)', \['methods' => 'POST', 'callback' => \[$this, 'rest\_api\_mollie\_webhook\_order'\], 'args' => \['payment\_id' => \['description' => \\\_\_('Payment ID.', 'pronamic-pay-doneren-met-mollie'), 'type' => 'string', 'required' => true\], 'id' => \['description' => \\\_\_('Mollie order ID.', 'pronamic-pay-doneren-met-mollie'), 'type' => 'string', 'required' => true\]\], 'permission\_callback' => '\_\_return\_true'\]);
packages/wp-pay-gateways/mollie/src/WebhookController.php:44 \\register\_rest\_route(Integration::REST\_ROUTE\_NAMESPACE, '/webhook', \['methods' => 'POST', 'callback' => \[$this, 'rest\_api\_mollie\_webhook'\], 'args' => \['id' => \['description' => \\\_\_('Mollie transaction ID.', 'pronamic-pay-doneren-met-mollie'), 'type' => 'string', 'required' => true\]\], 'permission\_callback' => '\_\_return\_true'\]);
packages/wp-pay-gateways/mollie/src/WebhookController.php:61 \\register\_rest\_route(Integration::REST\_ROUTE\_NAMESPACE, '/webhook/(?P<payment\_id>\\\\d+)', \['methods' => 'POST', 'callback' => \[$this, 'rest\_api\_mollie\_webhook\_payment'\], 'args' => \['payment\_id' => \['description' => \\\_\_('Payment ID.', 'pronamic-pay-doneren-met-mollie'), 'type' => 'string', 'required' => true\], 'id' => \['description' => \\\_\_('Mollie transaction ID.', 'pronamic-pay-doneren-met-mollie'), 'type' => 'string', 'required' => true\]\], 'permission\_callback' => '\_\_return\_true'\]);

## Using load_plugin_textdomain() for loading the plugin translations is not needed for WordPress.org directory since WordPress 4.6.

When your plugin is hosted on WordPress.org you don't longer need to include load_plugin_textdomain() for the translations under your plugin slug "pronamic-pay-doneren-met-mollie".

Therefore you can remove that call, WordPress will automatically load the translations for you when needed.

If you still support older WordPress versions you'll need to keep it, but please make sure to have the function call in a hook such as init to avoid issues for loading them too early. More information: https://make.wordpress.org/core/2024/10/21/i18n-improvements-6-7/.

From your plugin:

pronamic-pay-doneren-met-mollie.php:44 load\_plugin\_textdomain('pronamic-pay-doneren-met-mollie', false, dirname(plugin\_basename(\_\_FILE\_\_)) . '/languages');
packages/pronamic/wp-money/pronamic-money.php:35 load\_plugin\_textdomain('pronamic-pay-doneren-met-mollie', false, basename(\_\_DIR\_\_) . '/languages');
packages/pronamic/wp-datetime/pronamic-datetime.php:35 load\_plugin\_textdomain('pronamic-pay-doneren-met-mollie', false, basename(\_\_DIR\_\_) . '/languages');

## Nonces and User Permissions Needed for Security

Please add a nonce check to your input calls ($_POST, $_GET, $REQUEST) to prevent unauthorized access.

If you use wp_ajax_ to trigger submission checks, remember they also need a nonce check.

👮 Checking permissions: Keep in mind, a nonce check alone is not bulletproof security. Do not rely on nonces for authorization purposes. When needed, use it together with current_user_can() in order to prevent users without the right permissions from accessing things they shouldn't.

Also make sure that the nonce logic is correct by making sure it cannot be bypassed. Checking the nonce with current_user_can() is great, but mixing it with other checks can make the condition more complex and, without realising it, bypassable, remember that anything can be sent through an input, don't trust any input.

Keep performance in mind. Don't check for post submission outside of functions. Doing so means that the check will run on every single load of the plugin, which means that every single person who views any page on a site using your plugin will be checking for a submission. This will make your code slow and unwieldy for users on any high traffic site, leading to instability and eventually crashes.

The following links may assist you in development:

From your plugin:

packages/wp-pay-gateways/mollie/views/page-payment.php:1 No nonce check was found validating the origin of inputs in the lines 1-21
# ↳ Line 21: $mollie\_payment\_id = \\array\_key\_exists( 'id', $\_GET ) ? \\sanitize\_text\_field( \\wp\_unslash( $\_GET\['id'\] ) ) : '';
# ↳ Line 21: $mollie\_payment\_id = \\array\_key\_exists( 'id', $\_GET ) ? \\sanitize\_text\_field( \\wp\_unslash( $\_GET\['id'\] ) ) : '';
packages/wp-pay-gateways/mollie/views/page-mandate.php:1 No nonce check was found validating the origin of inputs in the lines 1-29
# ↳ Line 29: $config\_id = array\_key\_exists( 'config\_id', $\_GET ) ? \\sanitize\_text\_field( \\wp\_unslash( $\_GET\['config\_id'\] ) ) : null;
# ↳ Line 29: $config\_id = array\_key\_exists( 'config\_id', $\_GET ) ? \\sanitize\_text\_field( \\wp\_unslash( $\_GET\['config\_id'\] ) ) : null;
# ↳ Line 32: $mollie\_customer\_id = array\_key\_exists( 'customer\_id', $\_GET ) ? \\sanitize\_text\_field( \\wp\_unslash( $\_GET\['customer\_id'\] ) ) : null;
# ↳ Line 32: $mollie\_customer\_id = array\_key\_exists( 'customer\_id', $\_GET ) ? \\sanitize\_text\_field( \\wp\_unslash( $\_GET\['customer\_id'\] ) ) : null;
# ↳ Line 39: $mollie\_mandate\_id = array\_key\_exists( 'mandate\_id', $\_GET ) ? \\sanitize\_text\_field( \\wp\_unslash( $\_GET\['mandate\_id'\] ) ) : null;
# ↳ Line 39: $mollie\_mandate\_id = array\_key\_exists( 'mandate\_id', $\_GET ) ? \\sanitize\_text\_field( \\wp\_unslash( $\_GET\['mandate\_id'\] ) ) : null;
packages/wp-pay/core/src/Admin/AdminPaymentPostType.php:166 No nonce check was found validating the origin of inputs in the lines 166-179 - in the context of the classMethod AdminPaymentPostType::maybe\_display\_anonymized\_notice()
# ↳ Line 179: $post\_id = filter\_input( INPUT\_GET, 'post', FILTER\_SANITIZE\_NUMBER\_INT );
packages/woocommerce/action-scheduler/classes/abstracts/ActionScheduler\_Abstract\_ListTable.php:736 No nonce check was found validating the origin of inputs in the lines 736-739 - in the context of the classMethod ActionScheduler\_Abstract\_ListTable::display\_table()
# ↳ Line 739: $arg\_value = isset( $\_GET\[ $arg \] ) ? sanitize\_text\_field( wp\_unslash( $\_GET\[ $arg \] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
packages/woocommerce/action-scheduler/classes/abstracts/ActionScheduler\_Abstract\_ListTable.php:383 No nonce check was found validating the origin of inputs in the lines 383-384 - in the context of the classMethod ActionScheduler\_Abstract\_ListTable::get\_request\_search\_query()
# ↳ Line 384: $search\_query = ( ! empty( $\_GET\['s'\] ) ) ? sanitize\_text\_field( wp\_unslash( $\_GET\['s'\] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended

... out of a total of 21 incidences.
Please, make sure that the nonce logic is correct.

packages/woocommerce/action-scheduler/classes/abstracts/ActionScheduler\_Abstract\_ListTable.php:632 if ( wp\_verify\_nonce( $nonce, $action . '::' . $row\_id ) && method\_exists( $this, $method ) ) {

## Data Must be Sanitized, Escaped, and Validated

When you include POST/GET/REQUEST/FILE calls in your plugin, it's important to sanitize, validate, and escape them. The goal here is to prevent a user from accidentally sending trash data through the system, as well as protecting them from potential security issues.

SANITIZE: Data that is input (either by a user or automatically) must be sanitized as soon as possible. This lessens the possibility of XSS vulnerabilities and MITM attacks where posted data is subverted.

VALIDATE: All data should be validated, no matter what. Even when you sanitize, remember that you don’t want someone putting in ‘dog’ when the only valid values are numbers.

ESCAPE: Data that is output must be escaped properly when it is echo'd, so it can't hijack admin screens. There are many esc_*() functions you can use to make sure you don't show people the wrong data.

To help you with this, WordPress comes with a number of sanitization and escaping functions. You can read about those here:

Remember: You must use the most appropriate functions for the context. If you’re sanitizing email, use sanitize_email(), if you’re outputting HTML, use wp_kses_post(), and so on.

An easy mantra here is this:

Sanitize early
Escape Late
Always Validate

Clean everything, check everything, escape everything, and never trust the users to always have input sane data. After all, users come from all walks of life.

Example(s) from your plugin:

packages/woocommerce/action-scheduler/lib/WP\_Async\_Request.php:151 'cookies'   => $\_COOKIE,

✔️ You can check this using Plugin Check.

## Processing the whole input

We strongly recommend you never attempt to process the whole $_POST/$_REQUEST/$_GET stack. This makes your plugin slower as you're needlessly cycling through data you don't need. Instead, you should only be attempting to process the items within that are required for your plugin to function.

Example(s) from your plugin:

packages/woocommerce/action-scheduler/lib/WP\_Async\_Request.php:151 'cookies'   => $\_COOKIE,

## Variables and options must be escaped when echo'd

Much related to sanitizing everything, all variables that are echoed need to be escaped when they're echoed, so it can't hijack users or (worse) admin screens. There are many esc_*() functions you can use to make sure you don't show people the wrong data, as well as some that will allow you to echo HTML safely.

At this time, we ask you escape all $-variables, options, and any sort of generated data when it is being echoed. That means you should not be escaping when you build a variable, but when you output it at the end. We call this 'escaping late.'

Besides protecting yourself from a possible XSS vulnerability, escaping late makes sure that you're keeping the future you safe. While today your code may be only outputted hardcoded content, that may not be true in the future. By taking the time to properly escape when you echo, you prevent a mistake in the future from becoming a critical security issue.

This remains true of options you've saved to the database. Even if you've properly sanitized when you saved, the tools for sanitizing and escaping aren't interchangeable. Sanitizing makes sure it's safe for processing and storing in the database. Escaping makes it safe to output.

Also keep in mind that sometimes a function is echoing when it should really be returning content instead. This is a common mistake when it comes to returning JSON encoded content. Very rarely is that actually something you should be echoing at all. Echoing is because it needs to be on the screen, read by a human. Returning (which is what you would do with an API) can be json encoded, though remember to sanitize when you save to that json object!

There are a number of options to secure all types of content (html, email, etc). Yes, even HTML needs to be properly escaped.

https://developer.wordpress.org/apis/security/escaping/

Remember: You must use the most appropriate functions for the context. There is pretty much an option for everything you could echo. Even echoing HTML safely.

Example(s) from your plugin:

packages/woocommerce/action-scheduler/classes/abstracts/ActionScheduler\_Abstract\_ListTable.php:748 echo $this->search\_box( $this->get\_search\_box\_button\_text(), 'plugin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
packages/woocommerce/action-scheduler/classes/abstracts/ActionScheduler\_Abstract\_ListTable.php:726 echo implode( " | \\n", $status\_list\_items ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
 -----> echo implode(" | \\n", $status\_list\_items);
# ↳ Detected origin: array()
# ↳ Remember to ALWAYS escape as LATE as possible as with a PROPER function for the context.
packages/woocommerce/action-scheduler/classes/WP\_CLI/Action/Next\_Command.php:53 echo $action\_id;
# ↳ Detected origin: $store->query\_action($params)
# ↳ Remember to ALWAYS escape as LATE as possible as with a PROPER function for the context.
packages/woocommerce/action-scheduler/classes/WP\_CLI/Action/List\_Command.php:92 echo implode( ' ', $actions );
# ↳ Detected origin: as\_get\_scheduled\_actions($query\_args, 'ids')
# ↳ Remember to ALWAYS escape as LATE as possible as with a PROPER function for the context.
packages/woocommerce/action-scheduler/classes/WP\_CLI/System\_Command.php:40 echo $this->get\_current\_datastore();
packages/woocommerce/action-scheduler/classes/WP\_CLI/System\_Command.php:51 echo $this->get\_current\_runner();
packages/woocommerce/action-scheduler/classes/WP\_CLI/System\_Command.php:160 echo $path;
# ↳ Detected origin: $source
# ↳ Remember to ALWAYS escape as LATE as possible as with a PROPER function for the context.
packages/woocommerce/action-scheduler/classes/WP\_CLI/System\_Command.php:107 echo $latest;
# ↳ Detected origin: $this->get\_latest\_version()
# ↳ Remember to ALWAYS escape as LATE as possible as with a PROPER function for the context.

... out of a total of 9 incidences.
✔️ You can check this using Plugin Check.

## Generic function/class/define/namespace/option names

All plugins must have unique function names, namespaces, defines, class and option names. This prevents your plugin from conflicting with other plugins or themes. We need you to update your plugin to use more unique and distinct names.

A good way to do this is with a prefix. For example, if your plugin is called "Pronamic Pay doneren met Mollie" then you could use names like these:

  • function pronpado_save_post(){ ... }

  • class PRONPADO_Admin { ... }

  • update_option( 'pronpado_settings', $settings );

  • define( 'PRONPADO_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );

  • global $pronpado_options;

  • namespace pronamic\pronamicpaydonerenmetmollie;

Disclaimer: These are just examples that may have been self-generated from your plugin name, we trust you can find better options. If you have a good alternative, please use it instead, this is just an example.

Don't try to use two (2) or three (3) letter prefixes anymore. We host nearly 100-thousand plugins on WordPress.org alone. There are tens of thousands more outside our servers. Believe us, you’re going to run into conflicts.

You also need to avoid the use of __ (double underscores), wp_ , or _ (single underscore) as a prefix. Those are reserved for WordPress itself. You can use them inside your classes, but not as stand-alone function.

Please remember, if you're using _n() or __() for translation, that's fine. We're only talking about functions you've created for your plugin, not the core functions from WordPress. In fact, those core features are why you need to not use those prefixes in your own plugin! You don't want to break WordPress for your users.

Related to this, using if (!function_exists('NAME')) { around all your functions and classes sounds like a great idea until you realize the fatal flaw. If something else has a function with the same name and their code loads first, your plugin will break. Using if-exists should be reserved for shared libraries only.

Remember: Good prefix names are unique and distinct to your plugin. This will help you and the next person in debugging, as well as prevent conflicts.

Analysis result:

\# This plugin is using the prefix "actionscheduler" for 59 element(s).
# This plugin is using the prefix "cronexpression" for 10 element(s).
# This plugin is using the prefix "get\_pronamic" for 12 element(s).
# This plugin is using the prefix "pronamic" for 114 element(s).
# This plugin is using the prefix "action" for 95 element(s).
# This plugin is using the prefix "as" for 12 element(s).

# Cannot use "get" as a prefix.
packages/wp-pay/core/includes/functions.php:29 function get\_pronamic\_payment
packages/wp-pay/core/includes/functions.php:52 function get\_pronamic\_payment\_by\_meta
packages/wp-pay/core/includes/functions.php:79 function get\_pronamic\_payments\_by\_meta
packages/wp-pay/core/includes/functions.php:123 function get\_pronamic\_payment\_by\_purchase\_id
packages/wp-pay/core/includes/functions.php:134 function get\_pronamic\_payment\_by\_transaction\_id
packages/wp-pay/core/includes/functions.php:145 function get\_pronamic\_payments\_by\_user\_id
packages/wp-pay/core/includes/functions.php:160 function get\_pronamic\_payments\_by\_source
packages/wp-pay/core/includes/functions.php:193 function get\_pronamic\_subscription
packages/wp-pay/core/includes/functions.php:205 function get\_pronamic\_subscription\_by\_meta
packages/wp-pay/core/includes/functions.php:229 function get\_pronamic\_subscriptions\_by\_meta
packages/wp-pay/core/includes/functions.php:272 function get\_pronamic\_subscriptions\_by\_user\_id
packages/wp-pay/core/includes/functions.php:287 function get\_pronamic\_subscriptions\_by\_source
# Cannot use "as" as a prefix.
packages/woocommerce/action-scheduler/functions.php:19 function as\_enqueue\_async\_action
packages/woocommerce/action-scheduler/functions.php:69 function as\_schedule\_single\_action
packages/woocommerce/action-scheduler/functions.php:121 function as\_schedule\_recurring\_action
packages/woocommerce/action-scheduler/functions.php:207 function as\_schedule\_cron\_action
packages/woocommerce/action-scheduler/functions.php:264 function as\_unschedule\_action
packages/woocommerce/action-scheduler/functions.php:309 function as\_unschedule\_all\_actions
packages/woocommerce/action-scheduler/functions.php:343 function as\_next\_scheduled\_action
packages/woocommerce/action-scheduler/functions.php:396 function as\_has\_scheduled\_action
packages/woocommerce/action-scheduler/functions.php:439 function as\_get\_scheduled\_actions
packages/woocommerce/action-scheduler/functions.php:486 function as\_get\_datetime\_object
# Cannot use "wp" as a prefix.
packages/woocommerce/action-scheduler/lib/WP\_Async\_Request.php:23 class WP\_Async\_Request
# Cannot use "wc" as a prefix.
packages/woocommerce/action-scheduler/deprecated/functions.php:22 function wc\_schedule\_single\_action
packages/woocommerce/action-scheduler/deprecated/functions.php:40 function wc\_schedule\_recurring\_action
packages/woocommerce/action-scheduler/deprecated/functions.php:68 function wc\_schedule\_cron\_action
packages/woocommerce/action-scheduler/deprecated/functions.php:82 function wc\_unschedule\_action
packages/woocommerce/action-scheduler/deprecated/functions.php:98 function wc\_next\_scheduled\_action
packages/woocommerce/action-scheduler/deprecated/functions.php:126 function wc\_get\_scheduled\_actions

# Looks like there are elements not using common prefixes.
packages/wp-pay/core/views/meta-box-subscription-payments.php:208 do\_action('manage\_' . $payments\_post\_type . '\_posts\_custom\_column', 'pronamic\_payment\_status', $payment\_id);
packages/wp-pay/core/views/meta-box-subscription-payments.php:223 do\_action('manage\_' . $payments\_post\_type . '\_posts\_custom\_column', 'pronamic\_payment\_title', $payment\_id);
packages/wp-pay/core/views/meta-box-subscription-payments.php:226 do\_action('manage\_' . $payments\_post\_type . '\_posts\_custom\_column', 'pronamic\_payment\_transaction', $payment\_id);
packages/wp-pay/core/views/meta-box-subscription-payments.php:229 do\_action('manage\_' . $payments\_post\_type . '\_posts\_custom\_column', 'pronamic\_payment\_amount', $payment\_id);
packages/wp-pay/core/views/meta-box-subscription-payments.php:232 do\_action('manage\_' . $payments\_post\_type . '\_posts\_custom\_column', 'pronamic\_payment\_date', $payment\_id);
packages/wp-pay/core/views/meta-box-payment-info.php:148 do\_action('manage\_' . $payments\_post\_type . '\_posts\_custom\_column', 'pronamic\_payment\_transaction', $payment->get\_id());
packages/wp-pay/core/views/page-dashboard.php:82 apply\_filters('manage\_edit-' . $payments\_post\_type . '\_columns', \[\]);
packages/wp-pay/core/views/page-dashboard.php:146 do\_action('manage\_' . $payments\_post\_type . '\_posts\_custom\_column', $custom\_column, $payment\_id);
packages/wp-pay/core/views/page-dashboard.php:225 apply\_filters('manage\_edit-' . $subscriptions\_post\_type . '\_columns', \[\]);
packages/wp-pay/core/views/page-dashboard.php:289 do\_action('manage\_' . $subscriptions\_post\_type . '\_posts\_custom\_column', $custom\_column, $subscription\_id);
packages/woocommerce/action-scheduler/functions.php:40 apply\_filters('pre\_as\_enqueue\_async\_action', null, $hook, $args, $group, $priority, $unique);
packages/woocommerce/action-scheduler/functions.php:90 apply\_filters('pre\_as\_schedule\_single\_action', null, $timestamp, $hook, $args, $group, $priority);
packages/woocommerce/action-scheduler/functions.php:163 apply\_filters('pre\_as\_schedule\_recurring\_action', null, $timestamp, $interval\_in\_seconds, $hook, $args, $group, $priority);
packages/woocommerce/action-scheduler/functions.php:229 apply\_filters('pre\_as\_schedule\_cron\_action', null, $timestamp, $schedule, $hook, $args, $group, $priority);
packages/woocommerce/action-scheduler/classes/abstracts/ActionScheduler\_Abstract\_Schema.php:129 update\_option($option\_name, $value\_to\_save);
# ↳ Detected name: schema-
packages/woocommerce/action-scheduler/lib/WP\_Async\_Request.php:114 apply\_filters($this->identifier . '\_query\_args', $args);
packages/woocommerce/action-scheduler/lib/WP\_Async\_Request.php:134 apply\_filters($this->identifier . '\_query\_url', $url);
packages/woocommerce/action-scheduler/lib/WP\_Async\_Request.php:160 apply\_filters($this->identifier . '\_post\_args', $args);
packages/wp-pay-gateways/mollie/views/page-payment.php:148 \\do\_action('manage\_' . AdminPaymentPostType::POST\_TYPE . '\_posts\_custom\_column', 'pronamic\_payment\_title', $payment->get\_id());

Note: Options and Transients must be prefixed.

This is really important because the options are stored in a shared location and under the name you have set. If two plugins use the same name for options, they will find an interesting conflict when trying to read information introduced by the other plugin.

Also, once your plugin has active users, changing the name of an option is going to be really tricky, so let's make it robust from the very beginning.

Example(s) from your plugin:

packages/woocommerce/action-scheduler/classes/abstracts/ActionScheduler\_Abstract\_Schema.php:129 update\_option($option\_name, $value\_to\_save);

## Unsafe SQL calls

When making database calls, it's highly important to protect your code from SQL injection vulnerabilities. You need to update your code to use wpdb calls and prepare() with your queries to protect them.

Please review the following:

Example(s) from your plugin:

packages/woocommerce/action-scheduler/classes/data-stores/ActionScheduler\_DBStore.php:289 $group\_id = (int) $wpdb->get\_var( $wpdb->prepare( "SELECT group\_id FROM {$wpdb->actionscheduler\_groups} WHERE slug=%s", $slug ) );
# There is a call to a wpdb::prepare() function, that's correct.
# You cannot add variables like "$wpdb->actionscheduler\_groups" directly to the SQL query.
# Using wpdb::prepare($query, $args) you will need to include placeholders for each variable within the query and include the variables in the second parameter.

packages/woocommerce/action-scheduler/classes/data-stores/ActionScheduler\_DBLogger.php:58 $entry = $wpdb->get\_row( $wpdb->prepare( "SELECT \* FROM {$wpdb->actionscheduler\_logs} WHERE log\_id=%d", $entry\_id ) );
# There is a call to a wpdb::prepare() function, that's correct.
# You cannot add variables like "$wpdb->actionscheduler\_logs" directly to the SQL query.
# Using wpdb::prepare($query, $args) you will need to include placeholders for each variable within the query and include the variables in the second parameter.

... out of a total of 36 incidences.

Note: Passing individual values to wpdb::prepare using placeholders is fairly straightforward, but what if we need to pass an array of values instead?

You'll need to create a placeholder for each item of the array and pass all the corresponding values to those placeholders, this seems tricky, but here is a snippet to do so.

$wordcamp\_id\_placeholders = implode( ', ', array\_fill( 0, count( $wordcamp\_ids ), '%d' ) );
$prepare\_values = array\_merge( array( $new\_status ), $wordcamp\_ids );
$wpdb->query( $wpdb->prepare( "
        UPDATE \`$table\_name\`
        SET \`post\_status\` = %s
        WHERE ID IN ( $wordcamp\_id\_placeholders )",
        $prepare\_values
) );

There is a core ticket that could make this easier in the future: https://core.trac.wordpress.org/ticket/54042

Example(s) from your plugin:

packages/woocommerce/action-scheduler/classes/abstracts/ActionScheduler\_Abstract\_ListTable.php:488 $columns = '\`' . implode( '\`, \`', $this->get\_table\_columns() ) . '\`';
packages/woocommerce/action-scheduler/classes/abstracts/ActionScheduler\_Abstract\_ListTable.php:491 $where = 'WHERE (' . implode( ') AND (', $where ) . ')';

... out of a total of 3 incidences.

👉 Your next steps

Please, before replying make sure to perform the following actions:

  1. Read this email.

  2. Take the time to understand the issues shared, check the included examples, check the documentation, research the issue on internet, and gain a better understanding of what's happening and how you can fix it. We want you to thoroughly understand these issues so that you can take them into account when maintaining your plugin in the future.

    • Note that there may be false positives - we are humans and make mistakes, we apologize if there is anything we have gotten wrong.

    • If you have doubts you can ask us for clarification, when asking us please be clear, concise, direct and include an example.

  3. You can make use of tools like PHPCS or Plugin Check to further help you with finding all the issues.

  4. Fix your plugin.

  5. Test your plugin on a clean WordPress installation. You can try Playground.

  6. Go to "Add your plugin" and upload an updated version of this plugin. You can update the code there whenever you need to, along the review process, and we will check the latest version.

  7. Reply to this email telling us that you have updated it, and let us know if there is anything we need to know or have in mind.
    Please do not list the changes made as we will review the whole plugin again, just share anything you want to clarify.

ℹ️ To make this process as quick as possible and to avoid burden on the volunteers devoting their time to review this plugin's code, we ask you to thoroughly check all shared issues and fix them before sending the code back to us. I know we already asked you to do so, and it is because we are really trying to make it very clear.

Disclaimers

Please note that due to the significant effort this kind of reviews require, we do a basic review the first time that we review your plugin. Once the issues we shared above are fixed, we will do a more in-depth review which might surface other issues.

While we try to make our reviews as exhaustive as possible we, like you, are humans and may have missed things. We appreciate your patience and understanding.

We recommend that you get ahead of us by checking for some common issues that require a more thorough review such as the use of nonces or determining plugin and content directories correctly.

We encourage all plugin authors to use tools like Plugin Check to ensure that most basic issues are fixed first. If you haven't used it yet, give it a try, it will save us both time and speed up the review process.
Please note: Automated tools can give false positives, or may miss issues. Plugin Check and other tools cannot guarantee that our reviewers won't find an issue that needs fixing or clarification.

We again remind you that should you wish to alter your permalink (not the display name, the plugin slug) "pronamic-pay-doneren-met-mollie", you must explicitly tell us what you would like it to be. Just changing the display name is not sufficient. We require you to clearly state, in the body of your email what your desired permalink is. Permalinks cannot be altered after approval, and we generally do not accept requests to rename them, should you fail to inform us during the review. If you previously asked for a permalink change and got a reply that is has been processed, you’re all good! While these emails will still use the original display name, you don’t need to panic. If you did not get a reply that we processed the permalink, let us know immediately.

If the corrections we requested in this initial review are not completed within 3 months (90 days), we will reject this submission in order to keep our queue manageable.

If you have questions, concerns, or need clarification, please reply to this email and just ask us.

Review ID: F1 pronamic-pay-doneren-met-mollie/pronamic/24Mar25/T1 24Mar25/3.3B

@remcotolsma remcotolsma self-assigned this Mar 25, 2025
@remcotolsma
Copy link
Member Author

Go to "Add your plugin" and upload an updated version of this plugin. You can update the code there whenever you need to, along the review process, and we will check the latest version.

✅ Just uploaded new version via https://wordpress.org/plugins/developers/add/.

Image

Reply to this email telling us that you have updated it, and let us know if there is anything we need to know or have in mind.

Hello WordPress Plugin Review Team,

Once again, thank you for your thorough review. We have carefully read your email and reviewed all the feedback in detail:
#9

The main point of concern is that much of the feedback relates to the Action Scheduler library/plugin that we have included in this plugin.
The Action Scheduler library/plugin is specifically designed for this purpose and is used similarly in the WooCommerce plugin and other popular plugins in the WordPress.org plugin directory.
Of course, we understand that, ideally, this plugin/library should also fully comply with all checks. That’s why we have previously brought this to the attention of the Action Scheduler developers at Automattic and WooCommerce during earlier plugin reviews, and we have done so again now:

We hope that this does not hinder the further review of our plugin. Otherwise, should we push Automattic/WooCommerce to address this as soon as possible?

Additionally, there were a few minor improvements that we were able to implement ourselves. Below is a brief response per section:


Not permitted files
✅ Resolved.

Out of Date Libraries
✅ Resolved.

PHP libraries that might conflict with the same library loaded by other plugins
⏳ Action Scheduler.

No publicly documented resource for your generated/compressed content
🟡 False positive, source files are included in:

pronamic-pay-doneren-met-mollie/packages/wp-pay-gateways/mollie/assets/src/wc-legacy-checkout.js

Calling files remotely
🟡 False positive, refers to locally included files.

Determine files and directories locations correctly
⏳ Action Scheduler.

Sanitization for register_setting()
✅ Resolved.

Review: Missing permission_callback in register_rest_route()
🟡 These are intentionally public webhook endpoints.

Using load_plugin_textdomain() for loading the plugin translations is not needed for WordPress.org directory since WordPress 4.6
✅ Issue resolved in pronamic-pay-doneren-met-mollie.php.
🟡 Issues in packages/pronamic/wp-money/pronamic-money.php and packages/pronamic/wp-datetime/pronamic-datetime.php are false positives.

Nonces and User Permissions Needed for Security
🟡 Issue in packages/wp-pay-gateways/mollie/views/page-payment.php – no nonce is possible or necessary.
🟡 Issue in packages/wp-pay-gateways/mollie/views/page-mandate.php – no nonce is possible or necessary.
🟡 Issue in packages/wp-pay/core/src/Admin/AdminPaymentPostType.php – no nonce is possible or necessary.
⏳ Other reports related to Action Scheduler.

Data Must be Sanitized, Escaped, and Validated
⏳ Action Scheduler.

Processing the whole input
⏳ Action Scheduler.

Variables and options must be escaped when echoed
⏳ Action Scheduler.

Generic function/class/define/namespace/option names
🟡 Issue: "Cannot use 'get' as a prefix" – this is actually the prefix get_pronamic_.
⏳ Other reports related to Action Scheduler.

Unsafe SQL calls
⏳ Action Scheduler.


We appreciate the time and effort you put into reviewing our plugin and your valuable feedback. Please let us know if there are any further concerns or if anything else is needed from our side.

Looking forward to your response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant