@@ -26,14 +26,202 @@ class Post_Type_License extends Base {
2626 */
2727 const LABEL = 'License ' ;
2828
29+ /**
30+ * Cache group for dotted slug lookups.
31+ *
32+ * @var string
33+ */
34+ const CACHE_GROUP = 'osi_license_slugs ' ;
35+
36+ /**
37+ * To register action/filters.
38+ *
39+ * @return void
40+ */
41+ protected function setup_hooks () {
42+ parent ::setup_hooks ();
43+
44+ // Save side: preserve dots when creating/updating license posts.
45+ add_filter ( 'wp_insert_post_data ' , array ( $ this , 'preserve_dots_on_save ' ), 10 , 2 );
46+ add_filter ( 'wp_unique_post_slug ' , array ( $ this , 'preserve_dots_in_unique_slug ' ), 10 , 6 );
47+
48+ // Query side: restore dotted slug during lookups so WP_Query finds the post.
49+ add_filter ( 'sanitize_title ' , array ( $ this , 'restore_dots_on_query ' ), 10 , 3 );
50+
51+ // Cache invalidation: clear when a license is saved, trashed, or deleted.
52+ add_action ( 'save_post_ ' . self ::SLUG , array ( $ this , 'clear_slug_cache ' ) );
53+ add_action ( 'before_delete_post ' , array ( $ this , 'clear_slug_cache ' ) );
54+ add_action ( 'wp_trash_post ' , array ( $ this , 'clear_slug_cache ' ) );
55+ }
56+
57+ /**
58+ * Preserve dots in the post slug when saving a license post.
59+ *
60+ * WordPress sanitize_title strips dots by default. This rebuilds
61+ * the slug with dots preserved using a placeholder swap.
62+ *
63+ * @param array $data An array of slashed, sanitized post data.
64+ * @param array $postarr An array of sanitized post data (unslashed).
65+ *
66+ * @return array The modified post data.
67+ */
68+ public function preserve_dots_on_save ( array $ data , array $ postarr ) {
69+ if ( self ::SLUG !== $ data ['post_type ' ] ) {
70+ return $ data ;
71+ }
72+
73+ $ raw_source = ! empty ( $ postarr ['post_name ' ] ) ? $ postarr ['post_name ' ] : $ data ['post_title ' ];
74+
75+ if ( false === strpos ( $ raw_source , '. ' ) ) {
76+ return $ data ;
77+ }
78+
79+ $ data ['post_name ' ] = $ this ->sanitize_slug_with_dots ( $ raw_source );
80+
81+ return $ data ;
82+ }
83+
84+ /**
85+ * Preserve dots in the unique post slug for license posts.
86+ *
87+ * WordPress may strip dots when checking slug uniqueness.
88+ * This ensures the dot-containing slug survives.
89+ *
90+ * @param string $slug The post slug.
91+ * @param integer $post_id Post ID.
92+ * @param string $post_status The post status.
93+ * @param string $post_type Post type.
94+ * @param integer $post_parent Post parent ID.
95+ * @param string $original_slug The original slug.
96+ *
97+ * @return string The slug with dots preserved for license posts.
98+ */
99+ public function preserve_dots_in_unique_slug ( string $ slug , int $ post_id , string $ post_status , string $ post_type , int $ post_parent , string $ original_slug ) {
100+ if ( self ::SLUG !== $ post_type ) {
101+ return $ slug ;
102+ }
103+
104+ if ( false === strpos ( $ original_slug , '. ' ) || false !== strpos ( $ slug , '. ' ) ) {
105+ return $ slug ;
106+ }
107+
108+ // Only restore the dotted slug if no other license post already uses it.
109+ global $ wpdb ;
110+ $ existing = $ wpdb ->get_var (
111+ $ wpdb ->prepare (
112+ "SELECT ID FROM {$ wpdb ->posts } WHERE post_name = %s AND post_type = %s AND ID != %d LIMIT 1 " ,
113+ $ original_slug ,
114+ $ post_type ,
115+ $ post_id
116+ )
117+ );
118+
119+ return $ existing ? $ slug : $ original_slug ;
120+ }
121+
122+ /**
123+ * Restore dotted slugs during query-time sanitization.
124+ *
125+ * When WordPress queries a post by slug, it runs sanitize_title
126+ * which strips dots. This checks whether a license post with the
127+ * dotted version of the slug exists and returns it if so.
128+ *
129+ * Uses wp_cache and a single $wpdb query for the existence check
130+ * to avoid infinite loops (get_posts would call sanitize_title again).
131+ *
132+ * @param string $title The sanitized title.
133+ * @param string $raw_title The raw title before sanitization.
134+ * @param string $context The context (e.g., 'save', 'display', 'query').
135+ *
136+ * @return string The dotted slug if a matching license exists, otherwise the default.
137+ */
138+ public function restore_dots_on_query ( string $ title , string $ raw_title , string $ context ) {
139+ // Skip during saves — the wp_insert_post_data hook handles that.
140+ if ( 'save ' === $ context ) {
141+ return $ title ;
142+ }
143+
144+ if ( false === strpos ( $ raw_title , '. ' ) ) {
145+ return $ title ;
146+ }
147+
148+ // Build what the dotted slug would look like.
149+ $ dotted_slug = $ this ->sanitize_slug_with_dots ( $ raw_title );
150+
151+ // If sanitization didn't change anything, no dots were stripped.
152+ if ( $ dotted_slug === $ title ) {
153+ return $ title ;
154+ }
155+
156+ // Check cache first.
157+ $ cache_key = 'slug_ ' . md5 ( $ dotted_slug );
158+ $ cached = wp_cache_get ( $ cache_key , self ::CACHE_GROUP );
159+
160+ if ( false !== $ cached ) {
161+ return $ cached ? $ dotted_slug : $ title ;
162+ }
163+
164+ // Check if a license post with this dotted slug exists.
165+ global $ wpdb ;
166+ $ exists = (bool ) $ wpdb ->get_var (
167+ $ wpdb ->prepare (
168+ "SELECT ID FROM {$ wpdb ->posts } WHERE post_name = %s AND post_type = %s LIMIT 1 " ,
169+ $ dotted_slug ,
170+ self ::SLUG
171+ )
172+ );
173+
174+ wp_cache_set ( $ cache_key , $ exists , self ::CACHE_GROUP );
175+
176+ return $ exists ? $ dotted_slug : $ title ;
177+ }
178+
179+ /**
180+ * Clear the slug cache when a license post is saved, trashed, or deleted.
181+ *
182+ * @param integer $post_id The post ID.
183+ *
184+ * @return void
185+ */
186+ public function clear_slug_cache ( int $ post_id ) {
187+ if ( self ::SLUG !== get_post_type ( $ post_id ) ) {
188+ return ;
189+ }
190+
191+ $ post = get_post ( $ post_id );
192+ if ( ! $ post ) {
193+ return ;
194+ }
195+
196+ $ cache_key = 'slug_ ' . md5 ( $ post ->post_name );
197+ wp_cache_delete ( $ cache_key , self ::CACHE_GROUP );
198+ }
199+
200+ /**
201+ * Sanitize a string as a slug while preserving dots.
202+ *
203+ * Swaps dots for a placeholder, runs the standard WordPress
204+ * sanitize_title_with_dashes, then restores the dots.
205+ *
206+ * @param string $raw The raw string to sanitize.
207+ *
208+ * @return string The sanitized slug with dots preserved.
209+ */
210+ private function sanitize_slug_with_dots ( string $ raw ) {
211+ $ placeholder = 'xdotx ' ;
212+ $ with_placeholder = str_replace ( '. ' , $ placeholder , $ raw );
213+ $ sanitized = sanitize_title_with_dashes ( $ with_placeholder , '' , 'save ' );
214+
215+ return str_replace ( $ placeholder , '. ' , $ sanitized );
216+ }
217+
29218 /**
30219 * To get list of labels for post type.
31220 *
32221 * @return array
33222 */
34223 public function get_labels () {
35-
36- return [
224+ return array (
37225 'name ' => __ ( 'Licenses ' , 'osi-features ' ),
38226 'singular_name ' => __ ( 'License ' , 'osi-features ' ),
39227 'all_items ' => __ ( 'Licenses ' , 'osi-features ' ),
@@ -45,7 +233,6 @@ public function get_labels() {
45233 'search_items ' => __ ( 'Search License ' , 'osi-features ' ),
46234 'not_found ' => __ ( 'No License found ' , 'osi-features ' ),
47235 'not_found_in_trash ' => __ ( 'No License found in Trash ' , 'osi-features ' ),
48- ];
49-
236+ );
50237 }
51238}
0 commit comments