@@ -4849,8 +4849,6 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true )
48494849 $ post_name = wp_add_trashed_suffix_to_post_name_for_post ( $ post_id );
48504850 }
48514851
4852- $ post_name = wp_unique_post_slug ( $ post_name , $ post_id , $ post_status , $ post_type , $ post_parent );
4853-
48544852 // Don't unslash.
48554853 $ post_mime_type = $ postarr ['post_mime_type ' ] ?? '' ;
48564854
@@ -4927,6 +4925,81 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true )
49274925 $ data = wp_unslash ( $ data );
49284926 $ where = array ( 'ID ' => $ post_id );
49294927
4928+ if ( ! $ update ) {
4929+ // If there is a suggested ID, use it if not already present.
4930+ if ( ! empty ( $ import_id ) ) {
4931+ $ import_id = (int ) $ import_id ;
4932+
4933+ if ( ! $ wpdb ->get_var ( $ wpdb ->prepare ( "SELECT ID FROM $ wpdb ->posts WHERE ID = %d " , $ import_id ) ) ) {
4934+ $ data ['ID ' ] = $ import_id ;
4935+ }
4936+ }
4937+ }
4938+
4939+ $ data_post_status = $ data ['post_status ' ];
4940+ $ data_post_type = $ data ['post_type ' ];
4941+ $ data_post_parent = (int ) $ data ['post_parent ' ];
4942+
4943+ /*
4944+ * Acquire a MySQL advisory lock to eliminate the TOCTOU race between the
4945+ * wp_unique_post_slug() SELECT and the INSERT/UPDATE below. Without this lock
4946+ * two concurrent requests can both observe the same slug as free and write
4947+ * duplicate post_name values.
4948+ *
4949+ * A single posts-table-scoped lock is used because attachment slug uniqueness
4950+ * overlaps all post types: using narrower locks allows an attachment and another
4951+ * post type to miss each other and write the same slug concurrently.
4952+ *
4953+ * The lock is skipped for statuses and post types where wp_unique_post_slug()
4954+ * performs no DB query (draft, pending, auto-draft, revisions, user_request,
4955+ * nav_menu_item), since no slug allocation race is possible there.
4956+ */
4957+ $ needs_slug_lock = ! (
4958+ in_array ( $ data_post_status , array ( 'draft ' , 'pending ' , 'auto-draft ' ), true )
4959+ || ( 'inherit ' === $ data_post_status && 'revision ' === $ data_post_type )
4960+ || 'user_request ' === $ data_post_type
4961+ || 'nav_menu_item ' === $ data_post_type
4962+ );
4963+
4964+ $ slug_lock_name = '' ;
4965+ $ lock_acquired = false ;
4966+
4967+ if ( $ needs_slug_lock ) {
4968+ $ slug_lock_name = 'wp_post_slug_ ' . md5 ( $ wpdb ->posts );
4969+
4970+ /**
4971+ * Filters the timeout in seconds for the advisory slug lock used during wp_insert_post().
4972+ *
4973+ * When the lock cannot be acquired within this many seconds the insertion or update
4974+ * fails closed to preserve slug uniqueness under concurrent load.
4975+ *
4976+ * @since x.x.x
4977+ *
4978+ * @param int $timeout Lock wait timeout in seconds. Default 10.
4979+ * @param string $slug_lock_name Advisory lock name, scoped to the posts table.
4980+ * @param string $post_type Post type being inserted or updated.
4981+ */
4982+ $ timeout = max ( 0 , (int ) apply_filters ( 'wp_post_slug_lock_timeout ' , 10 , $ slug_lock_name , $ data_post_type ) );
4983+ $ lock_result = $ wpdb ->get_var ( $ wpdb ->prepare ( 'SELECT GET_LOCK(%s, %d) ' , $ slug_lock_name , $ timeout ) );
4984+ $ lock_acquired = '1 ' === (string ) $ lock_result ;
4985+
4986+ if ( ! $ lock_acquired ) {
4987+ if ( $ wp_error ) {
4988+ return new WP_Error ( 'db_lock_error ' , __ ( 'Could not acquire post slug lock. ' ), $ wpdb ->last_error );
4989+ }
4990+
4991+ return 0 ;
4992+ }
4993+ }
4994+
4995+ $ data ['post_name ' ] = wp_unique_post_slug (
4996+ $ data ['post_name ' ],
4997+ $ post_id ,
4998+ $ data_post_status ,
4999+ $ data_post_type ,
5000+ $ data_post_parent
5001+ );
5002+
49305003 if ( $ update ) {
49315004 /**
49325005 * Fires immediately before an existing post is updated in the database.
@@ -4937,8 +5010,23 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true )
49375010 * @param array $data Array of unslashed post data.
49385011 */
49395012 do_action ( 'pre_post_update ' , $ post_id , $ data );
5013+ } else {
5014+ /**
5015+ * Fires immediately before a new post is inserted in the database.
5016+ *
5017+ * @since 6.9.0
5018+ *
5019+ * @param array $data Array of unslashed post data.
5020+ */
5021+ do_action ( 'pre_post_insert ' , $ data );
5022+ }
49405023
5024+ if ( $ update ) {
49415025 if ( false === $ wpdb ->update ( $ wpdb ->posts , $ data , $ where ) ) {
5026+ if ( $ lock_acquired ) {
5027+ $ wpdb ->query ( $ wpdb ->prepare ( 'SELECT RELEASE_LOCK(%s) ' , $ slug_lock_name ) );
5028+ }
5029+
49425030 if ( $ wp_error ) {
49435031 if ( 'attachment ' === $ post_type ) {
49445032 $ message = __ ( 'Could not update attachment in the database. ' );
@@ -4952,25 +5040,11 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true )
49525040 }
49535041 }
49545042 } else {
4955- // If there is a suggested ID, use it if not already present.
4956- if ( ! empty ( $ import_id ) ) {
4957- $ import_id = (int ) $ import_id ;
4958-
4959- if ( ! $ wpdb ->get_var ( $ wpdb ->prepare ( "SELECT ID FROM $ wpdb ->posts WHERE ID = %d " , $ import_id ) ) ) {
4960- $ data ['ID ' ] = $ import_id ;
5043+ if ( false === $ wpdb ->insert ( $ wpdb ->posts , $ data ) ) {
5044+ if ( $ lock_acquired ) {
5045+ $ wpdb ->query ( $ wpdb ->prepare ( 'SELECT RELEASE_LOCK(%s) ' , $ slug_lock_name ) );
49615046 }
4962- }
4963-
4964- /**
4965- * Fires immediately before a new post is inserted in the database.
4966- *
4967- * @since 6.9.0
4968- *
4969- * @param array $data Array of unslashed post data.
4970- */
4971- do_action ( 'pre_post_insert ' , $ data );
49725047
4973- if ( false === $ wpdb ->insert ( $ wpdb ->posts , $ data ) ) {
49745048 if ( $ wp_error ) {
49755049 if ( 'attachment ' === $ post_type ) {
49765050 $ message = __ ( 'Could not insert attachment into the database. ' );
@@ -4991,12 +5065,16 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true )
49915065 }
49925066
49935067 if ( empty ( $ data ['post_name ' ] ) && ! in_array ( $ data ['post_status ' ], array ( 'draft ' , 'pending ' , 'auto-draft ' ), true ) ) {
4994- $ data ['post_name ' ] = wp_unique_post_slug ( sanitize_title ( $ data ['post_title ' ], $ post_id ), $ post_id , $ data [ ' post_status ' ] , $ post_type , $ post_parent );
5068+ $ data ['post_name ' ] = wp_unique_post_slug ( sanitize_title ( $ data ['post_title ' ], $ post_id ), $ post_id , $ data_post_status , $ data_post_type , $ data_post_parent );
49955069
49965070 $ wpdb ->update ( $ wpdb ->posts , array ( 'post_name ' => $ data ['post_name ' ] ), $ where );
49975071 clean_post_cache ( $ post_id );
49985072 }
49995073
5074+ if ( $ lock_acquired ) {
5075+ $ wpdb ->query ( $ wpdb ->prepare ( 'SELECT RELEASE_LOCK(%s) ' , $ slug_lock_name ) );
5076+ }
5077+
50005078 if ( is_object_in_taxonomy ( $ post_type , 'category ' ) ) {
50015079 wp_set_post_categories ( $ post_id , $ post_category );
50025080 }
0 commit comments