Completed
Push — develop ( 3215cf...d39ea9 )
by David
07:50
created

Wordlift_Entity_Link_Service::slug_exists()   C

Complexity

Conditions 7
Paths 3

Size

Total Lines 49
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 17
nc 3
nop 2
dl 0
loc 49
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Services: Entity Link Service.
5
 *
6
 * The Wordlift_Entity_Link_Service handles linking and rendering of entities. In order to perform such actions it hooks
7
 * on different WordPress' actions and filters:
8
 *
9
 *  1. to change links to entities, also supporting an empty entity post type slug, e.g. http://example.org/entity-name
10
 *     instead of http://example.org/entity/entity-name, this requires hooking to:
11
 *     a) post_type_link in order to remove the 'entity' post type slug, when links are rendered by WordPress,
12
 *     b) pre_get_posts in order to alter the WP Query instance and add our own entity post type to the query when WordPress
13
 *        needs to decide which post to show (otherwise we would get a 404).
14
 *
15
 *  2. when using an empty entity post type slug (but we perform in any case, should the entity post type slug set to
16
 *     empty later on), we need to ensure that the posts/pages/entities' slugs to not conflict (even though WordPress
17
 *     itself allows conflicts, see https://core.trac.wordpress.org/ticket/13459 ). To do so we hook to a couple of filters
18
 *     to validate a slug:
19
 *     a) for posts and entities, to wp_unique_post_slug_is_bad_flat_slug, and we check that no other page, post or entity
20
 *        uses that slug,
21
 *     b) for pages, to wp_unique_post_slug_is_bad_hierarchical_slug.
22
 *     If we find that the slug (postname) is already used, we tell WordPress, which in turn will append a sequential number.
23
 *
24
 * @since 3.6.0
25
 */
26
class Wordlift_Entity_Link_Service {
27
28
	/**
29
	 * The entity type service.
30
	 *
31
	 * @since  3.6.0
32
	 * @access private
33
	 * @var Wordlift_Entity_Post_Type_Service $entity_type_service The entity type service.
34
	 */
35
	private $entity_type_service;
36
37
	/**
38
	 * The entity post type slug.
39
	 *
40
	 * @since  3.6.0
41
	 * @access private
42
	 * @var string $slug The entity post type slug.
43
	 */
44
	private $slug;
45
46
	/**
47
	 * A logger instance.
48
	 *
49
	 * @since  3.6.0
50
	 * @access private
51
	 * @var Wordlift_Log_Service
52
	 */
53
	private $log;
54
55
	/**
56
	 * Wordlift_Entity_Link_Service constructor.
57
	 *
58
	 * @since 3.6.0
59
	 *
60
	 * @param Wordlift_Entity_Post_Type_Service $entity_type_service
61
	 * @param string                            $slug The entity post type slug.
62
	 */
63
	public function __construct( $entity_type_service, $slug ) {
64
65
		$this->log = Wordlift_Log_Service::get_logger( 'Wordlift_Entity_Link_Service' );
66
67
		$this->entity_type_service = $entity_type_service;
68
		$this->slug                = $slug;
69
70
	}
71
72
	/**
73
	 * Intercept link generation to posts in order to customize links to entities.
74
	 *
75
	 * @since 3.6.0
76
	 *
77
	 * @param string  $post_link The post's permalink.
78
	 * @param WP_Post $post      The post in question.
79
	 * @param bool    $leavename Whether to keep the post name.
80
	 * @param bool    $sample    Is it a sample permalink.
81
	 *
82
	 * @return string The link to the post.
83
	 */
84
	public function post_type_link( $post_link, $post, $leavename, $sample ) {
0 ignored issues
show
Unused Code introduced by
The parameter $leavename is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $sample is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
85
86
		// Return the post link if this is not our post type.
87
		if ( ! empty( $this->slug ) || $this->entity_type_service->get_post_type() !== get_post_type( $post ) ) {
88
			return $post_link;
89
		}
90
91
		// Replace /slug/post_name/ with /post_name/
92
		// The slug comes from the Entity Type Service since that service is responsible for registering the default
93
		// slug.
94
		return str_replace( "/{$this->entity_type_service->get_slug()}/$post->post_name/", "/$post->post_name/", $post_link );
95
	}
96
97
	/**
98
	 * Alter the query to look for our own custom type.
99
	 *
100
	 * @since 3.6.0
101
	 *
102
	 * @param WP_Query $query
103
	 */
104
	public function pre_get_posts( $query ) {
105
106
		// If a slug has been set, we don't need to alter the query.
107
		if ( ! empty( $this->slug ) ) {
108
			return;
109
		}
110
111
		// Check if it's a query we should extend with our own custom post type.
112
		//
113
		// The `$query->query` count could be > 2 if the preview parameter is passed too.
114
		//
115
		// See https://github.com/insideout10/wordlift-plugin/issues/439
116
		if ( ! $query->is_main_query() || 2 > count( $query->query ) || ! isset( $query->query['page'] ) || empty( $query->query['name'] ) ) {
117
			return;
118
		}
119
120
		// Add our own post type to the query.
121
		$post_types = '' === $query->get( 'post_type' )
122
			? Wordlift_Entity_Service::valid_entity_post_types()
123
			: array_merge( (array) $query->get( 'post_type' ), (array) $this->entity_type_service->get_post_type() );
124
		$query->set( 'post_type', $post_types );
125
126
	}
127
128
	/**
129
	 * Hook to WordPress' wp_unique_post_slug_is_bad_flat_slug filter. This is called when a page is saved.
130
	 *
131
	 * @since 3.6.0
132
	 *
133
	 * @param bool   $bad_slug  Whether the post slug would be bad as a flat slug.
134
	 * @param string $slug      The post slug.
135
	 * @param string $post_type Post type.
136
	 *
137
	 * @return bool Whether the slug is bad.
138
	 */
139
	public function wp_unique_post_slug_is_bad_flat_slug( $bad_slug, $slug, $post_type ) {
140
141
		// The list of post types that might have conflicting slugs.
142
		$post_types = Wordlift_Entity_Service::valid_entity_post_types();
143
144
		// Ignore post types different from the ones we need to check.
145
		if ( ! in_array( $post_type, $post_types ) ) {
146
			return $bad_slug;
147
		}
148
149
		$exists = $this->slug_exists( $slug, $post_types );
150
151
		$this->log->debug( "Checking if a slug exists [ post type :: $post_type ][ slug :: $slug ][ exists :: " . ( $exists ? 'yes' : 'no' ) . ' ]' );
152
153
		return $exists;
154
	}
155
156
	/**
157
	 * Hook to WordPress' wp_unique_post_slug_is_bad_hierarchical_slug filter. This is called when a page is saved.
158
	 *
159
	 * @since 3.6.0
160
	 *
161
	 * @param bool   $bad_slug  Whether the post slug would be bad as a flat slug.
162
	 * @param string $slug      The post slug.
163
	 * @param string $post_type Post type.
164
	 * @param int    $post_parent
165
	 *
166
	 * @return bool Whether the slug is bad.
167
	 */
168
	public function wp_unique_post_slug_is_bad_hierarchical_slug( $bad_slug, $slug, $post_type, $post_parent ) {
0 ignored issues
show
Unused Code introduced by
The parameter $post_parent is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
169
170
		// We only care about pages here.
171
		if ( 'page' !== $post_type ) {
172
			return $bad_slug;
173
		}
174
175
		// We redirect the call to the flat hook, this means that this check is going to solve also the 6-years old issue
176
		// about overlapping slugs among pages and posts:
177
		// https://core.trac.wordpress.org/ticket/13459
178
		return $this->wp_unique_post_slug_is_bad_flat_slug( $bad_slug, $slug, $post_type );
179
	}
180
181
	/**
182
	 * Check whether a slug exists already for the specified post types.
183
	 *
184
	 * @since 3.6.0
185
	 *
186
	 * @param string $slug       The slug.
187
	 * @param array  $post_types An array of post types.
188
	 *
189
	 * @return bool True if the slug exists, otherwise false.
190
	 */
191
	private function slug_exists( $slug, $post_types ) {
192
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
193
194
		// Loop through all post types and check
195
		// whether they have archive pages and if
196
		// the archive slug matches the post slug.
197
		//
198
		// Note that the condition below checks only post types used by WordLift.
199
		// We don't check other post types for archive pages,
200
		// because this is a job of WordPress.
201
		//
202
		// There is a open ticket that should solve this, when it's merged:
203
		// https://core.trac.wordpress.org/ticket/13459
204
		foreach ( $post_types as $post_type ) {
205
206
			// Get the post type object for current post type.
207
			$post_type_object = get_post_type_object( $post_type );
208
209
			if (
210
				// Check whetherthe post type object is not empty.
211
				! empty( $post_type_object ) &&
212
				// And the post type has archive page.
213
				$post_type_object->has_archive &&
214
				// And `rewrite` options exists..
215
				! empty( $post_type_object->rewrite ) &&
216
				// And the `rewrite` slug property is not empty.
217
				! empty( $post_type_object->rewrite['slug'] ) &&
218
				// And if the rewrite slug equals to the slug.
219
				$post_type_object->rewrite['slug'] === $slug
220
			) {
221
				// Return true which means that the slug is already in use.
222
				return true;
223
			}
224
225
		}
226
227
		// Post slugs must be unique across all posts.
228
		$check_sql = $wpdb->prepare(
229
			"SELECT post_name
230
			FROM $wpdb->posts
231
			WHERE post_name = %s
232
			AND post_type IN ('" . implode( "', '", array_map( 'esc_sql', $post_types ) ) . "')
233
			LIMIT 1
234
			",
235
			$slug
236
		);
237
238
		return null !== $wpdb->get_var( $check_sql );
239
	}
240
241
}
242