$input The input for the mutation * @param \WP_Post_Type $post_type_object The post_type_object for the type of post being mutated * @param string $mutation_name The name of the mutation being performed * * @return array * @throws \Exception */ public static function prepare_post_object( $input, $post_type_object, $mutation_name ) { $insert_post_args = []; /** * Set the post_type for the insert */ $insert_post_args['post_type'] = $post_type_object->name; /** * Prepare the data for inserting the post * NOTE: These are organized in the same order as: https://developer.wordpress.org/reference/functions/wp_insert_post/ */ if ( ! empty( $input['authorId'] ) ) { $insert_post_args['post_author'] = Utils::get_database_id_from_id( $input['authorId'] ); } if ( ! empty( $input['date'] ) && false !== strtotime( $input['date'] ) ) { $insert_post_args['post_date'] = gmdate( 'Y-m-d H:i:s', strtotime( $input['date'] ) ); } if ( ! empty( $input['content'] ) ) { $insert_post_args['post_content'] = $input['content']; } if ( ! empty( $input['title'] ) ) { $insert_post_args['post_title'] = $input['title']; } if ( ! empty( $input['excerpt'] ) ) { $insert_post_args['post_excerpt'] = $input['excerpt']; } if ( ! empty( $input['status'] ) ) { $insert_post_args['post_status'] = $input['status']; } if ( ! empty( $input['commentStatus'] ) ) { $insert_post_args['comment_status'] = $input['commentStatus']; } if ( ! empty( $input['pingStatus'] ) ) { $insert_post_args['ping_status'] = $input['pingStatus']; } if ( ! empty( $input['password'] ) ) { $insert_post_args['post_password'] = $input['password']; } if ( ! empty( $input['slug'] ) ) { $insert_post_args['post_name'] = $input['slug']; } if ( ! empty( $input['toPing'] ) ) { $insert_post_args['to_ping'] = $input['toPing']; } if ( ! empty( $input['pinged'] ) ) { $insert_post_args['pinged'] = $input['pinged']; } if ( ! empty( $input['parentId'] ) ) { $insert_post_args['post_parent'] = Utils::get_database_id_from_id( $input['parentId'] ); } if ( ! empty( $input['menuOrder'] ) ) { $insert_post_args['menu_order'] = $input['menuOrder']; } if ( ! empty( $input['mimeType'] ) ) { $insert_post_args['post_mime_type'] = $input['mimeType']; } if ( ! empty( $input['commentCount'] ) ) { $insert_post_args['comment_count'] = $input['commentCount']; } /** * Filter the $insert_post_args * * @param array $insert_post_args The array of $input_post_args that will be passed to wp_insert_post * @param array $input The data that was entered as input for the mutation * @param \WP_Post_Type $post_type_object The post_type_object that the mutation is affecting * @param string $mutation_type The type of mutation being performed (create, edit, etc) * * @hookGroup models * @since 0.0.5 */ $insert_post_args = apply_filters( 'graphql_post_object_insert_post_args', $insert_post_args, $input, $post_type_object, $mutation_name ); /** * Return the $args */ return $insert_post_args; } /** * This updates additional data related to a post object, such as postmeta, term relationships, * etc. * * @param int $post_id The ID of the postObject being mutated * @param array $input The input for the mutation * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated * @param string $mutation_name The name of the mutation (ex: create, update, delete) * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers * @param string $default_post_status The default status posts should use if an intended status wasn't set * @param string $intended_post_status The intended post_status the post should have according to the mutation input * * @return void */ public static function update_additional_post_object_data( $post_id, $input, $post_type_object, $mutation_name, AppContext $context, ResolveInfo $info, $default_post_status = null, $intended_post_status = null ) { /** * Sets the post lock * * @param bool $is_locked Whether the post is locked * @param int $post_id The ID of the postObject being mutated * @param array $input The input for the mutation * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated * @param string $mutation_name The name of the mutation (ex: create, update, delete) * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers * @param ?string $intended_post_status The intended post_status the post should have according to the mutation input * @param ?string $default_post_status The default status posts should use if an intended status wasn't set */ if ( true === apply_filters( 'graphql_post_object_mutation_set_edit_lock', true, $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status ) ) { /** * Set the post_lock for the $new_post_id */ self::set_edit_lock( $post_id ); } /** * Update the _edit_last field */ update_post_meta( $post_id, '_edit_last', get_current_user_id() ); /** * Update the postmeta fields */ if ( ! empty( $input['desiredSlug'] ) ) { update_post_meta( $post_id, '_wp_desired_post_slug', $input['desiredSlug'] ); } /** * Set the object terms * * @param int $post_id The ID of the postObject being mutated * @param array $input The input for the mutation * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated * @param string $mutation_name The name of the mutation (ex: create, update, delete) */ self::set_object_terms( $post_id, $input, $post_type_object, $mutation_name ); /** * Run an action after the additional data has been updated. This is a great spot to hook into to * update additional data related to postObjects, such as setting relationships, updating additional postmeta, * or sending emails to Kevin. . .whatever you need to do with the postObject. * * @param int $post_id The ID of the postObject being mutated * @param array $input The input for the mutation * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated * @param string $mutation_name The name of the mutation (ex: create, update, delete) * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers * @param ?string $intended_post_status The intended post_status the post should have according to the mutation input * @param ?string $default_post_status The default status posts should use if an intended status wasn't set * * @hookGroup models * @since 0.0.5 */ do_action( 'graphql_post_object_mutation_update_additional_data', $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status ); /** * Sets the post lock * * @param bool $is_locked Whether the post is locked. * @param int $post_id The ID of the postObject being mutated * @param array $input The input for the mutation * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated * @param string $mutation_name The name of the mutation (ex: create, update, delete) * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers * @param ?string $intended_post_status The intended post_status the post should have according to the mutation input * @param ?string $default_post_status The default status posts should use if an intended status wasn't set * * @return bool */ if ( true === apply_filters( 'graphql_post_object_mutation_set_edit_lock', true, $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status ) ) { /** * Set the post_lock for the $new_post_id */ self::remove_edit_lock( $post_id ); } } /** * Given a $post_id and $input from the mutation, check to see if any term associations are * being made, and properly set the relationships * * @param int $post_id The ID of the postObject being mutated * @param array $input The input for the mutation * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated * @param string $mutation_name The name of the mutation (ex: create, update, delete) * * @return void */ protected static function set_object_terms( int $post_id, array $input, WP_Post_Type $post_type_object, string $mutation_name ) { /** * Fire an action before setting object terms during a GraphQL Post Object Mutation. * * One example use for this hook would be to create terms from the input that may not exist yet, so that they can be set as a relation below. * * @param int $post_id The ID of the postObject being mutated * @param array $input The input for the mutation * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated * @param string $mutation_name The name of the mutation (ex: create, update, delete) * * @hookGroup models * @since 0.0.5 */ do_action( 'graphql_post_object_mutation_set_object_terms', $post_id, $input, $post_type_object, $mutation_name ); /** * Get the allowed taxonomies and iterate through them to find the term inputs to use for setting relationships. */ $allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies( 'objects' ); foreach ( $allowed_taxonomies as $tax_object ) { /** * If the taxonomy is in the array of taxonomies registered to the post_type */ if ( in_array( $tax_object->name, get_object_taxonomies( $post_type_object->name ), true ) ) { /** * If there is input for the taxonomy, process it */ if ( isset( $input[ lcfirst( $tax_object->graphql_plural_name ) ] ) ) { $term_input = $input[ lcfirst( $tax_object->graphql_plural_name ) ]; /** * Default append to true, but allow input to set it to false. */ $append = ! isset( $term_input['append'] ) || false !== $term_input['append']; /** * Start an array of terms to connect */ $terms_to_connect = []; /** * Filter whether to allow terms to be created during a post mutation. * * If a post mutation includes term input for a term that does not already exist, * this will allow terms to be created in order to connect the term to the post object, * but if filtered to false, this will prevent the term that doesn't already exist * from being created during the mutation of the post. * * @param bool $allow_term_creation Whether new terms should be created during the post object mutation * @param \WP_Taxonomy $tax_object The Taxonomy object for the term being added to the Post Object * * @hookGroup models * @since 0.0.5 */ $allow_term_creation = apply_filters( 'graphql_post_object_mutations_allow_term_creation', true, $tax_object ); /** * If there are nodes in the term_input */ if ( ! empty( $term_input['nodes'] ) && is_array( $term_input['nodes'] ) ) { foreach ( $term_input['nodes'] as $node ) { $term_exists = false; /** * Handle the input for ID first. */ if ( ! empty( $node['id'] ) ) { if ( ! absint( $node['id'] ) ) { $id_parts = Relay::fromGlobalId( $node['id'] ); if ( ! empty( $id_parts['id'] ) ) { $term_exists = get_term_by( 'id', absint( $id_parts['id'] ), $tax_object->name ); if ( isset( $term_exists->term_id ) ) { $terms_to_connect[] = $term_exists->term_id; } } } else { $term_exists = get_term_by( 'id', absint( $node['id'] ), $tax_object->name ); if ( isset( $term_exists->term_id ) ) { $terms_to_connect[] = $term_exists->term_id; } } /** * Next, handle the input for slug if there wasn't an ID input */ } elseif ( ! empty( $node['slug'] ) ) { $sanitized_slug = sanitize_text_field( $node['slug'] ); $term_exists = get_term_by( 'slug', $sanitized_slug, $tax_object->name ); if ( isset( $term_exists->term_id ) ) { $terms_to_connect[] = $term_exists->term_id; } /** * Finally, handle the input for name if there wasn't an ID or slug input. * * Matching by name lets users assign existing terms (e.g. a standard * post format like "video") without needing the `edit_terms` capability * that creating a new term requires. */ } elseif ( ! empty( $node['name'] ) ) { $sanitized_name = sanitize_text_field( $node['name'] ); $term_exists = get_term_by( 'name', $sanitized_name, $tax_object->name ); if ( isset( $term_exists->term_id ) ) { $terms_to_connect[] = $term_exists->term_id; } } /** * If no term exists so far, and terms are set to be allowed to be created * during a post object mutation, create the term to connect based on the * input */ if ( ! $term_exists && true === $allow_term_creation ) { /** * If the current user cannot edit terms, don't create a term to connect. * * Skip just this node rather than aborting the whole taxonomy, so that * any existing terms the user matched (and is allowed to assign) are still * connected below. */ if ( ! isset( $tax_object->cap->edit_terms ) || ! current_user_can( $tax_object->cap->edit_terms ) ) { continue; } $created_term = self::create_term_to_connect( $node, $tax_object->name ); if ( ! empty( $created_term ) ) { $terms_to_connect[] = $created_term; } } } } /** * If the current user cannot assign terms of this taxonomy, skip it * without connecting any of its terms, but keep processing the * remaining taxonomies rather than aborting the whole mutation's * term assignment. */ if ( ! isset( $tax_object->cap->assign_terms ) || ! current_user_can( $tax_object->cap->assign_terms ) ) { continue; } if ( $append && 'category' === $tax_object->name ) { $default_category_id = absint( get_option( 'default_category' ) ); if ( ! in_array( $default_category_id, $terms_to_connect, true ) ) { wp_remove_object_terms( $post_id, $default_category_id, 'category' ); } } wp_set_object_terms( $post_id, $terms_to_connect, $tax_object->name, $append ); } } } } /** * Given an array of Term properties (slug, name, description, etc), create the term and return * a term_id * * @param array $node The node input for the term * @param string $taxonomy The taxonomy the term input is for * * @return int $term_id The ID of the created term. 0 if no term was created. */ protected static function create_term_to_connect( $node, $taxonomy ) { $created_term = []; $term_to_create = []; $term_args = []; if ( ! empty( $node['name'] ) ) { $term_to_create['name'] = sanitize_text_field( $node['name'] ); } elseif ( ! empty( $node['slug'] ) ) { $term_to_create['name'] = sanitize_text_field( $node['slug'] ); } if ( ! empty( $node['slug'] ) ) { $term_args['slug'] = sanitize_text_field( $node['slug'] ); } if ( ! empty( $node['description'] ) ) { $term_args['description'] = sanitize_text_field( $node['description'] ); } /** * @todo: consider supporting "parent" input in $term_args */ if ( isset( $term_to_create['name'] ) && ! empty( $term_to_create['name'] ) ) { $created_term = wp_insert_term( $term_to_create['name'], $taxonomy, $term_args ); } if ( is_wp_error( $created_term ) ) { if ( isset( $created_term->error_data['term_exists'] ) ) { return $created_term->error_data['term_exists']; } return 0; } /** * Return the created term, or 0 */ return isset( $created_term['term_id'] ) ? absint( $created_term['term_id'] ) : 0; } /** * This is a copy of the wp_set_post_lock function that exists in WordPress core, but is not * accessible because that part of WordPress is never loaded for WPGraphQL executions * * Mark the post as currently being edited by the current user * * @param int $post_id ID of the post being edited. * * @return int[]|false Array of the lock time and user ID. False if the post does not exist, or * there is no current user. */ public static function set_edit_lock( $post_id ) { $post = get_post( $post_id ); $user_id = get_current_user_id(); if ( empty( $post ) ) { return false; } if ( 0 === $user_id ) { return false; } $now = time(); $lock = "$now:$user_id"; update_post_meta( $post->ID, '_edit_lock', $lock ); return [ $now, $user_id ]; } /** * Remove the edit lock for a post * * @param int $post_id ID of the post to delete the lock for * * @return bool */ public static function remove_edit_lock( int $post_id ) { $post = get_post( $post_id ); if ( empty( $post ) ) { return false; } return delete_post_meta( $post->ID, '_edit_lock' ); } /** * Check the edit lock for a post * * @param false|int $post_id ID of the post to delete the lock for * @param array $input The input for the mutation * * @return false|int Return false if no lock or the user_id of the owner of the lock */ public static function check_edit_lock( $post_id, array $input ) { if ( false === $post_id ) { return false; } // If override the edit lock is set, return early if ( isset( $input['ignoreEditLock'] ) && true === $input['ignoreEditLock'] ) { return false; } if ( ! function_exists( 'wp_check_post_lock' ) ) { require_once ABSPATH . 'wp-admin/includes/post.php'; } return wp_check_post_lock( $post_id ); } }