diff --git a/includes/Free/Free_Loader.php b/includes/Free/Free_Loader.php index 6134c08f9..66ba7ab93 100644 --- a/includes/Free/Free_Loader.php +++ b/includes/Free/Free_Loader.php @@ -1012,6 +1012,11 @@ public function post_form_templates( $integrations ) { $integrations['post_form_template_events_calendar'] = new \WeDevs\Wpuf\Integrations\Events_Calendar\Templates\Event_Form_Template(); } + // Enable WP Job Manager free template. + if ( class_exists( 'WP_Job_Manager' ) ) { + $integrations['post_form_template_wp_job_manager'] = new \WeDevs\Wpuf\Integrations\WP_Job_Manager\Template_Free(); + } + return $integrations; } diff --git a/includes/Integrations.php b/includes/Integrations.php index 7fb050b08..1d149d347 100644 --- a/includes/Integrations.php +++ b/includes/Integrations.php @@ -23,6 +23,7 @@ class Integrations { 'WCMp' => 'WPUF_WCMp_Integration', 'ACF' => 'WPUF_ACF_Compatibility', 'Tribe__Events__Main' => 'Events_Calendar\Events_Calendar_Integration', + 'WP_Job_Manager' => 'WP_Job_Manager\WPUF_WP_Job_Manager_Integration', 'N8N' => 'WPUF_N8N_Integration', ]; diff --git a/includes/Integrations/WP_Job_Manager/Meta_Mapper.php b/includes/Integrations/WP_Job_Manager/Meta_Mapper.php new file mode 100644 index 000000000..5130780c3 --- /dev/null +++ b/includes/Integrations/WP_Job_Manager/Meta_Mapper.php @@ -0,0 +1,236 @@ +is_job_listing_form( $form_settings ) ) { + return; + } + + $this->apply_approval_override( $post_id, $form_settings ); + + // Signal to WPJM that this is a frontend submission — WPJM's own + // shutdown handler will fire `job_manager_job_submitted` on the next + // publish transition. See ensure_job_submission_action_triggered(). + update_post_meta( $post_id, '_public_submission', 1 ); + + $this->persist_meta( $post_id ); + } + + /** + * Handle post-edit submission. + * + * @since WPUF_SINCE + * + * @param int $post_id Updated post ID. + * @param int $form_id WPUF form ID. + * @param array $form_settings WPUF form settings. + * @param array $meta_vars Raw meta fields from the submission. + * + * @return void + */ + public function handle_after_update( $post_id, $form_id, $form_settings, $meta_vars = [] ) { + if ( ! $this->is_job_listing_form( $form_settings ) ) { + return; + } + + $this->persist_meta( $post_id ); + } + + /** + * Is this submission targeting `job_listing`? + * + * @since WPUF_SINCE + * + * @param array $form_settings WPUF form settings. + * + * @return bool + */ + private function is_job_listing_form( $form_settings ) { + return isset( $form_settings['post_type'] ) + && self::POST_TYPE === $form_settings['post_type']; + } + + /** + * Override post_status to `pending` when WPJM requires approval. + * + * WPUF has already inserted the post by this hook, so we flip the status + * before WPJM's own `transition_post_status` listener fires. + * + * @since WPUF_SINCE + * + * @param int $post_id Post ID. + * @param array $form_settings WPUF form settings. + * + * @return void + */ + private function apply_approval_override( $post_id, $form_settings ) { + $requires_approval = (bool) get_option( 'job_manager_submission_requires_approval' ); + + /** + * Filter the approval decision for a WPUF-submitted job listing. + * + * @since WPUF_SINCE + * + * @param bool $requires_approval Whether the submission should land as pending. + * @param int $post_id Post ID. + * @param array $form_settings WPUF form settings. + */ + $requires_approval = (bool) apply_filters( + 'wpuf_wpjm_should_approve', + $requires_approval, + $post_id, + $form_settings + ); + + if ( ! $requires_approval ) { + return; + } + + $post = get_post( $post_id ); + + if ( ! $post || 'pending' === $post->post_status ) { + return; + } + + wp_update_post( + [ + 'ID' => $post_id, + 'post_status' => 'pending', + ] + ); + } + + /** + * Persist posted meta to WPJM-known keys with WPJM's sanitizers applied. + * + * We read from `$_POST` directly because WPUF's `$meta_vars` arrives + * already split into typed arrays; the submitted field name is the + * canonical WPJM meta key in our templates, so pulling from `$_POST` + * keeps the mapping one-to-one and lets us skip fields the form omits. + * + * @since WPUF_SINCE + * + * @param int $post_id Post ID. + * + * @return void + */ + private function persist_meta( $post_id ) { + $mapping = $this->get_meta_mapping(); + + foreach ( $mapping as $field_name => $meta_key ) { + if ( ! isset( $_POST[ $field_name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + continue; + } + + $raw_value = wp_unslash( $_POST[ $field_name ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + $sanitized = $this->sanitize_for_meta( $raw_value, $meta_key ); + + update_post_meta( $post_id, $meta_key, $sanitized ); + } + } + + /** + * Default WPUF-field → WPJM-meta mapping. + * + * Both taxonomies (`job_listing_category`, `job_listing_type`) are handled + * by WPUF's taxonomy field directly and are intentionally absent here. + * + * @since WPUF_SINCE + * + * @return array + */ + private function get_meta_mapping() { + $mapping = [ + '_job_location' => '_job_location', + '_remote_position' => '_remote_position', + '_application' => '_application', + '_company_name' => '_company_name', + '_company_website' => '_company_website', + '_company_tagline' => '_company_tagline', + '_company_twitter' => '_company_twitter', + '_company_video' => '_company_video', + ]; + + /** + * Filter the WPUF-field → WPJM-meta mapping. + * + * @since WPUF_SINCE + * + * @param array $mapping Default mapping. + */ + return apply_filters( 'wpuf_wpjm_meta_mapping', $mapping ); + } + + /** + * Sanitize a value using WPJM's own callbacks where applicable. + * + * @since WPUF_SINCE + * + * @param mixed $value Raw value. + * @param string $meta_key WPJM meta key. + * + * @return mixed + */ + private function sanitize_for_meta( $value, $meta_key ) { + if ( ! class_exists( WP_Job_Manager_Post_Types::class ) ) { + return is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : sanitize_text_field( $value ); + } + + switch ( $meta_key ) { + case '_company_website': + case '_company_video': + return WP_Job_Manager_Post_Types::sanitize_meta_field_url( $value ); + + case '_application': + return WP_Job_Manager_Post_Types::sanitize_meta_field_application( $value ); + + case '_remote_position': + return (int) (bool) $value; + + default: + return WP_Job_Manager_Post_Types::sanitize_meta_field_based_on_input_type( $value, $meta_key ); + } + } +} diff --git a/includes/Integrations/WP_Job_Manager/Template_Free.php b/includes/Integrations/WP_Job_Manager/Template_Free.php new file mode 100644 index 000000000..a990fd36e --- /dev/null +++ b/includes/Integrations/WP_Job_Manager/Template_Free.php @@ -0,0 +1,287 @@ +enabled = class_exists( 'WP_Job_Manager' ); + $this->title = __( 'Post a Job', 'wp-user-frontend' ); + $this->description = __( + 'Form for submitting jobs. The WP Job Manager plugin is required.', + 'wp-user-frontend' + ); + $this->image = WPUF_ASSET_URI . '/images/templates/post.svg'; + $this->form_fields = $this->get_form_fields(); + $this->form_settings = $this->get_form_settings(); + } + + /** + * Get form fields + * + * @since WPUF_SINCE + * + * @return array + */ + public function get_form_fields() { + $form_fields = [ + [ + 'input_type' => 'text', + 'template' => 'post_title', + 'required' => 'yes', + 'label' => __( 'Job Title', 'wp-user-frontend' ), + 'name' => 'post_title', + 'is_meta' => 'no', + 'help' => '', + 'css' => '', + 'placeholder' => __( 'Enter the job title', 'wp-user-frontend' ), + 'default' => '', + 'size' => '40', + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'textarea', + 'template' => 'post_content', + 'required' => 'yes', + 'label' => __( 'Description', 'wp-user-frontend' ), + 'name' => 'post_content', + 'is_meta' => 'no', + 'help' => __( 'Full job description', 'wp-user-frontend' ), + 'css' => '', + 'rows' => '5', + 'cols' => '25', + 'placeholder' => '', + 'default' => '', + 'rich' => 'yes', + 'insert_image' => 'yes', + 'word_restriction' => '', + 'wpuf_cond' => $this->conditionals, + 'text_editor_control' => [], + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'text', + 'template' => 'text_field', + 'required' => 'no', + 'label' => __( 'Location', 'wp-user-frontend' ), + 'name' => '_job_location', + 'is_meta' => 'yes', + 'help' => __( 'e.g., "London, UK" or "Remote"', 'wp-user-frontend' ), + 'css' => '', + 'placeholder' => '', + 'default' => '', + 'size' => '40', + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'checkbox', + 'template' => 'checkbox_field', + 'required' => 'no', + 'label' => __( 'Remote Position', 'wp-user-frontend' ), + 'name' => '_remote_position', + 'is_meta' => 'yes', + 'options' => [ + '1' => __( 'This is a remote position', 'wp-user-frontend' ), + ], + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'taxonomy', + 'template' => 'taxonomy', + 'required' => 'no', + 'label' => __( 'Job Type', 'wp-user-frontend' ), + 'name' => 'job_listing_type', + 'is_meta' => 'no', + 'type' => 'select', + 'tax' => 'job_listing_type', + 'exclude_type' => 'exclude', + 'exclude' => '', + 'orderby' => 'name', + 'order' => 'ASC', + 'first' => __( '— Select —', 'wp-user-frontend' ), + 'woo_attr' => 'no', + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'text', + 'template' => 'text_field', + 'required' => 'yes', + 'label' => __( 'Application Email or URL', 'wp-user-frontend' ), + 'name' => '_application', + 'is_meta' => 'yes', + 'help' => __( 'Email or URL applicants should use to apply', 'wp-user-frontend' ), + 'css' => '', + 'placeholder' => '', + 'default' => '', + 'size' => '40', + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'text', + 'template' => 'text_field', + 'required' => 'yes', + 'label' => __( 'Company Name', 'wp-user-frontend' ), + 'name' => '_company_name', + 'is_meta' => 'yes', + 'help' => '', + 'css' => '', + 'placeholder' => '', + 'default' => '', + 'size' => '40', + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'url', + 'template' => 'website_url', + 'required' => 'no', + 'label' => __( 'Company Website', 'wp-user-frontend' ), + 'name' => '_company_website', + 'is_meta' => 'yes', + 'width' => 'large', + 'size' => 40, + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'text', + 'template' => 'text_field', + 'required' => 'no', + 'label' => __( 'Company Tagline', 'wp-user-frontend' ), + 'name' => '_company_tagline', + 'is_meta' => 'yes', + 'help' => '', + 'css' => '', + 'placeholder' => '', + 'default' => '', + 'size' => '40', + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'text', + 'template' => 'text_field', + 'required' => 'no', + 'label' => __( 'Twitter Username', 'wp-user-frontend' ), + 'name' => '_company_twitter', + 'is_meta' => 'yes', + 'help' => __( 'Without the @ symbol', 'wp-user-frontend' ), + 'css' => '', + 'placeholder' => '', + 'default' => '', + 'size' => '40', + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + [ + 'input_type' => 'url', + 'template' => 'website_url', + 'required' => 'no', + 'label' => __( 'Company Video', 'wp-user-frontend' ), + 'name' => '_company_video', + 'is_meta' => 'yes', + 'width' => 'large', + 'size' => 40, + 'wpuf_cond' => $this->conditionals, + 'id' => uniqid( 'wpuf_', true ), + 'is_new' => true, + ], + ]; + + /** + * Filter the WP Job Manager free-template form fields. + * + * @since WPUF_SINCE + * + * @param array $form_fields Field definitions. + * @param Template_Free $template The template instance. + */ + return apply_filters( 'wpuf_wpjm_free_form_fields', $form_fields, $this ); + } + + /** + * Get form settings + * + * @since WPUF_SINCE + * + * @return array + */ + public function get_form_settings() { + $post_status = get_option( 'job_manager_submission_requires_approval' ) + ? 'pending' + : 'publish'; + + return [ + 'post_type' => 'job_listing', + 'post_status' => $post_status, + 'default_cat' => '-1', + 'guest_post' => 'false', + 'message_restrict' => __( + 'This page is restricted. Please Log in / Register to view this page.', + 'wp-user-frontend' + ), + 'redirect_to' => 'post', + 'comment_status' => 'open', + 'submit_text' => __( 'Submit Job', 'wp-user-frontend' ), + 'edit_post_status' => $post_status, + 'edit_redirect_to' => 'same', + 'update_message' => sprintf( + // translators: %1$s is opening link tag, %2$s is closing link tag + __( + 'Job has been updated successfully. %1$sView job%2$s', + 'wp-user-frontend' + ), + '', + '' + ), + 'edit_url' => '', + 'update_text' => __( 'Update Job', 'wp-user-frontend' ), + 'form_template' => 'post_form_template_wp_job_manager', + 'notification' => [ + 'new' => 'on', + 'new_to' => get_option( 'admin_email' ), + 'new_subject' => __( 'A new job has been submitted', 'wp-user-frontend' ), + 'new_body' => __( + "Hi,\nA new job has been submitted on your site {sitename} ({siteurl}).\n\nJob Title: {post_title}\nDescription: {post_content}\nAuthor: {author}\nJob URL: {permalink}\nEdit URL: {editlink}", + 'wp-user-frontend' + ), + 'edit' => 'on', + 'edit_to' => get_option( 'admin_email' ), + 'edit_subject' => __( 'A job has been updated', 'wp-user-frontend' ), + 'edit_body' => __( + "Hi,\nThe job \"{post_title}\" has been updated.\n\nJob URL: {permalink}\nEdit URL: {editlink}", + 'wp-user-frontend' + ), + ], + ]; + } +} diff --git a/includes/Integrations/WP_Job_Manager/WPUF_WP_Job_Manager_Integration.php b/includes/Integrations/WP_Job_Manager/WPUF_WP_Job_Manager_Integration.php new file mode 100644 index 000000000..926f10b70 --- /dev/null +++ b/includes/Integrations/WP_Job_Manager/WPUF_WP_Job_Manager_Integration.php @@ -0,0 +1,74 @@ +init_handlers(); + } + + /** + * Initialize handlers + * + * @since WPUF_SINCE + * + * @return void + */ + private function init_handlers() { + if ( ! $this->is_wpjm_active() ) { + return; + } + + /** + * Kill-switch to disable the WPUF × WP Job Manager integration. + * + * @since WPUF_SINCE + * + * @param bool $enabled Whether the integration should run. + */ + if ( ! apply_filters( 'wpuf_enable_wpjm_integration', true ) ) { + return; + } + + $this->meta_mapper = new Meta_Mapper(); + $this->meta_mapper->register_hooks(); + + /** + * Fires once the WP Job Manager integration has initialized. + * + * @since WPUF_SINCE + * + * @param WPUF_WP_Job_Manager_Integration $integration The integration instance. + */ + do_action( 'wpuf_wpjm_integration_ready', $this ); + } + + /** + * Check if WP Job Manager is active + * + * @since WPUF_SINCE + * + * @return bool + */ + private function is_wpjm_active() { + return class_exists( 'WP_Job_Manager' ); + } +} diff --git a/wpuf-functions.php b/wpuf-functions.php index 0b0b14276..c35a36dc3 100644 --- a/wpuf-functions.php +++ b/wpuf-functions.php @@ -6384,6 +6384,13 @@ function wpuf_get_free_taxonomies() { // Built-in taxonomies that are always available $free_taxonomies = [ 'category', 'post_tag' ]; + // WP Job Manager taxonomies — allowed in free so users can build + // functional job submission forms without a Pro license. + if ( class_exists( 'WP_Job_Manager' ) ) { + $free_taxonomies[] = 'job_listing_category'; + $free_taxonomies[] = 'job_listing_type'; + } + // Allow filtering to add more free taxonomies //$free_taxonomies = apply_filters( 'wpuf_free_taxonomies', $free_taxonomies );