Skip to content

Commit ae449aa

Browse files
NielsdeBlaauwjrfnl
authored andcommitted
✨ New NamingConventions.ValidPostTypeSlug sniff
Adds a new `WordPress.NamingConventions.ValidPostTypeSlug` sniff. Checks if the first parameter given to a register_post_type() call is actually a valid value.
1 parent 981b7d4 commit ae449aa

File tree

5 files changed

+456
-0
lines changed

5 files changed

+456
-0
lines changed

WordPress-Extra/ruleset.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@
130130
<!-- Verify that everything in the global namespace is prefixed. -->
131131
<rule ref="WordPress.NamingConventions.PrefixAllGlobals"/>
132132

133+
<!-- Validates post type slugs for valid characters, length and reserved keywords. -->
134+
<rule ref="WordPress.NamingConventions.ValidPostTypeSlug"/>
135+
133136
<!-- Check that object instantiations always have braces & are not assigned by reference.
134137
https://github.com/WordPress/WordPress-Coding-Standards/issues/919
135138
Note: there is a similar upstream sniff `PSR12.Classes.ClassInstantiation`, however
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<documentation title="Validate post type slugs">
2+
<standard>
3+
<![CDATA[
4+
The post type slug used in register_post_type() must be between 1 and 20 characters.
5+
]]>
6+
</standard>
7+
<code_comparison>
8+
<code title="Valid: short post type slug.">
9+
<![CDATA[
10+
register_post_type(
11+
<em>'my_short_slug'</em>,
12+
array()
13+
);
14+
]]>
15+
</code>
16+
<code title="Invalid: too long post type slug.">
17+
<![CDATA[
18+
register_post_type(
19+
<em>'my_own_post_type_too_long'</em>,
20+
array()
21+
);
22+
]]>
23+
</code>
24+
</code_comparison>
25+
<standard>
26+
<![CDATA[
27+
The post type slug used in register_post_type() can only contain lowercase alphanumeric characters, dashes and underscores.
28+
]]>
29+
</standard>
30+
<code_comparison>
31+
<code title="Valid: no special characters in post type slug.">
32+
<![CDATA[
33+
register_post_type(
34+
<em>'my_post_type_slug'</em>,
35+
array()
36+
);
37+
]]>
38+
</code>
39+
<code title="Invalid: invalid characters in post type slug.">
40+
<![CDATA[
41+
register_post_type(
42+
<em>'my/post/type/slug'</em>,
43+
array()
44+
);
45+
]]>
46+
</code>
47+
</code_comparison>
48+
<standard>
49+
<![CDATA[
50+
One should be careful with passing dynamic slug names to "register_post_type()", as the slug may become too long and could contain invalid characters.
51+
]]>
52+
</standard>
53+
<code_comparison>
54+
<code title="Valid: static post type slug.">
55+
<![CDATA[
56+
register_post_type(
57+
<em>'my_post_active'</em>,
58+
array()
59+
);
60+
]]>
61+
</code>
62+
<code title="Invalid: dynamic post type slug.">
63+
<![CDATA[
64+
register_post_type(
65+
<em>"my_post_{$status}"</em>,
66+
array()
67+
);
68+
]]>
69+
</code>
70+
</code_comparison>
71+
<standard>
72+
<![CDATA[
73+
The post type slug used in register_post_type() can not use reserved keywords, such as the ones used by WordPress itself.
74+
]]>
75+
</standard>
76+
<code_comparison>
77+
<code title="Valid: prefixed post slug.">
78+
<![CDATA[
79+
register_post_type(
80+
<em>'prefixed_author'</em>,
81+
array()
82+
);
83+
]]>
84+
</code>
85+
<code title="Invalid: using a reserved keyword as slug.">
86+
<![CDATA[
87+
register_post_type(
88+
<em>'author'</em>,
89+
array()
90+
);
91+
]]>
92+
</code>
93+
</code_comparison>
94+
<standard>
95+
<![CDATA[
96+
The post type slug used in register_post_type() can not use reserved prefixes, such as 'wp_', which is used by WordPress itself.
97+
]]>
98+
</standard>
99+
<code_comparison>
100+
<code title="Valid: custom prefix post slug.">
101+
<![CDATA[
102+
register_post_type(
103+
<em>'prefixed_author'</em>,
104+
array()
105+
);
106+
]]>
107+
</code>
108+
<code title="Invalid: using a reserved prefix.">
109+
<![CDATA[
110+
register_post_type(
111+
<em>'wp_author'</em>,
112+
array()
113+
);
114+
]]>
115+
</code>
116+
</code_comparison>
117+
</documentation>
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<?php
2+
/**
3+
* WordPress Coding Standard.
4+
*
5+
* @package WPCS\WordPressCodingStandards
6+
* @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
7+
* @license https://opensource.org/licenses/MIT MIT
8+
*/
9+
10+
namespace WordPressCS\WordPress\Sniffs\NamingConventions;
11+
12+
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
13+
use PHP_CodeSniffer\Util\Tokens;
14+
15+
/**
16+
* Validates post type names.
17+
*
18+
* Checks the post type slug for invalid characters, long function names
19+
* and reserved names.
20+
*
21+
* @link https://developer.wordpress.org/reference/functions/register_post_type/
22+
*
23+
* @package WPCS\WordPressCodingStandards
24+
*
25+
* @since 2.2.0
26+
*/
27+
class ValidPostTypeSlugSniff extends AbstractFunctionParameterSniff {
28+
29+
/**
30+
* Max length of a post type name is limited by the SQL field.
31+
*
32+
* @since 2.2.0
33+
*
34+
* @var int
35+
*/
36+
const POST_TYPE_MAX_LENGTH = 20;
37+
38+
/**
39+
* Regex that whitelists characters that can be used as the post type slug.
40+
*
41+
* @link https://developer.wordpress.org/reference/functions/register_post_type/
42+
* @since 2.2.0
43+
*
44+
* @var string
45+
*/
46+
const POST_TYPE_CHARACTER_WHITELIST = '/^[a-z0-9_-]+$/';
47+
48+
/**
49+
* Array of functions that must be checked.
50+
*
51+
* @since 2.2.0
52+
*
53+
* @var array List of function names as keys. Value irrelevant.
54+
*/
55+
protected $target_functions = array(
56+
'register_post_type' => true,
57+
);
58+
59+
/**
60+
* Array of reserved post type names which can not be used by themes and plugins.
61+
*
62+
* @since 2.2.0
63+
*
64+
* @var array
65+
*/
66+
protected $reserved_names = array(
67+
'post' => true,
68+
'page' => true,
69+
'attachment' => true,
70+
'revision' => true,
71+
'nav_menu_item' => true,
72+
'custom_css' => true,
73+
'customize_changeset' => true,
74+
'oembed_cache' => true,
75+
'user_request' => true,
76+
'wp_block' => true,
77+
'action' => true,
78+
'author' => true,
79+
'order' => true,
80+
'theme' => true,
81+
);
82+
83+
/**
84+
* All valid tokens for in the first parameter of register_post_type().
85+
*
86+
* Set in `register()`.
87+
*
88+
* @since 2.2.0
89+
*
90+
* @var string
91+
*/
92+
private $valid_tokens = array();
93+
94+
/**
95+
* Returns an array of tokens this test wants to listen for.
96+
*
97+
* @since 2.2.0
98+
*
99+
* @return array
100+
*/
101+
public function register() {
102+
$this->valid_tokens = Tokens::$textStringTokens + Tokens::$heredocTokens + Tokens::$emptyTokens;
103+
return parent::register();
104+
}
105+
106+
/**
107+
* Process the parameter of a matched function.
108+
*
109+
* Errors on invalid post type names when reserved keywords are used,
110+
* the post type is too long, or contains invalid characters.
111+
*
112+
* @since 2.2.0
113+
*
114+
* @param int $stackPtr The position of the current token in the stack.
115+
* @param array $group_name The name of the group which was matched.
116+
* @param string $matched_content The token content (function name) which was matched.
117+
* @param array $parameters Array with information about the parameters.
118+
*
119+
* @return void
120+
*/
121+
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
122+
123+
$string_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $parameters[1]['start'], ( $parameters[1]['end'] + 1 ) );
124+
$has_invalid_tokens = $this->phpcsFile->findNext( $this->valid_tokens, $parameters[1]['start'], ( $parameters[1]['end'] + 1 ), true );
125+
if ( false !== $has_invalid_tokens || false === $string_pos ) {
126+
// Check for non string based slug parameter (we cannot determine if this is valid).
127+
$this->phpcsFile->addWarning(
128+
'The post type slug is not a string literal. It is not possible to automatically determine the validity of this slug. Found: %s.',
129+
$stackPtr,
130+
'NotStringLiteral',
131+
array(
132+
$parameters[1]['raw'],
133+
),
134+
3
135+
);
136+
return;
137+
}
138+
139+
$post_type = $this->strip_quotes( $this->tokens[ $string_pos ]['content'] );
140+
141+
if ( strlen( $post_type ) === 0 ) {
142+
// Error for using empty slug.
143+
$this->phpcsFile->addError(
144+
'register_post_type() called without a post type slug. The slug must be a non-empty string.',
145+
$parameters[1]['start'],
146+
'Empty'
147+
);
148+
return;
149+
}
150+
151+
$data = array(
152+
$this->tokens[ $string_pos ]['content'],
153+
);
154+
155+
// Warn for dynamic parts in the slug parameter.
156+
if ( 'T_DOUBLE_QUOTED_STRING' === $this->tokens[ $string_pos ]['type'] || ( 'T_HEREDOC' === $this->tokens[ $string_pos ]['type'] && strpos( $this->tokens[ $string_pos ]['content'], '$' ) !== false ) ) {
157+
$this->phpcsFile->addWarning(
158+
'The post type slug may, or may not, get too long with dynamic contents and could contain invalid characters. Found: %s.',
159+
$string_pos,
160+
'PartiallyDynamic',
161+
$data
162+
);
163+
$post_type = $this->strip_interpolated_variables( $post_type );
164+
}
165+
166+
if ( preg_match( self::POST_TYPE_CHARACTER_WHITELIST, $post_type ) === 0 ) {
167+
// Error for invalid characters.
168+
$this->phpcsFile->addError(
169+
'register_post_type() called with invalid post type %s. Post type contains invalid characters. Only lowercase alphanumeric characters, dashes, and underscores are allowed.',
170+
$string_pos,
171+
'InvalidCharacters',
172+
$data
173+
);
174+
}
175+
176+
if ( isset( $this->reserved_names[ $post_type ] ) ) {
177+
// Error for using reserved slug names.
178+
$this->phpcsFile->addError(
179+
'register_post_type() called with reserved post type %s. Reserved post types should not be used as they interfere with the functioning of WordPress itself.',
180+
$string_pos,
181+
'Reserved',
182+
$data
183+
);
184+
} elseif ( stripos( $post_type, 'wp_' ) === 0 ) {
185+
// Error for using reserved slug prefix.
186+
$this->phpcsFile->addError(
187+
'The post type passed to register_post_type() uses a prefix reserved for WordPress itself. Found: %s.',
188+
$string_pos,
189+
'ReservedPrefix',
190+
$data
191+
);
192+
}
193+
194+
// Error for slugs that are too long.
195+
if ( strlen( $post_type ) > self::POST_TYPE_MAX_LENGTH ) {
196+
$this->phpcsFile->addError(
197+
'A post type slug must not exceed %d characters. Found: %s (%d characters).',
198+
$string_pos,
199+
'TooLong',
200+
array(
201+
self::POST_TYPE_MAX_LENGTH,
202+
$this->tokens[ $string_pos ]['content'],
203+
strlen( $post_type ),
204+
)
205+
);
206+
}
207+
}
208+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
register_post_type( 'my-own-post-type', array() ); // OK.
4+
register_post_type( 'my_own_post_type', array() ); // OK.
5+
register_post_type( 'my-own-post-type-too-long', array() ); // Bad.
6+
register_post_type( 'author', array() ); // Bad. Reserved slug name.
7+
register_post_type( 'My-Own-Post-Type', array() ); // Bad. Invalid chars: uppercase.
8+
register_post_type( 'my/own/post/type', array() ); // Bad. Invalid chars: "/".
9+
10+
register_post_type( <<<EOD
11+
my_own_post_type
12+
EOD
13+
); // OK.
14+
register_post_type( <<<'EOD'
15+
my_own_post_type
16+
EOD
17+
); // OK.
18+
19+
register_post_type( <<<'EOD'
20+
my/own/post/type
21+
EOD
22+
); // Bad. Invalid chars: "/".
23+
24+
register_post_type( "my_post_type_{$i}" ); // Warning, post type may or may not get too long with dynamic contents in the id.
25+
26+
// Non string literals.
27+
register_post_type( sprintf( 'my_post_type_%d', $i ) ); // Non string literal. Warning with severity: 3
28+
register_post_type( post ); // = lowercase global constant. Non string literal. Warning with severity: 3
29+
register_post_type( self::ID ); // Non string literal. Warning with severity: 3
30+
register_post_type( $post_type_name ); // Non string literal. Warning with severity: 3
31+
register_post_type( $this->get_post_type_id() ); // Non string literal. Warning with severity: 3
32+
33+
register_post_type( null, array() ); // Non string literal. Warning with severity: 3
34+
register_post_type( 1000, array() ); // Non string literal. Warning with severity: 3
35+
36+
register_post_type( 'wp_', array() ); // Bad. Reserved prefix.
37+
register_post_type( 'wp_post_type', array() ); // Bad. Reserved prefix.
38+
39+
register_post_type( '', array() ); // Bad. Empty post type slug.
40+
register_post_type( /*comment*/, array() ); // Bad. No post type slug.
41+
42+
register_post_type( 'post_type_1', array() ); // OK.
43+
44+
register_post_type( <<<EOD
45+
my_here_doc_{$i}
46+
EOD
47+
); // Warning, post type may or may not get too long with dynamic contents in the id.
48+
49+
register_post_type( "my-own-post-type-too-long-{$i}" ); // 1x Error, Too long. 1x Warning, post type may or may not get too long with dynamic contents in the id.
50+
register_post_type( 'my/own/post/type/too/long', array() ); // Bad. Invalid chars: "/" and too long.
51+
52+
register_post_type( 'wp_block', array() ); // Bad. Must only error on reserved keyword, not invalid prefix.

0 commit comments

Comments
 (0)