Here are some information and links for developing your own WordPress plugin that may be useful for you. It is far from being complete. It is our first WordPress plugin.

Review

Before a plugin can be downloaded from the official WP Plugin Directory (wordpress.org), it is reviewed.

The WP review team noted in an email that the following are the four most common mistakes made by plugin developers that can lengthen the review process:

  1. Unsanitized input (issue found in approx. 99% of first reviews)
  2. Unescaped output (issue found in approx. 99% of first reviews)
  3. Missing or incorrect plugin and readme headers (issues found in approx. 70% of first reviews)
  4. Missing or inadequate function prefixes (issue found in approx. 60% of first reviews)

After the review has been successfully completed you get an email how to access “your” SVN repository, the Public URL to the plugin (wordpress.org, our plugin), and detailed information what to do. It’s in your responsability to upload the plugin the first time, to update and maintain it. It’s based on trust.

.

Code examples of the four most common errors and solutions (mail WP Review Team)
1) Unsanitized input (issue found in approx. 99% of first reviews)

Sanitizing is important to prevent the possibility of XSS vulnerabilities and MITM attacks.

This affects any data that is being read from any of these PHP global variables $_POST / $_GET / $_REQUEST / $_COOKIE / $_SERVER / $_SESSION / $_FILES (the file name)

We will request you to sanitize any of these variables in the code and also do it as soon as possible (before storing it in another variable or doing something else with it).

These are the standard WordPress functions for sanitizing https://developer.wordpress.org/apis/security/sanitizing/ . Other options like using filter_input( INPUT_POST, 'prefix_post_id', FILTER_SANITIZE_NUMBER_INT ); or abs($_POST['prefix_post_id']); are ok too.

Example of incorrect lines (corrected version immediately below
$post_id = $_POST['prefix_post_id'];
$post_id = absint( $_POST['prefix_post_id'] );

$post_title = isset( $_POST['prefix_post_title'] ) ? $_POST['prefix_post_title'] : '';
$post_title = isset( $_POST['prefix_post_title'] ) ? sanitize_title( $_POST['prefix_post_title'] ) : '';

$user_email = wp_unslash( $_POST['prefix_user_email'] );
$user_email = sanitize_email( $_POST['prefix_user_email'] );

if ( ! isset( $_POST['prefix_nonce'] ) || ! wp_verify_nonce( $_POST['prefix_nonce'] , 'prefix_nonce' ) )
if ( ! isset( $_POST['prefix_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash ( $_POST['prefix_nonce'] ) ) , 'prefix_nonce' ) )

How to detect this:
You can use the tool "WordPress Coding Standards for PHPCS" or just find any $_POST / $_GET / $_REQUEST / $_COOKIE / $_SERVER / $_SESSION / $_FILES variable on your code and wrap it with an appropriate sanitization function.

Special situations:
In some cases your plugin will need to sanitize an array, you can do that easily using array_map() , for example:
$post_id_array = array_map( 'absint', (array) $_REQUEST['prefix_post_id_array'] );
$city_names_array = map_deep( $_POST['city_names_array'], 'sanitize_text_field' );

For multidimensional arrays with different data types, you may have to create your own sanitize function.

2) Unescaped output (issue found in approx. 99% of first reviews)

Escaping is essential to prevent XSS vulnerabilities and breaking with the structure of the HTML document.

This affects any content that is being output in any way that PHP / WordPress allows such as with echo , print , printf , _e , _ex among others.

We will request you to escape any of these variables in the code and also do it as late as possible (immediately before being echoed).

These are the standard WordPress functions for escaping https://developer.wordpress.org/apis/security/escaping/
Other options that obviously limit the introduction of code like echo abs( $post_id ); are ok too.

Example of incorrect lines (corrected version immediately below):
echo '<h1>' . $title . '</h1>';
echo '<h1>' . esc_html($title) . '</h1>';

<input value="<?php echo $value ?>" type="text">
<input value="<?php echo esc_attr($value) ?>" type="text">

printf('<a href="%s">%s</a>', $url, $text);
printf('<a href="%s">%s</a>', esc_url($url), esc_html($text));

echo '<h1>' . prefix_process_title(esc_html($title)) . '</h1>';
echo '<h1>' . esc_html(prefix_process_title($title)) . '</h1>';

<h2><?php _e('Settings page', 'plugin-slug'); ?></h2><br /><h2><?php esc_html_e('Settings page', 'plugin-slug'); ?></h2>

How to detect this:
You can use the tool "WordPress Coding Standards for PHPCS" or just find any echo , print , printf on your code and wrap any variable element inside it with an appropriate escaping function as late as possible.

In case of the _e or _ex functions, use an alternative function that escapes the output, like esc_html_e or esc_attr_e , or simply use __ or _x functions wrapped by a escaping function and inside an echo .

Special situations:
In some cases, your plugin will need to echo HTML code. To do that, you can make use of wp_kses_post or wp_kses . The function wp_kses_post will allow any common HTML that can go inside a post content, wp_kses will allow any HTML that you set up using its second and third parameters, please refer to its documentation.
Examples: echo wp_kses_post($html_content); or echo wp_kses($html_content, 'post'); or echo wp_kses($html_content, array( 'a', 'div', 'span' )); 

Also, the plugin might need to echo a JSON object. To do this, instead of using json_encode($value); you can use wp_json_encode($value); which sanitizes the output.

3) Missing or incorrect plugin and readme headers (issues found in approx. 70% of first reviews)

Plugins must have the required parameters in the readme and the plugin headers in order to be displayed properly in the repository and to the users.
This is documented in these links:
• https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/
• https://developer.wordpress.org/plugins/plugin-basics/header-requirements/

Incorrect "Tested up to" parameter (readme file):
This should be the major version of WordPress that this plugin has been tested on. In the case of new plugins, we require it to be the latest version. This is necessary for plugins to be visible in the repository.
Example: Tested up to: 6.3 

Stable tag (readme file) does not match Version tag (plugin header):
These values need to match the current version of the plugin, and they need to be exactly the same.

Example:
Stable tag: 1.0.0
Version: 1.0.0

For example, this would be wrong, as 1.0 != 1.0.0
Stable tag: 1.0
Version: 1.0.0

Setting up the parameter "Update URI" (plugin header):
Just don't do that for plugins hosted in the WordPress.org repository, it doesn't make sense.

Failure to disclose third party or external services (readme file):
Any usage of third party services, such as connecting to an external API, needs to be documented in the readme file.

How to document this:
• Clearly explain that your plugin is relying on a third party service and under what circumstances.
• Provide a link to the service.
• Provide a link to the service's terms of use and/or privacy policies.
There is no specific format for this, but please make sure you provide enough info and links to terms of use / privacy policies.

4) Missing or inadequate function prefixes (issue found in approx. 60% of first reviews)

All active plugins in a WordPress installation will be loaded during execution. When two elements use the same name, a collision will happen. With more than 80k plugins in the repository, collisions are increasingly common.

That's why we need plugins to prefix their: function names, class names, namespaces, defines, option names, nonces and own input names with something that is related to this plugin, sufficiently complex and at least 5 characters long.

Example: a plugin named "Moon forms" could choose "moofor_ " or "moonforms_ " as a prefix but "mf_ ", "moon_ " or "forms_ " would be bad choices as they are too short, too generic or both.

This rule applies not only to function and class names (where namespaces are the best alternative) but also to defines, saving options, nonces and own input names, for example:
function moonforms_submit_button(){
class Moonforms_Form {
namespace Moonforms\Submit;

define('MOONFORMS_PLUGIN_URL', plugin_dir_url(__FILE__) );
define('MOONFORMS_PLUGIN_DIR', plugin_dir_path(__FILE__) );

update_option( 'moonforms-default-email', $email );

wp_nonce_field( 'moonforms-form', 'moonforms-nonce' );
if ( !empty( $_POST['moonforms-nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash ( $_POST['moonforms-nonce'] ) ), 'moonforms-form' ) ) {

<input name="moonforms-email" type="email">
$email = sanitize_email( $_POST['moonforms-email'] );

.

Remark: The review team is made up of 100% volunteers. Thanks!
Contact review team: plugins@wordpress.org (“reply to all emails within 7 business days”).

Links

Submit a plugin (wordpress.org), account required,
Include readme (wordpress.org) and screenshoots
Procedure steps (wordpress.org), developer information
Best practices (wordpress.org)
Plugin developer FAQ (wordpress.org)
WordPress Plugin Boilerplate (github.com)

Plugin Developer FAQ (wordpress.org)
Detailed Plugin Guidelines (wordpress.org)
WordPress Plugin Developer Handbook (wordpress.org)
The WordPress.org Plugin Directory … How to … (wordpress.org)

Icon, banner, screenshots – jpg/png

Create an “assets” folder in the root folder and place the following images in it.

Icon: icon-256×256.png (jpg, png, or SVG)
Banner: banner-1544×500.jpg (jpg, png)
Screenshots, naming:
screenshot-1.png (or .jpg)
screenshot-2.png … etc. – depends on the number of screenshots you have.
screenshot-1-de.png (or .jpg) – with language code
screenshot-2-de.png …

How Your Plugin Assets Work

readme.md

Readme template (txt, wordpress.org) => change “License: GPLv2 or later” to “License: GPLv3 or later”
WP readme generator (generatewp.com)
Readme Validator (wordpress.org)
How the readme is parsed (wordpress.org)
Stable tag: 1.9.0 :: last stable version

Timeline

21 November 2023: Finally, the plugin passed the review and is now available here in the official WordPress directory
Ongoin November 2023: Awaiting the review result and hopfully soon to be in the official WP plugin directory.
01 November 2023: After some updates, code refactoring and singing (very untalented), the plugin was resubmitted.
22 October 2023: Received an email from the review team with instructions for a 4 point self-check – see above. (“We are contacting you to let you know that your plugin is scheduled for review in the next 1-2 weeks.”)
28 July 2023: Plugin submitted. (“Currently there are 853 plugins awaiting review. The review queue is currently longer than normal, we apologize for the delays and ask for patience. The current wait for an initial review is at least 66 days.”)

Conclusion to take away so far: It can take “quite a while & work” to get everything in the right place and finally to get the plugin into the directory.

Dominik Fehr, wikinick@su-pa.net
Project website (su-pa.net)

last modified November 03 2023
initial version August 15 2023