From f4e5a67b6e1864c86e12062cc6e2cd310c691f92 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 29 Jan 2024 18:36:02 +0200 Subject: [PATCH 01/50] Create stats initializer class skeleton. --- includes/class-wp-job-manager-stats.php | 49 +++++++++++++++++++++++++ includes/class-wp-job-manager.php | 9 +++++ 2 files changed, 58 insertions(+) create mode 100644 includes/class-wp-job-manager-stats.php diff --git a/includes/class-wp-job-manager-stats.php b/includes/class-wp-job-manager-stats.php new file mode 100644 index 000000000..f994fdb4d --- /dev/null +++ b/includes/class-wp-job-manager-stats.php @@ -0,0 +1,49 @@ +init(); + } + + /** + * Get the instance. + * + * @return WP_Job_Manager_Stats + */ + public static function instance(): WP_Job_Manager_Stats { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Do initialization of all the things needed for stats. + */ + public function init() { + // Do init stuff. + } +} diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index d25c92096..acb14c06c 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -38,6 +38,13 @@ class WP_Job_Manager { */ public $post_types; + /** + * Stats. + * + * @var WP_Job_Manager_Stats + */ + public $stats; + /** * Main WP Job Manager Instance. * @@ -80,6 +87,7 @@ public function __construct() { include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-access-token.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-guest-user.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-guest-session.php'; + include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-wp-job-manager-stats.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui-settings.php'; @@ -93,6 +101,7 @@ public function __construct() { // Init classes. $this->forms = WP_Job_Manager_Forms::instance(); $this->post_types = WP_Job_Manager_Post_Types::instance(); + $this->stats = WP_Job_Manager_Stats::instance(); // Schedule cron jobs. add_action( 'init', [ __CLASS__, 'maybe_schedule_cron_jobs' ] ); From 79e8137d46120f20fb1716a9dce08f5356cf2e45 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 29 Jan 2024 19:00:46 +0200 Subject: [PATCH 02/50] Refactor to using existing trait. --- includes/class-wp-job-manager-stats.php | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/includes/class-wp-job-manager-stats.php b/includes/class-wp-job-manager-stats.php index f994fdb4d..781d53e81 100644 --- a/includes/class-wp-job-manager-stats.php +++ b/includes/class-wp-job-manager-stats.php @@ -5,6 +5,8 @@ * @package wp-job-manager */ +use WP_Job_Manager\Singleton; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -13,12 +15,7 @@ * This class is responsible for initializing all aspects of stats for wpjm. */ class WP_Job_Manager_Stats { - /** - * Holds the one instance of stats. - * - * @var null|WP_Job_Manager_Stats - */ - private static $instance = null; + use Singleton; /** * Constructor. @@ -27,19 +24,6 @@ private function __construct() { $this->init(); } - /** - * Get the instance. - * - * @return WP_Job_Manager_Stats - */ - public static function instance(): WP_Job_Manager_Stats { - if ( null === self::$instance ) { - self::$instance = new self(); - } - - return self::$instance; - } - /** * Do initialization of all the things needed for stats. */ From 8b98e3b4b29d0631c8a2eaa1ca24d53dc80335e9 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 29 Jan 2024 19:08:34 +0200 Subject: [PATCH 03/50] Namespace-it --- .../{class-wp-job-manager-stats.php => class-stats.php} | 4 ++-- includes/class-wp-job-manager.php | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) rename includes/{class-wp-job-manager-stats.php => class-stats.php} (88%) diff --git a/includes/class-wp-job-manager-stats.php b/includes/class-stats.php similarity index 88% rename from includes/class-wp-job-manager-stats.php rename to includes/class-stats.php index 781d53e81..98cc71116 100644 --- a/includes/class-wp-job-manager-stats.php +++ b/includes/class-stats.php @@ -5,7 +5,7 @@ * @package wp-job-manager */ -use WP_Job_Manager\Singleton; +namespace WP_Job_Manager; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -14,7 +14,7 @@ /** * This class is responsible for initializing all aspects of stats for wpjm. */ -class WP_Job_Manager_Stats { +class Stats { use Singleton; /** diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index acb14c06c..ee06cd377 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -10,6 +10,8 @@ exit; } +use WP_Job_Manager\Stats; + /** * Handles core plugin hooks and action setup. * @@ -87,7 +89,7 @@ public function __construct() { include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-access-token.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-guest-user.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-guest-session.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-wp-job-manager-stats.php'; + include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-stats.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui-settings.php'; @@ -101,7 +103,7 @@ public function __construct() { // Init classes. $this->forms = WP_Job_Manager_Forms::instance(); $this->post_types = WP_Job_Manager_Post_Types::instance(); - $this->stats = WP_Job_Manager_Stats::instance(); + $this->stats = Stats::instance(); // Schedule cron jobs. add_action( 'init', [ __CLASS__, 'maybe_schedule_cron_jobs' ] ); From ef45993d77b2dffe02782c0111a81146deb60558 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 29 Jan 2024 19:31:33 +0200 Subject: [PATCH 04/50] Add activation skeleton method. --- includes/class-stats.php | 8 ++++++++ includes/class-wp-job-manager.php | 1 + 2 files changed, 9 insertions(+) diff --git a/includes/class-stats.php b/includes/class-stats.php index 98cc71116..70266628f 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -30,4 +30,12 @@ private function __construct() { public function init() { // Do init stuff. } + + /** + * Perform plugin activation-related stats actions. + * + * @return void + */ + public function activate() { + } } diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index ee06cd377..63910aa65 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -152,6 +152,7 @@ public function add_to_allowed_redirect_hosts( $hosts ) { * Performs plugin activation steps. */ public function activate() { + Stats::instance()->activate(); WP_Job_Manager_Ajax::add_endpoint(); unregister_post_type( \WP_Job_Manager_Post_Types::PT_LISTING ); add_filter( 'pre_option_job_manager_enable_types', '__return_true' ); From d95908fd4a2abdcba8f49b39d06ff3ff941df3eb Mon Sep 17 00:00:00 2001 From: Fernando Jorge Mota Date: Tue, 30 Jan 2024 13:23:29 -0300 Subject: [PATCH 05/50] Add Stats Schema (#2719) Co-authored-by: Panos Kountanis --- .github/workflows/php.yml | 6 ++- includes/class-stats.php | 41 ++++++++++++++++++- .../class-wp-job-manager-data-cleaner.php | 33 +++++++++++++++ includes/class-wp-job-manager-install.php | 2 + 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index f3ea4d832..d89455d31 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -51,9 +51,11 @@ jobs: php: [ '7.4', '8.0', '8.1' ] include: - php: '7.4' - wp: '6.0' + wp: '6.2' - php: '7.4' - wp: '6.1' + wp: '6.3' + - php: '7.4' + wp: '6.4' env: WP_VERSION: ${{ matrix.wp }} PHP_VERSION: ${{ matrix.php }} diff --git a/includes/class-stats.php b/includes/class-stats.php index 70266628f..08c616db8 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -28,7 +28,46 @@ private function __construct() { * Do initialization of all the things needed for stats. */ public function init() { - // Do init stuff. + $this->initialize_wpdb(); + } + + /** + * Initialize the alias for the stats table on the wpdb object. + * + * @return void + */ + private function initialize_wpdb() { + global $wpdb; + if ( isset( $wpdb->wpjm_stats ) ) { + return; + } + $wpdb->wpjm_stats = $wpdb->prefix . 'wpjm_stats'; + $wpdb->tables[] = 'wpjm_stats'; + } + + /** + * Migrate the stats table to the latest version. + * + * @return void + */ + public function migrate() { + global $wpdb; + $collate = $wpdb->get_charset_collate(); + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + \dbDelta( + [ + "CREATE TABLE {$wpdb->wpjm_stats} ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `date` date NOT NULL, + `post_id` bigint(20) DEFAULT NULL, + `name` varchar(255) NOT NULL, + `group` varchar(255) DEFAULT '', + `count` bigint(20) unsigned not null default 1, + PRIMARY KEY (`id`), + INDEX `idx_wpjm_stats_name_date_group` (`name`, `date`, `group`) + ) {$collate}", + ] + ); } /** diff --git a/includes/class-wp-job-manager-data-cleaner.php b/includes/class-wp-job-manager-data-cleaner.php index ce992a3d0..4cdc77c9c 100644 --- a/includes/class-wp-job-manager-data-cleaner.php +++ b/includes/class-wp-job-manager-data-cleaner.php @@ -28,6 +28,15 @@ class WP_Job_Manager_Data_Cleaner { \WP_Job_Manager_Post_Types::PT_GUEST_USER, ]; + /** + * Custom tables to be deleted. + * + * @var array + */ + private const CUSTOM_TABLES = [ + 'wpjm_stats', + ]; + /** * Taxonomies to be deleted. * @@ -195,6 +204,7 @@ class WP_Job_Manager_Data_Cleaner { */ public static function cleanup_all() { self::cleanup_custom_post_types(); + self::cleanup_custom_tables(); self::cleanup_taxonomies(); self::cleanup_pages(); self::cleanup_cron_jobs(); @@ -231,6 +241,29 @@ private static function cleanup_custom_post_types() { } } + /** + * Cleanup data for custom tables. + * + * @return void + */ + private static function cleanup_custom_tables() { + global $wpdb; + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- We need to delete the custom tables. + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- We don't cache DROP TABLE. + // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnsupportedPlaceholder -- %i is supported since WP 6.2. + // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- %i is supported since WP 6.2. + // phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange -- We really need to delete the custom tables. + foreach ( self::CUSTOM_TABLES as $custom_table ) { + $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . $custom_table ) ); + } + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery + // phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching + // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnsupportedPlaceholder + // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + // phpcs:enable WordPress.DB.DirectDatabaseQuery.SchemaChange + } + /** * Cleanup data for taxonomies. * diff --git a/includes/class-wp-job-manager-install.php b/includes/class-wp-job-manager-install.php index 87b86aabb..0ae9e4275 100644 --- a/includes/class-wp-job-manager-install.php +++ b/includes/class-wp-job-manager-install.php @@ -76,6 +76,8 @@ public static function install() { update_option( 'job_manager_permalinks', wp_json_encode( $permalink_options ) ); } + \WP_Job_Manager\Stats::instance()->migrate(); + delete_transient( 'wp_job_manager_addons_html' ); update_option( 'wp_job_manager_version', JOB_MANAGER_VERSION ); } From db67abfd1652f760cb77985697bf3a998fce6cc8 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Wed, 31 Jan 2024 15:09:15 +0200 Subject: [PATCH 06/50] Adding log_stat method and example use. --- includes/class-stats.php | 98 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/includes/class-stats.php b/includes/class-stats.php index 08c616db8..26e6b46b5 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -29,6 +29,7 @@ private function __construct() { */ public function init() { $this->initialize_wpdb(); + $this->hook(); } /** @@ -59,17 +60,82 @@ public function migrate() { "CREATE TABLE {$wpdb->wpjm_stats} ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `date` date NOT NULL, - `post_id` bigint(20) DEFAULT NULL, + `post_id` bigint(20) DEFAULT 0, `name` varchar(255) NOT NULL, `group` varchar(255) DEFAULT '', `count` bigint(20) unsigned not null default 1, PRIMARY KEY (`id`), - INDEX `idx_wpjm_stats_name_date_group` (`name`, `date`, `group`) + INDEX `idx_wpjm_stats_name_date_group` (`name`, `date`, `group`), + UNIQUE INDEX `idx_wpjm_stats_name_date_group_post_id` (`name`, `date`, `group`, `post_id`) ) {$collate}", ] ); } + /** + * Log a stat into the db. + * + * @param string $name The stat name. + * @param string $group The group this stat belongs to. + * @param int $post_id The post_id this stat belongs to. + * @param int $increment_by The amount to increment the stat by. + * + * @return bool + */ + public function log_stat( string $name, string $group = '', int $post_id = 0, int $increment_by = 1 ) { + global $wpdb; + + if ( + strlen( $name ) > 255 || + strlen( $group ) > 255 || + ! is_numeric( $post_id ) || + ! is_numeric( $increment_by ) ) { + return false; + } + + $date_today = gmdate( 'Y-m-d' ); + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + $result = $wpdb->query( + $wpdb->prepare( + "INSERT INTO {$wpdb->wpjm_stats} " . + '(`name`, `date`, `group`, `post_id`, `count` ) ' . + 'VALUES (%s, %s, %s, %d, %d) ' . + 'ON DUPLICATE KEY UPDATE `count` = `count` + %d', + $name, + $date_today, + $group, + $post_id, + $increment_by, + $increment_by + ) + ); + + if ( false === $result ) { + return false; + } + + $cache_key = $this->get_cache_key_for_stat( $name, $group, $post_id ); + + wp_cache_delete( $cache_key, 'wpjm_stats' ); + + return true; + } + + /** + * Get a cache key for a stat. + * + * @param string $name The name. + * @param string $group The optional group. + * @param int $post_id The optional post id. + * + * @return string + */ + private function get_cache_key_for_stat( string $name, string $group = '', int $post_id = 0 ) { + $hash = md5( "{$name}_{$group}_{$post_id}" ); + return "wpjm_stat_{$hash}"; + } + /** * Perform plugin activation-related stats actions. * @@ -77,4 +143,32 @@ public function migrate() { */ public function activate() { } + + /** + * Run any hooks related to stats. + * + * @return void + */ + private function hook() { + add_action( 'wp', [ $this, 'maybe_log_listing_view' ] ); + } + + /** + * Log a (non-unique) listing page view. + * + * @return void + */ + public function maybe_log_listing_view() { + if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { + return; + } + + $post_id = absint( get_queried_object_id() ); + $post_type = get_post_type( $post_id ); + if ( 'job_listing' !== $post_type ) { + return; + } + + $this->log_stat( 'job_listing_view', '', $post_id ); + } } From 73db35f0f418997508b5f834de3e258495e9af8e Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Thu, 1 Feb 2024 12:44:59 +0200 Subject: [PATCH 07/50] Update includes/class-stats.php Co-authored-by: Fernando Jorge Mota --- includes/class-stats.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-stats.php b/includes/class-stats.php index 26e6b46b5..a557d6855 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -165,7 +165,7 @@ public function maybe_log_listing_view() { $post_id = absint( get_queried_object_id() ); $post_type = get_post_type( $post_id ); - if ( 'job_listing' !== $post_type ) { + if ( \WP_Job_Manager_Post_Types::PT_LISTING !== $post_type ) { return; } From 7670ec6d35bbc5ead52797cee0aa3114d907a0eb Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Thu, 1 Feb 2024 13:12:09 +0200 Subject: [PATCH 08/50] Change log_stat method signature. --- includes/class-stats.php | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/includes/class-stats.php b/includes/class-stats.php index a557d6855..0fb5314a9 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -17,6 +17,12 @@ class Stats { use Singleton; + const DEFAULT_LOG_STAT_ARGS = [ + 'group' => '', + 'post_id' => 0, + 'increment_by' => 1, + ]; + /** * Constructor. */ @@ -76,15 +82,24 @@ public function migrate() { * Log a stat into the db. * * @param string $name The stat name. - * @param string $group The group this stat belongs to. - * @param int $post_id The post_id this stat belongs to. - * @param int $increment_by The amount to increment the stat by. + * @param array $args { + * Optional args for this stat. + * + * @type string $group The group this stat belongs to. + * @type int $post_id The post_id this stat belongs to. + * @type int $increment_by The amount to increment the stat by. + * } * * @return bool */ - public function log_stat( string $name, string $group = '', int $post_id = 0, int $increment_by = 1 ) { + public function log_stat( string $name, array $args = [] ) { global $wpdb; + $args = array_merge( self::DEFAULT_LOG_STAT_ARGS, $args ); + $group = $args['group']; + $post_id = $args['post_id']; + $increment_by = $args['increment_by']; + if ( strlen( $name ) > 255 || strlen( $group ) > 255 || @@ -169,6 +184,6 @@ public function maybe_log_listing_view() { return; } - $this->log_stat( 'job_listing_view', '', $post_id ); + $this->log_stat( 'job_listing_view', [ 'post_id' => $post_id ] ); } } From 52a1dadc7fd471752d7533ea0b0f9753a16075c8 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Thu, 1 Feb 2024 13:19:05 +0200 Subject: [PATCH 09/50] Schema tweaks. --- includes/class-stats.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/class-stats.php b/includes/class-stats.php index 0fb5314a9..70e91c188 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -66,12 +66,11 @@ public function migrate() { "CREATE TABLE {$wpdb->wpjm_stats} ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `date` date NOT NULL, - `post_id` bigint(20) DEFAULT 0, + `post_id` bigint(20) DEFAULT NULL, `name` varchar(255) NOT NULL, `group` varchar(255) DEFAULT '', `count` bigint(20) unsigned not null default 1, PRIMARY KEY (`id`), - INDEX `idx_wpjm_stats_name_date_group` (`name`, `date`, `group`), UNIQUE INDEX `idx_wpjm_stats_name_date_group_post_id` (`name`, `date`, `group`, `post_id`) ) {$collate}", ] From a2e65b2b2063be9bb16047ceaa03c4ee0718309f Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Wed, 31 Jan 2024 15:52:27 +0200 Subject: [PATCH 10/50] Admin Stats Settings --- .../admin/class-wp-job-manager-settings.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/includes/admin/class-wp-job-manager-settings.php b/includes/admin/class-wp-job-manager-settings.php index 71ffcd225..663b4dcbd 100644 --- a/includes/admin/class-wp-job-manager-settings.php +++ b/includes/admin/class-wp-job-manager-settings.php @@ -528,6 +528,45 @@ protected function init_settings() { ], ], ], + 'stats' => [ + __( 'Stats', 'wp-job-manager' ), + [ + [ + 'name' => 'job_manager_stats_enable', + 'std' => '0', + 'label' => __( 'Enable stats', 'wp-job-manager' ), + 'cb_label' => __( 'Enable stats', 'wp-job-manager' ), + 'desc' => __( 'Enable recording various stats (e.g. listing views) and showing them to employers', 'wp-job-manager' ), + 'type' => 'checkbox', + 'attributes' => [], + ], + [ + 'name' => 'job_manager_stats_require_paid_listing', + 'std' => '0', + 'label' => __( 'Require paid listing', 'wp-job-manager' ), + 'cb_label' => __( 'Require paid listing', 'wp-job-manager' ), + 'desc' => __( 'Only display stats to employers that purchased a paid-listing package', 'wp-job-manager' ), + 'type' => 'checkbox', + 'attributes' => [], + ], + [ + 'name' => 'job_manager_stats_default_date_range', + 'std' => '30', + 'label' => __( 'Stats default date range', 'wp-job-manager' ), + 'desc' => __( 'The default date range (in days) of stats to display in the frontend dashboard', 'wp-job-manager' ), + 'type' => 'number', + 'attributes' => [], + ], + [ + 'name' => 'job_manager_stats_retention', + 'std' => '90', + 'label' => __( 'Stats retention', 'wp-job-manager' ), + 'desc' => __( 'Max time stats data will be retained.', 'wp-job-manager' ), + 'type' => 'number', + 'attributes' => [], + ], + ], + ], 'job_visibility' => [ __( 'Job Visibility', 'wp-job-manager' ), [ From 722f56508875ab91cef41cf2706e8a879b07402b Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Thu, 1 Feb 2024 13:28:06 +0200 Subject: [PATCH 11/50] Update includes/admin/class-wp-job-manager-settings.php Co-authored-by: Fernando Jorge Mota --- includes/admin/class-wp-job-manager-settings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/admin/class-wp-job-manager-settings.php b/includes/admin/class-wp-job-manager-settings.php index 663b4dcbd..9ed144522 100644 --- a/includes/admin/class-wp-job-manager-settings.php +++ b/includes/admin/class-wp-job-manager-settings.php @@ -560,7 +560,7 @@ protected function init_settings() { [ 'name' => 'job_manager_stats_retention', 'std' => '90', - 'label' => __( 'Stats retention', 'wp-job-manager' ), + 'label' => __( 'Retention Period (days)', 'wp-job-manager' ), 'desc' => __( 'Max time stats data will be retained.', 'wp-job-manager' ), 'type' => 'number', 'attributes' => [], From 539088a0b816f273789cec30a457d4f8cc4ba83e Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Thu, 1 Feb 2024 13:31:34 +0200 Subject: [PATCH 12/50] Update includes/admin/class-wp-job-manager-settings.php Co-authored-by: Fernando Jorge Mota --- includes/admin/class-wp-job-manager-settings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/admin/class-wp-job-manager-settings.php b/includes/admin/class-wp-job-manager-settings.php index 9ed144522..446d3d2b8 100644 --- a/includes/admin/class-wp-job-manager-settings.php +++ b/includes/admin/class-wp-job-manager-settings.php @@ -561,7 +561,7 @@ protected function init_settings() { 'name' => 'job_manager_stats_retention', 'std' => '90', 'label' => __( 'Retention Period (days)', 'wp-job-manager' ), - 'desc' => __( 'Max time stats data will be retained.', 'wp-job-manager' ), + 'desc' => __( 'Enter the maximum number of days for which statistics data will be retained.', 'wp-job-manager' ), 'type' => 'number', 'attributes' => [], ], From 5330cd35df2be9ae6f7cb295e1c2d443ba7a0a21 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Thu, 1 Feb 2024 13:40:07 +0200 Subject: [PATCH 13/50] Remove paid listing setting for now. --- includes/admin/class-wp-job-manager-settings.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/includes/admin/class-wp-job-manager-settings.php b/includes/admin/class-wp-job-manager-settings.php index 446d3d2b8..eb0dfca10 100644 --- a/includes/admin/class-wp-job-manager-settings.php +++ b/includes/admin/class-wp-job-manager-settings.php @@ -540,15 +540,6 @@ protected function init_settings() { 'type' => 'checkbox', 'attributes' => [], ], - [ - 'name' => 'job_manager_stats_require_paid_listing', - 'std' => '0', - 'label' => __( 'Require paid listing', 'wp-job-manager' ), - 'cb_label' => __( 'Require paid listing', 'wp-job-manager' ), - 'desc' => __( 'Only display stats to employers that purchased a paid-listing package', 'wp-job-manager' ), - 'type' => 'checkbox', - 'attributes' => [], - ], [ 'name' => 'job_manager_stats_default_date_range', 'std' => '30', From b33e865fda0dd5d056e7b10aec7d461a99bda594 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Fri, 2 Feb 2024 17:08:50 +0200 Subject: [PATCH 14/50] Add way to log unique stats from the frontend. --- assets/js/wpjm-stats.js | 78 ++++++++++++++++++++++++ includes/class-stats.php | 126 ++++++++++++++++++++++++++++++++++++++- webpack.config.js | 1 + 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 assets/js/wpjm-stats.js diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js new file mode 100644 index 000000000..b3fbf2c05 --- /dev/null +++ b/assets/js/wpjm-stats.js @@ -0,0 +1,78 @@ +/* global wpjm_stats */ +( function () { + function ready(fn) { + if (document.readyState !== 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } + } + + function createCookie(name,value,days,path) { + if (days) { + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + } + else var expires = ""; + document.cookie = name+"="+value+expires+"; path=" + path; + } + + function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; + } + + function eraseCookie(name) { + createCookie(name,"",-1); + } + + ready( function () { + var statsToRecord = []; + var jobStatsSettings = window.job_manager_stats; + var ajaxUrl = jobStatsSettings.ajax_url; + var ajaxNonce = jobStatsSettings.ajax_nonce; + var cookiesToSet = []; + var setCookies = function () { + cookiesToSet.forEach( function ( uniqueKey ) { + createCookie( uniqueKey, 1, 1, '' ); + } ); + }; + + + jobStatsSettings.stats_to_log.forEach( function ( statToRecord ) { + // do something. + var statToRecordKey = statToRecord.name; + var uniqueKey = statToRecord.unique_key || ''; + if ( uniqueKey.length === 0 ) { + statsToRecord.push( statToRecordKey ); + } else { + if ( null === readCookie( uniqueKey) ) { + cookiesToSet.push( uniqueKey ); + statsToRecord.push( statToRecordKey ); + } + } + } ); + // console.log( statsToRecord ); return; + + var postData = new URLSearchParams(); + postData.append( '_ajax_nonce', ajaxNonce ); + postData.append( 'post_id', jobStatsSettings.post_id || 0 ); + postData.append( 'action', 'job_manager_log_stat' ); + postData.append( 'stats', statsToRecord.join(',') ); + fetch( ajaxUrl, { + method: 'POST', + credentials: "same-origin", + body: postData, + } ).finally( function () { + setCookies(); + console.log( 'sent'); + } ); + } ); +}() ); diff --git a/includes/class-stats.php b/includes/class-stats.php index 70e91c188..58c6ea384 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -164,7 +164,9 @@ public function activate() { * @return void */ private function hook() { - add_action( 'wp', [ $this, 'maybe_log_listing_view' ] ); + add_action( 'wp_enqueue_scripts', [ $this, 'frontend_scripts' ] ); + add_action( 'wp_ajax_job_manager_log_stat', [ $this, 'maybe_log_stat_ajax' ] ); + add_action( 'wp_ajax_nopriv_job_manager_log_stat', [ $this, 'maybe_log_stat_ajax' ] ); } /** @@ -185,4 +187,126 @@ public function maybe_log_listing_view() { $this->log_stat( 'job_listing_view', [ 'post_id' => $post_id ] ); } + + /** + * Log multiple stats in one go. Triggered in an ajax call. + * + * @return void + */ + public function maybe_log_stat_ajax() { + if ( ! ( defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) { + return; + } + + $post_data = stripslashes_deep( $_POST ); + + if ( ! isset( $post_data['_ajax_nonce'] ) || ! wp_verify_nonce( $post_data['_ajax_nonce'], 'ajax-nonce' ) ) { + return; + } + + $post_id = isset( $post_data['post_id'] ) ? absint( $post_data['post_id'] ) : 0; + + if ( empty( $post_id ) ) { + return; + } + + $post_type = get_post_type( $post_id ); + + if ( \WP_Job_Manager_Post_Types::PT_LISTING !== $post_type ) { + return; + } + + $stats = isset( $post_data['stats'] ) ? explode( ',', sanitize_text_field( $post_data['stats'] ) ) : []; + + // TODO: Maybe optimize this into a single insert? + foreach ( $stats as $stat_name ) { + $stat_name = trim( strtolower( $stat_name ) ); + if ( ! in_array( $stat_name, $this->get_registered_stat_names(), true ) ) { + continue; + } + $this->log_stat( trim( $stat_name ), [ 'post_id' => $post_id ] ); + } + } + + /** + * Get stat names. + * + * @return int[]|string[] + */ + private function get_registered_stat_names() { + return array_keys( $this->get_registered_stats() ); + } + + /** + * Register any frontend JS scripts. + * + * @return void + */ + public function frontend_scripts() { + $post_id = absint( get_queried_object_id() ); + $post_type = get_post_type( $post_id ); + if ( \WP_Job_Manager_Post_Types::PT_LISTING !== $post_type ) { + return; + } + + \WP_Job_Manager::register_script( + 'wp-job-manager-stats', + 'js/wpjm-stats.js', + [ 'jquery' ], + true + ); + + $script_data = [ + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'ajax_nonce' => wp_create_nonce( 'ajax-nonce' ), + 'post_id' => $post_id, + 'stats_to_log' => $this->get_stats_for_ajax( $post_id ), + ]; + + wp_enqueue_script( 'wp-job-manager-stats' ); + wp_localize_script( + 'wp-job-manager-stats', + 'job_manager_stats', + $script_data + ); + } + + /** + * Get all the registered stats. + * + * @return array + */ + private function get_registered_stats() { + return (array) apply_filters( + 'wpjm_get_registered_stats', + [ + 'job_listing_view' => [], + 'job_listing_view_unique' => [ + 'unique' => true, + ], + ] + ); + } + + /** + * Prepare stats for wp_localize. + * + * @param int $post_id Optional post id. + * + * @return array + */ + private function get_stats_for_ajax( $post_id = 0 ) { + $ajax_stats = []; + foreach ( $this->get_registered_stats() as $stat_name => $stat_data ) { + $stat_ajax = [ + 'name' => $stat_name, + ]; + if ( ! empty( $stat_data['unique'] ) ) { + $stat_ajax['unique_key'] = $stat_name . '_' . $post_id; + } + $ajax_stats[] = $stat_ajax; + } + + return $ajax_stats; + } } diff --git a/webpack.config.js b/webpack.config.js index 7dc2f7d6b..fd64e8baa 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,6 +11,7 @@ const files = { 'js/job-submission': 'js/job-submission.js', 'js/multiselect': 'js/multiselect.js', 'js/term-multiselect': 'js/term-multiselect.js', + 'js/wpjm-stats': 'js/wpjm-stats.js', 'js/admin/job-editor': 'js/admin/job-editor.js', 'js/admin/wpjm-notice-dismiss': 'js/admin/wpjm-notice-dismiss.js', 'js/admin/job-tags-upsell': 'js/admin/job-tags-upsell.js', From c0ea2a95f4e73adf8e34e32f51c8e730bafba621 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 15:08:06 +0200 Subject: [PATCH 15/50] Rename global eslint hint. --- assets/js/wpjm-stats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index b3fbf2c05..2638974ed 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -1,4 +1,4 @@ -/* global wpjm_stats */ +/* global job_manager_stats */ ( function () { function ready(fn) { if (document.readyState !== 'loading') { From a8c855cf7a75e23136e224b7ac6ebd460193fd30 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 15:12:45 +0200 Subject: [PATCH 16/50] Added sources of js funcs. --- assets/js/wpjm-stats.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index 2638974ed..31eeaa0e7 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -1,5 +1,6 @@ /* global job_manager_stats */ ( function () { + // From https://youmightnotneedjquery.com/#ready function ready(fn) { if (document.readyState !== 'loading') { fn(); @@ -8,11 +9,13 @@ } } - function createCookie(name,value,days,path) { + // Cookie funcs are copied verbatim from http://www.quirksmode.org/js/cookies.html, tweaked to include path. + function createCookie( name, value, days, path ) { + var expires = ""; if (days) { var date = new Date(); - date.setTime(date.getTime()+(days*24*60*60*1000)); - var expires = "; expires="+date.toGMTString(); + date.setTime( date.getTime() + ( days * 24 * 60 * 60 * 1000 ) ); + var expires = "; expires=" + date.toGMTString(); } else var expires = ""; document.cookie = name+"="+value+expires+"; path=" + path; From 2d6f3b8469aa0a208cc9ca491602a908b6843453 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 15:33:04 +0200 Subject: [PATCH 17/50] Format js used wp-scripts format --- assets/js/wpjm-stats.js | 50 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index 31eeaa0e7..3d2d5cdf2 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -1,42 +1,41 @@ /* global job_manager_stats */ ( function () { // From https://youmightnotneedjquery.com/#ready - function ready(fn) { - if (document.readyState !== 'loading') { + function ready( fn ) { + if ( document.readyState !== 'loading' ) { fn(); } else { - document.addEventListener('DOMContentLoaded', fn); + document.addEventListener( 'DOMContentLoaded', fn ); } } // Cookie funcs are copied verbatim from http://www.quirksmode.org/js/cookies.html, tweaked to include path. function createCookie( name, value, days, path ) { - var expires = ""; - if (days) { + var expires = ''; + if ( days ) { var date = new Date(); - date.setTime( date.getTime() + ( days * 24 * 60 * 60 * 1000 ) ); - var expires = "; expires=" + date.toGMTString(); - } - else var expires = ""; - document.cookie = name+"="+value+expires+"; path=" + path; + date.setTime( date.getTime() + days * 24 * 60 * 60 * 1000 ); + var expires = '; expires=' + date.toGMTString(); + } else var expires = ''; + document.cookie = name + '=' + value + expires + '; path=' + path; } - function readCookie(name) { - var nameEQ = name + "="; - var ca = document.cookie.split(';'); - for(var i=0;i < ca.length;i++) { - var c = ca[i]; - while (c.charAt(0)==' ') c = c.substring(1,c.length); - if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + function readCookie( name ) { + var nameEQ = name + '='; + var ca = document.cookie.split( ';' ); + for ( var i = 0; i < ca.length; i++ ) { + var c = ca[ i ]; + while ( c.charAt( 0 ) == ' ' ) c = c.substring( 1, c.length ); + if ( c.indexOf( nameEQ ) == 0 ) return c.substring( nameEQ.length, c.length ); } return null; } - function eraseCookie(name) { - createCookie(name,"",-1); + function eraseCookie( name ) { + createCookie( name, '', -1 ); } - ready( function () { + ready( function () { var statsToRecord = []; var jobStatsSettings = window.job_manager_stats; var ajaxUrl = jobStatsSettings.ajax_url; @@ -48,7 +47,6 @@ } ); }; - jobStatsSettings.stats_to_log.forEach( function ( statToRecord ) { // do something. var statToRecordKey = statToRecord.name; @@ -56,7 +54,7 @@ if ( uniqueKey.length === 0 ) { statsToRecord.push( statToRecordKey ); } else { - if ( null === readCookie( uniqueKey) ) { + if ( null === readCookie( uniqueKey ) ) { cookiesToSet.push( uniqueKey ); statsToRecord.push( statToRecordKey ); } @@ -68,14 +66,14 @@ postData.append( '_ajax_nonce', ajaxNonce ); postData.append( 'post_id', jobStatsSettings.post_id || 0 ); postData.append( 'action', 'job_manager_log_stat' ); - postData.append( 'stats', statsToRecord.join(',') ); + postData.append( 'stats', statsToRecord.join( ',' ) ); fetch( ajaxUrl, { method: 'POST', - credentials: "same-origin", + credentials: 'same-origin', body: postData, } ).finally( function () { setCookies(); - console.log( 'sent'); + console.log( 'sent' ); } ); } ); -}() ); +} )(); From 06f02778a941de0724c5252602067b49ed38a005 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 15:35:34 +0200 Subject: [PATCH 18/50] Update assets/js/wpjm-stats.js --- assets/js/wpjm-stats.js | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index 3d2d5cdf2..690735c89 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -60,7 +60,6 @@ } } } ); - // console.log( statsToRecord ); return; var postData = new URLSearchParams(); postData.append( '_ajax_nonce', ajaxNonce ); From 2c44daecbc6859d8866a274a37a1ab62adf9015e Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 15:39:32 +0200 Subject: [PATCH 19/50] Update includes/class-stats.php --- includes/class-stats.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-stats.php b/includes/class-stats.php index 58c6ea384..239a3e09e 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -252,7 +252,7 @@ public function frontend_scripts() { \WP_Job_Manager::register_script( 'wp-job-manager-stats', 'js/wpjm-stats.js', - [ 'jquery' ], + [], true ); From 2e94e88dcc63f9f39136db5359b84ae85a9e928e Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 17:10:46 +0200 Subject: [PATCH 20/50] Update includes/class-stats.php Co-authored-by: Fernando Jorge Mota --- includes/class-stats.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-stats.php b/includes/class-stats.php index 239a3e09e..3277e3c0e 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -194,7 +194,7 @@ public function maybe_log_listing_view() { * @return void */ public function maybe_log_stat_ajax() { - if ( ! ( defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) { + if ( ! wp_doing_ajax() ) { return; } From d13e5a7766a86ddd04ffd650ba28e547f4e7ba85 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 17:29:13 +0200 Subject: [PATCH 21/50] Use localstorage instead of cookies for uniques. --- assets/js/wpjm-stats.js | 58 ++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index 690735c89..33756d57c 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -9,30 +9,20 @@ } } - // Cookie funcs are copied verbatim from http://www.quirksmode.org/js/cookies.html, tweaked to include path. - function createCookie( name, value, days, path ) { - var expires = ''; - if ( days ) { - var date = new Date(); - date.setTime( date.getTime() + days * 24 * 60 * 60 * 1000 ); - var expires = '; expires=' + date.toGMTString(); - } else var expires = ''; - document.cookie = name + '=' + value + expires + '; path=' + path; + function updateDailyUnique( key ) { + var date = new Date(); + var expiresAtTimestamp = date.getTime() + 24 * 60 * 60 * 1000; + window.localStorage[key] = expiresAtTimestamp; } - function readCookie( name ) { - var nameEQ = name + '='; - var ca = document.cookie.split( ';' ); - for ( var i = 0; i < ca.length; i++ ) { - var c = ca[ i ]; - while ( c.charAt( 0 ) == ' ' ) c = c.substring( 1, c.length ); - if ( c.indexOf( nameEQ ) == 0 ) return c.substring( nameEQ.length, c.length ); + function getDailyUnique( name ) { + if ( window.localStorage[name] ) { + var date = new Date(); + var now = date.getTime(); + var expiration = parseInt( window.localStorage[name], 10 ); + return expiration >= now; } - return null; - } - - function eraseCookie( name ) { - createCookie( name, '', -1 ); + return false; } ready( function () { @@ -40,10 +30,10 @@ var jobStatsSettings = window.job_manager_stats; var ajaxUrl = jobStatsSettings.ajax_url; var ajaxNonce = jobStatsSettings.ajax_nonce; - var cookiesToSet = []; - var setCookies = function () { - cookiesToSet.forEach( function ( uniqueKey ) { - createCookie( uniqueKey, 1, 1, '' ); + var uniquesToSet = []; + var setUniques = function () { + uniquesToSet.forEach( function ( uniqueKey ) { + updateDailyUnique( uniqueKey ); } ); }; @@ -54,24 +44,26 @@ if ( uniqueKey.length === 0 ) { statsToRecord.push( statToRecordKey ); } else { - if ( null === readCookie( uniqueKey ) ) { - cookiesToSet.push( uniqueKey ); + if ( false === getDailyUnique( uniqueKey ) ) { + uniquesToSet.push( uniqueKey ); statsToRecord.push( statToRecordKey ); } } } ); - var postData = new URLSearchParams(); - postData.append( '_ajax_nonce', ajaxNonce ); - postData.append( 'post_id', jobStatsSettings.post_id || 0 ); - postData.append( 'action', 'job_manager_log_stat' ); - postData.append( 'stats', statsToRecord.join( ',' ) ); + var postData = new URLSearchParams( { + _ajax_nonce: ajaxNonce, + post_id: jobStatsSettings.post_id || 0, + action: 'job_manager_log_stat', + stats: statsToRecord.join( ',' ) + } ); + fetch( ajaxUrl, { method: 'POST', credentials: 'same-origin', body: postData, } ).finally( function () { - setCookies(); + setUniques(); console.log( 'sent' ); } ); } ); From ab7a67541941cc7f2e7440cfb27bcdac42455e13 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 17:51:02 +0200 Subject: [PATCH 22/50] Make this a bit more how it would look eventually. --- includes/class-stats.php | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/includes/class-stats.php b/includes/class-stats.php index 3277e3c0e..22216ef2b 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -218,13 +218,17 @@ public function maybe_log_stat_ajax() { $stats = isset( $post_data['stats'] ) ? explode( ',', sanitize_text_field( $post_data['stats'] ) ) : []; + $registered_stats = $this->get_registered_stats(); + // TODO: Maybe optimize this into a single insert? foreach ( $stats as $stat_name ) { $stat_name = trim( strtolower( $stat_name ) ); if ( ! in_array( $stat_name, $this->get_registered_stat_names(), true ) ) { continue; } - $this->log_stat( trim( $stat_name ), [ 'post_id' => $post_id ] ); + + $log_callback = $registered_stats[ $stat_name ]['log_callback'] ?? [ $this, 'log_stat' ]; + call_user_func( $log_callback, trim( $stat_name ), [ 'post_id' => $post_id ] ); } } @@ -280,9 +284,12 @@ private function get_registered_stats() { return (array) apply_filters( 'wpjm_get_registered_stats', [ - 'job_listing_view' => [], + 'job_listing_view' => [ + 'log_callback' => [ $this, 'log_stat' ], // Example of overriding how we log this. + ], 'job_listing_view_unique' => [ - 'unique' => true, + 'unique' => true, + 'unique_callback' => [ self::class, 'unique_by_post_id' ], ], ] ); @@ -301,12 +308,27 @@ private function get_stats_for_ajax( $post_id = 0 ) { $stat_ajax = [ 'name' => $stat_name, ]; + if ( ! empty( $stat_data['unique'] ) ) { - $stat_ajax['unique_key'] = $stat_name . '_' . $post_id; + $unique_callback = $stat_data['unique_callback']; + $stat_ajax['unique_key'] = call_user_func( $unique_callback, $stat_name, $post_id ); } + $ajax_stats[] = $stat_ajax; } return $ajax_stats; } + + /** + * Derive unique key by post id. + * + * @param string $stat_name Name. + * @param int $post_id Post id. + * + * @return string + */ + public function unique_by_post_id( $stat_name, $post_id ) { + return $stat_name . '_' . $post_id; + } } From 9d6f161068537b46e6c7d9926847d0f17b9b88a2 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 17:53:56 +0200 Subject: [PATCH 23/50] Update assets/js/wpjm-stats.js --- assets/js/wpjm-stats.js | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index 33756d57c..6ce145c34 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -64,7 +64,6 @@ body: postData, } ).finally( function () { setUniques(); - console.log( 'sent' ); } ); } ); } )(); From 5de691464b1a9aaa0b96756afdd0ec5f572f2145 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 18:01:06 +0200 Subject: [PATCH 24/50] self care. --- includes/class-stats.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-stats.php b/includes/class-stats.php index 22216ef2b..c24908dcd 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -289,7 +289,7 @@ private function get_registered_stats() { ], 'job_listing_view_unique' => [ 'unique' => true, - 'unique_callback' => [ self::class, 'unique_by_post_id' ], + 'unique_callback' => [ $this, 'unique_by_post_id' ], ], ] ); From 4fc9edea4d2c930017b567dc83624fc542ae955a Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Mon, 5 Feb 2024 19:16:23 +0200 Subject: [PATCH 25/50] Add wp-dom-ready. --- assets/js/wpjm-stats.js | 14 ++++---------- includes/class-stats.php | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index 6ce145c34..cbdce80d8 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -1,14 +1,8 @@ /* global job_manager_stats */ -( function () { - // From https://youmightnotneedjquery.com/#ready - function ready( fn ) { - if ( document.readyState !== 'loading' ) { - fn(); - } else { - document.addEventListener( 'DOMContentLoaded', fn ); - } - } +import domReady from '@wordpress/dom-ready'; + +( function () { function updateDailyUnique( key ) { var date = new Date(); var expiresAtTimestamp = date.getTime() + 24 * 60 * 60 * 1000; @@ -25,7 +19,7 @@ return false; } - ready( function () { + domReady( function () { var statsToRecord = []; var jobStatsSettings = window.job_manager_stats; var ajaxUrl = jobStatsSettings.ajax_url; diff --git a/includes/class-stats.php b/includes/class-stats.php index c24908dcd..83f62226c 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -256,7 +256,7 @@ public function frontend_scripts() { \WP_Job_Manager::register_script( 'wp-job-manager-stats', 'js/wpjm-stats.js', - [], + [ 'wp-dom-ready' ], true ); From ce25e6c602f5489a4bb20cfa530cc2b116d98267 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Tue, 6 Feb 2024 11:35:19 +0100 Subject: [PATCH 26/50] Update syntax --- assets/js/wpjm-stats.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index cbdce80d8..0a1729199 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -4,37 +4,36 @@ import domReady from '@wordpress/dom-ready'; ( function () { function updateDailyUnique( key ) { - var date = new Date(); - var expiresAtTimestamp = date.getTime() + 24 * 60 * 60 * 1000; + const date = new Date(); + const expiresAtTimestamp = date.getTime() + 24 * 60 * 60 * 1000; window.localStorage[key] = expiresAtTimestamp; } function getDailyUnique( name ) { if ( window.localStorage[name] ) { - var date = new Date(); - var now = date.getTime(); - var expiration = parseInt( window.localStorage[name], 10 ); + const date = new Date(); + const now = date.getTime(); + const expiration = parseInt( window.localStorage[ name ], 10 ); return expiration >= now; } return false; } domReady( function () { - var statsToRecord = []; - var jobStatsSettings = window.job_manager_stats; - var ajaxUrl = jobStatsSettings.ajax_url; - var ajaxNonce = jobStatsSettings.ajax_nonce; - var uniquesToSet = []; - var setUniques = function () { - uniquesToSet.forEach( function ( uniqueKey ) { + const statsToRecord = []; + const jobStatsSettings = window.job_manager_stats; + const ajaxUrl = jobStatsSettings.ajax_url; + const ajaxNonce = jobStatsSettings.ajax_nonce; + const uniquesToSet = []; + const setUniques = function() { + uniquesToSet.forEach( function( uniqueKey ) { updateDailyUnique( uniqueKey ); } ); }; - jobStatsSettings.stats_to_log.forEach( function ( statToRecord ) { - // do something. - var statToRecordKey = statToRecord.name; - var uniqueKey = statToRecord.unique_key || ''; + jobStatsSettings.stats_to_log?.forEach( function ( statToRecord ) { + const statToRecordKey = statToRecord.name; + const uniqueKey = statToRecord.unique_key || ''; if ( uniqueKey.length === 0 ) { statsToRecord.push( statToRecordKey ); } else { @@ -45,11 +44,11 @@ import domReady from '@wordpress/dom-ready'; } } ); - var postData = new URLSearchParams( { + const postData = new URLSearchParams( { _ajax_nonce: ajaxNonce, post_id: jobStatsSettings.post_id || 0, action: 'job_manager_log_stat', - stats: statsToRecord.join( ',' ) + stats: statsToRecord.join( ',' ), } ); fetch( ajaxUrl, { From 89b01f5578706208c7c5f8b683c84c1a9f656a79 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Thu, 8 Feb 2024 11:58:36 +0100 Subject: [PATCH 27/50] Add job listing stats queries and views column (#2750) --- includes/class-job-listing-stats.php | 146 +++++++++++++++++++++++++++ includes/class-stats-dashboard.php | 78 ++++++++++++++ includes/class-stats.php | 23 ++++- 3 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 includes/class-job-listing-stats.php create mode 100644 includes/class-stats-dashboard.php diff --git a/includes/class-job-listing-stats.php b/includes/class-job-listing-stats.php new file mode 100644 index 000000000..2ab32f6ec --- /dev/null +++ b/includes/class-job-listing-stats.php @@ -0,0 +1,146 @@ +job_id = $job_id; + $this->start_date = get_post_datetime( $job_id )->format( 'Y-m-d' ); + + } + + /** + * Get total stats for a job listing. + * + * @return array + */ + public function get_total_stats() { + return [ + 'view' => $this->get_event_total( self::JOB_LISTING_VIEW ), + 'view_unique' => $this->get_event_total( self::JOB_LISTING_VIEW_UNIQUE ), + ]; + } + + /** + * Get daily stats for a job listing. + * + * @return array + */ + public function get_daily_stats() { + return [ + 'view' => $this->get_event_daily( self::JOB_LISTING_VIEW ), + 'view_unique' => $this->get_event_daily( self::JOB_LISTING_VIEW_UNIQUE ), + ]; + } + + /** + * Get totals for an event. + * + * @param string $event + * + * @return int + */ + public function get_event_total( $event ) { + + global $wpdb; + + $cache_key = 'wpjm_stats_sum_' . $event . '_' . $this->job_id; + $sum = wp_cache_get( $cache_key, Stats::CACHE_GROUP ); + + if ( false !== $sum ) { + return $sum; + } + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $sum = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM(count) FROM {$wpdb->wpjm_stats} + WHERE post_id = %d AND name = %s AND date >= %s", + $this->job_id, + $event, + $this->start_date + ) + ); + + $sum = is_numeric( $sum ) ? (int) $sum : 0; + + wp_cache_set( $cache_key, $sum, Stats::CACHE_GROUP, HOUR_IN_SECONDS ); + + return $sum; + } + + /** + * Get daily breakdown of stats for an event. + * + * @param string $event + * + * @return array + */ + public function get_event_daily( $event ) { + global $wpdb; + + $cache_key = 'wpjm_stats_daily_' . $event . '_' . $this->job_id; + $views = wp_cache_get( $cache_key, Stats::CACHE_GROUP ); + + if ( false !== $views ) { + return $views; + } + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $views = $wpdb->get_results( + $wpdb->prepare( + "SELECT date, count FROM {$wpdb->wpjm_stats} + WHERE post_id = %d AND name = %s AND date BETWEEN %s AND %s + ORDER BY date DESC", + $this->job_id, + $event, + $this->start_date, + ( new \DateTime( 'yesterday' ) )->format( 'Y-m-d' ) + ), + OBJECT_K + ); + + wp_cache_set( $cache_key, $views, Stats::CACHE_GROUP, strtotime( 'tomorrow' ) - time() ); + + return $views; + + } + + +} diff --git a/includes/class-stats-dashboard.php b/includes/class-stats-dashboard.php new file mode 100644 index 000000000..7b19676b5 --- /dev/null +++ b/includes/class-stats-dashboard.php @@ -0,0 +1,78 @@ +ID ); + $total = $stats->get_total_stats(); + + $views = $total['view']; + + // translators: %1d is the number of views. + $views_str = '' . sprintf( _n( '%1d view', '%1d views', $views, 'wp-job-manager' ), $views ) . ''; + + echo wp_kses_post( $views_str ); + } + + /** + * Output stats column for the job listing post type admin screen. + * + * @param string $column + */ + public function maybe_render_job_listing_posts_custom_column( $column ) { + global $post; + + if ( self::COLUMN_NAME === $column ) { + $this->render_stats_column( $post ); + } + } +} diff --git a/includes/class-stats.php b/includes/class-stats.php index 83f62226c..460230de3 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -17,12 +17,16 @@ class Stats { use Singleton; + const CACHE_GROUP = 'wpjm_stats'; + const DEFAULT_LOG_STAT_ARGS = [ 'group' => '', 'post_id' => 0, 'increment_by' => 1, ]; + private const TABLE = 'wpjm_stats'; + /** * Constructor. */ @@ -34,6 +38,12 @@ private function __construct() { * Do initialization of all the things needed for stats. */ public function init() { + + include_once __DIR__ . '/class-job-listing-stats.php'; + include_once __DIR__ . '/class-stats-dashboard.php'; + + Stats_Dashboard::instance(); + $this->initialize_wpdb(); $this->hook(); } @@ -48,8 +58,8 @@ private function initialize_wpdb() { if ( isset( $wpdb->wpjm_stats ) ) { return; } - $wpdb->wpjm_stats = $wpdb->prefix . 'wpjm_stats'; - $wpdb->tables[] = 'wpjm_stats'; + $wpdb->wpjm_stats = $wpdb->prefix . self::TABLE; + $wpdb->tables[] = self::TABLE; } /** @@ -77,6 +87,15 @@ public function migrate() { ); } + /** + * Check if collecting and showing statistics are enabled. + * + * @return bool + */ + public static function is_enabled() { + return get_option( 'job_manager_stats_enable', false ); + } + /** * Log a stat into the db. * From 08692178f8cf83aad389e51e8602a2097a4f6e78 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 14 Feb 2024 15:14:14 +0100 Subject: [PATCH 28/50] Update job dashboard template (#2754) --- assets/css/job-dashboard.scss | 98 +++++++++++++ assets/css/ui.dialog.scss | 5 +- assets/css/ui.elements.scss | 81 ++++++++++- assets/css/ui.neutral.scss | 21 ++- assets/css/ui.notice.scss | 24 +--- includes/class-wp-job-manager-shortcodes.php | 68 +++++++-- includes/ui/class-notice.php | 2 +- includes/ui/class-ui-elements.php | 69 ++++++++- phpcs.xml.dist | 1 + templates/job-dashboard.php | 144 +++++++++---------- templates/notice.php | 2 +- webpack.config.js | 1 + 12 files changed, 396 insertions(+), 120 deletions(-) create mode 100644 assets/css/job-dashboard.scss diff --git a/assets/css/job-dashboard.scss b/assets/css/job-dashboard.scss new file mode 100644 index 000000000..ac9dd77d3 --- /dev/null +++ b/assets/css/job-dashboard.scss @@ -0,0 +1,98 @@ + +@import "mixins"; + +#job-manager-job-dashboard { + +} + +.jm-dashboard-job, .jm-dashboard-header { + display: flex; + align-items: center; + padding: var(--jm-ui-space-sm); + gap: var(--jm-ui-space-sm); + margin: var(--jm-ui-space-sm) 0; + font-size: var(--jm-ui-font-size-m); +} + +.jm-dashboard-header { + color: fadeCurrentColor(85%); + margin-bottom: unset; + padding-bottom: unset; +} + +.jm-dashboard__intro { + display: flex; + justify-content: space-between; + align-items: baseline; + flex-wrap: wrap; + gap: var(--jm-ui-space-sm); +} + +.jm-dashboard-job { + border: var(--jm-ui-border-size) solid var(--jm-ui-border-light); +} + +.jm-dashboard-job-column { + flex: 1 1 calc(50% - var(--jm-ui-space-sm)); + min-width: 0; +} + +.jm-dashboard small { + font-size: var(--jm-ui-font-size-s); +} + +.jm-dashboard-job-column.job_title { + flex: 1 1 200%; + + .job-status { + text-transform: uppercase; + font-weight: 500; + font-size: var(--jm-ui-font-size-s); + line-height: var(--jm-ui-icon-size-s); + color: fadeCurrentColor( 80% ); + + .jm-ui-icon { + width: var(--jm-ui-icon-size-s); + height: var(--jm-ui-icon-size-s); + } + } +} + + +.jm-dashboard-job-column.actions { + flex: 0.5 1 100%; + text-align: right; +} + +.jm-dashboard-job-column a.job-title { + font-weight: 600; + font-size: var(--jm-ui-font-size); + text-decoration: unset; +} + +.jm-dashboard-job-column a.job-title:hover { + text-decoration: underline; +} + +.jm-dashboard-action { + display: block; + text-decoration: none; + +} +.jm-dashboard-action:where(:not(:hover):not(:focus)) { + color: inherit; +} + +.job-dashboard-action-delete { + color: var(--jm-ui-danger-color); +} + +@media (max-width: 600px) { + .jm-dashboard-job { + flex-wrap: wrap; + } + + .jm-dashboard-header { + display: none; + } +} diff --git a/assets/css/ui.dialog.scss b/assets/css/ui.dialog.scss index 0e66c911e..6ba949e3f 100644 --- a/assets/css/ui.dialog.scss +++ b/assets/css/ui.dialog.scss @@ -80,10 +80,7 @@ opacity: 0.7; .jm-ui-button__icon { - background-color: currentColor; - mask: var(--jm-ui-svg-close) no-repeat center center; - width: var(--jm-ui-icon-size); - height: var(--jm-ui-icon-size); + mask-image: var(--jm-ui-svg-close); } } diff --git a/assets/css/ui.elements.scss b/assets/css/ui.elements.scss index 326c5f4e9..ac3903b94 100644 --- a/assets/css/ui.elements.scss +++ b/assets/css/ui.elements.scss @@ -90,6 +90,13 @@ } } +.jm-ui-button__icon { + background-color: currentColor; + mask: var(--jm-ui-svg-close) no-repeat center center; + width: var(--jm-ui-icon-size); + height: var(--jm-ui-icon-size); +} + .jm-ui-link { border-radius: 2px; @@ -102,7 +109,7 @@ } } -.jm-ui-actions { +.jm-ui-actions-row { display: flex; flex-direction: row; gap: var(--jm-ui-space-s); @@ -115,3 +122,75 @@ margin-left: calc(-1 * var(--jm-ui-space-s)); } } + +.jm-ui-icon { + display: inline-block; + flex: 0 0 auto; + width: var(--jm-ui-icon-size); + height: var(--jm-ui-icon-size); + mask-size: 100% 100%; + + &[data-icon=check] { + background-color: currentColor; + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cg stroke-width=\'1.5\'%3e%3cpath stroke=\'%231E1E1E\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath stroke=\'black\' d=\'m15.96 8.18-5.34 7.18-3.1-2.3\'/%3e%3c/g%3e%3c/svg%3e'); + } + + &[data-icon=check-circle] { + background-color: currentColor; + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cg stroke-width=\'1.5\'%3e%3cpath stroke=\'%231E1E1E\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath stroke=\'black\' d=\'m15.96 8.18-5.34 7.18-3.1-2.3\'/%3e%3c/g%3e%3c/svg%3e'); + } + + &[data-icon=check] { + background-color: currentColor; + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='m17.93 7.98-7.2 9.68-4.52-3.36.9-1.2 3.31 2.46 6.3-8.48 1.2.9Z' clip-rule='evenodd'/%3e%3c/svg%3e"); + } + + &[data-icon=star] { + background-color: currentColor; + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' d='M11.78 4.45a.25.25 0 0 1 .44 0l2.07 4.2c.04.07.11.12.2.13l4.62.68c.2.02.28.28.14.42l-3.35 3.26a.25.25 0 0 0-.07.23l.79 4.6c.03.2-.18.36-.37.27l-4.13-2.18a.25.25 0 0 0-.24 0l-4.13 2.18a.25.25 0 0 1-.37-.27l.8-4.6a.25.25 0 0 0-.08-.23L4.75 9.88a.25.25 0 0 1 .14-.42l4.63-.68a.25.25 0 0 0 .19-.13l2.07-4.2Z'/%3e%3c/svg%3e"); + } + + &[data-icon=info] { + background-color: currentColor; + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3ccircle cx=\'12\' cy=\'12\' r=\'8\' stroke=\'black\' stroke-width=\'1.5\'/%3e%3cpath fill=\'black\' d=\'M11 11h2v6h-2zm0-4h2v2h-2z\'/%3e%3c/svg%3e'); + } + + &[data-icon=alert] { + background-color: currentColor; + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cpath stroke=\'%231E1E1E\' stroke-width=\'1.5\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath fill=\'%231E1E1E\' d=\'M13 7h-2v6h2V7Zm0 8h-2v2h2v-2Z\'/%3e%3c/svg%3e'); + } +} + +/* + * Action menu. + */ +.jm-ui-actions-menu { + position: relative; + display: inline-block; +} + +.jm-ui-action-menu__open-button { + cursor: pointer; + + .jm-ui-button__icon { + mask-image: var(--jm-ui-svg-ellipsis-v); + } +} + +.jm-ui-action-menu__content { + position: absolute; + right: 0; + top: calc(100% + 2px); + border: var(--jm-ui-border-size) solid var(--jm-ui-border-strong); + border-radius: var(--jm-ui-radius); + background-color: var(--jm-ui-background-color, #fff); + box-shadow: var(--jm-ui-shadow-popover); + padding: var(--jm-ui-space-s); + z-index: 10; + font-size: var(--jm-ui-font-size-s); + text-align: left; + white-space: nowrap; + + display: flex; + flex-direction: column; +} diff --git a/assets/css/ui.neutral.scss b/assets/css/ui.neutral.scss index 34500055a..5076c739b 100644 --- a/assets/css/ui.neutral.scss +++ b/assets/css/ui.neutral.scss @@ -13,15 +13,19 @@ --jm-ui-radius-4x: calc(4 * var(--jm-ui-radius)); --jm-ui-accent-color: inherit; + --jm-ui-danger-color: #cc1818; + --jm-ui-error-color: #cc1818; + --jm-ui-info-color: #4e71ec; + --jm-ui-success-color: #4ab866; --jm-ui-accent-color-contrast: #ffffff; --jm-ui-button-color: var(--jm-ui-accent-color); --jm-ui-button-contrast: var(--jm-ui-accent-color-contrast, #ffffff); --jm-ui-link-color: var(--jm-ui-accent-color, inherit); - --jm-ui-notice-error: #cc1818; - --jm-ui-notice-info: #4e71ec; - --jm-ui-notice-success: #4ab866; + --jm-ui-notice-error: var(--jm-ui-danger-color); + --jm-ui-notice-info: var(--jm-ui-info-color); + --jm-ui-notice-success: var(--jm-ui-success-color); --jm-ui-notice-strong: var(--jm-ui-border-strong); --jm-ui-notice-shadow: none; @@ -43,17 +47,24 @@ --jm-ui-space-xxl: calc(24 * var(--jm-ui-space-base)); // 96px --jm-ui-font-size: 16px; + --jm-ui-font-size-m: 14px; + --jm-ui-font-size-s: 12px; --jm-ui-heading-font-size: 20px; --jm-ui-large-font-size: 24px; --jm-ui-button-font-size: 14px; --jm-ui-icon-size: 24px; + --jm-ui-icon-size-m: 20px; + --jm-ui-icon-size-s: 18px; --jm-ui-shadow-modal: 0 0.7px 1px 0 rgba(0, 0, 0, 0.15), 0 2.7px 3.8px -0.2px rgba(0, 0, 0, 0.15), 0px 5.5px 7.8px -0.3px rgba(0, 0, 0, 0.15), 0.1px 11.5px 16.4px -0.5px rgba(0, 0, 0, 0.15); + --jm-ui-shadow-popover: 0px 2px 6px 0px rgba(0, 0, 0, 0.05); + --jm-ui-svg-close: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' d='m12 13.06 3.71 3.71 1.06-1.06-3.7-3.71 3.7-3.71-1.06-1.06-3.71 3.7-3.71-3.7-1.06 1.06 3.7 3.71-3.7 3.71 1.06 1.06 3.71-3.7Z'/%3e%3c/svg%3e"); - --jm-ui-svg-arrow-down: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='32' height='6' viewBox='0 0 16 10'%3e%3cpath fill='currentColor' fill-rule='evenodd' d='M0 1.6 1.53 0 8 6.95 14.5 0 16 1.6 8 10 0 1.6Z' clip-rule='evenodd'/%3e%3c/svg%3e"); - --jm-ui-svg-check: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='%231E1E1E' stroke-width='1.5' d='m18.93 6-8.9 11.97-5.16-3.84'/%3e%3c/svg%3e"); + --jm-ui-svg-arrow-down: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='32' height='6' viewBox='0 0 16 10'%3e%3cpath fill='black' fill-rule='evenodd' d='M0 1.6 1.53 0 8 6.95 14.5 0 16 1.6 8 10 0 1.6Z' clip-rule='evenodd'/%3e%3c/svg%3e"); + --jm-ui-svg-check: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='black' stroke-width='1.5' d='m18.93 6-8.9 11.97-5.16-3.84'/%3e%3c/svg%3e"); + --jm-ui-svg-ellipsis-v: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='M11 19v-2h2v2h-2Zm0-6v-2h2v2h-2Zm0-6V5h2v2h-2Z' clip-rule='evenodd'/%3e%3c/svg%3e"); } diff --git a/assets/css/ui.notice.scss b/assets/css/ui.notice.scss index 204e3e0b7..4817d82bd 100644 --- a/assets/css/ui.notice.scss +++ b/assets/css/ui.notice.scss @@ -33,7 +33,7 @@ &.color-#{$color} { border-color: var(--jm-ui-notice-#{$color}); background-color: color-mix(in srgb, transparent, var(--jm-ui-notice-#{$color}) var(--jm-ui-background-opacity)); - .jm-notice__icon { + .jm-ui-icon { color: var(--jm-ui-notice-#{$color}); } } @@ -91,28 +91,6 @@ } } -.jm-notice__icon { - flex: 0 0 auto; - width: var(--jm-ui-icon-size); - height: var(--jm-ui-icon-size); - mask-size: 100% 100%; - - &[data-icon=check] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cg stroke-width=\'1.5\'%3e%3cpath stroke=\'%231E1E1E\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath stroke=\'black\' d=\'m15.96 8.18-5.34 7.18-3.1-2.3\'/%3e%3c/g%3e%3c/svg%3e'); - } - - &[data-icon=info] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3ccircle cx=\'12\' cy=\'12\' r=\'8\' stroke=\'black\' stroke-width=\'1.5\'/%3e%3cpath fill=\'black\' d=\'M11 11h2v6h-2zm0-4h2v2h-2z\'/%3e%3c/svg%3e'); - } - - &[data-icon=alert] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cpath stroke=\'%231E1E1E\' stroke-width=\'1.5\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath fill=\'%231E1E1E\' d=\'M13 7h-2v6h2V7Zm0 8h-2v2h2v-2Z\'/%3e%3c/svg%3e'); - } -} - .jm-notice__header { display: flex; flex-direction: row; diff --git a/includes/class-wp-job-manager-shortcodes.php b/includes/class-wp-job-manager-shortcodes.php index a2eef4b79..4d701e0c5 100644 --- a/includes/class-wp-job-manager-shortcodes.php +++ b/includes/class-wp-job-manager-shortcodes.php @@ -5,6 +5,9 @@ * @package wp-job-manager */ +use WP_Job_Manager\UI\Notice; +use WP_Job_Manager\UI\UI_Elements; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -71,6 +74,9 @@ public function __construct() { add_shortcode( 'job_apply', [ $this, 'output_job_apply' ] ); add_filter( 'paginate_links', [ $this, 'filter_paginate_links' ], 10, 1 ); + + add_action( 'job_manager_job_dashboard_column_date', [ $this, 'job_dashboard_date_column_expires' ] ); + add_action( 'job_manager_job_dashboard_column_job_title', [ $this, 'job_dashboard_title_column_status' ] ); } /** @@ -214,7 +220,7 @@ public function job_dashboard_handler() { // Message. // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = '
' . wp_kses_post( sprintf( __( '%s has been filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ) . '
'; + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); break; case 'mark_not_filled': // Check status. @@ -227,7 +233,7 @@ public function job_dashboard_handler() { // Message. // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = '
' . wp_kses_post( sprintf( __( '%s has been marked as not filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ) . '
'; + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been marked as not filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); break; case 'delete': // Trash it. @@ -235,7 +241,7 @@ public function job_dashboard_handler() { // Message. // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = '
' . wp_kses_post( sprintf( __( '%s has been deleted', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ) . '
'; + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been deleted', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); break; case 'duplicate': @@ -288,10 +294,10 @@ public function job_dashboard_handler() { */ $success_message = apply_filters( 'job_manager_job_dashboard_success_message', '', $action, $job_id ); if ( $success_message ) { - $this->job_dashboard_message = '
' . $success_message . '
'; + $this->job_dashboard_message = Notice::success( $success_message ); } } catch ( Exception $e ) { - $this->job_dashboard_message = '
' . wp_kses_post( $e->getMessage() ) . '
'; + $this->job_dashboard_message = Notice::error( $e->getMessage() ); } } } @@ -375,6 +381,8 @@ public function job_dashboard( $atts ) { ); $posts_per_page = $new_atts['posts_per_page']; + WP_Job_Manager::register_style( 'wp-job-manager-job-dashboard', 'css/job-dashboard.css', [ 'wp-job-manager-ui' ] ); + wp_enqueue_style( 'wp-job-manager-job-dashboard' ); wp_enqueue_script( 'wp-job-manager-job-dashboard' ); ob_start(); @@ -397,15 +405,13 @@ public function job_dashboard( $atts ) { // Cache IDs for access check later on. $this->job_dashboard_job_ids = wp_list_pluck( $jobs->posts, 'ID' ); - echo wp_kses_post( $this->job_dashboard_message ); + echo '
' . wp_kses_post( $this->job_dashboard_message ) . '
'; $job_dashboard_columns = apply_filters( 'job_manager_job_dashboard_columns', [ 'job_title' => __( 'Title', 'wp-job-manager' ), - 'filled' => __( 'Filled?', 'wp-job-manager' ), 'date' => __( 'Date', 'wp-job-manager' ), - 'expires' => __( 'Listing Expires', 'wp-job-manager' ), ] ); @@ -990,6 +996,52 @@ public function output_job_apply( $atts ) { return ob_get_clean(); } + + /** + * Add expiration details to the job dashboard date column. + * + * @param \WP_Post $job + * + * @output string + */ + public function job_dashboard_date_column_expires( $job ) { + $expiration = WP_Job_Manager_Post_Types::instance()->get_job_expiration( $job ); + + if ( 'publish' === $job->post_status && ! empty( $expiration ) ) { + + // translators: Placeholder is the expiration date of the job listing. + echo '
' . UI_Elements::rel_time( $expiration, __( 'Expires in %s', 'wp-job-manager' ) ) . '
'; + } + } + + /** + * Add job status to the job dashboard title column. + * + * @param \WP_Post $job + * + * @output string + */ + public function job_dashboard_title_column_status( $job ) { + + echo '
'; + + $status = []; + + if ( is_position_filled( $job ) ) { + $status[] = '' . esc_html__( 'Filled', 'wp-job-manager' ) . ''; + } + + if ( is_position_featured( $job ) && 'publish' === $job->post_status ) { + $status[] = '' . esc_html__( 'Featured', 'wp-job-manager' ) . ''; + } + + $status[] = '' . esc_html( get_the_job_status( $job ) ) . ''; + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. + echo implode( ', ', $status ); + + echo '
'; + } } WP_Job_Manager_Shortcodes::instance(); diff --git a/includes/ui/class-notice.php b/includes/ui/class-notice.php index 116e7d121..690927c00 100644 --- a/includes/ui/class-notice.php +++ b/includes/ui/class-notice.php @@ -32,7 +32,7 @@ public static function success( $args ) { return self::render( array_merge( - [ 'icon' => 'check' ], + [ 'icon' => 'check-circle' ], $args ) ); diff --git a/includes/ui/class-ui-elements.php b/includes/ui/class-ui-elements.php index 7fe0c4bc2..5c2bdb7bb 100644 --- a/includes/ui/class-ui-elements.php +++ b/includes/ui/class-ui-elements.php @@ -26,10 +26,11 @@ class UI_Elements { * Generate HTML for a notice icon. * * @param string|null $icon Icon name or a safe SVG code. + * @param string $label Accessible icon label. * * @return string Icon HTML. */ - public static function icon( $icon ) { + public static function icon( $icon, $label = '' ) { if ( empty( $icon ) ) { return ''; @@ -39,19 +40,23 @@ public static function icon( $icon ) { $is_classname = preg_match( '/^[\w-]+$/i', $icon ); - $html = '
' . implode( '', $actions_html ) . '
'; + return '
' . implode( '', $actions_html ) . '
'; + + } + + /** + * Generate an actions button with a dropdown menu. + * + * @param string $content Escaped HTML content. + * + * @return string Actions menu HTML. + */ + public static function actions_menu( $content ) { + $label = __( 'Actions', 'wp-job-manager' ); + return << + + + +
+ {$content} +
+ +HTML; + + } + + /** + * Generate HTML for a relative time string. + * + * @param string|\DateTimeInterface|int $time Time string, DateTime object, or timestamp. + * @param string $format_string Sprintf-compatible format string. Should contain a %s placeholder for the time. + * + * @return string + */ + public static function rel_time( $time, $format_string = '%s' ) { + + if ( is_string( $time ) ) { + $timestamp = strtotime( $time ); + } + + if ( $time instanceof \DateTimeInterface ) { + $timestamp = $time->getTimestamp(); + } + + if ( is_numeric( $time ) ) { + $timestamp = $time; + } + + if ( empty( $timestamp ) ) { + return ''; + } + + $abs_time = date_i18n( get_option( 'date_format' ), $timestamp ); + $rel_time = human_time_diff( $timestamp ); + + return ''; } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 7045b0717..370520b85 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -67,6 +67,7 @@ + diff --git a/templates/job-dashboard.php b/templates/job-dashboard.php index cba5b6230..614d1320d 100644 --- a/templates/job-dashboard.php +++ b/templates/job-dashboard.php @@ -8,97 +8,95 @@ * @author Automattic * @package wp-job-manager * @category Template - * @version 1.41.0 + * @version $$next-version$$ * + * @since $$next-version$$ Switched to a responsive layout. job_manager_job_dashboard_column_{$key} action is called for all columns. * @since 1.34.4 Available job actions are passed in an array (`$job_actions`, keyed by job ID) and not generated in the template. * @since 1.35.0 Switched to new date functions. * * @var array $job_dashboard_columns Array of the columns to show on the job dashboard page. - * @var int $max_num_pages Maximum number of pages - * @var WP_Post[] $jobs Array of job post results. - * @var array $job_actions Array of actions available for each job. + * @var int $max_num_pages Maximum number of pages + * @var WP_Post[] $jobs Array of job post results. + * @var array $job_actions Array of actions available for each job. */ +use WP_Job_Manager\UI\UI_Elements; + if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } -$submission_limit = ! empty( get_option( 'job_manager_submission_limit' ) ) ? absint( get_option( 'job_manager_submission_limit' ) ) : false; -$submit_job_form_page_id = get_option( 'job_manager_submit_job_form_page_id' ); +$submit_job_form_page_id = get_option( 'job_manager_submit_job_form_page_id' ); ?> -
-

- - - - $column ) : ?> - - - - - + +
+
+

+ +
+ +
+ +
+
+
+ $column ) : ?> +
+ +
+
+
-
- - +
- +
$column ) : ?> -
+
+ ID ) ) . '">' . ( wpjm_get_the_job_title( $job ) ?? $job->ID ) . ''; + break; + case 'date': + echo '
' . esc_html( wp_date( apply_filters( 'job_manager_get_dashboard_date_format', 'M d, Y' ), get_post_datetime( $job )->getTimestamp() ) ) . '
'; + + break; + } + + do_action( 'job_manager_job_dashboard_column_' . $key, $job ); + + ?> +
- +
+ ID ] ) ) { + foreach ( $job_actions[ $job->ID ] as $action => $value ) { + $action_url = add_query_arg( [ + 'action' => $action, + 'job_id' => $job->ID + ] ); + if ( $value['nonce'] ) { + $action_url = wp_nonce_url( $action_url, $value['nonce'] ); + } + $actions_html .= '' . esc_html( $value['label'] ) . '' . "\n"; + } + } + + echo UI_Elements::actions_menu( $actions_html ); + ?> +
+ - - - - - - - - -
- - post_status == 'publish' ) : ?> - - - () - - - () - - ' : ''; ?> -
    - ID ] ) ) { - foreach ( $job_actions[ $job->ID ] as $action => $value ) { - $action_url = add_query_arg( [ - 'action' => $action, - 'job_id' => $job->ID - ] ); - if ( $value['nonce'] ) { - $action_url = wp_nonce_url( $action_url, $value['nonce'] ); - } - echo '
  • ' . esc_html( $value['label'] ) . '
  • '; - } - } - ?> -
- - getTimestamp() ) ); ?> - - get_job_expiration( $job ); - echo esc_html( $job_expires ? wp_date( get_option( 'date_format' ), $job_expires->getTimestamp() ) : '–' ); - ?> - - - - - -
- -
+
+ $max_num_pages ] ); ?> diff --git a/templates/notice.php b/templates/notice.php index 105a1d77e..ca3906af4 100644 --- a/templates/notice.php +++ b/templates/notice.php @@ -42,7 +42,7 @@ ?> -
+
diff --git a/webpack.config.js b/webpack.config.js index fd64e8baa..b80bb7402 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -23,6 +23,7 @@ const files = { 'css/ui': 'css/ui.scss', 'css/job-listings': 'css/job-listings.scss', 'css/job-submission': 'css/job-submission.scss', + 'css/job-dashboard': 'css/job-dashboard.scss', 'css/setup': 'css/setup.scss', 'css/menu': 'css/menu.scss', }; From 3bf968f33ce11944d4a515dc0a3672c2ea305fdc Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 14 Feb 2024 15:23:02 +0100 Subject: [PATCH 29/50] Refactor shortcode class (#2759) --- includes/class-job-dashboard-shortcode.php | 534 +++++++++++++++++++ includes/class-wp-job-manager-shortcodes.php | 501 +---------------- 2 files changed, 557 insertions(+), 478 deletions(-) create mode 100644 includes/class-job-dashboard-shortcode.php diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php new file mode 100644 index 000000000..f1fde82e1 --- /dev/null +++ b/includes/class-job-dashboard-shortcode.php @@ -0,0 +1,534 @@ + '25', + ], + $attrs + ); + $posts_per_page = $attrs['posts_per_page']; + + \WP_Job_Manager::register_style( 'wp-job-manager-job-dashboard', 'css/job-dashboard.css', [ 'wp-job-manager-ui' ] ); + wp_enqueue_style( 'wp-job-manager-job-dashboard' ); + wp_enqueue_script( 'wp-job-manager-job-dashboard' ); + + ob_start(); + + // If doing an action, show conditional content if needed.... + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used safely. + $action = isset( $_REQUEST['action'] ) ? sanitize_title( wp_unslash( $_REQUEST['action'] ) ) : false; + if ( ! empty( $action ) ) { + // Show alternative content if a plugin wants to. + if ( has_action( 'job_manager_job_dashboard_content_' . $action ) ) { + do_action( 'job_manager_job_dashboard_content_' . $action, $attrs ); + + return ob_get_clean(); + } + } + + // ....If not show the job dashboard. + $jobs = new \WP_Query( $this->get_job_dashboard_query_args( $posts_per_page ) ); + + // Cache IDs for access check later on. + $this->job_dashboard_job_ids = wp_list_pluck( $jobs->posts, 'ID' ); + + echo '
' . wp_kses_post( $this->job_dashboard_message ) . '
'; + + $job_dashboard_columns = apply_filters( + 'job_manager_job_dashboard_columns', + [ + 'job_title' => __( 'Title', 'wp-job-manager' ), + 'date' => __( 'Date', 'wp-job-manager' ), + ] + ); + + $job_actions = []; + foreach ( $jobs->posts as $job ) { + $job_actions[ $job->ID ] = $this->get_job_actions( $job ); + } + + get_job_manager_template( + 'job-dashboard.php', + [ + 'jobs' => $jobs->posts, + 'job_actions' => $job_actions, + 'max_num_pages' => $jobs->max_num_pages, + 'job_dashboard_columns' => $job_dashboard_columns, + ] + ); + + return ob_get_clean(); + } + + /** + * Get the actions available to the user for a job listing on the job dashboard page. + * + * @param \WP_Post $job The job post object. + * + * @return array + */ + public function get_job_actions( $job ) { + if ( + ! get_current_user_id() + || ! $job instanceof \WP_Post + || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type + || ! $this->is_job_available_on_dashboard( $job ) + ) { + return []; + } + + $base_nonce_action_name = 'job_manager_my_job_actions'; + + $actions = []; + switch ( $job->post_status ) { + case 'publish': + if ( \WP_Job_Manager_Post_Types::job_is_editable( $job->ID ) ) { + $actions['edit'] = [ + 'label' => __( 'Edit', 'wp-job-manager' ), + 'nonce' => false, + ]; + } + if ( is_position_filled( $job ) ) { + $actions['mark_not_filled'] = [ + 'label' => __( 'Mark not filled', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + } else { + $actions['mark_filled'] = [ + 'label' => __( 'Mark filled', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + } + if ( + get_option( 'job_manager_renewal_days' ) > 0 + && \WP_Job_Manager_Helper_Renewals::job_can_be_renewed( $job ) + && \WP_Job_Manager_Helper_Renewals::is_wcpl_renew_compatible() + && \WP_Job_Manager_Helper_Renewals::is_spl_renew_compatible() + ) { + $actions['renew'] = [ + 'label' => __( 'Renew', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + } + + $actions['duplicate'] = [ + 'label' => __( 'Duplicate', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + break; + case 'expired': + if ( job_manager_get_permalink( 'submit_job_form' ) ) { + $actions['relist'] = [ + 'label' => __( 'Relist', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + } + break; + case 'pending_payment': + case 'pending': + if ( \WP_Job_Manager_Post_Types::job_is_editable( $job->ID ) ) { + $actions['edit'] = [ + 'label' => __( 'Edit', 'wp-job-manager' ), + 'nonce' => false, + ]; + } + break; + case 'draft': + case 'preview': + $actions['continue'] = [ + 'label' => __( 'Continue Submission', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + break; + } + + $actions['delete'] = [ + 'label' => __( 'Delete', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + + /** + * Filter the actions available to the current user for a job on the job dashboard page. + * + * @since 1.0.0 + * + * @param array $actions Actions to filter. + * @param \WP_Post $job Job post object. + */ + $actions = apply_filters( 'job_manager_my_job_actions', $actions, $job ); + + // For backwards compatibility, convert `nonce => true` to the nonce action name. + foreach ( $actions as $key => $action ) { + if ( true === $action['nonce'] ) { + $actions[ $key ]['nonce'] = $base_nonce_action_name; + } + } + + return $actions; + } + + /** + * Filters the url from paginate_links to avoid multiple calls for same action in job dashboard + * + * @param string $link + * + * @return string + */ + public function filter_paginate_links( $link ) { + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used for comparison only. + if ( $this->is_job_dashboard_page() && isset( $_GET['action'] ) && in_array( + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used for comparison only. + $_GET['action'], + [ + 'mark_filled', + 'mark_not_filled', + ], + true + ) ) { + return remove_query_arg( [ 'action', 'job_id', '_wpnonce' ], $link ); + } + + return $link; + } + + /** + * Displays edit job form. + */ + public function edit_job() { + global $job_manager; + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output should be appropriately escaped in the form generator. + echo $job_manager->forms->get_form( 'edit-job' ); + } + + /** + * Helper function used to check if page is WPJM dashboard page. + * + * Checks if page has 'job_dashboard' shortcode. + * + * @access private + * @return bool True if page is dashboard page, false otherwise. + */ + private function is_job_dashboard_page() { + global $post; + + if ( is_page() && has_shortcode( $post->post_content, 'job_dashboard' ) ) { + return true; + } + + return false; + } + + /** + * Handles actions on job dashboard. + * + * @throws \Exception On action handling error. + */ + public function handle_actions() { + + /** + * Determine if the shortcode action handler should run. + * + * @since 1.35.0 + * + * @param bool $should_run_handler Should the handler run. + */ + $should_run_handler = apply_filters( 'job_manager_should_run_shortcode_action_handler', $this->is_job_dashboard_page() ); + + if ( ! $should_run_handler || + empty( $_REQUEST['action'] ) + || empty( $_REQUEST['job_id'] ) + || empty( $_REQUEST['_wpnonce'] ) + ) { + return; + } + + $job_id = absint( $_REQUEST['job_id'] ); + $action = sanitize_title( wp_unslash( $_REQUEST['action'] ) ); + + $job = get_post( $job_id ); + $job_actions = $this->get_job_actions( $job ); + + if ( ! isset( $job_actions[ $action ] ) + || empty( $job_actions[ $action ]['nonce'] ) + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce should not be modified. + || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), $job_actions[ $action ]['nonce'] ) + ) { + return; + } + + try { + if ( empty( $job ) || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type || ! job_manager_user_can_edit_job( $job_id ) ) { + throw new \Exception( __( 'Invalid ID', 'wp-job-manager' ) ); + } + + switch ( $action ) { + case 'mark_filled': + // Check status. + if ( 1 === intval( $job->_filled ) ) { + throw new \Exception( __( 'This position has already been filled', 'wp-job-manager' ) ); + } + + // Update. + update_post_meta( $job_id, '_filled', 1 ); + + // Message. + // translators: Placeholder %s is the job listing title. + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); + break; + case 'mark_not_filled': + // Check status. + if ( 1 !== intval( $job->_filled ) ) { + throw new \Exception( __( 'This position is not filled', 'wp-job-manager' ) ); + } + + // Update. + update_post_meta( $job_id, '_filled', 0 ); + + // Message. + // translators: Placeholder %s is the job listing title. + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been marked as not filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); + break; + case 'delete': + // Trash it. + wp_trash_post( $job_id ); + + // Message. + // translators: Placeholder %s is the job listing title. + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been deleted', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); + + break; + case 'duplicate': + if ( ! job_manager_get_permalink( 'submit_job_form' ) ) { + throw new \Exception( __( 'Missing submission page.', 'wp-job-manager' ) ); + } + + $new_job_id = job_manager_duplicate_listing( $job_id ); + + if ( $new_job_id ) { + wp_safe_redirect( add_query_arg( [ 'job_id' => absint( $new_job_id ) ], job_manager_get_permalink( 'submit_job_form' ) ) ); + exit; + } + + break; + case 'relist': + case 'renew': + case 'continue': + if ( ! job_manager_get_permalink( 'submit_job_form' ) ) { + throw new \Exception( __( 'Missing submission page.', 'wp-job-manager' ) ); + } + + $query_args = [ + 'job_id' => absint( $job_id ), + 'action' => $action, + ]; + + if ( 'renew' === $action ) { + $query_args['nonce'] = wp_create_nonce( 'job_manager_renew_job_' . $job_id ); + } + wp_safe_redirect( add_query_arg( $query_args, job_manager_get_permalink( 'submit_job_form' ) ) ); + exit; + default: + do_action( 'job_manager_job_dashboard_do_action_' . $action, $job_id ); + break; + } + + do_action( 'job_manager_my_job_do_action', $action, $job_id ); + + /** + * Set a success message for a custom dashboard action handler. + * + * When left empty, no success message will be shown. + * + * @since 1.31.1 + * + * @param string $message Text for the success message. Default: empty string. + * @param string $action The name of the custom action. + * @param int $job_id The ID for the job that's been altered. + */ + $success_message = apply_filters( 'job_manager_job_dashboard_success_message', '', $action, $job_id ); + if ( $success_message ) { + $this->job_dashboard_message = Notice::success( $success_message ); + } + } catch ( \Exception $e ) { + $this->job_dashboard_message = Notice::error( $e->getMessage() ); + } + + } + + /** + * Add expiration details to the job dashboard date column. + * + * @param \WP_Post $job + * + * @output string + */ + public function job_dashboard_date_column_expires( $job ) { + $expiration = \WP_Job_Manager_Post_Types::instance()->get_job_expiration( $job ); + + if ( 'publish' === $job->post_status && ! empty( $expiration ) ) { + + // translators: Placeholder is the expiration date of the job listing. + echo '
' . UI_Elements::rel_time( $expiration, __( 'Expires in %s', 'wp-job-manager' ) ) . '
'; + } + } + + /** + * Add job status to the job dashboard title column. + * + * @param \WP_Post $job + * + * @output string + */ + public function job_dashboard_title_column_status( $job ) { + + echo '
'; + + $status = []; + + if ( is_position_filled( $job ) ) { + $status[] = '' . esc_html__( 'Filled', 'wp-job-manager' ) . ''; + } + + if ( is_position_featured( $job ) && 'publish' === $job->post_status ) { + $status[] = '' . esc_html__( 'Featured', 'wp-job-manager' ) . ''; + } + + $status[] = '' . esc_html( get_the_job_status( $job ) ) . ''; + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. + echo implode( ', ', $status ); + + echo '
'; + } + + /** + * Check if a job is listed on the current user's job dashboard page. + * + * @param \WP_Post $job Job post object. + * + * @return bool + */ + private function is_job_available_on_dashboard( \WP_Post $job ) { + // Check cache of currently displayed job dashboard IDs first to avoid lots of queries. + if ( isset( $this->job_dashboard_job_ids ) && in_array( (int) $job->ID, $this->job_dashboard_job_ids, true ) ) { + return true; + } + + $args = $this->get_job_dashboard_query_args(); + $args['p'] = $job->ID; + $args['fields'] = 'ids'; + + $query = new \WP_Query( $args ); + + return (int) $query->post_count > 0; + } + + /** + * Helper that generates the job dashboard query args. + * + * @param int $posts_per_page Number of posts per page. + * + * @return array + */ + private function get_job_dashboard_query_args( $posts_per_page = -1 ) { + $job_dashboard_args = [ + 'post_type' => \WP_Job_Manager_Post_Types::PT_LISTING, + 'post_status' => [ 'publish', 'expired', 'pending', 'draft', 'preview' ], + 'ignore_sticky_posts' => 1, + 'posts_per_page' => $posts_per_page, + 'orderby' => 'date', + 'order' => 'desc', + 'author' => get_current_user_id(), + ]; + + if ( get_option( 'job_manager_enable_scheduled_listings' ) ) { + $job_dashboard_args['post_status'][] = 'future'; + } + + if ( $posts_per_page > 0 ) { + $job_dashboard_args['offset'] = ( max( 1, get_query_var( 'paged' ) ) - 1 ) * $posts_per_page; + } + + /** + * Customize the query that is used to get jobs on the job dashboard. + * + * @since 1.0.0 + * + * @param array $job_dashboard_args Arguments to pass to \WP_Query. + */ + return apply_filters( 'job_manager_get_dashboard_jobs_args', $job_dashboard_args ); + } + +} diff --git a/includes/class-wp-job-manager-shortcodes.php b/includes/class-wp-job-manager-shortcodes.php index 4d701e0c5..f1fc8b64e 100644 --- a/includes/class-wp-job-manager-shortcodes.php +++ b/includes/class-wp-job-manager-shortcodes.php @@ -5,13 +5,14 @@ * @package wp-job-manager */ -use WP_Job_Manager\UI\Notice; -use WP_Job_Manager\UI\UI_Elements; +use WP_Job_Manager\Job_Dashboard_Shortcode; if ( ! defined( 'ABSPATH' ) ) { exit; } +require_once __DIR__ . '/class-job-dashboard-shortcode.php'; + /** * Handles the shortcodes for WP Job Manager. * @@ -19,82 +20,25 @@ */ class WP_Job_Manager_Shortcodes { - /** - * Dashboard message. - * - * @access private - * @var string - */ - private $job_dashboard_message = ''; - - /** - * Cache of job post IDs currently displayed on job dashboard. - * - * @var int[] - */ - private $job_dashboard_job_ids; - - /** - * The single instance of the class. - * - * @var self - * @since 1.26.0 - */ - private static $instance = null; - - /** - * Allows for accessing single instance of class. Class should only be constructed once per call. - * - * @since 1.26.0 - * @static - * @return self Main instance. - */ - public static function instance() { - if ( is_null( self::$instance ) ) { - self::$instance = new self(); - } - return self::$instance; - } + use \WP_Job_Manager\Singleton; /** * Constructor. */ public function __construct() { add_action( 'wp', [ $this, 'handle_redirects' ] ); - add_action( 'wp', [ $this, 'shortcode_action_handler' ] ); - add_action( 'job_manager_job_dashboard_content_edit', [ $this, 'edit_job' ] ); + add_action( 'job_manager_job_filters_end', [ $this, 'job_filter_job_types' ], 20 ); add_action( 'job_manager_job_filters_end', [ $this, 'job_filter_results' ], 30 ); add_action( 'job_manager_output_jobs_no_results', [ $this, 'output_no_results' ] ); add_shortcode( 'submit_job_form', [ $this, 'submit_job_form' ] ); - add_shortcode( 'job_dashboard', [ $this, 'job_dashboard' ] ); + add_shortcode( 'jobs', [ $this, 'output_jobs' ] ); add_shortcode( 'job', [ $this, 'output_job' ] ); add_shortcode( 'job_summary', [ $this, 'output_job_summary' ] ); add_shortcode( 'job_apply', [ $this, 'output_job_apply' ] ); - add_filter( 'paginate_links', [ $this, 'filter_paginate_links' ], 10, 1 ); - - add_action( 'job_manager_job_dashboard_column_date', [ $this, 'job_dashboard_date_column_expires' ] ); - add_action( 'job_manager_job_dashboard_column_job_title', [ $this, 'job_dashboard_title_column_status' ] ); - } - - /** - * Helper function used to check if page is WPJM dashboard page. - * - * Checks if page has 'job_dashboard' shortcode. - * - * @access private - * @return bool True if page is dashboard page, false otherwise. - */ - private function is_job_dashboard_page() { - global $post; - - if ( is_page() && has_shortcode( $post->post_content, 'job_dashboard' ) ) { - return true; - } - - return false; + Job_Dashboard_Shortcode::instance(); } /** @@ -148,24 +92,6 @@ public function handle_redirects() { } } - /** - * Handles actions which need to be run before the shortcode e.g. post actions. - */ - public function shortcode_action_handler() { - /** - * Determine if the shortcode action handler should run. - * - * @since 1.35.0 - * - * @param bool $should_run_handler Should the handler run. - */ - $should_run_handler = apply_filters( 'job_manager_should_run_shortcode_action_handler', $this->is_job_dashboard_page() ); - - if ( $should_run_handler ) { - $this->job_dashboard_handler(); - } - } - /** * Shows the job submission form. * @@ -176,394 +102,58 @@ public function submit_job_form( $atts = [] ) { return $GLOBALS['job_manager']->forms->get_form( 'submit-job', $atts ); } - /** - * Handles actions on job dashboard. - * - * @throws Exception On action handling error. - */ - public function job_dashboard_handler() { - if ( - ! empty( $_REQUEST['action'] ) - && ! empty( $_REQUEST['job_id'] ) - && ! empty( $_REQUEST['_wpnonce'] ) - ) { - - $job_id = isset( $_REQUEST['job_id'] ) ? absint( $_REQUEST['job_id'] ) : 0; - $action = sanitize_title( wp_unslash( $_REQUEST['action'] ) ); - - $job = get_post( $job_id ); - $job_actions = $this->get_job_actions( $job ); - - if ( - ! isset( $job_actions[ $action ] ) - || empty( $job_actions[ $action ]['nonce'] ) - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce should not be modified. - || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), $job_actions[ $action ]['nonce'] ) - ) { - return; - } - - try { - if ( empty( $job ) || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type || ! job_manager_user_can_edit_job( $job_id ) ) { - throw new Exception( __( 'Invalid ID', 'wp-job-manager' ) ); - } - - switch ( $action ) { - case 'mark_filled': - // Check status. - if ( 1 === intval( $job->_filled ) ) { - throw new Exception( __( 'This position has already been filled', 'wp-job-manager' ) ); - } - - // Update. - update_post_meta( $job_id, '_filled', 1 ); - - // Message. - // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); - break; - case 'mark_not_filled': - // Check status. - if ( 1 !== intval( $job->_filled ) ) { - throw new Exception( __( 'This position is not filled', 'wp-job-manager' ) ); - } - - // Update. - update_post_meta( $job_id, '_filled', 0 ); - - // Message. - // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been marked as not filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); - break; - case 'delete': - // Trash it. - wp_trash_post( $job_id ); - - // Message. - // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been deleted', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); - - break; - case 'duplicate': - if ( ! job_manager_get_permalink( 'submit_job_form' ) ) { - throw new Exception( __( 'Missing submission page.', 'wp-job-manager' ) ); - } - - $new_job_id = job_manager_duplicate_listing( $job_id ); - - if ( $new_job_id ) { - wp_safe_redirect( add_query_arg( [ 'job_id' => absint( $new_job_id ) ], job_manager_get_permalink( 'submit_job_form' ) ) ); - exit; - } - - break; - case 'relist': - case 'renew': - case 'continue': - if ( ! job_manager_get_permalink( 'submit_job_form' ) ) { - throw new Exception( __( 'Missing submission page.', 'wp-job-manager' ) ); - } - - $query_args = [ - 'job_id' => absint( $job_id ), - 'action' => $action, - ]; - - if ( 'renew' === $action ) { - $query_args['nonce'] = wp_create_nonce( 'job_manager_renew_job_' . $job_id ); - } - wp_safe_redirect( add_query_arg( $query_args, job_manager_get_permalink( 'submit_job_form' ) ) ); - exit; - default: - do_action( 'job_manager_job_dashboard_do_action_' . $action, $job_id ); - break; - } - - do_action( 'job_manager_my_job_do_action', $action, $job_id ); - - /** - * Set a success message for a custom dashboard action handler. - * - * When left empty, no success message will be shown. - * - * @since 1.31.1 - * - * @param string $message Text for the success message. Default: empty string. - * @param string $action The name of the custom action. - * @param int $job_id The ID for the job that's been altered. - */ - $success_message = apply_filters( 'job_manager_job_dashboard_success_message', '', $action, $job_id ); - if ( $success_message ) { - $this->job_dashboard_message = Notice::success( $success_message ); - } - } catch ( Exception $e ) { - $this->job_dashboard_message = Notice::error( $e->getMessage() ); - } - } - } - - /** - * Check if a job is listed on the current user's job dashboard page. - * - * @param WP_Post $job Job post object. - * - * @return bool - */ - private function is_job_available_on_dashboard( WP_Post $job ) { - // Check cache of currently displayed job dashboard IDs first to avoid lots of queries. - if ( isset( $this->job_dashboard_job_ids ) && in_array( (int) $job->ID, $this->job_dashboard_job_ids, true ) ) { - return true; - } - - $args = $this->get_job_dashboard_query_args(); - $args['p'] = $job->ID; - $args['fields'] = 'ids'; - - $query = new WP_Query( $args ); - - return (int) $query->post_count > 0; - } - - /** - * Helper that generates the job dashboard query args. - * - * @param int $posts_per_page Number of posts per page. - * - * @return array - */ - private function get_job_dashboard_query_args( $posts_per_page = -1 ) { - $job_dashboard_args = [ - 'post_type' => \WP_Job_Manager_Post_Types::PT_LISTING, - 'post_status' => [ 'publish', 'expired', 'pending', 'draft', 'preview' ], - 'ignore_sticky_posts' => 1, - 'posts_per_page' => $posts_per_page, - 'orderby' => 'date', - 'order' => 'desc', - 'author' => get_current_user_id(), - ]; - - if ( get_option( 'job_manager_enable_scheduled_listings' ) ) { - $job_dashboard_args['post_status'][] = 'future'; - } - - if ( $posts_per_page > 0 ) { - $job_dashboard_args['offset'] = ( max( 1, get_query_var( 'paged' ) ) - 1 ) * $posts_per_page; - } - - /** - * Customize the query that is used to get jobs on the job dashboard. - * - * @since 1.0.0 - * - * @param array $job_dashboard_args Arguments to pass to WP_Query. - */ - return apply_filters( 'job_manager_get_dashboard_jobs_args', $job_dashboard_args ); - } - /** * Handles shortcode which lists the logged in user's jobs. * + * @deprecated $$next-version$$ - Moved to Job_Dashboard_Shortcode. + * * @param array $atts * @return string */ public function job_dashboard( $atts ) { - if ( ! is_user_logged_in() ) { - ob_start(); - get_job_manager_template( 'job-dashboard-login.php' ); - return ob_get_clean(); - } - - $new_atts = shortcode_atts( - [ - 'posts_per_page' => '25', - ], - $atts - ); - $posts_per_page = $new_atts['posts_per_page']; - - WP_Job_Manager::register_style( 'wp-job-manager-job-dashboard', 'css/job-dashboard.css', [ 'wp-job-manager-ui' ] ); - wp_enqueue_style( 'wp-job-manager-job-dashboard' ); - wp_enqueue_script( 'wp-job-manager-job-dashboard' ); - - ob_start(); - - // If doing an action, show conditional content if needed.... - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used safely. - $action = isset( $_REQUEST['action'] ) ? sanitize_title( wp_unslash( $_REQUEST['action'] ) ) : false; - if ( ! empty( $action ) ) { - // Show alternative content if a plugin wants to. - if ( has_action( 'job_manager_job_dashboard_content_' . $action ) ) { - do_action( 'job_manager_job_dashboard_content_' . $action, $atts ); - - return ob_get_clean(); - } - } - - // ....If not show the job dashboard. - $jobs = new WP_Query( $this->get_job_dashboard_query_args( $posts_per_page ) ); - - // Cache IDs for access check later on. - $this->job_dashboard_job_ids = wp_list_pluck( $jobs->posts, 'ID' ); - - echo '
' . wp_kses_post( $this->job_dashboard_message ) . '
'; - - $job_dashboard_columns = apply_filters( - 'job_manager_job_dashboard_columns', - [ - 'job_title' => __( 'Title', 'wp-job-manager' ), - 'date' => __( 'Date', 'wp-job-manager' ), - ] - ); + _deprecated_function( __METHOD__, '$$next-version$$', 'Job_Dashboard_Shortcode::output_job_dashboard_shortcode' ); - $job_actions = []; - foreach ( $jobs->posts as $job ) { - $job_actions[ $job->ID ] = $this->get_job_actions( $job ); - } - - get_job_manager_template( - 'job-dashboard.php', - [ - 'jobs' => $jobs->posts, - 'job_actions' => $job_actions, - 'max_num_pages' => $jobs->max_num_pages, - 'job_dashboard_columns' => $job_dashboard_columns, - ] - ); - - return ob_get_clean(); + return Job_Dashboard_Shortcode::instance()->output_job_dashboard( $atts ); } /** * Get the actions available to the user for a job listing on the job dashboard page. * + * @deprecated $$next-version$$ - Moved to Job_Dashboard_Shortcode. + * * @param WP_Post $job The job post object. * * @return array */ public function get_job_actions( $job ) { - if ( - ! get_current_user_id() - || ! $job instanceof WP_Post - || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type - || ! $this->is_job_available_on_dashboard( $job ) - ) { - return []; - } - - $base_nonce_action_name = 'job_manager_my_job_actions'; - - $actions = []; - switch ( $job->post_status ) { - case 'publish': - if ( WP_Job_Manager_Post_Types::job_is_editable( $job->ID ) ) { - $actions['edit'] = [ - 'label' => __( 'Edit', 'wp-job-manager' ), - 'nonce' => false, - ]; - } - if ( is_position_filled( $job ) ) { - $actions['mark_not_filled'] = [ - 'label' => __( 'Mark not filled', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - } else { - $actions['mark_filled'] = [ - 'label' => __( 'Mark filled', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - } - if ( - get_option( 'job_manager_renewal_days' ) > 0 - && WP_Job_Manager_Helper_Renewals::job_can_be_renewed( $job ) - && WP_Job_Manager_Helper_Renewals::is_wcpl_renew_compatible() - && WP_Job_Manager_Helper_Renewals::is_spl_renew_compatible() - ) { - $actions['renew'] = [ - 'label' => __( 'Renew', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - } - - $actions['duplicate'] = [ - 'label' => __( 'Duplicate', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - break; - case 'expired': - if ( job_manager_get_permalink( 'submit_job_form' ) ) { - $actions['relist'] = [ - 'label' => __( 'Relist', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - } - break; - case 'pending_payment': - case 'pending': - if ( WP_Job_Manager_Post_Types::job_is_editable( $job->ID ) ) { - $actions['edit'] = [ - 'label' => __( 'Edit', 'wp-job-manager' ), - 'nonce' => false, - ]; - } - break; - case 'draft': - case 'preview': - $actions['continue'] = [ - 'label' => __( 'Continue Submission', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - break; - } + _deprecated_function( __METHOD__, '$$next-version$$', 'Job_Dashboard_Shortcode::get_job_actions' ); - $actions['delete'] = [ - 'label' => __( 'Delete', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - - /** - * Filter the actions available to the current user for a job on the job dashboard page. - * - * @since 1.0.0 - * - * @param array $actions Actions to filter. - * @param WP_Post $job Job post object. - */ - $actions = apply_filters( 'job_manager_my_job_actions', $actions, $job ); - - // For backwards compatibility, convert `nonce => true` to the nonce action name. - foreach ( $actions as $key => $action ) { - if ( true === $action['nonce'] ) { - $actions[ $key ]['nonce'] = $base_nonce_action_name; - } - } - - return $actions; + return Job_Dashboard_Shortcode::instance()->get_job_actions( $job ); } /** * Filters the url from paginate_links to avoid multiple calls for same action in job dashboard * + * @deprecated $$next-version$$ - Moved to Job_Dashboard_Shortcode. + * * @param string $link * @return string */ public function filter_paginate_links( $link ) { + _deprecated_function( __METHOD__, '$$next-version$$', 'Job_Dashboard_Shortcode::filter_paginate_links' ); - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used for comparison only. - if ( $this->is_job_dashboard_page() && isset( $_GET['action'] ) && in_array( $_GET['action'], [ 'mark_filled', 'mark_not_filled' ], true ) ) { - return remove_query_arg( [ 'action', 'job_id', '_wpnonce' ], $link ); - } - - return $link; + return Job_Dashboard_Shortcode::instance()->filter_paginate_links( $link ); } /** * Displays edit job form. + * + * @deprecated $$next-version$$ - Moved to Job_Dashboard_Shortcode. */ public function edit_job() { - global $job_manager; + _deprecated_function( __METHOD__, '$$next-version$$', 'Job_Dashboard_Shortcode::edit_job' ); - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output should be appropriately escaped in the form generator. - echo $job_manager->forms->get_form( 'edit-job' ); + Job_Dashboard_Shortcode::instance()->edit_job(); } /** @@ -997,51 +587,6 @@ public function output_job_apply( $atts ) { return ob_get_clean(); } - /** - * Add expiration details to the job dashboard date column. - * - * @param \WP_Post $job - * - * @output string - */ - public function job_dashboard_date_column_expires( $job ) { - $expiration = WP_Job_Manager_Post_Types::instance()->get_job_expiration( $job ); - - if ( 'publish' === $job->post_status && ! empty( $expiration ) ) { - - // translators: Placeholder is the expiration date of the job listing. - echo '
' . UI_Elements::rel_time( $expiration, __( 'Expires in %s', 'wp-job-manager' ) ) . '
'; - } - } - - /** - * Add job status to the job dashboard title column. - * - * @param \WP_Post $job - * - * @output string - */ - public function job_dashboard_title_column_status( $job ) { - - echo '
'; - - $status = []; - - if ( is_position_filled( $job ) ) { - $status[] = '' . esc_html__( 'Filled', 'wp-job-manager' ) . ''; - } - - if ( is_position_featured( $job ) && 'publish' === $job->post_status ) { - $status[] = '' . esc_html__( 'Featured', 'wp-job-manager' ) . ''; - } - - $status[] = '' . esc_html( get_the_job_status( $job ) ) . ''; - - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. - echo implode( ', ', $status ); - - echo '
'; - } } WP_Job_Manager_Shortcodes::instance(); From d74d5eccc3e37f54715603e056fbf799ecc76087 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 14 Feb 2024 15:38:38 +0100 Subject: [PATCH 30/50] Add search to job dashboard (#2760) --- assets/css/ui.elements.scss | 4 ++ assets/css/ui.form.scss | 7 +++ assets/css/ui.neutral.scss | 1 + includes/class-job-dashboard-shortcode.php | 59 ++++++++++++-------- templates/job-dashboard.php | 62 ++++++++++++++-------- 5 files changed, 89 insertions(+), 44 deletions(-) diff --git a/assets/css/ui.elements.scss b/assets/css/ui.elements.scss index ac3903b94..bdd3b9ebb 100644 --- a/assets/css/ui.elements.scss +++ b/assets/css/ui.elements.scss @@ -28,6 +28,10 @@ transition: color 0.2s ease-out, background 0.2s ease-out; + &:focus:not(:focus-visible) { + outline: unset; + } + &:focus-visible { outline: 1.5px solid fadeCurrentColor(85%); outline-offset: 1.5px; diff --git a/assets/css/ui.form.scss b/assets/css/ui.form.scss index 4a55b2c41..c431b9499 100644 --- a/assets/css/ui.form.scss +++ b/assets/css/ui.form.scss @@ -177,6 +177,13 @@ padding-right: var(--jm-ui-space-ml); } + input[type].jm-ui-input--search-icon { + background-image: var(--jm-ui-svg-search); + background-position: var(--jm-ui-space-xs) center; + background-repeat: no-repeat; + padding-left: calc(var(--jm-ui-space-ml) + var(--jm-ui-space-xs)); + } + .select2-container.select2-container.select2-container { input { diff --git a/assets/css/ui.neutral.scss b/assets/css/ui.neutral.scss index 5076c739b..3cb3fafcc 100644 --- a/assets/css/ui.neutral.scss +++ b/assets/css/ui.neutral.scss @@ -67,4 +67,5 @@ --jm-ui-svg-arrow-down: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='32' height='6' viewBox='0 0 16 10'%3e%3cpath fill='black' fill-rule='evenodd' d='M0 1.6 1.53 0 8 6.95 14.5 0 16 1.6 8 10 0 1.6Z' clip-rule='evenodd'/%3e%3c/svg%3e"); --jm-ui-svg-check: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='black' stroke-width='1.5' d='m18.93 6-8.9 11.97-5.16-3.84'/%3e%3c/svg%3e"); --jm-ui-svg-ellipsis-v: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='M11 19v-2h2v2h-2Zm0-6v-2h2v2h-2Zm0-6V5h2v2h-2Z' clip-rule='evenodd'/%3e%3c/svg%3e"); + --jm-ui-svg-search: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='M19 11a6 6 0 0 1-9.68 4.74l-3.79 3.79-1.06-1.06 3.79-3.8A6 6 0 1 1 19 11Zm-1.5 0a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z' clip-rule='evenodd'/%3e%3c/svg%3e"); } diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php index f1fde82e1..bb3bbb38c 100644 --- a/includes/class-job-dashboard-shortcode.php +++ b/includes/class-job-dashboard-shortcode.php @@ -96,8 +96,18 @@ public function output_job_dashboard( $attrs ) { } } + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $search = isset( $_GET['search'] ) ? sanitize_text_field( wp_unslash( $_GET['search'] ) ) : ''; + // ....If not show the job dashboard. - $jobs = new \WP_Query( $this->get_job_dashboard_query_args( $posts_per_page ) ); + $jobs = new \WP_Query( + $this->get_job_dashboard_query_args( + [ + 'posts_per_page' => $posts_per_page, + 's' => $search, + ] + ), + ); // Cache IDs for access check later on. $this->job_dashboard_job_ids = wp_list_pluck( $jobs->posts, 'ID' ); @@ -124,6 +134,7 @@ public function output_job_dashboard( $attrs ) { 'job_actions' => $job_actions, 'max_num_pages' => $jobs->max_num_pages, 'job_dashboard_columns' => $job_dashboard_columns, + 'search_input' => $search, ] ); @@ -222,7 +233,7 @@ public function get_job_actions( $job ) { * * @since 1.0.0 * - * @param array $actions Actions to filter. + * @param array $actions Actions to filter. * @param \WP_Post $job Job post object. */ $actions = apply_filters( 'job_manager_my_job_actions', $actions, $job ); @@ -306,8 +317,8 @@ public function handle_actions() { */ $should_run_handler = apply_filters( 'job_manager_should_run_shortcode_action_handler', $this->is_job_dashboard_page() ); - if ( ! $should_run_handler || - empty( $_REQUEST['action'] ) + if ( ! $should_run_handler + || empty( $_REQUEST['action'] ) || empty( $_REQUEST['job_id'] ) || empty( $_REQUEST['_wpnonce'] ) ) { @@ -413,9 +424,9 @@ public function handle_actions() { * * @since 1.31.1 * - * @param string $message Text for the success message. Default: empty string. - * @param string $action The name of the custom action. - * @param int $job_id The ID for the job that's been altered. + * @param string $message Text for the success message. Default: empty string. + * @param string $action The name of the custom action. + * @param int $job_id The ID for the job that's been altered. */ $success_message = apply_filters( 'job_manager_job_dashboard_success_message', '', $action, $job_id ); if ( $success_message ) { @@ -498,27 +509,29 @@ private function is_job_available_on_dashboard( \WP_Post $job ) { /** * Helper that generates the job dashboard query args. * - * @param int $posts_per_page Number of posts per page. + * @param array $args Additional query args. * * @return array */ - private function get_job_dashboard_query_args( $posts_per_page = -1 ) { - $job_dashboard_args = [ - 'post_type' => \WP_Job_Manager_Post_Types::PT_LISTING, - 'post_status' => [ 'publish', 'expired', 'pending', 'draft', 'preview' ], - 'ignore_sticky_posts' => 1, - 'posts_per_page' => $posts_per_page, - 'orderby' => 'date', - 'order' => 'desc', - 'author' => get_current_user_id(), - ]; + private function get_job_dashboard_query_args( $args = [] ) { + $args = wp_parse_args( + $args, + [ + 'post_type' => \WP_Job_Manager_Post_Types::PT_LISTING, + 'post_status' => [ 'publish', 'expired', 'pending', 'draft', 'preview' ], + 'ignore_sticky_posts' => 1, + 'orderby' => 'date', + 'order' => 'desc', + 'author' => get_current_user_id(), + ] + ); if ( get_option( 'job_manager_enable_scheduled_listings' ) ) { - $job_dashboard_args['post_status'][] = 'future'; + $args['post_status'][] = 'future'; } - if ( $posts_per_page > 0 ) { - $job_dashboard_args['offset'] = ( max( 1, get_query_var( 'paged' ) ) - 1 ) * $posts_per_page; + if ( $args['posts_per_page'] > 0 ) { + $args['offset'] = ( max( 1, get_query_var( 'paged' ) ) - 1 ) * $args['posts_per_page']; } /** @@ -526,9 +539,9 @@ private function get_job_dashboard_query_args( $posts_per_page = -1 ) { * * @since 1.0.0 * - * @param array $job_dashboard_args Arguments to pass to \WP_Query. + * @param array $args Arguments to pass to \WP_Query. */ - return apply_filters( 'job_manager_get_dashboard_jobs_args', $job_dashboard_args ); + return apply_filters( 'job_manager_get_dashboard_jobs_args', $args ); } } diff --git a/templates/job-dashboard.php b/templates/job-dashboard.php index 614d1320d..77918ebcb 100644 --- a/templates/job-dashboard.php +++ b/templates/job-dashboard.php @@ -18,8 +18,10 @@ * @var int $max_num_pages Maximum number of pages * @var WP_Post[] $jobs Array of job post results. * @var array $job_actions Array of actions available for each job. + * @var string $search_input Search input. */ +use WP_Job_Manager\UI\Notice; use WP_Job_Manager\UI\UI_Elements; if ( ! defined( 'ABSPATH' ) ) { @@ -31,27 +33,45 @@
-

- -
- -
- +
+
+
+ +
+
+
+
+ + + +
-
- $column ) : ?> -
- -
-
-
- -
- + +
+ $search_input + // translators: Placeholder is the search term. + ? sprintf( __( 'No results found for "%s".', 'wp-job-manager' ), $search_input ) + : __( 'You do not have any active listings.', 'wp-job-manager' ) + ] + ); ?> +
+ +
+ $column ) : ?> +
+ +
+
+
$column ) : ?> @@ -95,8 +115,8 @@ class="jm-dashboard-empty">
- -
+
+
$max_num_pages ] ); ?>
From 109e94a02e0874fef9abf934b4f9208136639ec6 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Mon, 19 Feb 2024 17:51:42 +0100 Subject: [PATCH 31/50] Update `feature/stats` branch (#2764) Co-authored-by: Mikey Arce Co-authored-by: WPJM Bot --- assets/js/datepicker.js | 2 + changelog.txt | 5 + ...ass-wp-job-manager-email-notifications.php | 92 +++++++------------ includes/class-wp-job-manager.php | 9 +- includes/ui/class-ui-settings.php | 4 +- includes/ui/class-ui.php | 7 -- languages/wp-job-manager.pot | 78 ++++++++-------- package-lock.json | 4 +- package.json | 2 +- readme.txt | 21 ++--- wp-job-manager-autoload.php | 86 +++++++++++++++++ wp-job-manager.php | 8 +- 12 files changed, 180 insertions(+), 138 deletions(-) create mode 100644 wp-job-manager-autoload.php diff --git a/assets/js/datepicker.js b/assets/js/datepicker.js index 891c62399..1d8e248a5 100644 --- a/assets/js/datepicker.js +++ b/assets/js/datepicker.js @@ -1,7 +1,9 @@ /* global job_manager_datepicker */ jQuery(document).ready( function() { + var $date_today = new Date(); var datePickerOptions = { altFormat : 'yy-mm-dd', + minDate : $date_today, }; if ( typeof job_manager_datepicker !== 'undefined' ) { diff --git a/changelog.txt b/changelog.txt index b1362b92f..849e17fa1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,10 @@ # WP Job Manager +## 2.2.2 - 2024-02-15 +* Fix issue with rich e-mails on some e-mail providers (#2753) +* Fix: 'featured_first' argument now works when 'show_filters' is set to false. +* Improve checkbox and radio inputs for styled forms + ## 2.2.1 - 2024-01-31 * Fix PHP 7.x error for mixed returned type (#2726) diff --git a/includes/class-wp-job-manager-email-notifications.php b/includes/class-wp-job-manager-email-notifications.php index 16d2396ee..10d2404ea 100644 --- a/includes/class-wp-job-manager-email-notifications.php +++ b/includes/class-wp-job-manager-email-notifications.php @@ -18,7 +18,6 @@ final class WP_Job_Manager_Email_Notifications { const EMAIL_SETTING_PREFIX = 'job_manager_email_'; const EMAIL_SETTING_ENABLED = 'enabled'; const EMAIL_SETTING_PLAIN_TEXT = 'plain_text'; - const MULTIPART_BOUNDARY = 'jm-boundary'; /** * Notifications to be scheduled. @@ -767,7 +766,7 @@ private static function is_email_notification_valid( $email_class ) { * @return bool */ private static function send_email( $email_notification_key, WP_Job_Manager_Email $email ) { - add_filter( 'wp_mail_content_type', [ __CLASS__, 'mail_content_type' ] ); + global $job_manager_doing_email; $job_manager_doing_email = true; @@ -804,10 +803,11 @@ private static function send_email( $email_notification_key, WP_Job_Manager_Emai $is_plain_text_only = self::send_as_plain_text( $email_notification_key, $args ); $content_plain = self::get_email_content( $email_notification_key, $args, true ); - $content_html = null; - if ( ! $is_plain_text_only ) { - $content_html = self::get_email_content( $email_notification_key, $args, false ); + if ( $is_plain_text_only ) { + $body = $content_plain; + } else { + $body = self::get_email_content( $email_notification_key, $args, false ); } /** @@ -829,8 +829,6 @@ private static function send_email( $email_notification_key, WP_Job_Manager_Emai $headers[] = 'CC: ' . $args['cc']; } - $multipart_body = self::get_multipart_body( $content_html, $content_plain ); - /** * Allows for short-circuiting the actual sending of email notifications. * @@ -842,34 +840,45 @@ private static function send_email( $email_notification_key, WP_Job_Manager_Emai * @param string $content Email content. * @param array $headers Email headers. */ - if ( ! apply_filters( 'job_manager_email_do_send_notification', true, $email, $args, $multipart_body, $headers ) ) { + if ( ! apply_filters( 'job_manager_email_do_send_notification', true, $email, $args, $body, $headers ) ) { continue; } - if ( wp_mail( $to_email, $args['subject'], $multipart_body, $headers, $args['attachments'] ) ) { + $set_alt_body = function( $mailer ) use ( $content_plain ) { + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $mailer->AltBody = $content_plain; + + return $mailer; + }; + + $set_content_type = fn() => 'multipart/alternative'; + + if ( ! $is_plain_text_only ) { + add_filter( 'wp_mail_content_type', $set_content_type ); + add_filter( 'phpmailer_init', $set_alt_body ); + } + + if ( wp_mail( $to_email, $args['subject'], $body, $headers, $args['attachments'] ) ) { $sent_count++; } + + remove_filter( 'wp_mail_content_type', $set_content_type ); + remove_filter( 'phpmailer_init', $set_alt_body ); + + // Make sure AltBody is not sticking around for a different email. + global $phpmailer; + + if ( $phpmailer instanceof \PHPMailer\PHPMailer\PHPMailer ) { + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $phpmailer->AltBody = ''; + } } - remove_filter( 'wp_mail_content_type', [ __CLASS__, 'mail_content_type' ] ); $job_manager_doing_email = false; return $sent_count > 0; } - /** - * Set the "Content Type" header of the e-mail to multipart/alternative. - * - * @access private - * - * @since 2.2.0 - * - * @return string - */ - public static function mail_content_type() { - return 'multipart/alternative; boundary="' . self::MULTIPART_BOUNDARY . '"'; - } - /** * Generates the content for an email. * @@ -965,41 +974,4 @@ private static function get_styles() { return ob_get_clean(); } - /** - * Assemble multipart e-mail body. - * - * @param string $content_html - * @param string $content_plain - * - * @return string - */ - private static function get_multipart_body( string $content_html, string $content_plain ): string { - $multipart_body = ''; - - if ( ! empty( $content_plain ) ) { - - $multipart_body .= ' ---' . self::MULTIPART_BOUNDARY . ' -Content-Type: text/plain; charset="utf-8" - -' . $content_plain; - } - - if ( ! empty( $content_html ) ) { - $multipart_body .= ' ---' . self::MULTIPART_BOUNDARY . ' -Content-Type: text/html; charset="utf-8" - -' . $content_html; - } - - if ( ! empty( $multipart_body ) ) { - $multipart_body .= ' ---' . self::MULTIPART_BOUNDARY . '-- -'; - } - - return $multipart_body; - } - } diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index 63910aa65..123cba7aa 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -86,17 +86,14 @@ public function __construct() { include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-wp-job-manager-data-exporter.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-wp-job-manager-com-api.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/promoted-jobs/class-wp-job-manager-promoted-jobs.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-access-token.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-guest-user.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-guest-session.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-stats.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui-settings.php'; if ( is_admin() ) { include_once JOB_MANAGER_PLUGIN_DIR . '/includes/admin/class-wp-job-manager-admin.php'; } + \WP_Job_Manager\UI\UI::instance(); + \WP_Job_Manager\UI\UI_Settings::instance(); + // Load 3rd party customizations. include_once JOB_MANAGER_PLUGIN_DIR . '/includes/3rd-party/3rd-party.php'; diff --git a/includes/ui/class-ui-settings.php b/includes/ui/class-ui-settings.php index 5d79f1ac5..0fed89303 100644 --- a/includes/ui/class-ui-settings.php +++ b/includes/ui/class-ui-settings.php @@ -21,7 +21,7 @@ * * @internal */ -class UISettings { +class UI_Settings { use Singleton; @@ -333,5 +333,3 @@ public function preview_ui_elements() { } } - -UISettings::instance(); diff --git a/includes/ui/class-ui.php b/includes/ui/class-ui.php index 84cb29c1c..a899223c8 100644 --- a/includes/ui/class-ui.php +++ b/includes/ui/class-ui.php @@ -14,11 +14,6 @@ exit; // Exit if accessed directly. } -require_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui-elements.php'; -require_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-notice.php'; -require_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-modal-dialog.php'; -require_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-redirect-message.php'; - /** * Frontend UI elements of Job Manager. * @@ -103,5 +98,3 @@ private function generate_inline_css() { return $css; } } - -UI::instance(); diff --git a/languages/wp-job-manager.pot b/languages/wp-job-manager.pot index dae947cae..a06e33c51 100644 --- a/languages/wp-job-manager.pot +++ b/languages/wp-job-manager.pot @@ -2,16 +2,16 @@ # This file is distributed under the GPL2+. msgid "" msgstr "" -"Project-Id-Version: WP Job Manager 2.2.1\n" +"Project-Id-Version: WP Job Manager 2.2.2\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/wp-job-manager/\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2024-01-31T09:07:35+00:00\n" +"POT-Creation-Date: 2024-02-02T12:24:53+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"X-Generator: WP-CLI 2.5.0\n" +"X-Generator: WP-CLI 2.7.1\n" "X-Domain: wp-job-manager\n" #. Plugin Name of the plugin @@ -381,7 +381,6 @@ msgstr "" msgid "%1$s updated. View" msgstr "" -#. translators: %1$s is the singular name of the job listing post type; %2$s is the URL to view the listing. #: includes/admin/class-wp-job-manager-cpt.php:457 msgid "Custom field updated." msgstr "" @@ -479,7 +478,6 @@ msgstr "" msgid "View" msgstr "" -#. translators: Placeholder %s is the singular label of the job listing post type. #: includes/admin/class-wp-job-manager-cpt.php:567 #: includes/class-wp-job-manager-post-types.php:464 #: includes/class-wp-job-manager-shortcodes.php:454 @@ -1504,7 +1502,6 @@ msgstr[1] "" msgid "You must be logged in to upload files using this method." msgstr "" -#. translators: Placeholder %s is the singular label of the job listing post type. #: includes/class-wp-job-manager-data-exporter.php:51 #: includes/class-wp-job-manager-post-types.php:481 msgid "Company Logo" @@ -1717,7 +1714,6 @@ msgstr "" msgid "Jobs" msgstr "" -#. translators: Placeholder %s is the plural label of the job listing post type. #: includes/class-wp-job-manager-post-types.php:461 msgid "Add New" msgstr "" @@ -1766,7 +1762,7 @@ msgid "This is where you can create and manage %s." msgstr "" #: includes/class-wp-job-manager-post-types.php:522 -#: wp-job-manager-functions.php:377 +#: wp-job-manager-functions.php:381 msgctxt "post status" msgid "Expired" msgstr "" @@ -1779,7 +1775,7 @@ msgstr[0] "" msgstr[1] "" #: includes/class-wp-job-manager-post-types.php:535 -#: wp-job-manager-functions.php:378 +#: wp-job-manager-functions.php:382 msgctxt "post status" msgid "Preview" msgstr "" @@ -1935,7 +1931,6 @@ msgstr "" msgid "Missing submission page." msgstr "" -#. translators: Placeholder %s is the plural label for the job listing post type. #: includes/class-wp-job-manager-shortcodes.php:405 #: includes/widgets/class-wp-job-manager-widget-featured-jobs.php:36 #: includes/widgets/class-wp-job-manager-widget-featured-jobs.php:52 @@ -1972,7 +1967,7 @@ msgid "Continue Submission" msgstr "" #: includes/class-wp-job-manager-shortcodes.php:715 -#: includes/class-wp-job-manager-shortcodes.php:754 +#: includes/class-wp-job-manager-shortcodes.php:755 msgid "Load more listings" msgstr "" @@ -2204,7 +2199,7 @@ msgstr "" #. translators: Placeholder %1$s is field label; %2$s is the file mime type; %3$s is the allowed mime-types. #. translators: %1$s is the file field label; %2$s is the file type; %3$s is the list of allowed file types. #: includes/forms/class-wp-job-manager-form-submit-job.php:500 -#: wp-job-manager-functions.php:1412 +#: wp-job-manager-functions.php:1416 msgid "\"%1$s\" (filetype %2$s) needs to be one of the following file types: %3$s" msgstr "" @@ -2688,12 +2683,12 @@ msgid "Maximum file size: %s." msgstr "" #: templates/form-fields/multiselect-field.php:20 -#: wp-job-manager-functions.php:1179 +#: wp-job-manager-functions.php:1183 msgid "No results match" msgstr "" #: templates/form-fields/multiselect-field.php:20 -#: wp-job-manager-functions.php:1180 +#: wp-job-manager-functions.php:1184 msgid "Select Some Options" msgstr "" @@ -2798,7 +2793,6 @@ msgstr "" msgid "%s submitted successfully. Your listing will be visible once approved." msgstr "" -#. translators: %1$s is the URL to view the listing; %2$s is #: templates/job-submitted.php:61 msgid " View your %2$s" msgstr "" @@ -2816,117 +2810,117 @@ msgstr "" msgid "Requires Attention" msgstr "" -#: wp-job-manager-functions.php:376 +#: wp-job-manager-functions.php:380 msgctxt "post status" msgid "Draft" msgstr "" -#: wp-job-manager-functions.php:379 +#: wp-job-manager-functions.php:383 msgctxt "post status" msgid "Pending approval" msgstr "" -#: wp-job-manager-functions.php:380 +#: wp-job-manager-functions.php:384 msgctxt "post status" msgid "Pending payment" msgstr "" -#: wp-job-manager-functions.php:381 +#: wp-job-manager-functions.php:385 msgctxt "post status" msgid "Active" msgstr "" -#: wp-job-manager-functions.php:382 +#: wp-job-manager-functions.php:386 msgctxt "post status" msgid "Scheduled" msgstr "" -#: wp-job-manager-functions.php:503 +#: wp-job-manager-functions.php:507 msgid "Reset" msgstr "" -#: wp-job-manager-functions.php:507 +#: wp-job-manager-functions.php:511 msgid "RSS" msgstr "" -#: wp-job-manager-functions.php:616 +#: wp-job-manager-functions.php:620 msgid "Invalid email address." msgstr "" -#: wp-job-manager-functions.php:624 +#: wp-job-manager-functions.php:628 msgid "Your email address isn’t correct." msgstr "" -#: wp-job-manager-functions.php:628 +#: wp-job-manager-functions.php:632 msgid "This email is already registered, please choose another one." msgstr "" -#: wp-job-manager-functions.php:939 +#: wp-job-manager-functions.php:943 msgid "Full Time" msgstr "" -#: wp-job-manager-functions.php:940 +#: wp-job-manager-functions.php:944 msgid "Part Time" msgstr "" -#: wp-job-manager-functions.php:941 +#: wp-job-manager-functions.php:945 msgid "Contractor" msgstr "" -#: wp-job-manager-functions.php:942 +#: wp-job-manager-functions.php:946 msgid "Temporary" msgstr "" -#: wp-job-manager-functions.php:943 +#: wp-job-manager-functions.php:947 msgid "Intern" msgstr "" -#: wp-job-manager-functions.php:944 +#: wp-job-manager-functions.php:948 msgid "Volunteer" msgstr "" -#: wp-job-manager-functions.php:945 +#: wp-job-manager-functions.php:949 msgid "Per Diem" msgstr "" -#: wp-job-manager-functions.php:946 +#: wp-job-manager-functions.php:950 msgid "Other" msgstr "" -#: wp-job-manager-functions.php:1013 +#: wp-job-manager-functions.php:1017 msgid "Passwords must be at least 8 characters long." msgstr "" -#: wp-job-manager-functions.php:1178 +#: wp-job-manager-functions.php:1182 msgid "Choose a category…" msgstr "" #. translators: %s is the list of allowed file types. -#: wp-job-manager-functions.php:1415 +#: wp-job-manager-functions.php:1419 msgid "Uploaded files need to be one of the following file types: %s" msgstr "" -#: wp-job-manager-functions.php:1711 +#: wp-job-manager-functions.php:1715 msgid "--" msgstr "" -#: wp-job-manager-functions.php:1712 +#: wp-job-manager-functions.php:1716 msgid "Year" msgstr "" -#: wp-job-manager-functions.php:1713 +#: wp-job-manager-functions.php:1717 msgid "Month" msgstr "" -#: wp-job-manager-functions.php:1714 +#: wp-job-manager-functions.php:1718 msgid "Week" msgstr "" -#: wp-job-manager-functions.php:1715 +#: wp-job-manager-functions.php:1719 msgid "Day" msgstr "" -#: wp-job-manager-functions.php:1716 +#: wp-job-manager-functions.php:1720 msgid "Hour" msgstr "" diff --git a/package-lock.json b/package-lock.json index b55e234b3..aa805bdf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wp-job-manager", - "version": "2.2.1", + "version": "2.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wp-job-manager", - "version": "2.2.1", + "version": "2.2.2", "license": "GPL-2.0-or-later", "dependencies": { "select2": "4.0.13" diff --git a/package.json b/package.json index 175e31815..41520fa58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wp-job-manager", - "version": "2.2.1", + "version": "2.2.2", "description": "WP Job Manager", "author": "Automattic", "license": "GPL-2.0-or-later", diff --git a/readme.txt b/readme.txt index 49c945f64..c1325dd31 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: job manager, job listing, job board, job management, job lists, job list, Requires at least: 6.2 Tested up to: 6.4 Requires PHP: 7.2 -Stable tag: 2.2.1 +Stable tag: 2.2.2 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -147,6 +147,11 @@ You can view (and contribute) translations via the [translate.wordpress.org](htt == Changelog == +### 2.2.2 - 2024-02-15 +* Fix issue with rich e-mails on some e-mail providers (#2753) +* Fix: 'featured_first' argument now works when 'show_filters' is set to false. +* Improve checkbox and radio inputs for styled forms + ### 2.2.1 - 2024-01-31 * Fix PHP 7.x error for mixed returned type (#2726) @@ -182,17 +187,3 @@ Fixes: ### 2.1.0 - 2023-11-17 * Fix: Remove public update endpoint and add nonce check (#2642) -### 2.0.0 - 2023-11-17 -* Enhancement: Improve settings descriptions (#2639) -* Enhancement: Add directApply in Google job schema (#2635) -* Enhancement: Add 'Don't show this again' link to dismiss promote job modal in the editor (#2632) -* Enhancement: Add landing pages for Applications and Resumes extensions (#2621) -* Fix: Align actions in notices in the center (#2637) -* Fix: Safeguard array in WP_Job_Manager_Settings::input_capabilities (#2631) -* Fix: Escape menu titles and various admin labels (#2630) -* Fix: Incorrectly duplicated string in settings (#2628) -* Fix: Add array initialization to avoid warning (#2619) -* Fix: Do not check for plugin updates when there are no plugins (#2605) -* Change: Reorganize administration menu (#2621) -* Change: Update naming from Add-ons to Extensions, Marketplace (#2621) - diff --git a/wp-job-manager-autoload.php b/wp-job-manager-autoload.php new file mode 100644 index 000000000..2de528282 --- /dev/null +++ b/wp-job-manager-autoload.php @@ -0,0 +1,86 @@ + directory mappings. + * + * @var array + */ + private static $autoload_map = []; + + /** + * Add the autoloader. + */ + public static function init() { + spl_autoload_register( [ self::class, 'autoload' ] ); + } + + /** + * Register a new plugin with a class prefix and directory to autoload. + * + * @param string $namespace Root namespace. Should start with WP_Job_Manager_. + * @param string $dir Directory to autoload. + */ + public static function register( $namespace, $dir ) { + self::$autoload_map[ $namespace ] = $dir; + } + + /** + * Autoload plugin classes. + * + * @access private + * + * @param string $class_name Class name. + */ + public static function autoload( $class_name ) { + + if ( ! str_starts_with( $class_name, 'WP_Job_Manager' ) || ! str_contains( $class_name, '\\' ) ) { + return; + } + + [ $namespace, $file_name ] = explode( '\\', $class_name, 2 ); + + if ( empty( $namespace ) || empty( $file_name ) || empty( self::$autoload_map[ $namespace ] ) ) { + return; + } + + $root_dir = self::$autoload_map[ $namespace ]; + + $file_name = strtolower( $file_name ); + $dirs = explode( '\\', $file_name ); + $file_name = array_pop( $dirs ); + $file_name = str_replace( '_', '-', $file_name ); + + $file_dir = implode( '/', [ $root_dir, ...$dirs ] ); + + $file_paths = [ + 'class-' . $file_name . '.php', + 'trait-' . $file_name . '.php', + ]; + + foreach ( $file_paths as $file_path ) { + $file_path = $file_dir . '/' . $file_path; + if ( file_exists( $file_path ) ) { + require $file_path; + return; + } + } + + } + +} diff --git a/wp-job-manager.php b/wp-job-manager.php index 8f8539c9e..fbb25f095 100644 --- a/wp-job-manager.php +++ b/wp-job-manager.php @@ -3,7 +3,7 @@ * Plugin Name: WP Job Manager * Plugin URI: https://wpjobmanager.com/ * Description: Manage job listings from the WordPress admin panel, and allow users to post jobs directly to your site. - * Version: 2.2.1 + * Version: 2.2.2 * Author: Automattic * Author URI: https://wpjobmanager.com/ * Requires at least: 6.2 @@ -21,11 +21,15 @@ } // Define constants. -define( 'JOB_MANAGER_VERSION', '2.2.1' ); +define( 'JOB_MANAGER_VERSION', '2.2.2' ); define( 'JOB_MANAGER_PLUGIN_DIR', untrailingslashit( plugin_dir_path( __FILE__ ) ) ); define( 'JOB_MANAGER_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) ) ); define( 'JOB_MANAGER_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); +require_once dirname( __FILE__ ) . '/wp-job-manager-autoload.php'; +WP_Job_Manager_Autoload::init(); +WP_Job_Manager_Autoload::register( 'WP_Job_Manager', JOB_MANAGER_PLUGIN_DIR . '/includes' ); + require_once dirname( __FILE__ ) . '/includes/class-wp-job-manager-dependency-checker.php'; if ( ! WP_Job_Manager_Dependency_Checker::check_dependencies() ) { return; From adcf36ba2e309b0d4108510053bd970b7e5b8eb3 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Thu, 15 Feb 2024 13:53:35 +0200 Subject: [PATCH 32/50] Add job listing apply clicked unique stat. --- assets/js/wpjm-stats.js | 114 ++++++++++++++++++++++++++++++--------- includes/class-stats.php | 23 ++++++-- 2 files changed, 109 insertions(+), 28 deletions(-) diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index 0a1729199..1fdd91dce 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -1,8 +1,11 @@ /* global job_manager_stats */ import domReady from '@wordpress/dom-ready'; +import { createHooks } from '@wordpress/hooks'; ( function () { + window.wpjmStatHooks = window.wpjmStatHooks || createHooks(); + function updateDailyUnique( key ) { const date = new Date(); const expiresAtTimestamp = date.getTime() + 24 * 60 * 60 * 1000; @@ -19,30 +22,23 @@ import domReady from '@wordpress/dom-ready'; return false; } - domReady( function () { - const statsToRecord = []; + function setUniques( uniquesToSet ) { + uniquesToSet.forEach( function( uniqueKey ) { + updateDailyUnique( uniqueKey ); + } ); + } + + window.wpjmLogStats = window.wpjmLogStats || function ( statsToRecord, uniquesToSet ) { const jobStatsSettings = window.job_manager_stats; const ajaxUrl = jobStatsSettings.ajax_url; - const ajaxNonce = jobStatsSettings.ajax_nonce; - const uniquesToSet = []; - const setUniques = function() { - uniquesToSet.forEach( function( uniqueKey ) { - updateDailyUnique( uniqueKey ); - } ); - }; - - jobStatsSettings.stats_to_log?.forEach( function ( statToRecord ) { - const statToRecordKey = statToRecord.name; - const uniqueKey = statToRecord.unique_key || ''; - if ( uniqueKey.length === 0 ) { - statsToRecord.push( statToRecordKey ); - } else { - if ( false === getDailyUnique( uniqueKey ) ) { - uniquesToSet.push( uniqueKey ); - statsToRecord.push( statToRecordKey ); - } - } - } ); + const ajaxNonce = jobStatsSettings.ajax_nonce; + + uniquesToSet = uniquesToSet || []; + statsToRecord = statsToRecord || []; + + if ( statsToRecord.length < 1 ) { + return Promise.resolve(); // Could also be an error. + } const postData = new URLSearchParams( { _ajax_nonce: ajaxNonce, @@ -51,12 +47,82 @@ import domReady from '@wordpress/dom-ready'; stats: statsToRecord.join( ',' ), } ); - fetch( ajaxUrl, { + return fetch( ajaxUrl, { method: 'POST', credentials: 'same-origin', body: postData, } ).finally( function () { - setUniques(); + setUniques( uniquesToSet ); } ); + }; + + function hookStatsForTrigger( statsByTrigger, triggerName ) { + const statsToRecord = []; + const uniquesToSet = []; + const stats = statsByTrigger[triggerName] || []; + const events = {}; + + stats.forEach( function ( statToRecord ) { + const statToRecordKey = statToRecord.name; + const uniqueKey = statToRecord.unique_key || ''; + const isUnique = uniqueKey.length > 0; + + if ( ! isUnique ) { + statsToRecord.push( statToRecordKey ); + } else { + if ( false === getDailyUnique( uniqueKey ) ) { + uniquesToSet.push( uniqueKey ); + statsToRecord.push( statToRecordKey ); + } + } + + if ( statToRecord.element && statToRecord.event ) { + const elemToAttach = document.querySelector( statToRecord.element ); + if ( elemToAttach && ! events[statToRecord.element] ) { + elemToAttach.addEventListener( statToRecord.event, function ( e ) { + if ( isUnique && false !== getDailyUnique( uniqueKey ) ) { + return; + } + + window.wpjmStatHooks.doAction( triggerName ); + } ); + events[statToRecord.element] = true; + } + } + } ); + + // Hook action to call logStats. + window.wpjmStatHooks.addAction( triggerName, 'wpjm-stats', function () { + window.wpjmLogStats( statsToRecord, uniquesToSet ); + }, 10 ); + } + + + domReady( function () { + const jobStatsSettings = window.job_manager_stats; + + const statsByTrigger = jobStatsSettings.stats_to_log?.reduce( function ( accum, statToRecord ) { + const triggerName = statToRecord.trigger || ''; + + if ( triggerName.length < 1 ) { + return accum; + } + + if ( ! accum[triggerName] ) { + accum[triggerName] = []; + } + + accum[triggerName].push( statToRecord ); + + return accum; + }, {} ); + + Object.keys( statsByTrigger ).forEach( function ( triggerName) { + hookStatsForTrigger( statsByTrigger, triggerName ); + } ); + + // Kick things off. + console.log('kick thing off'); + window.wpjmStatHooks.doAction( 'page-load' ); } ); } )(); diff --git a/includes/class-stats.php b/includes/class-stats.php index 460230de3..1ad7ff93f 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -275,7 +275,10 @@ public function frontend_scripts() { \WP_Job_Manager::register_script( 'wp-job-manager-stats', 'js/wpjm-stats.js', - [ 'wp-dom-ready' ], + [ + 'wp-dom-ready', + 'wp-hooks', + ], true ); @@ -303,10 +306,19 @@ private function get_registered_stats() { return (array) apply_filters( 'wpjm_get_registered_stats', [ - 'job_listing_view' => [ + 'job_listing_view' => [ 'log_callback' => [ $this, 'log_stat' ], // Example of overriding how we log this. + 'trigger' => 'page-load', + ], + 'job_listing_view_unique' => [ + 'unique' => true, + 'unique_callback' => [ $this, 'unique_by_post_id' ], + 'trigger' => 'page-load', ], - 'job_listing_view_unique' => [ + 'job_listing_apply_button_clicked' => [ + 'trigger' => 'apply-button-clicked', + 'element' => 'input.application_button', + 'event' => 'click', 'unique' => true, 'unique_callback' => [ $this, 'unique_by_post_id' ], ], @@ -325,7 +337,10 @@ private function get_stats_for_ajax( $post_id = 0 ) { $ajax_stats = []; foreach ( $this->get_registered_stats() as $stat_name => $stat_data ) { $stat_ajax = [ - 'name' => $stat_name, + 'name' => $stat_name, + 'trigger' => $stat_data['trigger'] ?? '', + 'element' => $stat_data['element'] ?? '', + 'event' => $stat_data['event'] ?? '', ]; if ( ! empty( $stat_data['unique'] ) ) { From 611cf06a7ff2196be8bb20cc92bd19b0e0336ce2 Mon Sep 17 00:00:00 2001 From: Fernando Jorge Mota Date: Tue, 20 Feb 2024 19:22:14 -0300 Subject: [PATCH 33/50] Update `feature/stats` branch with latest updates from trunk (#2772) Co-authored-by: Mikey Arce Co-authored-by: Peter Kiss Co-authored-by: WPJM Bot --- .eslintrc.js | 172 +- .github/workflows/php.yml | 34 + .psalm/psalm-baseline.xml | 1200 +++++++++++ .psalm/psalm-loader.php | 18 + composer.json | 4 +- composer.lock | 4076 ++++++++++++++++++++++++++++--------- psalm.xml | 36 + 7 files changed, 4416 insertions(+), 1124 deletions(-) create mode 100644 .psalm/psalm-baseline.xml create mode 100644 .psalm/psalm-loader.php create mode 100644 psalm.xml diff --git a/.eslintrc.js b/.eslintrc.js index 60dbae418..e368d0c1c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,176 +1,18 @@ module.exports = { - root: true, - parser: 'babel-eslint', - extends: [ - 'wordpress', - 'plugin:wordpress/esnext', - 'plugin:react/recommended', - 'plugin:jsx-a11y/recommended', - ], + extends: [ 'plugin:@wordpress/eslint-plugin/recommended', 'prettier' ], env: { browser: true, + jquery: true, + node: true, es6: true, }, - parserOptions: { - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, globals: { wp: true, - window: true, - document: true, - "jQuery": true, - "ajaxurl": true, - "woo_localized_data": true - }, - plugins: [ - 'wordpress', - 'react', - 'jsx-a11y', - ], - settings: { - react: { - pragma: 'wp', - }, }, rules: { - 'array-bracket-spacing': [ 'error', 'always' ], - 'arrow-parens': [ 'error', 'always' ], - 'arrow-spacing': 'error', - 'brace-style': [ 'error', '1tbs' ], - camelcase: [ 'error', { properties: 'never' } ], - 'comma-dangle': [ 'error', 'always-multiline' ], - 'comma-spacing': 'error', - 'comma-style': 'error', - 'computed-property-spacing': [ 'error', 'always' ], - 'dot-notation': 'error', - 'eol-last': 'error', - eqeqeq: 'error', - 'func-call-spacing': 'error', - indent: [ 'error', 'tab', { SwitchCase: 1 } ], - 'jsx-a11y/label-has-for': [ 'error', { - required: 'id', - } ], - 'jsx-a11y/media-has-caption': 'off', - 'jsx-a11y/no-noninteractive-tabindex': 'off', - 'jsx-a11y/role-has-required-aria-props': 'off', - 'jsx-quotes': 'error', - 'key-spacing': 'error', - 'keyword-spacing': 'error', - 'lines-around-comment': 'off', - 'no-alert': 'error', - 'no-bitwise': 'error', - 'no-caller': 'error', - 'no-console': 'error', - 'no-debugger': 'error', - 'no-dupe-args': 'error', - 'no-dupe-keys': 'error', - 'no-duplicate-case': 'error', - 'no-else-return': 'error', - 'no-eval': 'error', - 'no-extra-semi': 'error', - 'no-fallthrough': 'error', - 'no-lonely-if': 'error', - 'no-mixed-operators': 'error', - 'no-mixed-spaces-and-tabs': 'error', - 'no-multiple-empty-lines': [ 'error', { max: 1 } ], - 'no-multi-spaces': 'error', - 'no-multi-str': 'off', - 'no-negated-in-lhs': 'error', - 'no-nested-ternary': 'error', - 'no-redeclare': 'error', - 'no-restricted-syntax': [ - 'error', - { - selector: 'CallExpression[callee.name=/^__|_n|_x$/]:not([arguments.0.type=/^Literal|BinaryExpression$/])', - message: 'Translate function arguments must be string literals.', - }, - { - selector: 'CallExpression[callee.name=/^_n|_x$/]:not([arguments.1.type=/^Literal|BinaryExpression$/])', - message: 'Translate function arguments must be string literals.', - }, - { - selector: 'CallExpression[callee.name=_nx]:not([arguments.2.type=/^Literal|BinaryExpression$/])', - message: 'Translate function arguments must be string literals.', - }, - ], - 'no-shadow': 'error', - 'no-undef': 'error', - 'no-undef-init': 'error', - 'no-unreachable': 'error', - 'no-unsafe-negation': 'error', - 'no-unused-expressions': 'error', - 'no-unused-vars': 'error', - 'no-useless-return': 'error', - 'no-whitespace-before-property': 'error', - 'object-curly-spacing': [ 'error', 'always' ], - 'padded-blocks': [ 'error', 'never' ], - 'quote-props': [ 'error', 'as-needed' ], - 'react/display-name': 'off', - 'react/jsx-curly-spacing': [ 'error', { - when: 'always', - children: true, - } ], - 'react/jsx-equals-spacing': 'error', - 'react/jsx-indent': [ 'error', 'tab' ], - 'react/jsx-indent-props': [ 'error', 'tab' ], - 'react/jsx-key': 'error', - 'react/jsx-tag-spacing': 'error', - 'react/no-children-prop': 'off', - 'react/prop-types': 'off', - semi: 'error', - 'semi-spacing': 'error', - 'space-before-blocks': [ 'error', 'always' ], - 'space-before-function-paren': [ 'error', { - anonymous: 'never', - named: 'never', - asyncArrow: 'always', - } ], - 'space-in-parens': [ 'error', 'always' ], - 'space-infix-ops': [ 'error', { int32Hint: false } ], - 'space-unary-ops': [ 'error', { - overrides: { - '!': true, - yield: true, - }, - } ], - 'valid-jsdoc': [ 'error', { - prefer: { - arg: 'param', - argument: 'param', - extends: 'augments', - returns: 'return', - }, - preferType: { - array: 'Array', - bool: 'boolean', - Boolean: 'boolean', - float: 'number', - Float: 'number', - int: 'number', - integer: 'number', - Integer: 'number', - Number: 'number', - object: 'Object', - String: 'string', - Void: 'void', - }, - requireParamDescription: false, - requireReturn: false, - } ], - 'valid-typeof': 'error', - yoda: 'off', + camelcase: 'warn', + eqeqeq: 'warn', + 'no-console': 'warn', + '@wordpress/no-unused-vars-before-return': 'off', }, - overrides: [ - { - files: 'packages/**/*.js?', - settings: { - react: { - pragma: 'createElement', - }, - }, - }, - ], }; diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index d89455d31..2e51beea4 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -97,3 +97,37 @@ jobs: - name: Run tests run: npm run test-php + psalm: + name: Psalm + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 10 + matrix: + php: [ '7.4', '8.2' ] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Get cached composer directories + uses: actions/cache@v3 + with: + path: ~/.cache/composer/ + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + - uses: actions/cache@v3 + with: + path: vendor/ + key: ${{ runner.os }}-vendor-${{ hashFiles('composer.lock') }} + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + coverage: none + + - name: Install + run: composer self-update && composer install --no-ansi --no-interaction --prefer-dist --no-progress + + - name: Run Psalm + run: ./vendor/bin/psalm diff --git a/.psalm/psalm-baseline.xml b/.psalm/psalm-baseline.xml new file mode 100644 index 000000000..bd2d53724 --- /dev/null +++ b/.psalm/psalm-baseline.xml @@ -0,0 +1,1200 @@ + + + + + $meta_field + + + + + $result + $result + + + + bool + false|void + + + + + $url + $url + + + string|bool + + + + + + + + string + string + + + $args + $email + + + + + false + false + + + + + + __construct + get_attachments + get_cc + get_description + get_enabled_force_value + get_from + get_headers + get_settings + get_subject + is_default_enabled + + + string + + + + + + + + + 0 ? absint( $value ) : '']]> + + + int + + + $value + + + clear_fields + get_form_name + get_posted_file_field + get_posted_multiselect_field + get_posted_term_checklist_field + get_posted_term_multiselect_field + get_posted_term_select_field + get_posted_wp_editor_field + previous_step + set_step + + + $key + + + bool|WP_Error + bool|WP_Error + int + + + COOKIEPATH + COOKIEPATH + COOKIE_DOMAIN + COOKIE_DOMAIN + + + + + + + + $plugin_data + + + + + self + + + + + HOUR_IN_SECONDS + WEEK_IN_SECONDS + + + $add_ons + $add_ons + $categories + + + + + new Notices_Conditions_Checker() + + + check + + + $is_user_notification + + + display_core_setup + init_core_notices + is_admin_on_standard_job_manager_screen + reset_notices + + + array + array + array + array + + + + + + + + WP_Job_manager + WP_Job_manager + + + + + + self + + + $wp_version + + + + + $r + + + bulk_action_handle_approve_job + bulk_action_handle_expire_job + bulk_action_handle_mark_job_filled + bulk_action_handle_mark_job_not_filled + + + array + array + array + array + array + array + array + self + string + string + string + string + + + + is_array( $handled_jobs ) + + + $post + + + + + self + + + + + true + + + void + + + + + + array + array + array + array + array + array + self + + + DAY_IN_SECONDS + + + $title + + + + + settings_group]]> + settings_group]]> + 0 + + + settings_group]]> + settings_group]]> + + + + + + null + + + get_settings + input_capabilities + input_color + input_hidden + input_input + input_multi_checkbox + input_multi_enable_expand + input_number + input_page + input_password + input_radio + input_select + input_textarea + sanitize_capabilities + sanitize_renewal_days + sanitize_submission_duration + sanitize_submission_limit + + + $ignored_attributes + $ignored_attributes + $ignored_attributes + $ignored_attributes + $ignored_attributes + $ignored_placeholder + $ignored_placeholder + $ignored_placeholder + $ignored_placeholder + $ignored_placeholder + $ignored_placeholder + $ignored_placeholder + $ignored_placeholder + $ignored_placeholder + + + stirng|int + + + + + self + + + maybe_output_opt_in_checkbox + opt_in_text + + + + + $taxonomy + $taxonomy + $tt_id + + + array + array + self + string + + + + + \WP_Job_Manager_Post_Types::TAX_LISTING_TYPE + + + input_author + input_checkbox + input_file + input_info + input_multiselect + input_radio + input_select + input_text + input_textarea + + + $post + $post + + + int + self + + + DOING_AUTOSAVE + + + $name + $name + $post + + + + + $search + + + + + + + + + + $step + + + + + $this + opt_in_text()]]> + + + + + $this + maybe_output_opt_in_checkbox()]]> + + + + + current_guest_has_account + + + COOKIEPATH + COOKIE_DOMAIN + DAY_IN_SECONDS + + + + + create + create_token + + + DAY_IN_SECONDS + + + $post + + + + + job_dashboard_job_ids )]]> + + + + + get_daily_stats + + + HOUR_IN_SECONDS + OBJECT_K + + + + + array + + + + + $post_id + + + + + + maybe_log_listing_view + unique_by_post_id + + + bool + + + + + -1 + + + self + + + return; + + + + + add_endpoint + + + array + self + + + EP_ALL + + + $api_class + + + + + self + + + + + clear_expired_transients + + + $meta_id + $object_id + $term_id + $terms + $tt_id + $tt_ids + + + DAY_IN_SECONDS + + + + + class WP_Job_Manager_Category_Walker extends Walker { + + + $object + + + + + $remote_data + + + DAY_IN_SECONDS + DAY_IN_SECONDS + + + + + $role_name + + + + + + + + user_data_exporter + + + array + + + + + array + + + + + $email_class + $job + $job + \WP_Job_Manager_Post_Types::TAX_LISTING_CATEGORY + \WP_Job_Manager_Post_Types::TAX_LISTING_TYPE + + + $email_class + + + global $phpmailer; + + + $email_class + $template_segment + $template_segment + + + clear_deferred_notifications + get_deferred_notification_count + get_deferred_notification_hashes + send_notifications + + + array + + + $email_class::get_enabled_force_value() + + + $email + $sent_to_admin + $sent_to_admin + + + $fields + + + + + ! class_exists( $form_class ) + + + + + getMessage() )]]> + + + + + + $key + + + self + string + string|bool + + + DAY_IN_SECONDS + HOUR_IN_SECONDS + + + + + term_id]]> + + + + + $job_data + $pending_jobs + esc_html__( 'This is where guest user data is stored.', 'wp-job-manager' ), + 'public' => false, + 'show_ui' => false, + 'capability_type' => self::CAP_GUEST_USER, + 'map_meta_cap' => false, + 'publicly_queryable' => false, + 'exclude_from_search' => true, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'supports' => [ 'title', 'custom-fields' ], + 'has_archive' => false, + 'show_in_nav_menus' => false, + 'show_in_rest' => false, + ] + ), + ]]]> + + + false + string + + + null + + + null + + + auth_check_can_edit_job_listings + auth_check_can_edit_others_job_listings + auth_check_can_manage_job_listings + force_classic_block + maybe_generate_geolocation_data + sanitize_employment_type + sanitize_job_type_meta_box_input + sanitize_meta_field_application + sanitize_meta_field_based_on_input_type + sanitize_meta_field_date + set_expirey + + + $allowed + $allowed + $allowed + $meta_id + $meta_id + $meta_key + $meta_key + $meta_key + $meta_key + $meta_key + $post_id + $post_id + $taxonomy + + + WP_REST_Response + array + array + false + string + + + + + + is_numeric( $cats ) + + + + + WP_REST_Response + + + $meta_value + + + + + slug' ) )]]> + + + edit_job + filter_paginate_links + get_job_actions + job_dashboard + + + string + string + string + string|null + string|null + + + term_id]]> + + + + + \WP_Job_Manager_Post_Types::TAX_LISTING_CATEGORY + \WP_Job_Manager_Post_Types::TAX_LISTING_TYPE + + + array + + + $data + + + + + __construct + get_text_domain + + + array + + + false + + + + + void + + + $setting + + + + + Stats::instance() + + + $stats + + + array + bool + bool + + + COOKIEPATH + COOKIEPATH + COOKIE_DOMAIN + COOKIE_DOMAIN + WP_LANG_DIR + + + WP_Job_Manager_Stats + + + + + class WP_Job_Manager_Email_Admin_Expiring_Job extends WP_Job_Manager_Email_Employer_Expiring_Job { + + + + + class WP_Job_Manager_Email_Admin_New_Job extends WP_Job_Manager_Email_Template { + + + WP_Job_Manager_Email_Admin_New_Job + + + + + class WP_Job_Manager_Email_Admin_Updated_Job extends WP_Job_Manager_Email_Template { + + + WP_Job_Manager_Email_Admin_Updated_Job + + + + + class WP_Job_Manager_Email_Employer_Expiring_Job extends WP_Job_Manager_Email_Template { + + + mixed + + + get_from + get_subject + + + + + class WP_Job_Manager_Form_Edit_Job extends WP_Job_Manager_Form_Submit_Job { + + + + + job_id . '&post_type=attachment&fields=ids&numberposts=-1']]> + \WP_Job_Manager_Post_Types::TAX_LISTING_CATEGORY + \WP_Job_Manager_Post_Types::TAX_LISTING_TYPE + false + false + + + class WP_Job_Manager_Form_Submit_Job extends WP_Job_Manager_Form { + + + job_id]]> + + + + + + + done + done_before + instance + localize_job_form_scripts + preview + preview_handler + + + $preview_job + + + ! is_string( $attachment_url ) + empty( $attachment_url ) || ! is_string( $attachment_url ) + + + COOKIEPATH + COOKIEPATH + COOKIE_DOMAIN + COOKIE_DOMAIN + WP_CONTENT_DIR + WP_CONTENT_URL + + + slug]]> + + + $field + $field + + + job_types + + + + + plugin_update_check + request + + + $args + + + + + wp_json_encode( $request_body ), + 'headers' => [ 'Content-Type: application/json' ], + 'timeout' => $timeout, + ]]]> + + + $plugin_versions + + + \stdClass + + + DAY_IN_SECONDS + + + + + bool + bool + + + + + $text + + + string + + + renew_preview_handler + + + array + self + string + + + DAY_IN_SECONDS + JOB_MANAGER_SPL_VERSION + JOB_MANAGER_WCPL_VERSION + + + + + false + + + get_plugin_versions()]]> + + + + + + __call + + + array + array + bool + false|object|array + mixed + object + + + HOUR_IN_SECONDS + + + $item + $plugin_data + + + $active_plugins + $inactive_plugins + $plugin_name + $show_bulk_activate + + + + + MINUTE_IN_SECONDS + + + + + $plugin_data + + + $key + $key + $plugin_section_first + + + + + $verified, + ] + )]]> + + + WP_REST_Response + + + get_items + get_job_data + refresh_status + update_job_status + verify_token + + + $request + + + WP_REST_Response + + + + + $meta_ids + + + self + + + + + HOUR_IN_SECONDS + MINUTE_IN_SECONDS + + + + + + + + boolean + + + + + + WP_Post|false|null + boolean + self + + + + + hint + + + + + MINUTE_IN_SECONDS + + + Redirect_Message + + + + + actions_menu + + + $html + + + + + string + + + + + WP_Job_Manager_Widget_Featured_Jobs + + + + + WP_Job_Manager_Widget_Recent_Jobs + + + + + + + + + caches[self::CACHE_KEY_CSS][$cssKey]]]> + caches[self::CACHE_KEY_SELECTOR][$selectorKey]]]> + + + addAllowedMediaType + addExcludedSelector + addUnprocessableHtmlTag + disableInlineStyleAttributesParsing + disableInvisibleNodeRemoval + disableStyleBlocksParsing + emogrifyBodyContent + enableCssToHtmlMapping + removeAllowedMediaType + removeExcludedSelector + removeUnprocessableHtmlTag + setDebug + + + $type + + + bool + + + parentNode && is_callable([$node->parentNode, 'removeChild'])]]> + + + string[] + string[] + + + int + + + + + false + + + $callback + + + true + + + null|WP_Error + + + + + + enqueue_script_deps + output_opt_in_js + + + $notice + + + array + null|WP_Error + + + DAY_IN_SECONDS + + + WP_Theme + + + $plugin_data + + + + + false + + + + + WP_Die_Exception + + + + + $args + $event + $message + $preempt + $r + $title + $url + + + WP_UnitTestCase + + + WP_Job_Manager_Usage_Tracking_Test + + + + + string + + + name]]> + + + + + $categories + $category + $password + + + + string|DateTimeImmutable + + + -1 + 1 + get_terms( $args ) + get_terms( $args ) + get_terms( $args ) + wp_rand( -1, 1 ) + + + WP_Term[] + WP_Term[] + array + bool + + + + + + + + + + DAY_IN_SECONDS + DAY_IN_SECONDS + LOGGED_IN_COOKIE + + + $file_data_value + + + + + wp_get_theme() + + + + is_array( $args ) + is_array( $class ) + + + WP_CONTENT_DIR + WP_CONTENT_URL + WP_CONTENT_URL + + + $post + + + diff --git a/.psalm/psalm-loader.php b/.psalm/psalm-loader.php new file mode 100644 index 000000000..8fb9e8390 --- /dev/null +++ b/.psalm/psalm-loader.php @@ -0,0 +1,18 @@ +=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + "php": ">=7.1" }, "require-dev": { - "composer/composer": "*", + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", "ext-json": "*", - "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0", - "yoast/phpunit-polyfills": "^1.0" + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "psalm/phar": "^3.11@dev", + "react/promise": "^2" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "branch-alias": { + "dev-master": "2.x-dev" + } }, "autoload": { + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ], "psr-4": { - "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "Amp\\": "lib" } }, "notification-url": "https://packagist.org/downloads/", @@ -49,73 +54,86 @@ ], "authors": [ { - "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcbf", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" ], "support": { - "issues": "https://github.com/PHPCSStandards/composer-installer/issues", - "source": "https://github.com/PHPCSStandards/composer-installer" + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.2" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-02-20T17:52:18+00:00" }, { - "name": "doctrine/instantiator", - "version": "1.5.0", + "name": "amphp/byte-stream", + "version": "v1.8.1", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "url": "https://github.com/amphp/byte-stream.git", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "amphp/amp": "^2", + "php": ">=7.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { + "files": [ + "lib/functions.php" + ], "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "Amp\\ByteStream\\": "lib" } }, "notification-url": "https://packagist.org/downloads/", @@ -124,230 +142,1963 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "http://amphp.org/byte-stream", "keywords": [ - "constructor", - "instantiate" + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" ], "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" + "url": "https://github.com/amphp", + "type": "github" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2021-03-30T17:13:30+00:00" }, { - "name": "myclabs/deep-copy", - "version": "1.11.1", + "name": "composer/pcre", + "version": "3.1.1", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "url": "https://github.com/composer/pcre.git", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "php": "^7.4 || ^8.0" }, "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" + "Composer\\Pcre\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "PCRE", + "preg", + "regex", + "regular expression" ], "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.1" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2023-10-11T07:11:09+00:00" }, { - "name": "nikic/php-parser", - "version": "v4.15.5", + "name": "composer/semver", + "version": "3.4.0", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e" + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/11e2663a5bc9db5d714eedb4277ee300403b4a9e", - "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": ">=7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" }, - "bin": [ - "bin/php-parse" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-main": "3.x-dev" } }, "autoload": { "psr-4": { - "PhpParser\\": "lib/PhpParser" + "Composer\\Semver\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Nikita Popov" + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" } ], - "description": "A PHP parser written in PHP", + "description": "Semver library that offers utilities, version constraint parsing and validation.", "keywords": [ - "parser", - "php" + "semantic", + "semver", + "validation", + "versioning" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.5" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" }, - "time": "2023-05-19T20:20:00+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.3", + "name": "composer/xdebug-handler", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" }, + "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" }, { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "url": "https://github.com/composer", + "type": "github" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" - }, - "time": "2021-07-20T11:28:43+00:00" + "time": "2022-02-25T21:32:43+00:00" }, { - "name": "phar-io/version", - "version": "3.2.1", + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "time": "2023-01-05T11:28:13+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.3" + }, + "time": "2024-01-30T19:34:25+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" + }, + "time": "2021-06-11T22:34:44+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.2", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" + }, + "time": "2022-03-02T22:36:06+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "0.5.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/b58e5a3933e541dc286cc91fc4f3898bbc6f1623", + "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^9.5.26 || ^8.5.31", + "theofidry/php-cs-fixer-config": "^1.0", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/0.5.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2022-12-24T12:35:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v4.4.1", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" + }, + "time": "2024-01-31T06:18:54+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.15.5", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/11e2663a5bc9db5d714eedb4277ee300403b4a9e", + "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.5" + }, + "time": "2023-05-19T20:20:00+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "6d6063cf9464a306ca2a0529705d41312b08500b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/6d6063cf9464a306ca2a0529705d41312b08500b", + "reference": "6d6063cf9464a306ca2a0529705d41312b08500b", + "shasum": "" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^4.13", + "php": "^7.4 || ~8.0.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.3", + "phpstan/phpstan": "^1.10.12", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.8" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.4.1" + }, + "time": "2023-11-10T00:33:47+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "time": "2022-10-25T01:46:02+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.4", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/phpcompatibility-paragonie": "^1.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "time": "2022-10-24T09:00:36+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fad452781b3d774e3337b0c0b245dd8e5a4455fc", + "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.0" + }, + "time": "2024-01-11T11:49:22+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.25.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" + }, + "time": "2024-01-04T17:06:16+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.26", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-03-06T12:58:08+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/17d621b3aff84d0c8b62539e269e87d8d5baa76e", + "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.8" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-05-11T05:14:45+00:00" + }, + { + "name": "psr/container", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } }, - "type": "library", "autoload": { "classmap": [ "src/" @@ -358,243 +2109,448 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", - "role": "Developer" + "role": "lead" } ], - "description": "Library for handling version information and constraints", + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" }, - "time": "2022-02-21T01:04:05+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" }, { - "name": "phpcompatibility/php-compatibility", - "version": "9.3.5", + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", "shasum": "" }, "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + "php": ">=7.3" }, - "conflict": { - "squizlabs/php_codesniffer": "2.6.2" + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" }, "require-dev": { - "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^9.3" }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Wim Godden", - "homepage": "https://github.com/wimg", - "role": "lead" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], - "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", - "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "compatibility", - "phpcs", - "standards" + "diff", + "udiff", + "unidiff", + "unified diff" ], "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibility" + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, - "time": "2019-12-27T09:44:58+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-07T05:35:17+00:00" }, { - "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.2", + "name": "sebastian/environment", + "version": "5.1.5", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { - "phpcompatibility/php-compatibility": "^9.0" + "php": ">=7.3" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7", - "paragonie/random_compat": "dev-master", - "paragonie/sodium_compat": "dev-master" + "phpunit/phpunit": "^9.3" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Wim Godden", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "lead" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", - "homepage": "http://phpcompatibility.com/", + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", "keywords": [ - "compatibility", - "paragonie", - "phpcs", - "polyfill", - "standards", - "static analysis" + "Xdebug", + "environment", + "hhvm" ], "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, - "time": "2022-10-25T01:46:02+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" }, { - "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.4", + "name": "sebastian/exporter", + "version": "4.0.5", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { - "phpcompatibility/php-compatibility": "^9.0", - "phpcompatibility/phpcompatibility-paragonie": "^1.0" + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7" + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Wim Godden", - "role": "lead" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Juliette Reinders Folmer", - "role": "lead" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", - "homepage": "http://phpcompatibility.com/", + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ - "compatibility", - "phpcs", - "standards", - "static analysis", - "wordpress" + "export", + "exporter" ], "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, - "time": "2022-10-24T09:00:36+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "9.2.26", + "name": "sebastian/global-state", + "version": "5.0.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { + "ext-dom": "*", "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "ext-uopz": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -609,20 +2565,17 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", "keywords": [ - "coverage", - "testing", - "xunit" + "global state" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" }, "funding": [ { @@ -630,23 +2583,24 @@ "type": "github" } ], - "time": "2023-03-06T12:58:08+00:00" + "time": "2022-02-14T08:28:10+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "name": "sebastian/lines-of-code", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", "shasum": "" }, "require": { + "nikic/php-parser": "^4.6", "php": ">=7.3" }, "require-dev": { @@ -655,7 +2609,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -674,15 +2628,11 @@ "role": "lead" } ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" }, "funding": [ { @@ -690,36 +2640,34 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2020-11-28T06:42:11+00:00" }, { - "name": "phpunit/php-invoker", - "version": "3.1.1", + "name": "sebastian/object-enumerator", + "version": "4.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "ext-pcntl": "*", "phpunit/phpunit": "^9.3" }, - "suggest": { - "ext-pcntl": "*" - }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -734,18 +2682,14 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" }, "funding": [ { @@ -753,20 +2697,20 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2020-10-26T13:12:34+00:00" }, { - "name": "phpunit/php-text-template", + "name": "sebastian/object-reflector", "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", "shasum": "" }, "require": { @@ -793,18 +2737,14 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" }, "funding": [ { @@ -812,20 +2752,20 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2020-10-26T13:14:26+00:00" }, { - "name": "phpunit/php-timer", - "version": "5.0.3", + "name": "sebastian/recursion-context", + "version": "4.0.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -837,7 +2777,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -852,18 +2792,22 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -871,68 +2815,35 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { - "name": "phpunit/phpunit", - "version": "9.6.8", + "name": "sebastian/resource-operations", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/17d621b3aff84d0c8b62539e269e87d8d5baa76e", - "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", - "sebastian/version": "^3.0.2" + "php": ">=7.3" }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "require-dev": { + "phpunit/phpunit": "^9.0" }, - "bin": [ - "phpunit" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-master": "3.0-dev" } }, "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], "classmap": [ "src/" ] @@ -944,62 +2855,47 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.8" + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" }, "funding": [ - { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" } ], - "time": "2023-05-11T05:14:45+00:00" + "time": "2020-09-28T06:45:17+00:00" }, { - "name": "sebastian/cli-parser", - "version": "1.0.1", + "name": "sebastian/type", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -1018,11 +2914,11 @@ "role": "lead" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -1030,32 +2926,29 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { - "name": "sebastian/code-unit", - "version": "1.0.8", + "name": "sebastian/version", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { "php": ">=7.3" }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1074,11 +2967,11 @@ "role": "lead" } ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" }, "funding": [ { @@ -1086,1024 +2979,1291 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2020-09-28T06:39:44+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "name": "sirbrillig/phpcs-variable-analysis", + "version": "v2.11.16", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", + "reference": "dc5582dc5a93a235557af73e523c389aac9a8e88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/dc5582dc5a93a235557af73e523c389aac9a8e88", + "reference": "dc5582dc5a93a235557af73e523c389aac9a8e88", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "^3.5.6" }, "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", + "phpcsstandards/phpcsdevcs": "^1.1", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", + "sirbrillig/phpcs-import-detection": "^1.1", + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta" }, + "type": "phpcodesniffer-standard", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "VariableAnalysis\\": "VariableAnalysis/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "BSD-2-Clause" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Sam Graham", + "email": "php-codesniffer-variableanalysis@illusori.co.uk" + }, + { + "name": "Payton Swick", + "email": "payton@foolord.com" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "description": "A PHPCS sniff to detect problems with variables.", + "keywords": [ + "phpcs", + "static analysis" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", + "source": "https://github.com/sirbrillig/phpcs-variable-analysis", + "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2023-03-31T16:46:32+00:00" }, { - "name": "sebastian/comparator", - "version": "4.0.8", + "name": "spatie/array-to-xml", + "version": "2.17.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "url": "https://github.com/spatie/array-to-xml.git", + "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", + "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "php": "^7.4|^8.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "mockery/mockery": "^1.2", + "pestphp/pest": "^1.21", + "phpunit/phpunit": "^9.0", + "spatie/pest-plugin-snapshots": "^1.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Spatie\\ArrayToXml\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://freek.dev", + "role": "Developer" } ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", + "description": "Convert an array to xml", + "homepage": "https://github.com/spatie/array-to-xml", "keywords": [ - "comparator", - "compare", - "equality" + "array", + "convert", + "xml" ], "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/spatie/array-to-xml/tree/2.17.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", "type": "github" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2022-12-26T08:22:07+00:00" }, { - "name": "sebastian/complexity", - "version": "2.0.2", + "name": "squizlabs/php_codesniffer", + "version": "3.7.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.x-dev" } }, - "autoload": { - "classmap": [ - "src/" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", + "name": "Greg Sherwood", "role": "lead" } ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-02-22T23:07:41+00:00" }, { - "name": "sebastian/diff", - "version": "4.0.5", + "name": "symfony/console", + "version": "v5.4.35", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "url": "https://github.com/symfony/console.git", + "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/symfony/console/zipball/dbdf6adcb88d5f83790e1efb57ef4074309d3931", + "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" }, + "type": "library", "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" + "cli", + "command-line", + "console", + "terminal" ], "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/symfony/console/tree/v5.4.35" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-01-23T14:28:09+00:00" }, { - "name": "sebastian/environment", - "version": "5.1.5", + "name": "symfony/deprecation-contracts", + "version": "v2.5.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-posix": "*" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { - "classmap": [ - "src/" + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { - "name": "sebastian/exporter", - "version": "4.0.5", + "name": "symfony/filesystem", + "version": "v5.4.35", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "url": "https://github.com/symfony/filesystem.git", + "reference": "5a553607d4ffbfa9c0ab62facadea296c9db7086" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/5a553607d4ffbfa9c0ab62facadea296c9db7086", + "reference": "5a553607d4ffbfa9c0ab62facadea296c9db7086", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/symfony/filesystem/tree/v5.4.35" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-01-23T13:51:25+00:00" }, { - "name": "sebastian/global-state", - "version": "5.0.5", + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=7.1" }, - "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.3" + "provide": { + "ext-ctype": "*" }, "suggest": { - "ext-uopz": "*" + "ext-ctype": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", "keywords": [ - "global state" + "compatibility", + "ctype", + "polyfill", + "portable" ], "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "sebastian/lines-of-code", - "version": "1.0.3", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" + "php": ">=7.1" }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "4.0.4", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=7.1" }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "sebastian/object-reflector", - "version": "2.0.4", + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { - "name": "sebastian/recursion-context", - "version": "4.0.5", + "name": "symfony/polyfill-php73", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "sebastian/resource-operations", - "version": "3.0.3", + "name": "symfony/polyfill-php80", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, + "require": { + "php": ">=7.1" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "sebastian/type", - "version": "3.2.1", + "name": "symfony/service-contracts", + "version": "v2.5.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" }, - "require-dev": { - "phpunit/phpunit": "^9.5" + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2022-05-30T19:17:29+00:00" }, { - "name": "sebastian/version", - "version": "3.0.2", + "name": "symfony/string", + "version": "v5.4.35", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "url": "https://github.com/symfony/string.git", + "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/symfony/string/zipball/c209c4d0559acce1c9a2067612cfb5d35756edc2", + "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" }, + "type": "library", "autoload": { - "classmap": [ - "src/" + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "source": "https://github.com/symfony/string/tree/v5.4.35" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2024-01-23T13:51:25+00:00" }, { - "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.16", + "name": "theseer/tokenizer", + "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "dc5582dc5a93a235557af73e523c389aac9a8e88" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/dc5582dc5a93a235557af73e523c389aac9a8e88", - "reference": "dc5582dc5a93a235557af73e523c389aac9a8e88", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", "shasum": "" }, "require": { - "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5.6" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", - "phpcsstandards/phpcsdevcs": "^1.1", - "phpstan/phpstan": "^1.7", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", - "sirbrillig/phpcs-import-detection": "^1.1", - "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, - "type": "phpcodesniffer-standard", + "type": "library", "autoload": { - "psr-4": { - "VariableAnalysis\\": "VariableAnalysis/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "BSD-3-Clause" ], "authors": [ { - "name": "Sam Graham", - "email": "php-codesniffer-variableanalysis@illusori.co.uk" - }, - { - "name": "Payton Swick", - "email": "payton@foolord.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "A PHPCS sniff to detect problems with variables.", - "keywords": [ - "phpcs", - "static analysis" - ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", - "source": "https://github.com/sirbrillig/phpcs-variable-analysis", - "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" }, - "time": "2023-03-31T16:46:32+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.7.2", + "name": "vimeo/psalm", + "version": "5.13.1", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + "url": "https://github.com/vimeo/psalm.git", + "reference": "086b94371304750d1c673315321a55d15fc59015" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/086b94371304750d1c673315321a55d15fc59015", + "reference": "086b94371304750d1c673315321a55d15fc59015", "shasum": "" }, "require": { + "amphp/amp": "^2.4.2", + "amphp/byte-stream": "^1.5", + "composer-runtime-api": "^2", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^2.0 || ^3.0", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", "ext-simplexml": "*", "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" + "felixfbecker/advanced-json-rpc": "^3.1", + "felixfbecker/language-server-protocol": "^1.5.2", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "nikic/php-parser": "^4.14", + "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0", + "sebastian/diff": "^4.0 || ^5.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", + "symfony/console": "^4.1.6 || ^5.0 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0" + }, + "provide": { + "psalm/psalm": "self.version" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "amphp/phpunit-util": "^2.0", + "bamarni/composer-bin-plugin": "^1.4", + "brianium/paratest": "^6.9", + "ext-curl": "*", + "mockery/mockery": "^1.5", + "nunomaduro/mock-final-classes": "^1.1", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpdoc-parser": "^1.6", + "phpunit/phpunit": "^9.6", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/process": "^4.4 || ^5.0 || ^6.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" }, "bin": [ - "bin/phpcs", - "bin/phpcbf" + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalter" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "5.x-dev", + "dev-4.x": "4.x-dev", + "dev-3.x": "3.x-dev", + "dev-2.x": "2.x-dev", + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psalm\\": "src/Psalm/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Greg Sherwood", - "role": "lead" + "name": "Matthew Brown" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "description": "A static analysis tool for finding errors in PHP applications", "keywords": [ - "phpcs", - "standards", + "code", + "inspection", + "php", "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm/tree/5.13.1" }, - "time": "2023-02-22T23:07:41+00:00" + "time": "2023-06-27T16:39:49+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.2.1", + "name": "webmozart/assert", + "version": "1.11.0", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", + "ext-ctype": "*", "php": "^7.2 || ^8.0" }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Webmozart\\Assert\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2022-06-03T18:03:27+00:00" }, { "name": "wp-coding-standards/wpcs", diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 000000000..1e8dbf877 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + From afef27a2eceb03fb094837bb091236467af91b2a Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Fri, 23 Feb 2024 15:19:19 +0100 Subject: [PATCH 34/50] Add Job Overlay (#2762) --- assets/css/job-dashboard.scss | 51 ++++++--- assets/css/job-overlay.scss | 79 ++++++++++++++ assets/css/ui.dialog.scss | 33 +++--- assets/css/ui.elements.scss | 62 ++++++++--- assets/css/ui.neutral.scss | 4 +- assets/js/job-dashboard.js | 60 +++++++++- includes/class-job-dashboard-shortcode.php | 121 +++++++++++++++++++-- includes/class-job-overlay.php | 89 +++++++++++++++ includes/class-wp-job-manager.php | 5 +- includes/ui/class-modal-dialog.php | 4 +- includes/ui/class-ui-elements.php | 3 +- templates/job-dashboard-overlay.php | 82 ++++++++++++++ templates/job-dashboard.php | 21 +--- 13 files changed, 537 insertions(+), 77 deletions(-) create mode 100644 assets/css/job-overlay.scss create mode 100644 includes/class-job-overlay.php create mode 100644 templates/job-dashboard-overlay.php diff --git a/assets/css/job-dashboard.scss b/assets/css/job-dashboard.scss index ac9dd77d3..b9959ac1f 100644 --- a/assets/css/job-dashboard.scss +++ b/assets/css/job-dashboard.scss @@ -1,5 +1,6 @@ -@import "mixins"; +@import 'mixins'; +@import 'job-overlay'; #job-manager-job-dashboard { @@ -28,6 +29,10 @@ gap: var(--jm-ui-space-sm); } +.jm-dashboard__filters { + font-size: var(--jm-ui-button-font-size); +} + .jm-dashboard-job { border: var(--jm-ui-border-size) solid var(--jm-ui-border-light); } @@ -41,21 +46,40 @@ font-size: var(--jm-ui-font-size-s); } -.jm-dashboard-job-column.job_title { +.jm-dashboard .job_title { flex: 1 1 200%; +} + +.jm-dashboard .job-status { + text-transform: uppercase; + font-weight: 500; + font-size: var(--jm-ui-font-size-s); + line-height: var(--jm-ui-icon-size-s); + color: fadeCurrentColor( 70% ); + margin: var(--jm-ui-space-xxs) 0; + + .jm-ui-row { + gap: var(--jm-ui-space-xxxs); + } - .job-status { - text-transform: uppercase; - font-weight: 500; - font-size: var(--jm-ui-font-size-s); - line-height: var(--jm-ui-icon-size-s); - color: fadeCurrentColor( 80% ); - - .jm-ui-icon { - width: var(--jm-ui-icon-size-s); - height: var(--jm-ui-icon-size-s); - } + .jm-separator { + color: fadeCurrentColor( 20% ); } + + .jm-ui-icon { + width: var(--jm-ui-icon-size-s); + height: var(--jm-ui-icon-size-s); + } + + .job-status-rejected { + color: var(--jm-ui-error-color); + } +} + +.jm-dashboard img.company_logo { + width: var(--jm-ui-icon-size); + height: var(--jm-ui-icon-size); + object-fit: contain; } @@ -67,6 +91,7 @@ .jm-dashboard-job-column a.job-title { font-weight: 600; font-size: var(--jm-ui-font-size); + line-height: 1.2; text-decoration: unset; } diff --git a/assets/css/job-overlay.scss b/assets/css/job-overlay.scss new file mode 100644 index 000000000..e68c7ad65 --- /dev/null +++ b/assets/css/job-overlay.scss @@ -0,0 +1,79 @@ + +.jm-dashboard__overlay { + width: var(--wp--style--global--wide-size, 1200px); + height: 80vh; + --jm-dialog-padding: var(--jm-ui-space-ml); + + .jm-ui-spinner { + --size: 64px; + } + + .jm-dialog-close { + top: var(--jm-dialog-padding); + right: var(--jm-dialog-padding); + color: inherit; + } +} + +.jm-job-overlay { + align-self: stretch; + flex: 1; + padding: var(--jm-ui-space-ml); + display: flex; + flex-direction: column; + gap: var(--jm-ui-space-sm); + animation: jm-fade-in 200ms ease-in; + + font-size: var(--jm-ui-font-size-m); +} + +.jm-job-overlay-header { + border-bottom: var(--jm-ui-border-size) solid var(--jm-ui-border-faint); + display: flex; + justify-content: space-between; + gap: var(--jm-ui-space-m); + padding-right: calc( var(--jm-ui-icon-size) + var(--jm-ui-space-s) ); + padding-bottom: var(--jm-ui-space-sm); + flex: 0 0 min-content; + + .job_title { + font-size: var(--jm-ui-heading-font-size); + line-height: 1.2; + font-weight: 600; + } +} +.jm-job-overlay-content { + flex: 1 1 auto; + overflow: auto; +} + +.jm-job-overlay-footer { + border-top: var(--jm-ui-border-size) solid var(--jm-ui-border-faint); + padding-top: var(--jm-dialog-padding); + + .jm-ui-actions-row { + gap: var(--jm-ui-space-sm); + } + +} + +.jm-job-overlay-details-box { + background: var(--jm-ui-faint-color); + padding: var(--jm-ui-space-m); +} + +@media (max-width: 600px) { + .jm-dashboard__overlay + { + height: 100%; + } +} + +@keyframes jm-job-overlay-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/assets/css/ui.dialog.scss b/assets/css/ui.dialog.scss index 6ba949e3f..c3f1610fa 100644 --- a/assets/css/ui.dialog.scss +++ b/assets/css/ui.dialog.scss @@ -22,7 +22,7 @@ .jm-dialog { font-size: var(--jm-ui-font-size); - --jm-local-notice-padding: var(--jm-ui-space-l); + --jm-dialog-padding: var(--jm-ui-space-l); } .jm-dialog .jm-notice { @@ -30,7 +30,7 @@ border: unset; width: 100%; min-width: unset; - padding: var(--jm-local-notice-padding); + padding: var(--jm-dialog-padding); .jm-notice__details { align-self: stretch; @@ -38,15 +38,12 @@ } .jm-dialog::backdrop { - background-color: unset; + background-color: transparent; + backdrop-filter: blur(4px); } .jm-dialog-modal { position: relative; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; width: var(--wp--style--global--content-size, 640px); max-width: calc(100% - var(--jm-ui-space-s) * 2); max-height: 100%; @@ -57,6 +54,17 @@ color: var(--jm-ui-text-color, #1a1a1a); box-shadow: var(--jm-ui-shadow-modal); + overscroll-behavior: contain; + +} + +.jm-dialog-modal-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; } .jm-dialog-backdrop { @@ -67,15 +75,14 @@ bottom: 0; z-index: -1; background-color: rgb(0 0 0 / 0.1); - backdrop-filter: blur(4px); + } .jm-dialog-close { position: absolute; z-index: 1; - padding: var(--jm-ui-space-xxxs); - top: calc(var(--jm-local-notice-padding) - var(--jm-ui-space-xxxs)); - right: calc(var(--jm-local-notice-padding) - var(--jm-ui-space-xxxs) - 8px); + top: calc(var(--jm-dialog-padding) - var(--jm-ui-space-xs)); + right: calc(var(--jm-dialog-padding) - var(--jm-ui-space-xs) - 8px); cursor: pointer; opacity: 0.7; @@ -89,7 +96,7 @@ animation: jm-dialog-open 0.2s cubic-bezier(.08, .6, .5, .98); } -.jm-dialog[open]::backdrop { +.jm-dialog[open] .jm-dialog-backdrop { animation: jm-dialog-backdrop-fade-in 0.2s cubic-bezier(.08, .6, .5, .98); } @@ -159,7 +166,7 @@ } .jm-dialog { - --jm-local-notice-padding: var(--jm-ui-space-sm); + --jm-dialog-padding: var(--jm-ui-space-sm); } .jm-dialog .jm-form, .jm-dialog .jm-notice { diff --git a/assets/css/ui.elements.scss b/assets/css/ui.elements.scss index bdd3b9ebb..eaff78dff 100644 --- a/assets/css/ui.elements.scss +++ b/assets/css/ui.elements.scss @@ -1,4 +1,16 @@ +.jm-ui-row { + display: flex; + gap: var(--jm-ui-space-xs); + align-items: center; +} + +.jm-ui-col { + display: flex; + flex-direction: column; + gap: var(--jm-ui-space-xs); +} + .jm-ui-button, .jm-ui-button--outline, .jm-ui-button--link, @@ -133,35 +145,34 @@ width: var(--jm-ui-icon-size); height: var(--jm-ui-icon-size); mask-size: 100% 100%; + background-color: currentColor; &[data-icon=check] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cg stroke-width=\'1.5\'%3e%3cpath stroke=\'%231E1E1E\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath stroke=\'black\' d=\'m15.96 8.18-5.34 7.18-3.1-2.3\'/%3e%3c/g%3e%3c/svg%3e'); + mask-image: var(--jm-ui-svg-check); } &[data-icon=check-circle] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cg stroke-width=\'1.5\'%3e%3cpath stroke=\'%231E1E1E\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath stroke=\'black\' d=\'m15.96 8.18-5.34 7.18-3.1-2.3\'/%3e%3c/g%3e%3c/svg%3e'); - } - - &[data-icon=check] { - background-color: currentColor; - mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='m17.93 7.98-7.2 9.68-4.52-3.36.9-1.2 3.31 2.46 6.3-8.48 1.2.9Z' clip-rule='evenodd'/%3e%3c/svg%3e"); + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cg stroke-width='1.5'%3e%3cpath stroke='black' d='M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z'/%3e%3cpath stroke='black' d='m15.96 8.18-5.34 7.18-3.1-2.3'/%3e%3c/g%3e%3c/svg%3e"); } &[data-icon=star] { - background-color: currentColor; mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' d='M11.78 4.45a.25.25 0 0 1 .44 0l2.07 4.2c.04.07.11.12.2.13l4.62.68c.2.02.28.28.14.42l-3.35 3.26a.25.25 0 0 0-.07.23l.79 4.6c.03.2-.18.36-.37.27l-4.13-2.18a.25.25 0 0 0-.24 0l-4.13 2.18a.25.25 0 0 1-.37-.27l.8-4.6a.25.25 0 0 0-.08-.23L4.75 9.88a.25.25 0 0 1 .14-.42l4.63-.68a.25.25 0 0 0 .19-.13l2.07-4.2Z'/%3e%3c/svg%3e"); } &[data-icon=info] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3ccircle cx=\'12\' cy=\'12\' r=\'8\' stroke=\'black\' stroke-width=\'1.5\'/%3e%3cpath fill=\'black\' d=\'M11 11h2v6h-2zm0-4h2v2h-2z\'/%3e%3c/svg%3e'); + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3ccircle cx='12' cy='12' r='8' stroke='black' stroke-width='1.5'/%3e%3cpath fill='black' d='M11 11h2v6h-2zm0-4h2v2h-2z'/%3e%3c/svg%3e"); } &[data-icon=alert] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cpath stroke=\'%231E1E1E\' stroke-width=\'1.5\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath fill=\'%231E1E1E\' d=\'M13 7h-2v6h2V7Zm0 8h-2v2h2v-2Z\'/%3e%3c/svg%3e'); + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='black' stroke-width='1.5' d='M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z'/%3e%3cpath fill='black' d='M13 7h-2v6h2V7Zm0 8h-2v2h2v-2Z'/%3e%3c/svg%3e"); + } + + &[data-icon=location] { + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' d='M12 9c-.8 0-1.5.7-1.5 1.5S11.2 12 12 12s1.5-.7 1.5-1.5S12.8 9 12 9Zm0-5c-3.6 0-6.5 2.8-6.5 6.2 0 .8.3 1.8.9 3.1.5 1.1 1.2 2.3 2 3.6.7 1 3 3.8 3.2 3.9l.4.5.4-.5c.2-.2 2.6-2.9 3.2-3.9.8-1.2 1.5-2.5 2-3.6.6-1.3.9-2.3.9-3.1C18.5 6.8 15.6 4 12 4Zm4.3 8.7c-.5 1-1.1 2.2-1.9 3.4-.5.7-1.7 2.2-2.4 3-.7-.8-1.9-2.3-2.4-3-.8-1.2-1.4-2.3-1.9-3.3-.6-1.4-.7-2.2-.7-2.5 0-2.6 2.2-4.7 5-4.7s5 2.1 5 4.7c0 .2-.1 1-.7 2.4Z'/%3e%3c/svg%3e"); + } + + &[data-icon=edit] { + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' d='m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z'/%3e%3c/svg%3e"); } } @@ -175,6 +186,9 @@ .jm-ui-action-menu__open-button { cursor: pointer; + &::-webkit-details-marker { + display: none; + } .jm-ui-button__icon { mask-image: var(--jm-ui-svg-ellipsis-v); @@ -198,3 +212,23 @@ display: flex; flex-direction: column; } + +.jm-ui-spinner { + display: inline-block; + --size: 24px; + width: var(--size); + height: var(--size); + border: 2px solid currentColor; + border-bottom-color: transparent; + border-radius: 50%; + animation: jm-ui-spinner-spin 1s linear infinite; +} + +@keyframes jm-ui-spinner-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/assets/css/ui.neutral.scss b/assets/css/ui.neutral.scss index 3cb3fafcc..ec95b7933 100644 --- a/assets/css/ui.neutral.scss +++ b/assets/css/ui.neutral.scss @@ -1,6 +1,7 @@ :root { --jm-ui-border-light: #{fadeCurrentColor(15%)}; + --jm-ui-border-faint: #{fadeCurrentColor(5%)}; --jm-ui-border-strong: currentColor; --jm-ui-border-size: 1px; @@ -12,6 +13,7 @@ --jm-ui-radius-2x: calc(2 * var(--jm-ui-radius)); --jm-ui-radius-4x: calc(4 * var(--jm-ui-radius)); + --jm-ui-faint-color: #{fadeCurrentColor(2.5%)}; --jm-ui-accent-color: inherit; --jm-ui-danger-color: #cc1818; --jm-ui-error-color: #cc1818; @@ -54,7 +56,7 @@ --jm-ui-button-font-size: 14px; --jm-ui-icon-size: 24px; --jm-ui-icon-size-m: 20px; - --jm-ui-icon-size-s: 18px; + --jm-ui-icon-size-s: 14px; --jm-ui-shadow-modal: 0 0.7px 1px 0 rgba(0, 0, 0, 0.15), 0 2.7px 3.8px -0.2px rgba(0, 0, 0, 0.15), diff --git a/assets/js/job-dashboard.js b/assets/js/job-dashboard.js index e87642bbc..632c05250 100644 --- a/assets/js/job-dashboard.js +++ b/assets/js/job-dashboard.js @@ -1,8 +1,58 @@ /* global job_manager_job_dashboard */ -jQuery(document).ready(function($) { - $('.job-dashboard-action-delete').click(function() { - return window.confirm( job_manager_job_dashboard.i18n_confirm_delete ); - }); +import domReady from '@wordpress/dom-ready'; -}); \ No newline at end of file +// eslint-disable-next-line camelcase +const { i18nConfirmDelete, overlayEndpoint } = job_manager_job_dashboard; + +function setupEvents( root ) { + root + .querySelectorAll( '.job-dashboard-action-delete' ) + .forEach( el => el.addEventListener( 'click', confirmDelete ) ); +} + +function confirmDelete( event ) { + // eslint-disable-next-line no-alert + if ( ! window.confirm( i18nConfirmDelete ) ) { + event.preventDefault(); + } +} + +async function showOverlay( event ) { + const overlayDialog = document.getElementById( 'jmDashboardOverlay' ); + + if ( ! overlayDialog ) { + return true; + } + + event.preventDefault(); + overlayDialog.showModal(); + + const contentElement = overlayDialog.querySelector( '.jm-dialog-modal-content' ); + contentElement.innerHTML = ''; + + try { + const response = await fetch( `${ overlayEndpoint }?job_id=${ this.dataset.jobId }` ); + + if ( ! response.ok ) { + throw new Error( response.statusText ); + } + + const { data } = await response.json(); + + contentElement.innerHTML = data; + } + catch ( error ) { + contentElement.innerHTML = `
${ error.message }
`; + } + + setupEvents( contentElement ); +} + +domReady( () => { + setupEvents( document ); + + document + .querySelectorAll( '.jm-dashboard-job .job-title' ) + .forEach( el => el.addEventListener( 'click', showOverlay ) ); +} ); diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php index bb3bbb38c..078ea8ffa 100644 --- a/includes/class-job-dashboard-shortcode.php +++ b/includes/class-job-dashboard-shortcode.php @@ -14,6 +14,8 @@ exit; } +require_once __DIR__ . '/class-job-overlay.php'; + /** * Job Dashboard Shortcode. * @@ -51,8 +53,13 @@ public function __construct() { add_filter( 'paginate_links', [ $this, 'filter_paginate_links' ], 10, 1 ); - add_action( 'job_manager_job_dashboard_column_date', [ $this, 'job_dashboard_date_column_expires' ] ); - add_action( 'job_manager_job_dashboard_column_job_title', [ $this, 'job_dashboard_title_column_status' ] ); + add_action( 'job_manager_job_dashboard_column_date', [ self::class, 'the_date' ] ); + add_action( 'job_manager_job_dashboard_column_date', [ self::class, 'the_expiration_date' ] ); + + add_action( 'job_manager_job_dashboard_column_job_title', [ self::class, 'the_job_title' ], 10 ); + add_action( 'job_manager_job_dashboard_column_job_title', [ self::class, 'the_status' ], 12 ); + + Job_Overlay::instance(); } /** @@ -445,7 +452,7 @@ public function handle_actions() { * * @output string */ - public function job_dashboard_date_column_expires( $job ) { + public static function the_expiration_date( $job ) { $expiration = \WP_Job_Manager_Post_Types::instance()->get_job_expiration( $job ); if ( 'publish' === $job->post_status && ! empty( $expiration ) ) { @@ -455,6 +462,50 @@ public function job_dashboard_date_column_expires( $job ) { } } + /** + * Show location. + * + * @param \WP_Post $job + * + * @output string + */ + public static function the_location( $job ) { + $location = get_the_job_location( $job ); + + if ( ! $location ) { + return; + } + + ?> +
+ + +
+ ID ) . '" href="' . esc_url( get_permalink( $job->ID ) ) . '">' . esc_html( get_the_title( $job ) ?? $job->ID ) . ''; + } + + /** + * Show job title. + * + * @param \WP_Post $job + * + * @output string + */ + public static function the_date( $job ) { + echo '
' . esc_html( wp_date( apply_filters( 'job_manager_get_dashboard_date_format', 'M d, Y' ), get_post_datetime( $job )->getTimestamp() ) ) . '
'; + } + /** * Add job status to the job dashboard title column. * @@ -462,28 +513,55 @@ public function job_dashboard_date_column_expires( $job ) { * * @output string */ - public function job_dashboard_title_column_status( $job ) { + public static function the_status( $job ) { - echo '
'; + echo '
'; $status = []; if ( is_position_filled( $job ) ) { - $status[] = '' . esc_html__( 'Filled', 'wp-job-manager' ) . ''; + $status[] = '' + . UI_Elements::icon( 'check' ) + . esc_html__( 'Filled', 'wp-job-manager' ) . ''; } if ( is_position_featured( $job ) && 'publish' === $job->post_status ) { - $status[] = '' . esc_html__( 'Featured', 'wp-job-manager' ) . ''; + $status[] = '' + . UI_Elements::icon( 'star' ) + . esc_html__( 'Featured', 'wp-job-manager' ) . ''; } - $status[] = '' . esc_html( get_the_job_status( $job ) ) . ''; + $status_icon = [ + 'pending' => 'alert', + 'pending_payment' => 'alert', + 'draft' => 'edit', + 'expired' => 'alert', + ][ $job->post_status ] ?? null; + + $status[] = '' + . ( $status_icon ? UI_Elements::icon( $status_icon ) : '' ) + . esc_html( get_the_job_status( $job ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. - echo implode( ', ', $status ); + echo implode( '|', $status ); echo '
'; } + /** + * Get the URL of the [job_dashboard] page. + * + * @return string + */ + public static function get_job_dashboard_page_url() { + $page_id = get_option( 'job_manager_job_dashboard_page_id' ); + if ( $page_id ) { + return get_permalink( $page_id ); + } else { + return home_url( '/' ); + } + } + /** * Check if a job is listed on the current user's job dashboard page. * @@ -491,7 +569,7 @@ public function job_dashboard_title_column_status( $job ) { * * @return bool */ - private function is_job_available_on_dashboard( \WP_Post $job ) { + public function is_job_available_on_dashboard( \WP_Post $job ) { // Check cache of currently displayed job dashboard IDs first to avoid lots of queries. if ( isset( $this->job_dashboard_job_ids ) && in_array( (int) $job->ID, $this->job_dashboard_job_ids, true ) ) { return true; @@ -544,4 +622,27 @@ private function get_job_dashboard_query_args( $args = [] ) { return apply_filters( 'job_manager_get_dashboard_jobs_args', $args ); } + /** + * Check if the current user has multiple companies. + * + * @return bool + */ + private function user_has_multiple_companies() { + global $wpdb; + + $user_id = get_current_user_id(); + + $cache_key = 'wpjm_user_' . $user_id . '_companies_count'; + $companies_count = get_transient( $cache_key ); + + if ( false === $companies_count ) { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Query is cached. + $companies_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT meta_value) FROM {$wpdb->postmeta} WHERE meta_key = '_company_name' AND `meta_value` != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_author = %d AND post_type = 'job_listing')", $user_id ) ); + + set_transient( $cache_key, $companies_count, 24 * HOUR_IN_SECONDS ); + } + + return $companies_count > 1; + } + } diff --git a/includes/class-job-overlay.php b/includes/class-job-overlay.php new file mode 100644 index 000000000..9e787fc3e --- /dev/null +++ b/includes/class-job-overlay.php @@ -0,0 +1,89 @@ +is_job_available_on_dashboard( $job ) ) { + wp_send_json_error( + Notice::error( + [ + 'message' => __( 'Invalid Job ID.', 'wp-job-manager' ), + 'classes' => [ 'type-dialog' ], + ] + ) + ); + + return; + } + + $job_actions = $shortcode->get_job_actions( $job ); + + ob_start(); + + get_job_manager_template( + 'job-dashboard-overlay.php', + [ + 'job' => $job, + 'job_actions' => $job_actions, + ] + ); + + $content = ob_get_clean(); + + wp_send_json_success( $content ); + + } + + /** + * Output the modal element. + */ + public function output_modal_element() { + $overlay = new Modal_Dialog( + [ + 'id' => 'jmDashboardOverlay', + 'class' => 'jm-dashboard__overlay', + ] + ); + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in Modal_Dialog class. + echo $overlay->render( '' ); + } +} diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index 123cba7aa..8603b40ec 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -540,7 +540,7 @@ public function frontend_scripts() { wp_register_script( 'jquery-deserialize', JOB_MANAGER_PLUGIN_URL . '/assets/lib/jquery-deserialize/jquery.deserialize.js', [ 'jquery' ], '1.2.1', true ); self::register_script( 'wp-job-manager-ajax-filters', 'js/ajax-filters.js', $ajax_filter_deps, true ); - self::register_script( 'wp-job-manager-job-dashboard', 'js/job-dashboard.js', [ 'jquery' ], true ); + self::register_script( 'wp-job-manager-job-dashboard', 'js/job-dashboard.js', null, true ); self::register_script( 'wp-job-manager-job-application', 'js/job-application.js', [ 'jquery' ], true ); self::register_script( 'wp-job-manager-job-submission', 'js/job-submission.js', [ 'jquery' ], true ); wp_localize_script( 'wp-job-manager-ajax-filters', 'job_manager_ajax_filters', $ajax_data ); @@ -575,7 +575,8 @@ public function frontend_scripts() { 'wp-job-manager-job-dashboard', 'job_manager_job_dashboard', [ - 'i18n_confirm_delete' => esc_html__( 'Are you sure you want to delete this listing?', 'wp-job-manager' ), + 'i18nConfirmDelete' => esc_html__( 'Are you sure you want to delete this listing?', 'wp-job-manager' ), + 'overlayEndpoint' => WP_Job_Manager_Ajax::get_endpoint( 'job_dashboard_overlay' ), ] ); diff --git a/includes/ui/class-modal-dialog.php b/includes/ui/class-modal-dialog.php index 7bb28ffc9..f51bac49c 100644 --- a/includes/ui/class-modal-dialog.php +++ b/includes/ui/class-modal-dialog.php @@ -88,8 +88,8 @@ public function render( $content ) {
- {$content} - +
{$content}
+
diff --git a/includes/ui/class-ui-elements.php b/includes/ui/class-ui-elements.php index 5c2bdb7bb..9af2220d5 100644 --- a/includes/ui/class-ui-elements.php +++ b/includes/ui/class-ui-elements.php @@ -81,6 +81,7 @@ public static function button( $args, $class ) { 'label' => '', 'url' => '', 'onclick' => '', + 'class' => '', ] ); @@ -89,7 +90,7 @@ public static function button( $args, $class ) { } $attrs = [ - 'class' => $class, + 'class' => join( ' ', [ $class, $args['class'] ] ), 'href' => esc_url( $args['url'] ), ]; diff --git a/templates/job-dashboard-overlay.php b/templates/job-dashboard-overlay.php new file mode 100644 index 000000000..b6e9d9ae9 --- /dev/null +++ b/templates/job-dashboard-overlay.php @@ -0,0 +1,82 @@ + + +
+
+
+ +
ID ); ?>
+ +
+
+ get_permalink( $job->ID ), + 'label' => __( 'View', 'wp-job-manager' ), + ], 'jm-ui-button--link' ); + ?> +
+
+
+
+ +
+
+ +
+ + +
+
+
+ +
+
+ +
+
+ +
diff --git a/templates/job-dashboard.php b/templates/job-dashboard.php index 77918ebcb..ea5e806b1 100644 --- a/templates/job-dashboard.php +++ b/templates/job-dashboard.php @@ -21,6 +21,7 @@ * @var string $search_input Search input. */ +use WP_Job_Manager\Job_Overlay; use WP_Job_Manager\UI\Notice; use WP_Job_Manager\UI\UI_Elements; @@ -77,21 +78,7 @@ class="jm-dashboard-job-column "> $column ) : ?>
- ID ) ) . '">' . ( wpjm_get_the_job_title( $job ) ?? $job->ID ) . ''; - break; - case 'date': - echo '
' . esc_html( wp_date( apply_filters( 'job_manager_get_dashboard_date_format', 'M d, Y' ), get_post_datetime( $job )->getTimestamp() ) ) . '
'; - - break; - } - - do_action( 'job_manager_job_dashboard_column_' . $key, $job ); - - ?> +
@@ -101,7 +88,7 @@ class="jm-dashboard-job-column ">ID ] as $action => $value ) { $action_url = add_query_arg( [ 'action' => $action, - 'job_id' => $job->ID + 'job_id' => $job->ID, ] ); if ( $value['nonce'] ) { $action_url = wp_nonce_url( $action_url, $value['nonce'] ); @@ -119,4 +106,6 @@ class="jm-dashboard-job-column ">
$max_num_pages ] ); ?> + + output_modal_element(); ?>
From 8533d14b0ac75238da0b80dab7a7de29e49e9119 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Fri, 23 Feb 2024 17:02:52 +0100 Subject: [PATCH 35/50] Add company column to dashboard when user has multiple companies (#2769) --- .psalm/psalm-baseline.xml | 25 ----------------- assets/css/job-dashboard.scss | 25 ++++++++++++++--- assets/css/job-overlay.scss | 1 + includes/class-job-dashboard-shortcode.php | 31 +++++++++++++++++++--- includes/class-stats.php | 6 ++--- includes/class-wp-job-manager.php | 2 +- 6 files changed, 54 insertions(+), 36 deletions(-) diff --git a/.psalm/psalm-baseline.xml b/.psalm/psalm-baseline.xml index dd2e4b2b4..83648ff8b 100644 --- a/.psalm/psalm-baseline.xml +++ b/.psalm/psalm-baseline.xml @@ -55,11 +55,6 @@ COOKIE_DOMAIN - - - - - new Notices_Conditions_Checker() @@ -268,12 +263,6 @@ slug' ) )]]> - - - - - job_dashboard_job_ids )]]> - term_id]]> @@ -345,10 +334,6 @@ job_id]]> - - - - ! is_string( $attachment_url ) empty( $attachment_url ) || ! is_string( $attachment_url ) @@ -396,9 +381,6 @@ get_plugin_versions()]]> - - - $item $plugin_data @@ -455,9 +437,6 @@ null|WP_Error - - - @@ -501,10 +480,6 @@ array bool - - - - diff --git a/assets/css/job-dashboard.scss b/assets/css/job-dashboard.scss index b9959ac1f..589c89c5b 100644 --- a/assets/css/job-dashboard.scss +++ b/assets/css/job-dashboard.scss @@ -2,8 +2,10 @@ @import 'mixins'; @import 'job-overlay'; -#job-manager-job-dashboard { +.jm-dashboard { + container-type: inline-size; + --jm-dashboard-company-logo-size: calc(var(--jm-ui-icon-size) + 2 * var(--jm-ui-space-xs)); } .jm-dashboard-job, .jm-dashboard-header { @@ -50,6 +52,14 @@ flex: 1 1 200%; } +.jm-dashboard-job-column.company { + + flex: 0 0 var(--jm-dashboard-company-logo-size); + .jm-dashboard-header & { + visibility: hidden; + } +} + .jm-dashboard .job-status { text-transform: uppercase; font-weight: 500; @@ -77,8 +87,8 @@ } .jm-dashboard img.company_logo { - width: var(--jm-ui-icon-size); - height: var(--jm-ui-icon-size); + width: var(--jm-dashboard-company-logo-size); + height: var(--jm-dashboard-company-logo-size); object-fit: contain; } @@ -112,7 +122,8 @@ color: var(--jm-ui-danger-color); } -@media (max-width: 600px) { +@container (width < 540px) +{ .jm-dashboard-job { flex-wrap: wrap; } @@ -120,4 +131,10 @@ .jm-dashboard-header { display: none; } + + + .jm-dashboard-job-column.company ~ .jm-dashboard-job-column.job_title { + flex-basis: calc( 100% - var(--jm-dashboard-company-logo-size) - var(--jm-ui-space-sm) ); + order: -1; + } } diff --git a/assets/css/job-overlay.scss b/assets/css/job-overlay.scss index e68c7ad65..4517d8b17 100644 --- a/assets/css/job-overlay.scss +++ b/assets/css/job-overlay.scss @@ -60,6 +60,7 @@ .jm-job-overlay-details-box { background: var(--jm-ui-faint-color); padding: var(--jm-ui-space-m); + --jm-dashboard-company-logo-size: var(--jm-ui-icon-size); } @media (max-width: 600px) { diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php index 078ea8ffa..92bae7a7e 100644 --- a/includes/class-job-dashboard-shortcode.php +++ b/includes/class-job-dashboard-shortcode.php @@ -53,14 +53,28 @@ public function __construct() { add_filter( 'paginate_links', [ $this, 'filter_paginate_links' ], 10, 1 ); + add_action( 'job_manager_job_dashboard_column_company', [ self::class, 'the_company' ] ); add_action( 'job_manager_job_dashboard_column_date', [ self::class, 'the_date' ] ); add_action( 'job_manager_job_dashboard_column_date', [ self::class, 'the_expiration_date' ] ); + add_action( 'job_manager_job_dashboard_columns', [ $this, 'maybe_display_company_column' ], 8 ); add_action( 'job_manager_job_dashboard_column_job_title', [ self::class, 'the_job_title' ], 10 ); add_action( 'job_manager_job_dashboard_column_job_title', [ self::class, 'the_status' ], 12 ); Job_Overlay::instance(); } + /** + * Add 'company' column if user has multiple companies. + * + * @param array $columns + */ + public function maybe_display_company_column( $columns ) { + if ( $this->user_has_multiple_companies() ) { + $columns = array_merge( [ 'company' => __( 'Company', 'wp-job-manager' ) ], $columns ); + } + + return $columns; + } /** * Handles shortcode which lists the logged in user's jobs. @@ -462,6 +476,17 @@ public static function the_expiration_date( $job ) { } } + /** + * Show company details. + * + * @param \WP_Post $job + * + * @output string + */ + public static function the_company( $job ) { + the_company_logo( 'thumbnail', '', $job ); + } + /** * Show location. * @@ -492,7 +517,7 @@ public static function the_location( $job ) { * @output string */ public static function the_job_title( $job ) { - echo '' . esc_html( get_the_title( $job ) ?? $job->ID ) . ''; + echo '' . esc_html( get_the_title( $job ) ?? $job->ID ) . ''; } /** @@ -556,7 +581,7 @@ public static function the_status( $job ) { public static function get_job_dashboard_page_url() { $page_id = get_option( 'job_manager_job_dashboard_page_id' ); if ( $page_id ) { - return get_permalink( $page_id ); + return (string) get_permalink( $page_id ); } else { return home_url( '/' ); } @@ -571,7 +596,7 @@ public static function get_job_dashboard_page_url() { */ public function is_job_available_on_dashboard( \WP_Post $job ) { // Check cache of currently displayed job dashboard IDs first to avoid lots of queries. - if ( isset( $this->job_dashboard_job_ids ) && in_array( (int) $job->ID, $this->job_dashboard_job_ids, true ) ) { + if ( ! empty( $this->job_dashboard_job_ids ) && in_array( (int) $job->ID, $this->job_dashboard_job_ids, true ) ) { return true; } diff --git a/includes/class-stats.php b/includes/class-stats.php index 1ad7ff93f..78ff9f047 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -115,14 +115,14 @@ public function log_stat( string $name, array $args = [] ) { $args = array_merge( self::DEFAULT_LOG_STAT_ARGS, $args ); $group = $args['group']; - $post_id = $args['post_id']; + $post_id = absint( $args['post_id'] ); $increment_by = $args['increment_by']; if ( strlen( $name ) > 255 || strlen( $group ) > 255 || - ! is_numeric( $post_id ) || - ! is_numeric( $increment_by ) ) { + ! $post_id || + ! is_integer( $increment_by ) ) { return false; } diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index 8603b40ec..13efed702 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -43,7 +43,7 @@ class WP_Job_Manager { /** * Stats. * - * @var WP_Job_Manager_Stats + * @var WP_Job_Manager\Stats */ public $stats; From 5803e8348397d7ca7bf174020ebba471644db6d9 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Wed, 28 Feb 2024 13:40:47 +0200 Subject: [PATCH 36/50] Introduce stat for jobs impressions. (#2771) --- assets/js/stats/impressions.js | 100 ++++++++++++++++ assets/js/stats/observers.js | 19 +++ assets/js/stats/unique.js | 53 ++++++++ assets/js/stats/utils.js | 21 ++++ assets/js/wpjm-stats.js | 213 ++++++++++++++++++--------------- includes/class-stats.php | 193 ++++++++++++++++++++++++----- 6 files changed, 470 insertions(+), 129 deletions(-) create mode 100644 assets/js/stats/impressions.js create mode 100644 assets/js/stats/observers.js create mode 100644 assets/js/stats/unique.js create mode 100644 assets/js/stats/utils.js diff --git a/assets/js/stats/impressions.js b/assets/js/stats/impressions.js new file mode 100644 index 000000000..1c181edf0 --- /dev/null +++ b/assets/js/stats/impressions.js @@ -0,0 +1,100 @@ +import { debounce, filterZeroes, findIdInClassNames } from './utils'; +import { waitForSelector } from './observers'; + +function createStatsQueue() { + const alreadySent = {}; + let queue = []; + + const logThem = debounce( function ( statName, listingIds ) { + const stats = listingIds.map( function ( id ) { + alreadySent[ id ] = true; + return { name: statName, post_id: id }; + } ); + + return window.wpjmLogStats( stats ).finally( function () { + queue = []; + } ); + }, 1000 ); + + return { + queueListingImpressionStats( statName, listingIds ) { + listingIds.forEach( function ( listingId ) { + if ( ! alreadySent[ listingId ] ) { + queue.push( listingId ); + } + } ); + logThem( statName, queue ); + }, + }; +} + +function observeForVisibility( + jobListingContainer, + jobListingElement, + visibleCallback, + alreadyViewedListings +) { + const options = { + root: null, + rootMargin: '0px', + threshold: 1.0, + }; + + const observer = new IntersectionObserver( function ( entries ) { + entries.forEach( function ( entry ) { + if ( entry.isIntersecting && entry.intersectionRatio > 0.99 ) { + const node = entry.target; + if ( 1 === node.nodeType && node.classList.contains( 'job_listing' ) ) { + const nodeId = findIdInClassNames( node ); + if ( nodeId > 0 && ! alreadyViewedListings[ nodeId ] ) { + alreadyViewedListings[ nodeId ] = true; + visibleCallback( node ); + } + } + observer.unobserve( node ); + } + } ); + }, options ); + + observer.observe( jobListingElement ); +} + +function waitForNextVisibleListing( listingVisibleCallback ) { + const jobListingsContainer = document.querySelector( 'ul.job_listings' ); + const config = { childList: true }; + const alreadyViewed = {}; + + const observer = new MutationObserver( function ( mutations ) { + mutations.forEach( function ( mutation ) { + mutation.addedNodes.forEach( function ( node ) { + if ( 1 === node.nodeType && node.classList.contains( 'job_listing' ) ) { + observeForVisibility( jobListingsContainer, node, listingVisibleCallback, alreadyViewed ); + } + } ); + } ); + } ); + + observer.observe( jobListingsContainer, config ); +} + +export function initListingImpression( stats ) { + if ( 0 === stats.length ) { + return; + } + const statName = stats[ 0 ].name; + const debouncedSender = createStatsQueue(); + waitForSelector( 'li.job_listing' ).then( function () { + const allVisibleListings = document.querySelectorAll( 'li.job_listing' ); + const initialListingIds = filterZeroes( + [ ...allVisibleListings ].map( function ( elem ) { + return findIdInClassNames( elem ); + } ) + ); + debouncedSender.queueListingImpressionStats( statName, initialListingIds ); + + waitForNextVisibleListing( function ( elem ) { + const maybeId = findIdInClassNames( elem ); + maybeId > 0 && debouncedSender.queueListingImpressionStats( statName, [ maybeId ] ); + } ); + } ); +} diff --git a/assets/js/stats/observers.js b/assets/js/stats/observers.js new file mode 100644 index 000000000..08e465cc4 --- /dev/null +++ b/assets/js/stats/observers.js @@ -0,0 +1,19 @@ +export function waitForSelector( selector ) { + return new Promise( function ( resolve ) { + if ( document.querySelector( selector ) ) { + return resolve( document.querySelector( selector ) ); + } + + const observer = new MutationObserver( function () { + if ( document.querySelector( selector ) ) { + observer.disconnect(); + resolve( document.querySelector( selector ) ); + } + } ); + + observer.observe( document.documentElement, { + childList: true, + subtree: true, + } ); + } ); +} diff --git a/assets/js/stats/unique.js b/assets/js/stats/unique.js new file mode 100644 index 000000000..3af65bde6 --- /dev/null +++ b/assets/js/stats/unique.js @@ -0,0 +1,53 @@ +export function updateDailyUnique( key ) { + const date = new Date(); + const expiresAtTimestamp = date.getTime() + 24 * 60 * 60 * 1000; + window.localStorage[ key ] = expiresAtTimestamp; +} + +export function getDailyUnique( name ) { + if ( window.localStorage[ name ] ) { + const date = new Date(); + const now = date.getTime(); + const expiration = parseInt( window.localStorage[ name ], 10 ); + return Number.isNaN( expiration ) ? false : expiration >= now; + } + return false; +} + +export function setUniques( uniquesToSet ) { + uniquesToSet.forEach( function ( uniqueKey ) { + updateDailyUnique( uniqueKey ); + } ); +} + +export function checkUniqueRecordedToday( statToRecord ) { + const uniqueKey = statToRecord.unique_key || ''; + return checkUnique( statToRecord ) && true === getDailyUnique( uniqueKey ); +} + +export function checkUnique( statToRecord ) { + const uniqueKey = statToRecord.unique_key || ''; + const isUnique = uniqueKey.length > 0; + + return isUnique; +} + +export function scheduleStaleUniqueCleanup( statsToRecord ) { + const twoDaysInMillis = 24 * 60 * 60 * 1000 * 2; + const cleanup = function () { + const keyPrefixes = statsToRecord + .filter( s => s.unique_key && s.unique_key.length > 0 ) + .map( s => s.name ); + + for ( let i = 0; i < localStorage.length; i++ ) { + const key = localStorage.key( i ); + const expiry = parseInt( localStorage.getItem( key ), 10 ); + const containsUniqueKeyPrefix = keyPrefixes.some( k => key.indexOf( k ) === 0 ); + const dateNow = +new Date(); + if ( ! isNaN( expiry ) && containsUniqueKeyPrefix && expiry + twoDaysInMillis < dateNow ) { + localStorage.removeItem( key ); + } + } + }; + setTimeout( cleanup, 1000 ); +} diff --git a/assets/js/stats/utils.js b/assets/js/stats/utils.js new file mode 100644 index 000000000..835e644d1 --- /dev/null +++ b/assets/js/stats/utils.js @@ -0,0 +1,21 @@ +export function debounce( func, delay ) { + let timeoutId; + + return function ( ...args ) { + clearTimeout( timeoutId ); + + timeoutId = setTimeout( () => { + func( ...args ); + }, delay ); + }; +} + +export function filterZeroes( list ) { + return list.filter( function ( i ) { + return i > 0; + } ); +} + +export function findIdInClassNames( node ) { + return +node.className.match( /\bpost-(\d+)\b/ )?.[ 1 ] || 0; +} diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index 1fdd91dce..80ef4eb4d 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -1,128 +1,143 @@ -/* global job_manager_stats */ - import domReady from '@wordpress/dom-ready'; import { createHooks } from '@wordpress/hooks'; +import { + setUniques, + checkUniqueRecordedToday, + checkUnique, + scheduleStaleUniqueCleanup, +} from './stats/unique'; +import { initListingImpression } from './stats/impressions'; + +const WPJMStats = { + statsToRecord: [], + init( statsToRecord ) { + WPJMStats.statsToRecord = statsToRecord; + WPJMStats.hooks.doAction( 'init', WPJMStats ); + + const statsByTrigger = statsToRecord?.reduce( function ( accum, statToRecord ) { + const triggerName = statToRecord.trigger || ''; -( function () { - window.wpjmStatHooks = window.wpjmStatHooks || createHooks(); - - function updateDailyUnique( key ) { - const date = new Date(); - const expiresAtTimestamp = date.getTime() + 24 * 60 * 60 * 1000; - window.localStorage[key] = expiresAtTimestamp; - } - - function getDailyUnique( name ) { - if ( window.localStorage[name] ) { - const date = new Date(); - const now = date.getTime(); - const expiration = parseInt( window.localStorage[ name ], 10 ); - return expiration >= now; - } - return false; - } - - function setUniques( uniquesToSet ) { - uniquesToSet.forEach( function( uniqueKey ) { - updateDailyUnique( uniqueKey ); - } ); - } + if ( triggerName.length < 1 ) { + return accum; + } - window.wpjmLogStats = window.wpjmLogStats || function ( statsToRecord, uniquesToSet ) { - const jobStatsSettings = window.job_manager_stats; - const ajaxUrl = jobStatsSettings.ajax_url; - const ajaxNonce = jobStatsSettings.ajax_nonce; + if ( ! accum[ triggerName ] ) { + accum[ triggerName ] = []; + } - uniquesToSet = uniquesToSet || []; - statsToRecord = statsToRecord || []; + accum[ triggerName ].push( statToRecord ); - if ( statsToRecord.length < 1 ) { - return Promise.resolve(); // Could also be an error. - } + return accum; + }, {} ); - const postData = new URLSearchParams( { - _ajax_nonce: ajaxNonce, - post_id: jobStatsSettings.post_id || 0, - action: 'job_manager_log_stat', - stats: statsToRecord.join( ',' ), + Object.keys( statsByTrigger ).forEach( function ( triggerName ) { + WPJMStats.hookStatsForTrigger( statsByTrigger, triggerName ); } ); - return fetch( ajaxUrl, { - method: 'POST', - credentials: 'same-origin', - body: postData, - } ).finally( function () { - setUniques( uniquesToSet ); - } ); - }; + WPJMStats.hooks.doAction( 'page-load' ); + scheduleStaleUniqueCleanup( statsToRecord ); + }, - function hookStatsForTrigger( statsByTrigger, triggerName ) { - const statsToRecord = []; - const uniquesToSet = []; - const stats = statsByTrigger[triggerName] || []; - const events = {}; + hookStatsForTrigger( statsByTrigger, triggerName ) { + const statsToRecord = []; + const stats = statsByTrigger[ triggerName ] || []; + const statsByType = {}; stats.forEach( function ( statToRecord ) { - const statToRecordKey = statToRecord.name; - const uniqueKey = statToRecord.unique_key || ''; - const isUnique = uniqueKey.length > 0; - - if ( ! isUnique ) { - statsToRecord.push( statToRecordKey ); - } else { - if ( false === getDailyUnique( uniqueKey ) ) { - uniquesToSet.push( uniqueKey ); - statsToRecord.push( statToRecordKey ); - } + if ( ! statsByType[ statToRecord.type ] ) { + statsByType[ statToRecord.type ] = []; } - if ( statToRecord.element && statToRecord.event ) { - const elemToAttach = document.querySelector( statToRecord.element ); - if ( elemToAttach && ! events[statToRecord.element] ) { - elemToAttach.addEventListener( statToRecord.event, function ( e ) { - if ( isUnique && false !== getDailyUnique( uniqueKey ) ) { - return; - } - - window.wpjmStatHooks.doAction( triggerName ); - } ); - events[statToRecord.element] = true; - } - } + statsByType[ statToRecord.type ].push( statToRecord ); + statsToRecord.push( statToRecord ); } ); // Hook action to call logStats. - window.wpjmStatHooks.addAction( triggerName, 'wpjm-stats', function () { - window.wpjmLogStats( statsToRecord, uniquesToSet ); - }, 10 ); - } + WPJMStats.hooks.addAction( + triggerName, + 'wpjm-stats', + function () { + window.wpjmLogStats( statsToRecord ); + }, + 10 + ); + + Object.keys( statsByType ).forEach( function ( type ) { + WPJMStats.types[ type ] && WPJMStats.types[ type ]( statsByType[ type ] ); + } ); + }, + + hooks: createHooks(), + types: { + pageLoad( stats ) { + // This does not need to do anything special. + }, + domEvent( stats ) { + const events = {}; + stats.forEach( function ( statToRecord ) { + const triggerName = statToRecord.trigger; + if ( statToRecord.element && statToRecord.event ) { + const elemToAttach = document.querySelector( statToRecord.element ); + if ( elemToAttach && ! events[ statToRecord.element ] ) { + elemToAttach.addEventListener( statToRecord.event, function ( e ) { + WPJMStats.hooks.doAction( triggerName ); + } ); + events[ statToRecord.element ] = true; + } + } + } ); + }, + initListingImpression, + }, +}; +window.WPJMStats = window.WPJMStats || WPJMStats; - domReady( function () { +window.wpjmLogStats = + window.wpjmLogStats || + function ( stats ) { const jobStatsSettings = window.job_manager_stats; + const ajaxUrl = jobStatsSettings.ajax_url; + const ajaxNonce = jobStatsSettings.ajax_nonce; - const statsByTrigger = jobStatsSettings.stats_to_log?.reduce( function ( accum, statToRecord ) { - const triggerName = statToRecord.trigger || ''; + const uniquesToSet = []; + const statsToRecord = []; - if ( triggerName.length < 1 ) { - return accum; - } + if ( stats.length < 1 ) { + return Promise.resolve(); // Could also be an error. + } - if ( ! accum[triggerName] ) { - accum[triggerName] = []; + stats.forEach( function ( statToRecord ) { + if ( ! checkUniqueRecordedToday( statToRecord ) ) { + uniquesToSet.push( statToRecord.unique_key ); + statsToRecord.push( statToRecord ); + } else if ( ! checkUnique( statToRecord ) ) { + statsToRecord.push( statToRecord ); } + } ); - accum[triggerName].push( statToRecord ); + const postData = new URLSearchParams( { + _ajax_nonce: ajaxNonce, + post_id: jobStatsSettings.post_id || 0, + action: 'job_manager_log_stat', + stats: JSON.stringify( + statsToRecord.map( function ( stat ) { + const { name = '', group = '', post_id = 0 } = stat; + return { name, group, post_id }; + } ) + ), + } ); - return accum; - }, {} ); + setUniques( uniquesToSet ); - Object.keys( statsByTrigger ).forEach( function ( triggerName) { - hookStatsForTrigger( statsByTrigger, triggerName ); + return fetch( ajaxUrl, { + method: 'POST', + credentials: 'same-origin', + body: postData, } ); + }; - // Kick things off. - console.log('kick thing off'); - window.wpjmStatHooks.doAction( 'page-load' ); - } ); -} )(); +domReady( function () { + const jobStatsSettings = window.job_manager_stats; + WPJMStats.init( jobStatsSettings.stats_to_log ); +} ); diff --git a/includes/class-stats.php b/includes/class-stats.php index 78ff9f047..9e8ff3552 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -186,6 +186,8 @@ private function hook() { add_action( 'wp_enqueue_scripts', [ $this, 'frontend_scripts' ] ); add_action( 'wp_ajax_job_manager_log_stat', [ $this, 'maybe_log_stat_ajax' ] ); add_action( 'wp_ajax_nopriv_job_manager_log_stat', [ $this, 'maybe_log_stat_ajax' ] ); + add_action( 'wpjm_stats_frontend_scripts', [ $this, 'job_listing_frontend_scripts' ], 10, 1 ); + add_action( 'wpjm_stats_frontend_scripts', [ $this, 'jobs_frontend_scripts' ], 10, 1 ); } /** @@ -210,45 +212,64 @@ public function maybe_log_listing_view() { /** * Log multiple stats in one go. Triggered in an ajax call. * - * @return void + * @return bool */ public function maybe_log_stat_ajax() { if ( ! wp_doing_ajax() ) { - return; + return false; } $post_data = stripslashes_deep( $_POST ); if ( ! isset( $post_data['_ajax_nonce'] ) || ! wp_verify_nonce( $post_data['_ajax_nonce'], 'ajax-nonce' ) ) { - return; + return false; } - $post_id = isset( $post_data['post_id'] ) ? absint( $post_data['post_id'] ) : 0; + $stats_json = $post_data['stats'] ?? '[]'; + $stats = json_decode( $stats_json, true ); - if ( empty( $post_id ) ) { - return; + if ( empty( $stats ) ) { + return false; } - $post_type = get_post_type( $post_id ); + $errors = []; + $registered_stats = $this->get_registered_stats(); - if ( \WP_Job_Manager_Post_Types::PT_LISTING !== $post_type ) { - return; - } + foreach ( $stats as $stat_data ) { + $post_id = isset( $stat_data['post_id'] ) ? absint( $stat_data['post_id'] ) : 0; - $stats = isset( $post_data['stats'] ) ? explode( ',', sanitize_text_field( $post_data['stats'] ) ) : []; + if ( empty( $post_id ) ) { + $errors[] = [ 'missing post_id', $stat_data ]; + continue; + } - $registered_stats = $this->get_registered_stats(); + $post = get_post( $post_id ); + if ( ! $this->can_record_stats_for_post( $post ) ) { + $errors[] = [ 'cannot record', $stat_data, $post ]; + continue; + } + + if ( ! isset( $stat_data['name'] ) ) { + $errors[] = [ 'no name', $stat_data ]; + continue; + } + + $stat_name = trim( strtolower( $stat_data['name'] ) ); - // TODO: Maybe optimize this into a single insert? - foreach ( $stats as $stat_name ) { - $stat_name = trim( strtolower( $stat_name ) ); if ( ! in_array( $stat_name, $this->get_registered_stat_names(), true ) ) { + $errors[] = [ 'not registered', $stat_data ]; continue; } $log_callback = $registered_stats[ $stat_name ]['log_callback'] ?? [ $this, 'log_stat' ]; call_user_func( $log_callback, trim( $stat_name ), [ 'post_id' => $post_id ] ); } + + if ( ! empty( $errors ) ) { + return false; + } + + return true; } /** @@ -266,12 +287,85 @@ private function get_registered_stat_names() { * @return void */ public function frontend_scripts() { - $post_id = absint( get_queried_object_id() ); - $post_type = get_post_type( $post_id ); + $post_id = absint( get_queried_object_id() ); + $post = get_post( $post_id ); + + if ( 0 === $post_id ) { + return; + } + + /** + * Delegate registration to dedicated hooks per-screen. + */ + do_action( 'wpjm_stats_frontend_scripts', $post ); + } + + /** + * Register any frontend scripts for job listings. + * + * @param \WP_Post $post The post. + * + * @return void + */ + public function job_listing_frontend_scripts( $post ) { + $post_type = $post->post_type; if ( \WP_Job_Manager_Post_Types::PT_LISTING !== $post_type ) { return; } + $this->register_frontend_scripts_for_screen( 'listing', $post->ID ); + } + + /** + * Register any frontend scripts for a page containing 'jobs' shortcode. + * + * @param \WP_Post $post The post. + * + * @return void + */ + public function jobs_frontend_scripts( $post ) { + if ( $this->page_has_jobs_shortcode( $post ) ) { + $this->register_frontend_scripts_for_screen( 'jobs', $post->ID ); + } + } + + /** + * Check that a certain post/page is eligible for getting recorded stats. + * + * @param \WP_Post $post The post. + * + * @return bool + */ + private function can_record_stats_for_post( $post ) { + if ( $this->page_has_jobs_shortcode( $post ) ) { + return $this->filter_can_record_stats_for_post( true, $post ); + } elseif ( \WP_Job_Manager_Post_Types::PT_LISTING === $post->post_type ) { + return $this->filter_can_record_stats_for_post( true, $post ); + } + + return $this->filter_can_record_stats_for_post( false, $post ); + } + + /** + * Run filter. + * + * @param bool $can_record Can record. + * @param \WP_Post $post The post. + * + * @return bool + */ + private function filter_can_record_stats_for_post( $can_record, $post ) { + return (bool) apply_filters( 'wpjm_stats_can_record_stats_for_post', $can_record, $post ); + } + + /** + * Register scripts for given screen. + * + * @param string $page Which page. + * @param int $post_id Which id. + * @return void + */ + private function register_frontend_scripts_for_screen( $page = 'listing', $post_id = 0 ) { \WP_Job_Manager::register_script( 'wp-job-manager-stats', 'js/wpjm-stats.js', @@ -286,7 +380,7 @@ public function frontend_scripts() { 'ajax_url' => admin_url( 'admin-ajax.php' ), 'ajax_nonce' => wp_create_nonce( 'ajax-nonce' ), 'post_id' => $post_id, - 'stats_to_log' => $this->get_stats_for_ajax( $post_id ), + 'stats_to_log' => $this->get_stats_for_ajax( $post_id, $page ), ]; wp_enqueue_script( 'wp-job-manager-stats' ); @@ -295,6 +389,7 @@ public function frontend_scripts() { 'job_manager_stats', $script_data ); + } /** @@ -309,42 +404,69 @@ private function get_registered_stats() { 'job_listing_view' => [ 'log_callback' => [ $this, 'log_stat' ], // Example of overriding how we log this. 'trigger' => 'page-load', + 'type' => 'pageLoad', + 'page' => 'listing', ], 'job_listing_view_unique' => [ - 'unique' => true, - 'unique_callback' => [ $this, 'unique_by_post_id' ], - 'trigger' => 'page-load', + 'unique' => true, + 'type' => 'pageLoad', + 'trigger' => 'page-load', + 'page' => 'listing', ], 'job_listing_apply_button_clicked' => [ - 'trigger' => 'apply-button-clicked', - 'element' => 'input.application_button', - 'event' => 'click', - 'unique' => true, - 'unique_callback' => [ $this, 'unique_by_post_id' ], + 'trigger' => 'apply-button-clicked', + 'type' => 'domEvent', + 'element' => 'input.application_button', + 'event' => 'click', + 'unique' => true, + 'page' => 'listing', + ], + 'jobs_view' => [ + 'trigger' => 'page-load', + 'type' => 'pageLoad', + 'page' => 'jobs', + ], + 'jobs_view_unique' => [ + 'trigger' => 'page-load', + 'type' => 'pageLoad', + 'page' => 'jobs', + 'unique' => true, + ], + 'job_listing_impression' => [ + 'trigger' => 'job-listing-impression', + 'type' => 'initListingImpression', + 'page' => 'jobs', ], ] ); } /** - * Prepare stats for wp_localize. + * Determine what stats should be added to the kind of page the user is viewing. * - * @param int $post_id Optional post id. + * @param int $post_id Optional post id. + * @param string $page The page in question. * * @return array */ - private function get_stats_for_ajax( $post_id = 0 ) { + private function get_stats_for_ajax( $post_id = 0, $page = 'listing' ) { $ajax_stats = []; foreach ( $this->get_registered_stats() as $stat_name => $stat_data ) { + if ( $page !== $stat_data['page'] ) { + continue; + } + $stat_ajax = [ 'name' => $stat_name, + 'post_id' => $post_id, + 'type' => $stat_data['type'] ?? '', 'trigger' => $stat_data['trigger'] ?? '', 'element' => $stat_data['element'] ?? '', 'event' => $stat_data['event'] ?? '', ]; if ( ! empty( $stat_data['unique'] ) ) { - $unique_callback = $stat_data['unique_callback']; + $unique_callback = $stat_data['unique_callback'] ?? [ $this, 'unique_by_post_id' ]; $stat_ajax['unique_key'] = call_user_func( $unique_callback, $stat_name, $post_id ); } @@ -365,4 +487,15 @@ private function get_stats_for_ajax( $post_id = 0 ) { public function unique_by_post_id( $stat_name, $post_id ) { return $stat_name . '_' . $post_id; } + + /** + * Any page containing a job shortcode is eligible. + * + * @param \WP_Post $post The post. + * + * @return bool + */ + public function page_has_jobs_shortcode( $post ) { + return has_shortcode( $post->post_content, 'jobs' ); + } } From e4c23ac7cb568c6d930d3a7f2c575c23be35675c Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 13 Mar 2024 14:47:42 +0100 Subject: [PATCH 37/50] Add stats section to job overlay (#2776) --- assets/css/job-overlay.scss | 274 ++++++++++++++++++++- assets/css/mixins.scss | 5 + assets/css/ui.dialog.scss | 11 +- assets/css/ui.elements.scss | 41 +++ assets/css/ui.neutral.scss | 4 +- assets/js/job-dashboard.js | 32 ++- assets/js/ui.js | 25 ++ composer.lock | 2 +- includes/class-dev-tools.php | 109 ++++++++ includes/class-job-dashboard-shortcode.php | 5 +- includes/class-job-listing-stats.php | 45 ++-- includes/class-job-overlay.php | 43 +++- includes/class-stats-dashboard.php | 226 ++++++++++++++++- includes/class-stats.php | 47 ++-- includes/class-wp-job-manager.php | 8 +- includes/ui/class-modal-dialog.php | 6 +- includes/ui/class-ui.php | 36 ++- psalm.xml | 10 + templates/job-dashboard-overlay.php | 2 + templates/job-stats.php | 137 +++++++++++ 20 files changed, 982 insertions(+), 86 deletions(-) create mode 100644 assets/js/ui.js create mode 100644 includes/class-dev-tools.php create mode 100644 templates/job-stats.php diff --git a/assets/css/job-overlay.scss b/assets/css/job-overlay.scss index 4517d8b17..94350e893 100644 --- a/assets/css/job-overlay.scss +++ b/assets/css/job-overlay.scss @@ -1,9 +1,14 @@ +@import 'mixins'; .jm-dashboard__overlay { width: var(--wp--style--global--wide-size, 1200px); - height: 80vh; + height: 100vh; --jm-dialog-padding: var(--jm-ui-space-ml); + &, * { + box-sizing: border-box; + } + .jm-ui-spinner { --size: 64px; } @@ -13,6 +18,20 @@ right: var(--jm-dialog-padding); color: inherit; } + + .jm-dialog-modal { + display: block; + overflow: unset; + } + + .jm-dialog-modal-container { + display: block; + height: 100%; + } + + .jm-dialog-modal-content { + height: 100%; + } } .jm-job-overlay { @@ -23,8 +42,9 @@ flex-direction: column; gap: var(--jm-ui-space-sm); animation: jm-fade-in 200ms ease-in; - font-size: var(--jm-ui-font-size-m); + height: 100%; + } .jm-job-overlay-header { @@ -32,7 +52,7 @@ display: flex; justify-content: space-between; gap: var(--jm-ui-space-m); - padding-right: calc( var(--jm-ui-icon-size) + var(--jm-ui-space-s) ); + padding-right: calc(var(--jm-ui-icon-size) + var(--jm-ui-space-s)); padding-bottom: var(--jm-ui-space-sm); flex: 0 0 min-content; @@ -42,14 +62,17 @@ font-weight: 600; } } + .jm-job-overlay-content { flex: 1 1 auto; overflow: auto; + margin: 0 calc(-1 * var(--jm-ui-space-m)); + padding: 0 var(--jm-ui-space-m); } .jm-job-overlay-footer { border-top: var(--jm-ui-border-size) solid var(--jm-ui-border-faint); - padding-top: var(--jm-dialog-padding); + padding-top: var(--jm-ui-space-m); .jm-ui-actions-row { gap: var(--jm-ui-space-sm); @@ -64,8 +87,7 @@ } @media (max-width: 600px) { - .jm-dashboard__overlay - { + .jm-dashboard__overlay { height: 100%; } } @@ -78,3 +100,243 @@ opacity: 1; } } + + +.jm-job-stats { + + --jm-stat-color-page-view: var(--jm-ui-accent-color); + --jm-stat-color-unique-view: color-mix(in srgb, #000, var(--jm-stat-color-page-view) 40%); +} + +.jm-job-stat-details { + margin: var(--jm-ui-space-m) 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); + grid-gap: var(--jm-ui-space-l); + justify-content: space-between; + align-items: stretch; + + * { + box-sizing: border-box; + } + + > .jm-ui-col { + + display: flex; + flex-direction: column; + gap: var(--jm-ui-space-m); + //border-left: var(--jm-ui-border-size) solid var(--jm-ui-border-faint); + //margin-left: -1px; + } + + + .jm-stat-section { + display: flex; + flex-direction: column; + + } + + .jm-stat-row { + padding: var(--jm-ui-space-xs); + position: relative; + } + + .jm-stat-background { + position: absolute; + top: 1px; + bottom: 1px; + left: 0; + background: var(--jm-ui-accent-color); + opacity: 0.1; + z-index: -1; + } + + .jm-stat-row:where(:not(:last-child)) { + border-bottom: var(--jm-ui-border-size) solid var(--jm-ui-border-faint); + } + + .jm-stat-row:has(.jm-stat-background) { + border: unset; + } + + .jm-stat-label { + flex: 1; + } + + .jm-stat-value { + font-weight: 600; + } + + .jm-ui-icon { + + &[data-icon^=color] { + --jm-ui-icon-size: 14px; + margin: 5px; + border-radius: var(--jm-ui-radius); + mask-image: unset; + } + + &[data-icon=color-page-view] { + background: var(--jm-stat-color-page-view); + } + + &[data-icon=color-unique-view] { + --jm-ui-icon-size: 14px; + margin: 5px; + border-radius: var(--jm-ui-radius); + background: var(--jm-stat-color-unique-view); + } + + &[data-icon=message] { + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cpath fill=\'black\' fill-rule=\'evenodd\' d=\'M3 7c0-1.1.9-2 2-2h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Zm2-.5h14c.28 0 .5.22.5.5v.94L12 13.56 4.5 7.94V7c0-.28.22-.5.5-.5Zm-.5 3.31V17c0 .28.22.5.5.5h14a.5.5 0 0 0 .5-.5V9.81L12 15.44 4.5 9.8Z\' clip-rule=\'evenodd\'/%3e%3c/svg%3e'); + } + + &[data-icon=cursor] { + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='M10.49 18.51a36.83 36.83 0 0 0-1.03 2.08L5.24 3.84l14.7 9.06s-1.05.09-2.35.31c-1.57.27-3.5.73-4.41 1.46-.92.73-1.95 2.44-2.7 3.84Zm-.54-2.11c.23-.37.46-.75.7-1.1.46-.65 1-1.34 1.6-1.8a6.69 6.69 0 0 1 2.24-1.1c.4-.13.81-.24 1.23-.34l-8.12-5 2.35 9.34Z' clip-rule='evenodd'/%3e%3c/svg%3e"); + } + } +} + +.jm-section-header { + text-transform: uppercase; + font-weight: 200; + letter-spacing: -0.2px; + margin-bottom: var(--jm-ui-space-xs); + display: flex; + align-items: center; + gap: var(--jm-ui-space-xs); +} + +.jm-section-header__help { + font-weight: normal; + text-transform: none; + letter-spacing: normal; + height: 18px; + .jm-ui-icon { + color: fadeCurrentColor(50%); + --jm-ui-icon-size: 18px; + } +} + +.jm-job-stats-chart { + margin: var(--jm-ui-space-m) 0; +} + +.jm-chart { + position: relative; + margin: var(--jm-ui-space-m) 0; + padding: 2px; + + .jm-chart-bar-tooltip { + position: absolute; + display: none; + top: calc(100% + 6px); + min-width: 160px; + left: 0; + z-index: 1; + + .jm-ui-row { + justify-content: space-between; + margin: var(--jm-ui-space-xs) 0; + gap: var(--jm-ui-space-m); + } + + strong { + font-weight: 600; + } + } + + .jm-chart-bar:hover { + background: fadeCurrentColor(5%); + + .jm-chart-bar-tooltip { + display: block; + } + } + + .jm-chart-bar--right-edge .jm-chart-bar-tooltip { + left: unset; + right: 0; + } + + .jm-chart-bars { + display: flex; + } + + .jm-chart-bar { + flex: 1; + position: relative; + min-width: 1px; + height: 150px; + --jm-local-gap: min(0.1vw, 2px); + --jm-local-padding: 15%; + + } + + .jm-chart-bar-value, + .jm-chart-bar-inner-value, + { + position: absolute; + bottom: 0; + --jm-local-radius: 1px; + border-radius: var(--jm-local-radius) var(--jm-local-radius) 0 0; + animation: jm-chart-bar-scale-up 350ms ease-out; + transform-origin: bottom center; + } + + .jm-chart-bar-value { + left: var(--jm-local-gap); + right: var(--jm-local-gap); + padding-top: var(--jm-local-padding); + background: var(--jm-stat-color-page-view); + box-sizing: content-box; + } + + .jm-chart-bar-inner-value { + background: var(--jm-stat-color-unique-view); + left: calc(var(--jm-local-gap) + var(--jm-local-padding)); + right: calc(var(--jm-local-gap) + var(--jm-local-padding)); + } + + .jm-chart-bar.future-day .jm-chart-bar-value { + background: fadeCurrentColor(10%); + height: 0px !important; + animation: unset; + } + + .jm-chart-y-axis { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: -1; + } + .jm-chart-y-axis__label { + position: absolute; + left: 0; + right: 0; + color: fadeCurrentColor(50%); + border-bottom: 1px solid var(--jm-ui-border-faint); + font-size: var(--jm-ui-font-size-s); + text-align: right; + } + .jm-chart-x-axis { + display: flex; + justify-content: space-between; + color: fadeCurrentColor(50%); + font-size: var(--jm-ui-font-size-s); + + border-top: 2px solid var(--jm-ui-border-faint); + margin-top: 2px; + } +} + +@keyframes jm-chart-bar-scale-up { + from { + transform: scaleY(0); + } + to { + transform: scaleY(1); + } +} diff --git a/assets/css/mixins.scss b/assets/css/mixins.scss index d186f55cb..fe6fba67b 100644 --- a/assets/css/mixins.scss +++ b/assets/css/mixins.scss @@ -130,3 +130,8 @@ $temporary: #d93674; @function fadeCurrentColor( $opacity ) { @return color-mix(in srgb, transparent, currentColor #{$opacity}); } + + +@function fluid($desktop, $mobile) { + @return calc(var(--jm-ui-responsive-scale) * (#{$desktop} - #{$mobile}) + #{$mobile}px); +} diff --git a/assets/css/ui.dialog.scss b/assets/css/ui.dialog.scss index c3f1610fa..992015282 100644 --- a/assets/css/ui.dialog.scss +++ b/assets/css/ui.dialog.scss @@ -43,19 +43,23 @@ } .jm-dialog-modal { - position: relative; width: var(--wp--style--global--content-size, 640px); max-width: calc(100% - var(--jm-ui-space-s) * 2); max-height: 100%; margin: var(--jm-ui-space-s); - overflow: hidden; border-radius: var(--jm-ui-radius-2x); background-color: var(--jm-ui-background-color, #fff); color: var(--jm-ui-text-color, #1a1a1a); box-shadow: var(--jm-ui-shadow-modal); - + overflow: auto; overscroll-behavior: contain; +} +.jm-dialog-modal-container { + position: relative; + min-height: 100%; + display: flex; + } .jm-dialog-modal-content { @@ -63,7 +67,6 @@ flex-direction: column; align-items: center; justify-content: center; - height: 100%; width: 100%; } diff --git a/assets/css/ui.elements.scss b/assets/css/ui.elements.scss index eaff78dff..5c94ec373 100644 --- a/assets/css/ui.elements.scss +++ b/assets/css/ui.elements.scss @@ -147,6 +147,8 @@ mask-size: 100% 100%; background-color: currentColor; + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='black' stroke-dasharray='4 4' stroke-width='1.5' d='M20 12a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z'/%3e%3c/svg%3e"); + &[data-icon=check] { mask-image: var(--jm-ui-svg-check); } @@ -174,6 +176,22 @@ &[data-icon=edit] { mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' d='m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z'/%3e%3c/svg%3e"); } + + &[data-icon=search] { + mask-image: var(--jm-ui-svg-search); + } + + &[data-icon=help] { + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='black' stroke-width='1.5' d='M9.75 10.25a2.25 2.25 0 1 1 2.5 2.24.27.27 0 0 0-.25.26V14m0 1v1.5m8-4.5a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z'/%3e%3c/svg%3e"); + } +} + +.jm-ui-marker-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background-color: currentColor; } /* @@ -232,3 +250,26 @@ transform: rotate(360deg); } } + +.jm-ui-tooltip { + font-size: var(--jm-ui-font-size-s); + position: absolute; + color: var(--jm-ui-background-color, #fff); + background-color: var(--jm-ui-text-color, #1a1a1a); + padding: var(--jm-ui-space-s); + border-radius: var(--jm-ui-radius); + top: calc(100% + 6px); + width: max-content; + min-width: 18rem; + z-index: 1; + left: -2rem; +} + +.jm-ui-has-tooltip { + position: relative; +} + +.jm-ui-has-tooltip:not(:hover):not(:active):not(:focus) .jm-ui-tooltip { + display: none; +} + diff --git a/assets/css/ui.neutral.scss b/assets/css/ui.neutral.scss index ec95b7933..cea8f5680 100644 --- a/assets/css/ui.neutral.scss +++ b/assets/css/ui.neutral.scss @@ -5,6 +5,8 @@ --jm-ui-border-strong: currentColor; --jm-ui-border-size: 1px; + --jm-ui-responsive-scale: clamp(0px, (100vw - 400px) / (1200 - 400), 1px); + --jm-ui-background-color: initial; --jm-ui-text-color: initial; --jm-ui-background-opacity: 0; @@ -21,7 +23,7 @@ --jm-ui-success-color: #4ab866; --jm-ui-accent-color-contrast: #ffffff; - --jm-ui-button-color: var(--jm-ui-accent-color); + --jm-ui-button-color: var(--jm-ui-accent-color, inherit); --jm-ui-button-contrast: var(--jm-ui-accent-color-contrast, #ffffff); --jm-ui-link-color: var(--jm-ui-accent-color, inherit); diff --git a/assets/js/job-dashboard.js b/assets/js/job-dashboard.js index 632c05250..a8d8c418f 100644 --- a/assets/js/job-dashboard.js +++ b/assets/js/job-dashboard.js @@ -2,6 +2,8 @@ import domReady from '@wordpress/dom-ready'; +import './ui'; + // eslint-disable-next-line camelcase const { i18nConfirmDelete, overlayEndpoint } = job_manager_job_dashboard; @@ -18,21 +20,29 @@ function confirmDelete( event ) { } } -async function showOverlay( event ) { +async function showOverlay( eventOrId ) { const overlayDialog = document.getElementById( 'jmDashboardOverlay' ); if ( ! overlayDialog ) { return true; } - event.preventDefault(); + eventOrId.preventDefault?.(); overlayDialog.showModal(); + const id = eventOrId.target?.dataset.jobId ?? eventOrId; + + if ( ! id ) { + return; + } + + location.hash = id; + const contentElement = overlayDialog.querySelector( '.jm-dialog-modal-content' ); contentElement.innerHTML = ''; try { - const response = await fetch( `${ overlayEndpoint }?job_id=${ this.dataset.jobId }` ); + const response = await fetch( `${ overlayEndpoint }?job_id=${ id }` ); if ( ! response.ok ) { throw new Error( response.statusText ); @@ -41,11 +51,17 @@ async function showOverlay( event ) { const { data } = await response.json(); contentElement.innerHTML = data; - } - catch ( error ) { + } catch ( error ) { contentElement.innerHTML = `
${ error.message }
`; } + const clearHash = () => { + history.replaceState( null, '', window.location.pathname ); + overlayDialog.removeEventListener( 'close', clearHash ); + }; + + overlayDialog.addEventListener( 'close', clearHash ); + setupEvents( contentElement ); } @@ -55,4 +71,10 @@ domReady( () => { document .querySelectorAll( '.jm-dashboard-job .job-title' ) .forEach( el => el.addEventListener( 'click', showOverlay ) ); + + const urlHash = window.location.hash?.substring( 1 ); + + if ( urlHash > 0 ) { + showOverlay( +urlHash ); + } } ); diff --git a/assets/js/ui.js b/assets/js/ui.js new file mode 100644 index 000000000..ac7cc8309 --- /dev/null +++ b/assets/js/ui.js @@ -0,0 +1,25 @@ +// detect link color + +export const ACCENT_COLOR_CSS_VAR = '--jm-ui-accent-color'; + +function computeAccentColor() { + if ( getComputedStyle( document.documentElement ).getPropertyValue( ACCENT_COLOR_CSS_VAR ) ) { + return; + } + + const linkTag = document.createElement( 'a' ); + linkTag.setAttribute( 'href', '#?' ); + linkTag.style.display = 'none'; + const main = document.querySelector( 'main' ) ?? document.body; + main.appendChild( linkTag ); + const color = getComputedStyle( linkTag ).color; + linkTag.remove(); + + if ( ! color ) { + return; + } + + document.documentElement.style.setProperty( ACCENT_COLOR_CSS_VAR, color ); +} + +computeAccentColor(); diff --git a/composer.lock b/composer.lock index d54a5e2d9..72a675f75 100644 --- a/composer.lock +++ b/composer.lock @@ -4389,5 +4389,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/includes/class-dev-tools.php b/includes/class-dev-tools.php new file mode 100644 index 000000000..68cfaa97b --- /dev/null +++ b/includes/class-dev-tools.php @@ -0,0 +1,109 @@ + + * : The ID of the job to generate stats for. + * + * ## EXAMPLES + * + * wp jm dev stats generate 123 + * + * @when after_wp_load + * + * @param array $args + */ + public static function generate_stat_data( $args ) { + $job_id = absint( $args[0] ); + + $job = get_post( $job_id ); + + if ( ! $job_id || ! $job || 'job_listing' !== $job->post_type ) { + \WP_CLI::error( 'Invalid job ID' ); + } + + $stats = Stats::instance(); + + $start_date = get_post_datetime( $job ); + + $days = $start_date->diff( new \DateTime() )->days; + + $trend = 1; + $views = 0; + + $log = ''; + + $stats->clear_stats( $job_id ); + + for ( $i = 0; $i <= $days + 1; $i++ ) { + $views = (int) max( 0, $views + $trend * wp_rand( 0, 1000 ) ); + $trend = wp_rand( 0, 10 ) / 10 - 0.5; + $unique_views = wp_rand( (int) ( $views * 0.3 ), (int) ( $views * 0.8 ) ); + $impressions = wp_rand( (int) ( $views * 1.6 ), (int) ( $views * 2.6 ) ); + $date = $start_date->modify( "+{$i} days" )->format( 'Y-m-d' ); + + $log .= $views . ' '; + + $stats->log_stat( + Job_Listing_Stats::VIEW, + [ + 'post_id' => $job_id, + 'count' => $views, + 'date' => $date, + ] + ); + + $stats->log_stat( + Job_Listing_Stats::VIEW_UNIQUE, + [ + 'post_id' => $job_id, + 'count' => $unique_views, + 'date' => $date, + ] + ); + + $stats->log_stat( + Job_Listing_Stats::SEARCH_IMPRESSION, + [ + 'post_id' => $job_id, + 'count' => $impressions, + 'date' => $date, + ] + ); + + } + + \WP_CLI::log( \WP_CLI::colorize( 'Stats generated from %C' . $start_date->format( 'Y-m-d' ) . '%n for %C' . $days . ' days%n: ' ) . $log ); + + } +} diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php index 92bae7a7e..e8a63d884 100644 --- a/includes/class-job-dashboard-shortcode.php +++ b/includes/class-job-dashboard-shortcode.php @@ -576,7 +576,7 @@ public static function the_status( $job ) { /** * Get the URL of the [job_dashboard] page. * - * @return string + * @return string|false */ public static function get_job_dashboard_page_url() { $page_id = get_option( 'job_manager_job_dashboard_page_id' ); @@ -626,6 +626,7 @@ private function get_job_dashboard_query_args( $args = [] ) { 'orderby' => 'date', 'order' => 'desc', 'author' => get_current_user_id(), + 'posts_per_page' => -1, ] ); @@ -633,7 +634,7 @@ private function get_job_dashboard_query_args( $args = [] ) { $args['post_status'][] = 'future'; } - if ( $args['posts_per_page'] > 0 ) { + if ( ! empty( $args['posts_per_page'] ) && $args['posts_per_page'] > 0 ) { $args['offset'] = ( max( 1, get_query_var( 'paged' ) ) - 1 ) * $args['posts_per_page']; } diff --git a/includes/class-job-listing-stats.php b/includes/class-job-listing-stats.php index 2ab32f6ec..bdda1c48e 100644 --- a/includes/class-job-listing-stats.php +++ b/includes/class-job-listing-stats.php @@ -16,8 +16,10 @@ */ class Job_Listing_Stats { - const JOB_LISTING_VIEW = 'job_listing_view'; - const JOB_LISTING_VIEW_UNIQUE = 'job_listing_view_unique'; + const VIEW = 'job_listing_view'; + const VIEW_UNIQUE = 'job_listing_view_unique'; + const SEARCH_IMPRESSION = 'job_listing_impression'; + const APPLY_CLICK = 'job_listing_apply_button_clicked'; /** * Job listing post ID. @@ -27,22 +29,30 @@ class Job_Listing_Stats { private $job_id; /** - * Publish date of the job listing. + * Start date of the period queried. * * @var string */ private $start_date; + /** + * End date of the period queried. + * + * @var string + */ + private $end_date; + /** * Stats for a single job listing. * - * @param int $job_id + * @param int $job_id + * @param \DateTimeInterface[] $date_range Array of start and end date. Defaults to a range from the job's publishing date to the current day. */ - public function __construct( $job_id ) { + public function __construct( $job_id, $date_range = [] ) { $this->job_id = $job_id; - $this->start_date = get_post_datetime( $job_id )->format( 'Y-m-d' ); - + $this->start_date = ( $date_range[0] ?? get_post_datetime( $job_id ) )->format( 'Y-m-d' ); + $this->end_date = ( $date_range[1] ?? new \DateTime() )->format( 'Y-m-d' ); } /** @@ -52,8 +62,9 @@ public function __construct( $job_id ) { */ public function get_total_stats() { return [ - 'view' => $this->get_event_total( self::JOB_LISTING_VIEW ), - 'view_unique' => $this->get_event_total( self::JOB_LISTING_VIEW_UNIQUE ), + 'view' => $this->get_event_total( self::VIEW ), + 'view_unique' => $this->get_event_total( self::VIEW_UNIQUE ), + 'search' => $this->get_event_total( self::SEARCH_IMPRESSION ), ]; } @@ -64,8 +75,8 @@ public function get_total_stats() { */ public function get_daily_stats() { return [ - 'view' => $this->get_event_daily( self::JOB_LISTING_VIEW ), - 'view_unique' => $this->get_event_daily( self::JOB_LISTING_VIEW_UNIQUE ), + 'view' => $this->get_event_daily( self::VIEW ), + 'view_unique' => $this->get_event_daily( self::VIEW_UNIQUE ), ]; } @@ -91,10 +102,11 @@ public function get_event_total( $event ) { $sum = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(count) FROM {$wpdb->wpjm_stats} - WHERE post_id = %d AND name = %s AND date >= %s", + WHERE post_id = %d AND name = %s AND date BETWEEN %s AND %s", $this->job_id, $event, - $this->start_date + $this->start_date, + $this->end_date, ) ); @@ -127,20 +139,21 @@ public function get_event_daily( $event ) { $wpdb->prepare( "SELECT date, count FROM {$wpdb->wpjm_stats} WHERE post_id = %d AND name = %s AND date BETWEEN %s AND %s - ORDER BY date DESC", + ORDER BY date ASC", $this->job_id, $event, $this->start_date, - ( new \DateTime( 'yesterday' ) )->format( 'Y-m-d' ) + $this->end_date, ), OBJECT_K ); + $views = array_map( fn( $view ) => (int) $view->count, $views ); + wp_cache_set( $cache_key, $views, Stats::CACHE_GROUP, strtotime( 'tomorrow' ) - time() ); return $views; } - } diff --git a/includes/class-job-overlay.php b/includes/class-job-overlay.php index 9e787fc3e..317ce0db9 100644 --- a/includes/class-job-overlay.php +++ b/includes/class-job-overlay.php @@ -28,6 +28,7 @@ class Job_Overlay { */ public function __construct() { add_action( 'job_manager_ajax_job_dashboard_overlay', [ $this, 'ajax_job_overlay' ] ); + } /** @@ -54,19 +55,7 @@ public function ajax_job_overlay() { return; } - $job_actions = $shortcode->get_job_actions( $job ); - - ob_start(); - - get_job_manager_template( - 'job-dashboard-overlay.php', - [ - 'job' => $job, - 'job_actions' => $job_actions, - ] - ); - - $content = ob_get_clean(); + $content = $this->get_job_overlay( $job ); wp_send_json_success( $content ); @@ -86,4 +75,32 @@ public function output_modal_element() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in Modal_Dialog class. echo $overlay->render( '' ); } + + /** + * Get the job overlay content. + * + * @param \WP_Post $job + * + * @return string + */ + private function get_job_overlay( $job ) { + + $job_actions = Job_Dashboard_Shortcode::instance()->get_job_actions( $job ); + + ob_start(); + + get_job_manager_template( + 'job-dashboard-overlay.php', + [ + 'job' => $job, + 'job_actions' => $job_actions, + ] + ); + + $content = ob_get_clean(); + + return $content; + } + + } diff --git a/includes/class-stats-dashboard.php b/includes/class-stats-dashboard.php index 7b19676b5..48e82ae24 100644 --- a/includes/class-stats-dashboard.php +++ b/includes/class-stats-dashboard.php @@ -18,6 +18,11 @@ class Stats_Dashboard { private const COLUMN_NAME = 'stats'; + /** + * Max number of days to display in the daily stats chart. + */ + private const DAYS_PER_PAGE = 180; + /** * Constructor. */ @@ -32,18 +37,21 @@ private function __construct() { add_filter( 'manage_edit-job_listing_columns', [ $this, 'add_stats_column' ], 20 ); add_action( 'manage_job_listing_posts_custom_column', [ $this, 'maybe_render_job_listing_posts_custom_column' ], 2 ); + + add_action( 'job_manager_job_overlay_content', [ $this, 'output_job_stats' ], 12 ); } /** * Add a new column to the job dashboards. * * @param array $columns + * * @return array */ public function add_stats_column( $columns ) { $columns[ self::COLUMN_NAME ] = __( 'Views', 'wp-job-manager' ); - return $columns; + return $columns; } /** @@ -52,13 +60,14 @@ public function add_stats_column( $columns ) { * @param \WP_Post $job */ public function render_stats_column( $job ) { - $stats = new Job_Listing_Stats( $job->ID ); - $total = $stats->get_total_stats(); - - $views = $total['view']; + $stats = new Job_Listing_Stats( $job->ID ); + $views = $stats->get_event_total( Job_Listing_Stats::VIEW ); + $impressions = $stats->get_event_total( Job_Listing_Stats::SEARCH_IMPRESSION ); - // translators: %1d is the number of views. - $views_str = '' . sprintf( _n( '%1d view', '%1d views', $views, 'wp-job-manager' ), $views ) . ''; + // translators: %1d is the number of page views. + $views_str = '
' . sprintf( _n( '%1d view', '%1d views', $views, 'wp-job-manager' ), $views ) . '
'; + // translators: %1d is the number of impressions. + $views_str .= '' . sprintf( _n( '%1d impression', '%1d impressions', $impressions, 'wp-job-manager' ), $impressions ) . ''; echo wp_kses_post( $views_str ); } @@ -66,7 +75,7 @@ public function render_stats_column( $job ) { /** * Output stats column for the job listing post type admin screen. * - * @param string $column + * @param string $column */ public function maybe_render_job_listing_posts_custom_column( $column ) { global $post; @@ -75,4 +84,205 @@ public function maybe_render_job_listing_posts_custom_column( $column ) { $this->render_stats_column( $post ); } } + + /** + * Output job analytics section. + * + * @param \WP_Post $job + */ + public function output_job_stats( $job ) { + + $stat_summaries = $this->get_stat_summaries( $job ); + + $chart = $this->get_daily_stats_chart( $job ); + + get_job_manager_template( + 'job-stats.php', + [ + 'stats' => $stat_summaries, + 'chart' => $chart, + ] + ); + } + + /** + * Get data for the daily stats chart for a job. + * + * @param \WP_Post $job + * + * @return array + */ + private function get_daily_stats_chart( \WP_Post $job ): array { + + $start_date = get_post_datetime( $job ); + + if ( ! $start_date ) { + return []; + } + + $past_days = $start_date->diff( new \DateTime() )->days + 1; + + if ( $past_days > self::DAYS_PER_PAGE ) { + $start_date = $start_date->modify( '+' . ( $past_days - self::DAYS_PER_PAGE ) . ' day' ); + } + + $job_stats = new Job_Listing_Stats( $job->ID, $start_date ? [ $start_date ] : [] ); + + $daily_views = $job_stats->get_event_daily( Job_Listing_Stats::VIEW ); + $daily_uniques = $job_stats->get_event_daily( Job_Listing_Stats::VIEW_UNIQUE ); + $daily_impressions = $job_stats->get_event_daily( Job_Listing_Stats::SEARCH_IMPRESSION ); + + $max_views = ! empty( $daily_views ) ? max( $daily_views ) : 100; + $resolution = $max_views < 1000 ? 100 : 1000; + $max = max( ceil( $max_views / $resolution ) * $resolution, 100 ); + $by_day = []; + + foreach ( $daily_views as $date => $views ) { + $by_day[ $date ] = [ + 'date' => $date, + 'views' => $views, + 'uniques' => $daily_uniques[ $date ] ?? 0, + 'impressions' => $daily_impressions[ $date ] ?? 0, + 'class' => '', + ]; + } + + $end_date = \WP_Job_Manager_Post_Types::instance()->get_job_expiration( $job ); + + if ( ! $end_date ) { + $end_date = $start_date->modify( '+1 month' ); + } + + $all_days = $start_date->diff( $end_date )->days + 1; + + $all_days = min( $all_days, self::DAYS_PER_PAGE ); + + $today = ( new \DateTime() )->format( 'Y-m-d' ); + + for ( $i = 0; $i < $all_days; $i++ ) { + $date = $start_date->modify( '+' . $i . ' day' )->format( 'Y-m-d' ); + if ( empty( $by_day[ $date ] ) ) { + $by_day[ $date ] = [ + 'date' => $date, + 'views' => 0, + 'uniques' => 0, + 'impressions' => 0, + 'class' => 'future-day', + ]; + } + } + + if ( ! empty( $by_day[ $today ] ) ) { + $by_day[ $today ]['class'] = 'today'; + } + + ksort( $by_day ); + + $date_format = apply_filters( 'job_manager_get_dashboard_date_format', 'M d, Y' ); + + $by_day_formatted = []; + foreach ( $by_day as $date => $data ) { + $date_fmt = wp_date( $date_format, strtotime( $date ) ); + $by_day_formatted[ $date_fmt ] = $by_day[ $date ]; + $by_day_formatted[ $date_fmt ]['date'] = $date_fmt; + } + + $chart = [ + 'values' => $by_day_formatted, + 'max' => $max, + 'y-labels' => [ $max / 2, $max ], + ]; + + /** + * Filter the job daily stat data, displayed as a chart in the job overlay. + * + * @param array $stats Stat definition. + * @param \WP_Post $job Job post object. + */ + return apply_filters( 'job_manager_job_stats_chart', $chart, $job ); + } + + /** + * Get summaries grouped into sections for various stats. + * + * @param \WP_Post $job + * + * @return mixed + */ + private function get_stat_summaries( \WP_Post $job ) { + $job_stats = new Job_Listing_Stats( $job->ID ); + + /** + * Filter the job stat summaries, displayed in the job overlay. + * + * @param array $stats Stat definition. + * @param \WP_Post $job Job post object. + */ + $stats = apply_filters( + 'job_manager_job_stats_summary', + [ + 'views' => [ + 'title' => __( 'Total Views', 'wp-job-manager' ), + 'stats' => [ + [ + 'icon' => 'color-page-view', + 'label' => __( 'Page Views', 'wp-job-manager' ), + 'value' => $job_stats->get_event_total( Job_Listing_Stats::VIEW ), + ], + [ + 'icon' => 'color-unique-view', + 'label' => __( 'Unique Visitors', 'wp-job-manager' ), + 'value' => $job_stats->get_event_total( Job_Listing_Stats::VIEW_UNIQUE ), + ], + ], + 'column' => 1, + ], + 'impressions' => [ + 'title' => __( 'Impressions', 'wp-job-manager' ), + 'help' => __( 'How many times the listing was seen in search results.', 'wp-job-manager' ), + 'stats' => [ + [ + 'icon' => 'search', + 'label' => __( 'Search Impressions', 'wp-job-manager' ), + 'value' => $job_stats->get_event_total( Job_Listing_Stats::SEARCH_IMPRESSION ), + ], + ], + 'column' => 1, + ], + 'interest' => [ + 'title' => __( 'Interest', 'wp-job-manager' ), + 'stats' => [ + [ + 'icon' => 'search', + 'label' => __( 'Search clicks', 'wp-job-manager' ), + 'value' => 'TODO', + ], + [ + 'icon' => 'cursor', + 'label' => __( 'Apply clicks', 'wp-job-manager' ), + 'value' => $job_stats->get_event_total( Job_Listing_Stats::APPLY_CLICK ), + ], + [ + 'icon' => 'history', + 'label' => __( 'Repeat viewers', 'wp-job-manager' ), + 'value' => 'TODO', + ], + ], + 'column' => 2, + ], + ], + $job + ); + + $stat_columns = array_reduce( + $stats, + fn( $columns, $section ) => array_merge_recursive( + $columns, + [ 'column-' . $section['column'] => [ $section ] ] + ), + [] + ); + + return $stat_columns; + } } diff --git a/includes/class-stats.php b/includes/class-stats.php index 9e8ff3552..39335de74 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -20,9 +20,10 @@ class Stats { const CACHE_GROUP = 'wpjm_stats'; const DEFAULT_LOG_STAT_ARGS = [ - 'group' => '', - 'post_id' => 0, - 'increment_by' => 1, + 'group' => '', + 'post_id' => 0, + 'count' => 1, + 'date' => '', ]; private const TABLE = 'wpjm_stats'; @@ -99,13 +100,14 @@ public static function is_enabled() { /** * Log a stat into the db. * - * @param string $name The stat name. + * @param string $name The stat name. * @param array $args { * Optional args for this stat. * - * @type string $group The group this stat belongs to. - * @type int $post_id The post_id this stat belongs to. - * @type int $increment_by The amount to increment the stat by. + * @type string $group The group this stat belongs to. + * @type int $post_id The post_id this stat belongs to. + * @type int $count The amount to increment the stat by. + * @type string $date Date in YYYY-MM-DD format. * } * * @return bool @@ -113,20 +115,20 @@ public static function is_enabled() { public function log_stat( string $name, array $args = [] ) { global $wpdb; - $args = array_merge( self::DEFAULT_LOG_STAT_ARGS, $args ); - $group = $args['group']; - $post_id = absint( $args['post_id'] ); - $increment_by = $args['increment_by']; + $args = array_merge( self::DEFAULT_LOG_STAT_ARGS, $args ); + $group = $args['group']; + $post_id = absint( $args['post_id'] ); + $count = $args['count']; if ( strlen( $name ) > 255 || strlen( $group ) > 255 || ! $post_id || - ! is_integer( $increment_by ) ) { + ! is_integer( $count ) ) { return false; } - $date_today = gmdate( 'Y-m-d' ); + $date = ! empty( $args['date'] ) ? $args['date'] : gmdate( 'Y-m-d' ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery $result = $wpdb->query( @@ -136,11 +138,11 @@ public function log_stat( string $name, array $args = [] ) { 'VALUES (%s, %s, %s, %d, %d) ' . 'ON DUPLICATE KEY UPDATE `count` = `count` + %d', $name, - $date_today, + $date, $group, $post_id, - $increment_by, - $increment_by + $count, + $count ) ); @@ -155,6 +157,19 @@ public function log_stat( string $name, array $args = [] ) { return true; } + /** + * Delete all stats for a given job. + * + * @since $$next-version$$ + * + * @param int $post_id + */ + public function clear_stats( $post_id ) { + global $wpdb; + //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->wpjm_stats} WHERE post_id = %d", $post_id ) ); + } + /** * Get a cache key for a stat. * diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index 13efed702..ad4b91e80 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -10,8 +10,6 @@ exit; } -use WP_Job_Manager\Stats; - /** * Handles core plugin hooks and action setup. * @@ -100,7 +98,9 @@ public function __construct() { // Init classes. $this->forms = WP_Job_Manager_Forms::instance(); $this->post_types = WP_Job_Manager_Post_Types::instance(); - $this->stats = Stats::instance(); + $this->stats = WP_Job_Manager\Stats::instance(); + + WP_Job_Manager\Dev_Tools::init(); // Schedule cron jobs. add_action( 'init', [ __CLASS__, 'maybe_schedule_cron_jobs' ] ); @@ -149,7 +149,7 @@ public function add_to_allowed_redirect_hosts( $hosts ) { * Performs plugin activation steps. */ public function activate() { - Stats::instance()->activate(); + \WP_Job_Manager\Stats::instance()->activate(); WP_Job_Manager_Ajax::add_endpoint(); unregister_post_type( \WP_Job_Manager_Post_Types::PT_LISTING ); add_filter( 'pre_option_job_manager_enable_types', '__return_true' ); diff --git a/includes/ui/class-modal-dialog.php b/includes/ui/class-modal-dialog.php index f51bac49c..9f3f9dbdc 100644 --- a/includes/ui/class-modal-dialog.php +++ b/includes/ui/class-modal-dialog.php @@ -88,8 +88,10 @@ public function render( $content ) {
-
{$content}
- +
+
{$content}
+ +
diff --git a/includes/ui/class-ui.php b/includes/ui/class-ui.php index a899223c8..369ec2b62 100644 --- a/includes/ui/class-ui.php +++ b/includes/ui/class-ui.php @@ -46,23 +46,32 @@ class UI { private function __construct() { $this->has_ui = false; $this->css_variables = []; - add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ] ); + add_action( 'wp_enqueue_scripts', [ $this, 'register_styles' ], 5 ); + add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ], 99 ); } /** - * Register and enqueue styles. + * Register styles. + * + * @access private + */ + public function register_styles() { + \WP_Job_Manager::register_style( 'wp-job-manager-ui', 'css/ui.css', [] ); + } + + /** + * Enqueue styles and inline CSS. * * @access private */ public function enqueue_styles() { \WP_Job_Manager::register_style( 'wp-job-manager-ui', 'css/ui.css', [] ); - if ( $this->has_ui ) { + if ( $this->has_ui || wp_style_is( 'wp-job-manager-ui', 'enqueued' ) ) { wp_enqueue_style( 'wp-job-manager-ui' ); - if ( ! empty( $this->css_variables ) ) { - wp_add_inline_style( 'wp-job-manager-ui', $this->generate_inline_css() ); - } + wp_add_inline_style( 'wp-job-manager-ui', $this->generate_inline_css() ); + } } @@ -87,9 +96,20 @@ public static function ensure_styles( array $css_variables = [] ) { */ private function generate_inline_css() { - $css = ':root{'; + $vars = $this->css_variables; + + /** + * Set the accent color for frontend components. Leave blank to auto-detect and use the link color. + * + * @param string|false $color CSS color definition. + * + * @since $$next-version$$ + */ + $vars['--jm-ui-accent-color'] = apply_filters( 'job_manager_ui_accent_color', $vars['--jm-ui-accent-color'] ?? false ); + + $css = ':root {'; - foreach ( $this->css_variables as $name => $value ) { + foreach ( $vars as $name => $value ) { $css .= esc_attr( $name ) . ': ' . esc_attr( $value ) . ';'; } diff --git a/psalm.xml b/psalm.xml index 463a5a637..045f2ceec 100644 --- a/psalm.xml +++ b/psalm.xml @@ -24,6 +24,16 @@ + + + + + + + + + + diff --git a/templates/job-dashboard-overlay.php b/templates/job-dashboard-overlay.php index b6e9d9ae9..d46175a47 100644 --- a/templates/job-dashboard-overlay.php +++ b/templates/job-dashboard-overlay.php @@ -55,6 +55,8 @@
+ +
From 506208008ba7695dec7d76a196dcd17f3c04cb94 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 13 Mar 2024 14:55:00 +0100 Subject: [PATCH 38/50] Optimize stats, clean up code, respect settings (#2785) --- assets/js/job-dashboard.js | 14 +- assets/js/stats/impressions.js | 131 ++++---- assets/js/stats/observers.js | 10 +- assets/js/stats/unique.js | 72 +++-- assets/js/stats/utils.js | 20 +- assets/js/wpjm-stats.js | 169 ++++------ .../admin/class-wp-job-manager-settings.php | 41 +-- includes/class-dev-tools.php | 2 +- includes/class-job-listing-stats.php | 24 +- includes/class-stats.php | 305 +++++++----------- includes/class-wp-job-manager-install.php | 2 +- .../class-wp-job-manager-usage-tracking.php | 2 +- includes/class-wp-job-manager.php | 1 + 13 files changed, 326 insertions(+), 467 deletions(-) diff --git a/assets/js/job-dashboard.js b/assets/js/job-dashboard.js index a8d8c418f..2ae3c8acc 100644 --- a/assets/js/job-dashboard.js +++ b/assets/js/job-dashboard.js @@ -5,7 +5,7 @@ import domReady from '@wordpress/dom-ready'; import './ui'; // eslint-disable-next-line camelcase -const { i18nConfirmDelete, overlayEndpoint } = job_manager_job_dashboard; +const { i18nConfirmDelete, overlayEndpoint, statsEnabled } = job_manager_job_dashboard; function setupEvents( root ) { root @@ -65,9 +65,7 @@ async function showOverlay( eventOrId ) { setupEvents( contentElement ); } -domReady( () => { - setupEvents( document ); - +function setupStatsOverlay() { document .querySelectorAll( '.jm-dashboard-job .job-title' ) .forEach( el => el.addEventListener( 'click', showOverlay ) ); @@ -77,4 +75,12 @@ domReady( () => { if ( urlHash > 0 ) { showOverlay( +urlHash ); } +} + +domReady( () => { + setupEvents( document ); + + if ( statsEnabled ) { + setupStatsOverlay(); + } } ); diff --git a/assets/js/stats/impressions.js b/assets/js/stats/impressions.js index 1c181edf0..d2da66273 100644 --- a/assets/js/stats/impressions.js +++ b/assets/js/stats/impressions.js @@ -1,100 +1,99 @@ -import { debounce, filterZeroes, findIdInClassNames } from './utils'; +import { debounce, getPostId } from './utils'; import { waitForSelector } from './observers'; -function createStatsQueue() { - const alreadySent = {}; +function createStatsQueue( statName ) { + const sent = {}; let queue = []; - const logThem = debounce( function ( statName, listingIds ) { - const stats = listingIds.map( function ( id ) { - alreadySent[ id ] = true; - return { name: statName, post_id: id }; + function sendNow() { + const stats = queue.map( stat => { + sent[ stat.post_id ] = true; + return stat; } ); - return window.wpjmLogStats( stats ).finally( function () { - queue = []; - } ); - }, 1000 ); + queue = []; - return { - queueListingImpressionStats( statName, listingIds ) { - listingIds.forEach( function ( listingId ) { - if ( ! alreadySent[ listingId ] ) { - queue.push( listingId ); - } - } ); - logThem( statName, queue ); - }, - }; + window.WPJMStats.log( stats ); + } + + const sendLater = debounce( sendNow, 1000 ); + + function maybeSendLogs() { + if ( queue.length >= 10 ) { + sendNow(); + } else { + sendLater(); + } + } + + function queueLogStat( id ) { + if ( ! sent[ id ] ) { + queue.push( { name: statName, post_id: id } ); + maybeSendLogs(); + } + } + + return { queueLogStat }; } -function observeForVisibility( - jobListingContainer, - jobListingElement, - visibleCallback, - alreadyViewedListings -) { +function createIntersectionObserver( container, onVisible ) { + const viewed = {}; + const options = { root: null, rootMargin: '0px', threshold: 1.0, }; - const observer = new IntersectionObserver( function ( entries ) { - entries.forEach( function ( entry ) { + const observer = new IntersectionObserver( entries => { + entries.forEach( entry => { if ( entry.isIntersecting && entry.intersectionRatio > 0.99 ) { const node = entry.target; - if ( 1 === node.nodeType && node.classList.contains( 'job_listing' ) ) { - const nodeId = findIdInClassNames( node ); - if ( nodeId > 0 && ! alreadyViewedListings[ nodeId ] ) { - alreadyViewedListings[ nodeId ] = true; - visibleCallback( node ); - } + const nodeId = getPostId( node ); + if ( nodeId > 0 && ! viewed[ nodeId ] ) { + viewed[ nodeId ] = true; + onVisible( nodeId ); } observer.unobserve( node ); } } ); }, options ); - observer.observe( jobListingElement ); + return observer; } -function waitForNextVisibleListing( listingVisibleCallback ) { - const jobListingsContainer = document.querySelector( 'ul.job_listings' ); - const config = { childList: true }; - const alreadyViewed = {}; - - const observer = new MutationObserver( function ( mutations ) { - mutations.forEach( function ( mutation ) { - mutation.addedNodes.forEach( function ( node ) { - if ( 1 === node.nodeType && node.classList.contains( 'job_listing' ) ) { - observeForVisibility( jobListingsContainer, node, listingVisibleCallback, alreadyViewed ); +function observeMutations( container, { selector: itemSelector, onAdded, onRemoved } ) { + const observer = new MutationObserver( mutations => { + mutations.forEach( mutation => { + mutation.removedNodes.forEach( node => { + if ( node.matches?.( itemSelector ) ) { + onRemoved( node ); + } + } ); + mutation.addedNodes.forEach( node => { + if ( node.matches?.( itemSelector ) ) { + onAdded( node ); } } ); } ); } ); - observer.observe( jobListingsContainer, config ); + observer.observe( container, { childList: true } ); } -export function initListingImpression( stats ) { - if ( 0 === stats.length ) { - return; - } - const statName = stats[ 0 ].name; - const debouncedSender = createStatsQueue(); - waitForSelector( 'li.job_listing' ).then( function () { - const allVisibleListings = document.querySelectorAll( 'li.job_listing' ); - const initialListingIds = filterZeroes( - [ ...allVisibleListings ].map( function ( elem ) { - return findIdInClassNames( elem ); - } ) - ); - debouncedSender.queueListingImpressionStats( statName, initialListingIds ); - - waitForNextVisibleListing( function ( elem ) { - const maybeId = findIdInClassNames( elem ); - maybeId > 0 && debouncedSender.queueListingImpressionStats( statName, [ maybeId ] ); - } ); +export async function initImpressionStat( stat ) { + const { args: selectors } = stat; + const { queueLogStat } = createStatsQueue( stat.name ); + const container = await waitForSelector( selectors.container ); + + const intersections = createIntersectionObserver( container, queueLogStat ); + + const initialItems = container.querySelectorAll( selectors.item ); + initialItems.forEach( node => intersections.observe( node ) ); + + observeMutations( container, { + selector: selectors.item, + onAdded: node => intersections.observe( node ), + onRemoved: node => intersections.unobserve( node ), } ); } diff --git a/assets/js/stats/observers.js b/assets/js/stats/observers.js index 08e465cc4..f0da21f63 100644 --- a/assets/js/stats/observers.js +++ b/assets/js/stats/observers.js @@ -1,13 +1,15 @@ export function waitForSelector( selector ) { return new Promise( function ( resolve ) { - if ( document.querySelector( selector ) ) { - return resolve( document.querySelector( selector ) ); + let node = document.querySelector( selector ); + if ( node ) { + return resolve( node ); } const observer = new MutationObserver( function () { - if ( document.querySelector( selector ) ) { + node = document.querySelector( selector ); + if ( node ) { observer.disconnect(); - resolve( document.querySelector( selector ) ); + resolve( node ); } } ); diff --git a/assets/js/stats/unique.js b/assets/js/stats/unique.js index 3af65bde6..5f4c3e1ae 100644 --- a/assets/js/stats/unique.js +++ b/assets/js/stats/unique.js @@ -1,53 +1,57 @@ -export function updateDailyUnique( key ) { - const date = new Date(); - const expiresAtTimestamp = date.getTime() + 24 * 60 * 60 * 1000; +import { requestIdleCallback } from './utils'; + +const EXPIRATION = 24 * 60 * 60 * 1000; +const PREFIX = 'wpjm-stat-'; + +export function setAsRecordedToday( stat ) { + const key = `${ PREFIX }${ stat.unique_key }`; + const expiresAtTimestamp = Date.now() + EXPIRATION; window.localStorage[ key ] = expiresAtTimestamp; } -export function getDailyUnique( name ) { - if ( window.localStorage[ name ] ) { - const date = new Date(); - const now = date.getTime(); - const expiration = parseInt( window.localStorage[ name ], 10 ); - return Number.isNaN( expiration ) ? false : expiration >= now; - } - return false; +export function wasRecordedToday( stat ) { + const key = `${ PREFIX }${ stat.unique_key }`; + return ! isExpired( window.localStorage[ key ] ); } -export function setUniques( uniquesToSet ) { - uniquesToSet.forEach( function ( uniqueKey ) { - updateDailyUnique( uniqueKey ); - } ); +function isExpired( record ) { + if ( ! record ) { + return true; + } + const expiration = parseInt( record, 10 ); + return Number.isNaN( expiration ) ? true : expiration < Date.now(); } -export function checkUniqueRecordedToday( statToRecord ) { - const uniqueKey = statToRecord.unique_key || ''; - return checkUnique( statToRecord ) && true === getDailyUnique( uniqueKey ); +export function isUnique( stat ) { + return !! stat.unique_key; } -export function checkUnique( statToRecord ) { - const uniqueKey = statToRecord.unique_key || ''; - const isUnique = uniqueKey.length > 0; - - return isUnique; +export function filterAndRecordUniques( stats ) { + return stats.filter( stat => { + if ( isUnique( stat ) ) { + if ( wasRecordedToday( stat ) ) { + return false; + } + setAsRecordedToday( stat ); + } + return true; + } ); } -export function scheduleStaleUniqueCleanup( statsToRecord ) { - const twoDaysInMillis = 24 * 60 * 60 * 1000 * 2; +export function scheduleStaleUniqueCleanup() { const cleanup = function () { - const keyPrefixes = statsToRecord - .filter( s => s.unique_key && s.unique_key.length > 0 ) - .map( s => s.name ); - for ( let i = 0; i < localStorage.length; i++ ) { const key = localStorage.key( i ); - const expiry = parseInt( localStorage.getItem( key ), 10 ); - const containsUniqueKeyPrefix = keyPrefixes.some( k => key.indexOf( k ) === 0 ); - const dateNow = +new Date(); - if ( ! isNaN( expiry ) && containsUniqueKeyPrefix && expiry + twoDaysInMillis < dateNow ) { + + if ( ! key.startsWith( PREFIX ) ) { + continue; + } + + if ( isExpired( localStorage.getItem( key ) ) ) { localStorage.removeItem( key ); } } }; - setTimeout( cleanup, 1000 ); + + requestIdleCallback( cleanup ); } diff --git a/assets/js/stats/utils.js b/assets/js/stats/utils.js index 835e644d1..f48063423 100644 --- a/assets/js/stats/utils.js +++ b/assets/js/stats/utils.js @@ -10,12 +10,20 @@ export function debounce( func, delay ) { }; } -export function filterZeroes( list ) { - return list.filter( function ( i ) { - return i > 0; - } ); +export function getPostId( node ) { + return +node.className.match( /\bpost-(\d+)\b/ )?.[ 1 ] || 0; } -export function findIdInClassNames( node ) { - return +node.className.match( /\bpost-(\d+)\b/ )?.[ 1 ] || 0; +export function indexBy( list, key ) { + return list.reduce( function ( accum, item ) { + if ( ! item[ key ] ) { + return accum; + } + accum[ item[ key ] ] = item; + return accum; + }, {} ); } + +export const requestIdleCallback = window.requestIdleCallback + ? fn => window.requestIdleCallback( fn, { timeout: 500 } ) + : fn => setTimeout( fn, 100 ); diff --git a/assets/js/wpjm-stats.js b/assets/js/wpjm-stats.js index 80ef4eb4d..f8ce07bb7 100644 --- a/assets/js/wpjm-stats.js +++ b/assets/js/wpjm-stats.js @@ -1,143 +1,90 @@ import domReady from '@wordpress/dom-ready'; -import { createHooks } from '@wordpress/hooks'; -import { - setUniques, - checkUniqueRecordedToday, - checkUnique, - scheduleStaleUniqueCleanup, -} from './stats/unique'; -import { initListingImpression } from './stats/impressions'; +import { scheduleStaleUniqueCleanup, filterAndRecordUniques } from './stats/unique'; +import { initImpressionStat } from './stats/impressions'; +import { requestIdleCallback } from './stats/utils'; const WPJMStats = { - statsToRecord: [], - init( statsToRecord ) { - WPJMStats.statsToRecord = statsToRecord; - WPJMStats.hooks.doAction( 'init', WPJMStats ); + stats: [], + actions: [], + init( stats ) { + WPJMStats.stats = stats; - const statsByTrigger = statsToRecord?.reduce( function ( accum, statToRecord ) { - const triggerName = statToRecord.trigger || ''; - - if ( triggerName.length < 1 ) { - return accum; - } - - if ( ! accum[ triggerName ] ) { - accum[ triggerName ] = []; - } - - accum[ triggerName ].push( statToRecord ); - - return accum; - }, {} ); - - Object.keys( statsByTrigger ).forEach( function ( triggerName ) { - WPJMStats.hookStatsForTrigger( statsByTrigger, triggerName ); + stats.forEach( stat => { + WPJMStats.types[ stat.type ]?.( stat ); } ); - WPJMStats.hooks.doAction( 'page-load' ); - scheduleStaleUniqueCleanup( statsToRecord ); + WPJMStats.doAction( 'page-load' ); + scheduleStaleUniqueCleanup(); }, - hookStatsForTrigger( statsByTrigger, triggerName ) { - const statsToRecord = []; - const stats = statsByTrigger[ triggerName ] || []; - const statsByType = {}; - - stats.forEach( function ( statToRecord ) { - if ( ! statsByType[ statToRecord.type ] ) { - statsByType[ statToRecord.type ] = []; - } - - statsByType[ statToRecord.type ].push( statToRecord ); - statsToRecord.push( statToRecord ); - } ); - - // Hook action to call logStats. - WPJMStats.hooks.addAction( - triggerName, - 'wpjm-stats', - function () { - window.wpjmLogStats( statsToRecord ); - }, - 10 - ); - - Object.keys( statsByType ).forEach( function ( type ) { - WPJMStats.types[ type ] && WPJMStats.types[ type ]( statsByType[ type ] ); - } ); + doAction( action ) { + const stats = WPJMStats.actions[ action ]; + if ( stats ) { + WPJMStats.log( stats ); + } }, - - hooks: createHooks(), types: { - pageLoad( stats ) { - // This does not need to do anything special. + action( stat ) { + const { action } = stat; + if ( ! WPJMStats.actions[ action ] ) { + WPJMStats.actions[ action ] = []; + } + WPJMStats.actions[ action ].push( stat ); }, - domEvent( stats ) { - const events = {}; - stats.forEach( function ( statToRecord ) { - const triggerName = statToRecord.trigger; - if ( statToRecord.element && statToRecord.event ) { - const elemToAttach = document.querySelector( statToRecord.element ); - if ( elemToAttach && ! events[ statToRecord.element ] ) { - elemToAttach.addEventListener( statToRecord.event, function ( e ) { - WPJMStats.hooks.doAction( triggerName ); - } ); - events[ statToRecord.element ] = true; - } - } - } ); + domEvent( stat ) { + const { args } = stat; + if ( args.element && args.event ) { + const domElement = document.querySelector( args.element ); + const handler = stat.action + ? () => WPJMStats.doAction( stat.action ) + : () => WPJMStats.log( stat ); + domElement?.addEventListener( args.event, handler ); + } }, - initListingImpression, + impression: initImpressionStat, }, -}; - -window.WPJMStats = window.WPJMStats || WPJMStats; + async log( stats ) { + if ( stats.name ) { + stats = [ stats ]; + } -window.wpjmLogStats = - window.wpjmLogStats || - function ( stats ) { - const jobStatsSettings = window.job_manager_stats; - const ajaxUrl = jobStatsSettings.ajax_url; - const ajaxNonce = jobStatsSettings.ajax_nonce; + const { ajaxUrl, ajaxNonce, postId } = window.job_manager_stats; - const uniquesToSet = []; - const statsToRecord = []; + stats = filterAndRecordUniques( stats ); - if ( stats.length < 1 ) { - return Promise.resolve(); // Could also be an error. + if ( ! stats.length ) { + return false; } - stats.forEach( function ( statToRecord ) { - if ( ! checkUniqueRecordedToday( statToRecord ) ) { - uniquesToSet.push( statToRecord.unique_key ); - statsToRecord.push( statToRecord ); - } else if ( ! checkUnique( statToRecord ) ) { - statsToRecord.push( statToRecord ); - } + const payload = stats.map( stat => { + return [ 'name', 'group', 'post_id' ].reduce( ( m, field ) => { + if ( stat[ field ] ) { + m[ field ] = stat[ field ]; + } + return m; + }, {} ); } ); const postData = new URLSearchParams( { _ajax_nonce: ajaxNonce, - post_id: jobStatsSettings.post_id || 0, + post_id: postId, action: 'job_manager_log_stat', - stats: JSON.stringify( - statsToRecord.map( function ( stat ) { - const { name = '', group = '', post_id = 0 } = stat; - return { name, group, post_id }; - } ) - ), + stats: JSON.stringify( payload ), } ); - setUniques( uniquesToSet ); - return fetch( ajaxUrl, { method: 'POST', credentials: 'same-origin', body: postData, } ); - }; + }, +}; + +window.WPJMStats = WPJMStats; -domReady( function () { - const jobStatsSettings = window.job_manager_stats; - WPJMStats.init( jobStatsSettings.stats_to_log ); +domReady( () => { + requestIdleCallback( () => { + const { stats } = window.job_manager_stats; + WPJMStats.init( stats ); + } ); } ); diff --git a/includes/admin/class-wp-job-manager-settings.php b/includes/admin/class-wp-job-manager-settings.php index eb0dfca10..f3d6b613a 100644 --- a/includes/admin/class-wp-job-manager-settings.php +++ b/includes/admin/class-wp-job-manager-settings.php @@ -116,6 +116,15 @@ protected function init_settings() { 'default' => __( 'Default date format as defined in Settings', 'wp-job-manager' ), ], ], + [ + 'name' => \WP_Job_Manager\Stats::OPTION_ENABLE_STATS, + 'std' => '0', + 'label' => __( 'Job Statistics', 'wp-job-manager' ), + 'cb_label' => __( 'Enable job statistics', 'wp-job-manager' ), + 'desc' => __( 'Collect anonymous visitor data about job listings (page views, search impressions), and show them in the job dashboard.', 'wp-job-manager' ), + 'type' => 'checkbox', + 'attributes' => [], + ], [ 'name' => 'job_manager_google_maps_api_key', 'std' => '', @@ -128,7 +137,7 @@ protected function init_settings() { 'name' => 'job_manager_delete_data_on_uninstall', 'std' => '0', 'label' => __( 'Delete Data On Uninstall', 'wp-job-manager' ), - 'cb_label' => __( 'Delete WP Job Manager data when the plugin is deleted. Once the plugin is uninstalled, only job listings can be restored (30 days).', 'wp-job-manager' ), + 'cb_label' => __( 'Delete Job Manager data when the plugin is deleted. Once the plugin is uninstalled, only job listings can be restored (30 days).', 'wp-job-manager' ), 'desc' => '', 'type' => 'checkbox', 'attributes' => [], @@ -528,36 +537,6 @@ protected function init_settings() { ], ], ], - 'stats' => [ - __( 'Stats', 'wp-job-manager' ), - [ - [ - 'name' => 'job_manager_stats_enable', - 'std' => '0', - 'label' => __( 'Enable stats', 'wp-job-manager' ), - 'cb_label' => __( 'Enable stats', 'wp-job-manager' ), - 'desc' => __( 'Enable recording various stats (e.g. listing views) and showing them to employers', 'wp-job-manager' ), - 'type' => 'checkbox', - 'attributes' => [], - ], - [ - 'name' => 'job_manager_stats_default_date_range', - 'std' => '30', - 'label' => __( 'Stats default date range', 'wp-job-manager' ), - 'desc' => __( 'The default date range (in days) of stats to display in the frontend dashboard', 'wp-job-manager' ), - 'type' => 'number', - 'attributes' => [], - ], - [ - 'name' => 'job_manager_stats_retention', - 'std' => '90', - 'label' => __( 'Retention Period (days)', 'wp-job-manager' ), - 'desc' => __( 'Enter the maximum number of days for which statistics data will be retained.', 'wp-job-manager' ), - 'type' => 'number', - 'attributes' => [], - ], - ], - ], 'job_visibility' => [ __( 'Job Visibility', 'wp-job-manager' ), [ diff --git a/includes/class-dev-tools.php b/includes/class-dev-tools.php index 68cfaa97b..cf928213b 100644 --- a/includes/class-dev-tools.php +++ b/includes/class-dev-tools.php @@ -63,7 +63,7 @@ public static function generate_stat_data( $args ) { $log = ''; - $stats->clear_stats( $job_id ); + $stats->delete_stats( $job_id ); for ( $i = 0; $i <= $days + 1; $i++ ) { $views = (int) max( 0, $views + $trend * wp_rand( 0, 1000 ) ); diff --git a/includes/class-job-listing-stats.php b/includes/class-job-listing-stats.php index bdda1c48e..5d4835af7 100644 --- a/includes/class-job-listing-stats.php +++ b/includes/class-job-listing-stats.php @@ -16,10 +16,10 @@ */ class Job_Listing_Stats { - const VIEW = 'job_listing_view'; - const VIEW_UNIQUE = 'job_listing_view_unique'; - const SEARCH_IMPRESSION = 'job_listing_impression'; - const APPLY_CLICK = 'job_listing_apply_button_clicked'; + const VIEW = 'job_view'; + const VIEW_UNIQUE = 'job_view_unique'; + const SEARCH_IMPRESSION = 'job_search_impression'; + const APPLY_CLICK = 'job_apply_click'; /** * Job listing post ID. @@ -45,6 +45,8 @@ class Job_Listing_Stats { /** * Stats for a single job listing. * + * @since $$next-version$$ + * * @param int $job_id * @param \DateTimeInterface[] $date_range Array of start and end date. Defaults to a range from the job's publishing date to the current day. */ @@ -69,19 +71,7 @@ public function get_total_stats() { } /** - * Get daily stats for a job listing. - * - * @return array - */ - public function get_daily_stats() { - return [ - 'view' => $this->get_event_daily( self::VIEW ), - 'view_unique' => $this->get_event_daily( self::VIEW_UNIQUE ), - ]; - } - - /** - * Get totals for an event. + * Get total counts for an event. * * @param string $event * diff --git a/includes/class-stats.php b/includes/class-stats.php index 39335de74..5d5a79ec0 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -1,6 +1,6 @@ '', - 'post_id' => 0, - 'count' => 1, - 'date' => '', - ]; + /** + * Setting key for enabling stats. + */ + const OPTION_ENABLE_STATS = 'job_manager_stats_enable'; private const TABLE = 'wpjm_stats'; @@ -40,13 +43,15 @@ private function __construct() { */ public function init() { - include_once __DIR__ . '/class-job-listing-stats.php'; - include_once __DIR__ . '/class-stats-dashboard.php'; + $this->init_wpdb_alias(); + + if ( ! self::is_enabled() ) { + return; + } Stats_Dashboard::instance(); - $this->initialize_wpdb(); - $this->hook(); + $this->init_hooks(); } /** @@ -54,7 +59,7 @@ public function init() { * * @return void */ - private function initialize_wpdb() { + private function init_wpdb_alias() { global $wpdb; if ( isset( $wpdb->wpjm_stats ) ) { return; @@ -68,7 +73,7 @@ private function initialize_wpdb() { * * @return void */ - public function migrate() { + public function migrate_db() { global $wpdb; $collate = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; @@ -94,28 +99,40 @@ public function migrate() { * @return bool */ public static function is_enabled() { - return get_option( 'job_manager_stats_enable', false ); + return (bool) get_option( self::OPTION_ENABLE_STATS, false ); } /** * Log a stat into the db. * - * @param string $name The stat name. + * @param string $name The stat name. * @param array $args { * Optional args for this stat. * - * @type string $group The group this stat belongs to. - * @type int $post_id The post_id this stat belongs to. - * @type int $count The amount to increment the stat by. - * @type string $date Date in YYYY-MM-DD format. + * @type string $group The group this stat belongs to. + * @type int $post_id The post_id this stat belongs to. + * @type int $count The amount to increment the stat by. + * @type string $date Date in YYYY-MM-DD format. * } * * @return bool */ public function log_stat( string $name, array $args = [] ) { - global $wpdb; - $args = array_merge( self::DEFAULT_LOG_STAT_ARGS, $args ); + if ( ! self::is_enabled() ) { + return false; + } + + $args = wp_parse_args( + $args, + [ + 'group' => '', + 'post_id' => 0, + 'count' => 1, + 'date' => gmdate( 'Y-m-d' ), + ] + ); + $group = $args['group']; $post_id = absint( $args['post_id'] ); $count = $args['count']; @@ -128,9 +145,9 @@ public function log_stat( string $name, array $args = [] ) { return false; } - $date = ! empty( $args['date'] ) ? $args['date'] : gmdate( 'Y-m-d' ); + global $wpdb; - // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $result = $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->wpjm_stats} " . @@ -138,7 +155,7 @@ public function log_stat( string $name, array $args = [] ) { 'VALUES (%s, %s, %s, %d, %d) ' . 'ON DUPLICATE KEY UPDATE `count` = `count` + %d', $name, - $date, + $args['date'], $group, $post_id, $count, @@ -150,40 +167,20 @@ public function log_stat( string $name, array $args = [] ) { return false; } - $cache_key = $this->get_cache_key_for_stat( $name, $group, $post_id ); - - wp_cache_delete( $cache_key, 'wpjm_stats' ); - return true; } /** * Delete all stats for a given job. * - * @since $$next-version$$ - * * @param int $post_id */ - public function clear_stats( $post_id ) { + public function delete_stats( $post_id ) { global $wpdb; //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->wpjm_stats} WHERE post_id = %d", $post_id ) ); } - /** - * Get a cache key for a stat. - * - * @param string $name The name. - * @param string $group The optional group. - * @param int $post_id The optional post id. - * - * @return string - */ - private function get_cache_key_for_stat( string $name, string $group = '', int $post_id = 0 ) { - $hash = md5( "{$name}_{$group}_{$post_id}" ); - return "wpjm_stat_{$hash}"; - } - /** * Perform plugin activation-related stats actions. * @@ -197,31 +194,10 @@ public function activate() { * * @return void */ - private function hook() { - add_action( 'wp_enqueue_scripts', [ $this, 'frontend_scripts' ] ); - add_action( 'wp_ajax_job_manager_log_stat', [ $this, 'maybe_log_stat_ajax' ] ); - add_action( 'wp_ajax_nopriv_job_manager_log_stat', [ $this, 'maybe_log_stat_ajax' ] ); - add_action( 'wpjm_stats_frontend_scripts', [ $this, 'job_listing_frontend_scripts' ], 10, 1 ); - add_action( 'wpjm_stats_frontend_scripts', [ $this, 'jobs_frontend_scripts' ], 10, 1 ); - } - - /** - * Log a (non-unique) listing page view. - * - * @return void - */ - public function maybe_log_listing_view() { - if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { - return; - } - - $post_id = absint( get_queried_object_id() ); - $post_type = get_post_type( $post_id ); - if ( \WP_Job_Manager_Post_Types::PT_LISTING !== $post_type ) { - return; - } - - $this->log_stat( 'job_listing_view', [ 'post_id' => $post_id ] ); + private function init_hooks() { + add_action( 'wp_ajax_job_manager_log_stat', [ $this, 'ajax_log_stat' ] ); + add_action( 'wp_ajax_nopriv_job_manager_log_stat', [ $this, 'ajax_log_stat' ] ); + add_action( 'wp_enqueue_scripts', [ $this, 'maybe_enqueue_stats_scripts' ] ); } /** @@ -229,7 +205,7 @@ public function maybe_log_listing_view() { * * @return bool */ - public function maybe_log_stat_ajax() { + public function ajax_log_stat() { if ( ! wp_doing_ajax() ) { return false; } @@ -258,9 +234,8 @@ public function maybe_log_stat_ajax() { continue; } - $post = get_post( $post_id ); - if ( ! $this->can_record_stats_for_post( $post ) ) { - $errors[] = [ 'cannot record', $stat_data, $post ]; + if ( ! \WP_Job_Manager_Post_Types::PT_LISTING === get_post_type( $post_id ) ) { + $errors[] = [ 'cannot record', $stat_data, $post_id ]; continue; } @@ -271,7 +246,7 @@ public function maybe_log_stat_ajax() { $stat_name = trim( strtolower( $stat_data['name'] ) ); - if ( ! in_array( $stat_name, $this->get_registered_stat_names(), true ) ) { + if ( empty( $registered_stats[ $stat_name ] ) ) { $errors[] = [ 'not registered', $stat_data ]; continue; } @@ -296,106 +271,50 @@ private function get_registered_stat_names() { return array_keys( $this->get_registered_stats() ); } - /** - * Register any frontend JS scripts. - * - * @return void - */ - public function frontend_scripts() { - $post_id = absint( get_queried_object_id() ); - $post = get_post( $post_id ); - - if ( 0 === $post_id ) { - return; - } - - /** - * Delegate registration to dedicated hooks per-screen. - */ - do_action( 'wpjm_stats_frontend_scripts', $post ); - } - /** * Register any frontend scripts for job listings. * - * @param \WP_Post $post The post. - * - * @return void + * @access private */ - public function job_listing_frontend_scripts( $post ) { - $post_type = $post->post_type; - if ( \WP_Job_Manager_Post_Types::PT_LISTING !== $post_type ) { - return; - } + public function maybe_enqueue_stats_scripts() { - $this->register_frontend_scripts_for_screen( 'listing', $post->ID ); - } + \WP_Job_Manager::register_script( + 'wp-job-manager-stats', + 'js/wpjm-stats.js', + [ + 'wp-dom-ready', + 'wp-hooks', + ], + true + ); - /** - * Register any frontend scripts for a page containing 'jobs' shortcode. - * - * @param \WP_Post $post The post. - * - * @return void - */ - public function jobs_frontend_scripts( $post ) { - if ( $this->page_has_jobs_shortcode( $post ) ) { - $this->register_frontend_scripts_for_screen( 'jobs', $post->ID ); + global $post; + + if ( is_wpjm_job_listing() ) { + $this->enqueue_stats_script( 'listing', $post->ID ); } - } - /** - * Check that a certain post/page is eligible for getting recorded stats. - * - * @param \WP_Post $post The post. - * - * @return bool - */ - private function can_record_stats_for_post( $post ) { if ( $this->page_has_jobs_shortcode( $post ) ) { - return $this->filter_can_record_stats_for_post( true, $post ); - } elseif ( \WP_Job_Manager_Post_Types::PT_LISTING === $post->post_type ) { - return $this->filter_can_record_stats_for_post( true, $post ); + $this->enqueue_stats_script( 'jobs', $post->ID ); } - return $this->filter_can_record_stats_for_post( false, $post ); - } - - /** - * Run filter. - * - * @param bool $can_record Can record. - * @param \WP_Post $post The post. - * - * @return bool - */ - private function filter_can_record_stats_for_post( $can_record, $post ) { - return (bool) apply_filters( 'wpjm_stats_can_record_stats_for_post', $can_record, $post ); } /** * Register scripts for given screen. * - * @param string $page Which page. + * @param string $page Which page. * @param int $post_id Which id. + * * @return void */ - private function register_frontend_scripts_for_screen( $page = 'listing', $post_id = 0 ) { - \WP_Job_Manager::register_script( - 'wp-job-manager-stats', - 'js/wpjm-stats.js', - [ - 'wp-dom-ready', - 'wp-hooks', - ], - true - ); + private function enqueue_stats_script( $page = 'listing', $post_id = 0 ) { $script_data = [ - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'ajax_nonce' => wp_create_nonce( 'ajax-nonce' ), - 'post_id' => $post_id, - 'stats_to_log' => $this->get_stats_for_ajax( $post_id, $page ), + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'ajaxNonce' => wp_create_nonce( 'ajax-nonce' ), + 'postId' => $post_id, + 'stats' => $this->get_stats_for_ajax( $post_id, $page ), ]; wp_enqueue_script( 'wp-job-manager-stats' ); @@ -416,41 +335,44 @@ private function get_registered_stats() { return (array) apply_filters( 'wpjm_get_registered_stats', [ - 'job_listing_view' => [ - 'log_callback' => [ $this, 'log_stat' ], // Example of overriding how we log this. - 'trigger' => 'page-load', - 'type' => 'pageLoad', - 'page' => 'listing', + Job_Listing_Stats::VIEW => [ + 'type' => 'action', + 'action' => 'page-load', + 'page' => 'listing', ], - 'job_listing_view_unique' => [ - 'unique' => true, - 'type' => 'pageLoad', - 'trigger' => 'page-load', - 'page' => 'listing', + Job_Listing_Stats::VIEW_UNIQUE => [ + 'type' => 'action', + 'action' => 'page-load', + 'unique' => true, + 'page' => 'listing', ], - 'job_listing_apply_button_clicked' => [ - 'trigger' => 'apply-button-clicked', - 'type' => 'domEvent', - 'element' => 'input.application_button', - 'event' => 'click', - 'unique' => true, - 'page' => 'listing', + Job_Listing_Stats::APPLY_CLICK => [ + 'type' => 'domEvent', + 'args' => [ + 'element' => 'input.application_button', + 'event' => 'click', + ], + 'unique' => true, + 'page' => 'listing', ], - 'jobs_view' => [ - 'trigger' => 'page-load', - 'type' => 'pageLoad', - 'page' => 'jobs', + 'search_view' => [ + 'type' => 'action', + 'action' => 'page-load', + 'page' => 'jobs', ], - 'jobs_view_unique' => [ - 'trigger' => 'page-load', - 'type' => 'pageLoad', - 'page' => 'jobs', - 'unique' => true, + 'search_view_unique' => [ + 'type' => 'action', + 'action' => 'page-load', + 'page' => 'jobs', + 'unique' => true, ], - 'job_listing_impression' => [ - 'trigger' => 'job-listing-impression', - 'type' => 'initListingImpression', - 'page' => 'jobs', + Job_Listing_Stats::SEARCH_IMPRESSION => [ + 'type' => 'impression', + 'args' => [ + 'container' => 'ul.job_listings', + 'item' => 'li.job_listing', + ], + 'page' => 'jobs', ], ] ); @@ -460,7 +382,7 @@ private function get_registered_stats() { * Determine what stats should be added to the kind of page the user is viewing. * * @param int $post_id Optional post id. - * @param string $page The page in question. + * @param string $page The page in question. * * @return array */ @@ -475,9 +397,8 @@ private function get_stats_for_ajax( $post_id = 0, $page = 'listing' ) { 'name' => $stat_name, 'post_id' => $post_id, 'type' => $stat_data['type'] ?? '', - 'trigger' => $stat_data['trigger'] ?? '', - 'element' => $stat_data['element'] ?? '', - 'event' => $stat_data['event'] ?? '', + 'action' => $stat_data['action'] ?? '', + 'args' => $stat_data['args'] ?? '', ]; if ( ! empty( $stat_data['unique'] ) ) { @@ -494,6 +415,8 @@ private function get_stats_for_ajax( $post_id = 0, $page = 'listing' ) { /** * Derive unique key by post id. * + * @access private + * * @param string $stat_name Name. * @param int $post_id Post id. * @@ -511,6 +434,6 @@ public function unique_by_post_id( $stat_name, $post_id ) { * @return bool */ public function page_has_jobs_shortcode( $post ) { - return has_shortcode( $post->post_content, 'jobs' ); + return $post && has_shortcode( $post->post_content, 'jobs' ); } } diff --git a/includes/class-wp-job-manager-install.php b/includes/class-wp-job-manager-install.php index 0ae9e4275..c289376ab 100644 --- a/includes/class-wp-job-manager-install.php +++ b/includes/class-wp-job-manager-install.php @@ -76,7 +76,7 @@ public static function install() { update_option( 'job_manager_permalinks', wp_json_encode( $permalink_options ) ); } - \WP_Job_Manager\Stats::instance()->migrate(); + \WP_Job_Manager\Stats::instance()->migrate_db(); delete_transient( 'wp_job_manager_addons_html' ); update_option( 'wp_job_manager_version', JOB_MANAGER_VERSION ); diff --git a/includes/class-wp-job-manager-usage-tracking.php b/includes/class-wp-job-manager-usage-tracking.php index c3285f591..d354ab522 100644 --- a/includes/class-wp-job-manager-usage-tracking.php +++ b/includes/class-wp-job-manager-usage-tracking.php @@ -267,7 +267,7 @@ public function opt_in_checkbox_text() { return sprintf( // translators: the href tag contains the URL for the page telling users what data WPJM tracks. __( - 'Help us make WP Job Manager better by allowing us to collect + 'Help us make Job Manager better by allowing us to collect usage tracking data. No sensitive information is collected.', 'wp-job-manager' diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index ad4b91e80..d6f4e47bd 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -577,6 +577,7 @@ public function frontend_scripts() { [ 'i18nConfirmDelete' => esc_html__( 'Are you sure you want to delete this listing?', 'wp-job-manager' ), 'overlayEndpoint' => WP_Job_Manager_Ajax::get_endpoint( 'job_dashboard_overlay' ), + 'statsEnabled' => \WP_Job_Manager\Stats::is_enabled(), ] ); From 9c7280c2cb9b4d3f6878a7de9b606c4df874e185 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 20 Mar 2024 16:15:16 +0100 Subject: [PATCH 39/50] Add stats batch logging, unit tests, small tweaks (#2787) --- assets/css/ui.elements.scss | 6 +- includes/class-dev-tools.php | 50 ++- includes/class-job-listing-stats.php | 13 - includes/class-stats-dashboard.php | 30 +- includes/class-stats-script.php | 244 ++++++++++++ includes/class-stats.php | 359 +++++------------- includes/class-wp-job-manager.php | 1 - includes/ui/class-ui-elements.php | 10 +- templates/job-stats.php | 22 +- tests/php/tests/includes/test_class.stats.php | 171 +++++++++ 10 files changed, 579 insertions(+), 327 deletions(-) create mode 100644 includes/class-stats-script.php create mode 100644 tests/php/tests/includes/test_class.stats.php diff --git a/assets/css/ui.elements.scss b/assets/css/ui.elements.scss index 5c94ec373..1d2bf31e7 100644 --- a/assets/css/ui.elements.scss +++ b/assets/css/ui.elements.scss @@ -145,9 +145,11 @@ width: var(--jm-ui-icon-size); height: var(--jm-ui-icon-size); mask-size: 100% 100%; - background-color: currentColor; - mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='black' stroke-dasharray='4 4' stroke-width='1.5' d='M20 12a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z'/%3e%3c/svg%3e"); + &:where(:not(.jm-ui-icon--svg)) { + background-color: currentColor; + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cpath stroke=\'black\' stroke-dasharray=\'4 4\' stroke-width=\'1.5\' d=\'M20 12a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z\'/%3e%3c/svg%3e'); + } &[data-icon=check] { mask-image: var(--jm-ui-svg-check); diff --git a/includes/class-dev-tools.php b/includes/class-dev-tools.php index cf928213b..35910f1a2 100644 --- a/includes/class-dev-tools.php +++ b/includes/class-dev-tools.php @@ -65,6 +65,8 @@ public static function generate_stat_data( $args ) { $stats->delete_stats( $job_id ); + $records = []; + for ( $i = 0; $i <= $days + 1; $i++ ) { $views = (int) max( 0, $views + $trend * wp_rand( 0, 1000 ) ); $trend = wp_rand( 0, 10 ) / 10 - 0.5; @@ -74,35 +76,31 @@ public static function generate_stat_data( $args ) { $log .= $views . ' '; - $stats->log_stat( - Job_Listing_Stats::VIEW, - [ - 'post_id' => $job_id, - 'count' => $views, - 'date' => $date, - ] - ); - - $stats->log_stat( - Job_Listing_Stats::VIEW_UNIQUE, - [ - 'post_id' => $job_id, - 'count' => $unique_views, - 'date' => $date, - ] - ); - - $stats->log_stat( - Job_Listing_Stats::SEARCH_IMPRESSION, - [ - 'post_id' => $job_id, - 'count' => $impressions, - 'date' => $date, - ] - ); + $records[] = [ + 'name' => Job_Listing_Stats::VIEW, + 'post_id' => $job_id, + 'count' => $views, + 'date' => $date, + ]; + + $records[] = [ + 'name' => Job_Listing_Stats::VIEW_UNIQUE, + 'post_id' => $job_id, + 'count' => $unique_views, + 'date' => $date, + ]; + + $records[] = [ + 'name' => Job_Listing_Stats::SEARCH_IMPRESSION, + 'post_id' => $job_id, + 'count' => $impressions, + 'date' => $date, + ]; } + $stats->batch_log_stats( $records ); + \WP_CLI::log( \WP_CLI::colorize( 'Stats generated from %C' . $start_date->format( 'Y-m-d' ) . '%n for %C' . $days . ' days%n: ' ) . $log ); } diff --git a/includes/class-job-listing-stats.php b/includes/class-job-listing-stats.php index 5d4835af7..efcd3f3a9 100644 --- a/includes/class-job-listing-stats.php +++ b/includes/class-job-listing-stats.php @@ -57,19 +57,6 @@ public function __construct( $job_id, $date_range = [] ) { $this->end_date = ( $date_range[1] ?? new \DateTime() )->format( 'Y-m-d' ); } - /** - * Get total stats for a job listing. - * - * @return array - */ - public function get_total_stats() { - return [ - 'view' => $this->get_event_total( self::VIEW ), - 'view_unique' => $this->get_event_total( self::VIEW_UNIQUE ), - 'search' => $this->get_event_total( self::SEARCH_IMPRESSION ), - ]; - } - /** * Get total counts for an event. * diff --git a/includes/class-stats-dashboard.php b/includes/class-stats-dashboard.php index 48e82ae24..c7457cfd6 100644 --- a/includes/class-stats-dashboard.php +++ b/includes/class-stats-dashboard.php @@ -212,6 +212,12 @@ private function get_daily_stats_chart( \WP_Post $job ): array { private function get_stat_summaries( \WP_Post $job ) { $job_stats = new Job_Listing_Stats( $job->ID ); + $views = $job_stats->get_event_total( Job_Listing_Stats::VIEW ); + $views_unique = $job_stats->get_event_total( Job_Listing_Stats::VIEW_UNIQUE ); + $views_repeat = $views - $views_unique; + $search_impressions = $job_stats->get_event_total( Job_Listing_Stats::SEARCH_IMPRESSION ); + $search_clicks = $search_impressions ? $views_unique / $search_impressions * 100 : 0; + /** * Filter the job stat summaries, displayed in the job overlay. * @@ -226,13 +232,13 @@ private function get_stat_summaries( \WP_Post $job ) { 'stats' => [ [ 'icon' => 'color-page-view', - 'label' => __( 'Page Views', 'wp-job-manager' ), - 'value' => $job_stats->get_event_total( Job_Listing_Stats::VIEW ), + 'label' => __( 'Page views', 'wp-job-manager' ), + 'value' => $views, ], [ 'icon' => 'color-unique-view', - 'label' => __( 'Unique Visitors', 'wp-job-manager' ), - 'value' => $job_stats->get_event_total( Job_Listing_Stats::VIEW_UNIQUE ), + 'label' => __( 'Unique visitors', 'wp-job-manager' ), + 'value' => $views_unique, ], ], 'column' => 1, @@ -243,8 +249,8 @@ private function get_stat_summaries( \WP_Post $job ) { 'stats' => [ [ 'icon' => 'search', - 'label' => __( 'Search Impressions', 'wp-job-manager' ), - 'value' => $job_stats->get_event_total( Job_Listing_Stats::SEARCH_IMPRESSION ), + 'label' => __( 'Search impressions', 'wp-job-manager' ), + 'value' => $search_impressions, ], ], 'column' => 1, @@ -253,9 +259,9 @@ private function get_stat_summaries( \WP_Post $job ) { 'title' => __( 'Interest', 'wp-job-manager' ), 'stats' => [ [ - 'icon' => 'search', - 'label' => __( 'Search clicks', 'wp-job-manager' ), - 'value' => 'TODO', + 'icon' => 'search', + 'label' => __( 'Search click-through rate', 'wp-job-manager' ), + 'percent' => $search_clicks, ], [ 'icon' => 'cursor', @@ -263,9 +269,9 @@ private function get_stat_summaries( \WP_Post $job ) { 'value' => $job_stats->get_event_total( Job_Listing_Stats::APPLY_CLICK ), ], [ - 'icon' => 'history', - 'label' => __( 'Repeat viewers', 'wp-job-manager' ), - 'value' => 'TODO', + 'icon' => 'url("data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cg fill=\'black\'%3e%3cpath d=\'M16.6 7.4a6.5 6.5 0 0 0-9.17-.02L8.7 8.66l-3.9.36.36-3.9 1.2 1.2a8 8 0 1 1-2.3 6.72l1.49-.2A6.5 6.5 0 1 0 16.6 7.4Z\'/%3e%3cpath d=\'m12 7-1 5c0 .37.2.7.51.87l4.13 2.76-2.74-4.11L12 7Z\'/%3e%3c/g%3e%3c/svg%3e")', + 'label' => __( 'Repeat Views', 'wp-job-manager' ), + 'value' => $views_repeat, ], ], 'column' => 2, diff --git a/includes/class-stats-script.php b/includes/class-stats-script.php new file mode 100644 index 000000000..180b7a508 --- /dev/null +++ b/includes/class-stats-script.php @@ -0,0 +1,244 @@ +get_registered_stat_names(); + + $stats = array_filter( + $stats, + function( $stat ) use ( $registered_stats ) { + return in_array( $stat['name'], $registered_stats, true ); + } + ); + + return Stats::instance()->batch_log_stats( $stats ); + } + + /** + * Get stat names. + * + * @return int[]|string[] + */ + private function get_registered_stat_names() { + return array_keys( $this->get_registered_stats() ); + } + + /** + * Register any frontend scripts for job listings. + * + * @access private + */ + public function maybe_enqueue_stats_scripts() { + + \WP_Job_Manager::register_script( + 'wp-job-manager-stats', + 'js/wpjm-stats.js', + [ + 'wp-dom-ready', + 'wp-hooks', + ], + true + ); + + global $post; + + if ( is_wpjm_job_listing() ) { + $this->enqueue_stats_script( 'listing', $post->ID ); + } + + if ( $this->page_has_jobs_shortcode( $post ) ) { + $this->enqueue_stats_script( 'jobs', $post->ID ); + } + + } + + /** + * Register scripts for given screen. + * + * @param string $page Which page. + * @param int $post_id Which id. + * + * @return void + */ + private function enqueue_stats_script( $page = 'listing', $post_id = 0 ) { + + $script_data = [ + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'ajaxNonce' => wp_create_nonce( 'ajax-nonce' ), + 'postId' => $post_id, + 'stats' => $this->get_stats_for_ajax( $post_id, $page ), + ]; + + wp_enqueue_script( 'wp-job-manager-stats' ); + wp_localize_script( + 'wp-job-manager-stats', + 'job_manager_stats', + $script_data + ); + + } + + /** + * Get all the registered stats. + * + * @return array + */ + private function get_registered_stats() { + return (array) apply_filters( + 'wpjm_get_registered_stats', + [ + Job_Listing_Stats::VIEW => [ + 'type' => 'action', + 'action' => 'page-load', + 'page' => 'listing', + ], + Job_Listing_Stats::VIEW_UNIQUE => [ + 'type' => 'action', + 'action' => 'page-load', + 'unique' => true, + 'page' => 'listing', + ], + Job_Listing_Stats::APPLY_CLICK => [ + 'type' => 'domEvent', + 'args' => [ + 'element' => 'input.application_button', + 'event' => 'click', + ], + 'unique' => true, + 'page' => 'listing', + ], + 'search_view' => [ + 'type' => 'action', + 'action' => 'page-load', + 'page' => 'jobs', + ], + 'search_view_unique' => [ + 'type' => 'action', + 'action' => 'page-load', + 'page' => 'jobs', + 'unique' => true, + ], + Job_Listing_Stats::SEARCH_IMPRESSION => [ + 'type' => 'impression', + 'args' => [ + 'container' => 'ul.job_listings', + 'item' => 'li.job_listing', + ], + 'page' => 'jobs', + ], + ] + ); + } + + /** + * Determine what stats should be added to the kind of page the user is viewing. + * + * @param int $post_id Optional post id. + * @param string $page The page in question. + * + * @return array + */ + private function get_stats_for_ajax( $post_id = 0, $page = 'listing' ) { + $ajax_stats = []; + foreach ( $this->get_registered_stats() as $stat_name => $stat_data ) { + if ( $page !== $stat_data['page'] ) { + continue; + } + + $stat_ajax = [ + 'name' => $stat_name, + 'post_id' => $post_id, + 'type' => $stat_data['type'] ?? '', + 'action' => $stat_data['action'] ?? '', + 'args' => $stat_data['args'] ?? '', + ]; + + if ( ! empty( $stat_data['unique'] ) ) { + $unique_callback = $stat_data['unique_callback'] ?? [ $this, 'get_post_id_unique_key' ]; + $stat_ajax['unique_key'] = call_user_func( $unique_callback, $stat_name, $post_id ); + } + + $ajax_stats[] = $stat_ajax; + } + + return $ajax_stats; + } + + /** + * Derive unique key by post id. + * + * @access private + * + * @param string $stat_name Name. + * @param int $post_id Post id. + * + * @return string + */ + public function get_post_id_unique_key( $stat_name, $post_id ) { + return $stat_name . '_' . $post_id; + } + + /** + * Any page containing a job shortcode is eligible. + * + * @param \WP_Post $post The post. + * + * @return bool + */ + private function page_has_jobs_shortcode( $post ) { + return $post && has_shortcode( $post->post_content, 'jobs' ); + } + +} diff --git a/includes/class-stats.php b/includes/class-stats.php index 5d5a79ec0..f3d052ece 100644 --- a/includes/class-stats.php +++ b/includes/class-stats.php @@ -50,8 +50,7 @@ public function init() { } Stats_Dashboard::instance(); - - $this->init_hooks(); + Stats_Script::instance(); } /** @@ -83,8 +82,8 @@ public function migrate_db() { `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `date` date NOT NULL, `post_id` bigint(20) DEFAULT NULL, - `name` varchar(255) NOT NULL, - `group` varchar(255) DEFAULT '', + `name` varchar(50) NOT NULL, + `group` varchar(50) DEFAULT '', `count` bigint(20) unsigned not null default 1, PRIMARY KEY (`id`), UNIQUE INDEX `idx_wpjm_stats_name_date_group_post_id` (`name`, `date`, `group`, `post_id`) @@ -123,139 +122,61 @@ public function log_stat( string $name, array $args = [] ) { return false; } - $args = wp_parse_args( - $args, + return $this->batch_log_stats( [ - 'group' => '', - 'post_id' => 0, - 'count' => 1, - 'date' => gmdate( 'Y-m-d' ), + array_merge( + [ 'name' => $name ], + $args + ), ] ); - - $group = $args['group']; - $post_id = absint( $args['post_id'] ); - $count = $args['count']; - - if ( - strlen( $name ) > 255 || - strlen( $group ) > 255 || - ! $post_id || - ! is_integer( $count ) ) { - return false; - } - - global $wpdb; - - // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $result = $wpdb->query( - $wpdb->prepare( - "INSERT INTO {$wpdb->wpjm_stats} " . - '(`name`, `date`, `group`, `post_id`, `count` ) ' . - 'VALUES (%s, %s, %s, %d, %d) ' . - 'ON DUPLICATE KEY UPDATE `count` = `count` + %d', - $name, - $args['date'], - $group, - $post_id, - $count, - $count - ) - ); - - if ( false === $result ) { - return false; - } - - return true; } /** - * Delete all stats for a given job. + * Log a stat for multiple posts in one query. * - * @param int $post_id - */ - public function delete_stats( $post_id ) { - global $wpdb; - //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->wpjm_stats} WHERE post_id = %d", $post_id ) ); - } - - /** - * Perform plugin activation-related stats actions. + * @param array[] $stats { + * Array of stats to log, with the following fields. * - * @return void - */ - public function activate() { - } - - /** - * Run any hooks related to stats. - * - * @return void - */ - private function init_hooks() { - add_action( 'wp_ajax_job_manager_log_stat', [ $this, 'ajax_log_stat' ] ); - add_action( 'wp_ajax_nopriv_job_manager_log_stat', [ $this, 'ajax_log_stat' ] ); - add_action( 'wp_enqueue_scripts', [ $this, 'maybe_enqueue_stats_scripts' ] ); - } - - /** - * Log multiple stats in one go. Triggered in an ajax call. + * @type string $name The stat name. + * @type int $post_id Post ids to log the stat for. + * @type string $group Additional data (eg keyword) for the stat. + * @type int $count The amount to increment the stat by. + * @type string $date Date in YYYY-MM-DD format. + * } * * @return bool */ - public function ajax_log_stat() { - if ( ! wp_doing_ajax() ) { - return false; - } + public function batch_log_stats( array $stats ) { - $post_data = stripslashes_deep( $_POST ); - - if ( ! isset( $post_data['_ajax_nonce'] ) || ! wp_verify_nonce( $post_data['_ajax_nonce'], 'ajax-nonce' ) ) { + if ( ! self::is_enabled() ) { return false; } - - $stats_json = $post_data['stats'] ?? '[]'; - $stats = json_decode( $stats_json, true ); + $stats = array_map( [ $this, 'parse_stats' ], $stats ); + $stats = array_filter( $stats ); if ( empty( $stats ) ) { return false; } - $errors = []; - $registered_stats = $this->get_registered_stats(); - - foreach ( $stats as $stat_data ) { - $post_id = isset( $stat_data['post_id'] ) ? absint( $stat_data['post_id'] ) : 0; - - if ( empty( $post_id ) ) { - $errors[] = [ 'missing post_id', $stat_data ]; - continue; - } - - if ( ! \WP_Job_Manager_Post_Types::PT_LISTING === get_post_type( $post_id ) ) { - $errors[] = [ 'cannot record', $stat_data, $post_id ]; - continue; - } - - if ( ! isset( $stat_data['name'] ) ) { - $errors[] = [ 'no name', $stat_data ]; - continue; - } - - $stat_name = trim( strtolower( $stat_data['name'] ) ); + global $wpdb; - if ( empty( $registered_stats[ $stat_name ] ) ) { - $errors[] = [ 'not registered', $stat_data ]; - continue; - } + $values = []; - $log_callback = $registered_stats[ $stat_name ]['log_callback'] ?? [ $this, 'log_stat' ]; - call_user_func( $log_callback, trim( $stat_name ), [ 'post_id' => $post_id ] ); + foreach ( $stats as $stat ) { + $values[] = $wpdb->prepare( '(%s, %s, %s, %d, %d)', $stat['name'], $stat['date'], $stat['group'], $stat['post_id'], $stat['count'] ); } - if ( ! empty( $errors ) ) { + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, + $result = $wpdb->query( + "INSERT INTO {$wpdb->wpjm_stats} " . + '(`name`, `date`, `group`, `post_id`, `count` )' . + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + 'VALUES ' . implode( ', ', $values ) . + 'ON DUPLICATE KEY UPDATE `count` = `count` + VALUES(`count`)', + ); + + if ( false === $result ) { return false; } @@ -263,177 +184,93 @@ public function ajax_log_stat() { } /** - * Get stat names. + * Process and validate stat details. * - * @return int[]|string[] - */ - private function get_registered_stat_names() { - return array_keys( $this->get_registered_stats() ); - } - - /** - * Register any frontend scripts for job listings. + * @param array $args { + * Stat data. + * + * @type string $name The stat name. + * @type string $group Additional data (eg keyword) for the stat. + * @type int $post_id The post_id this stat belongs to. + * @type int $count The amount to increment the stat by. + * @type string $date Date in YYYY-MM-DD format. + * } * - * @access private + * @return array|false */ - public function maybe_enqueue_stats_scripts() { - - \WP_Job_Manager::register_script( - 'wp-job-manager-stats', - 'js/wpjm-stats.js', + private function parse_stats( $args ) { + $args = wp_parse_args( + $args, [ - 'wp-dom-ready', - 'wp-hooks', - ], - true + 'name' => '', + 'group' => '', + 'post_id' => 0, + 'count' => 1, + 'date' => gmdate( 'Y-m-d' ), + ] ); - global $post; + $args['post_id'] = absint( $args['post_id'] ); - if ( is_wpjm_job_listing() ) { - $this->enqueue_stats_script( 'listing', $post->ID ); - } - - if ( $this->page_has_jobs_shortcode( $post ) ) { - $this->enqueue_stats_script( 'jobs', $post->ID ); + if ( + empty( $args['name'] ) || + strlen( $args['name'] ) > 50 || + strlen( $args['group'] ) > 50 || + empty( $args['post_id'] ) || + ! is_integer( $args['count'] ) ) { + return false; } + return $args; } /** - * Register scripts for given screen. - * - * @param string $page Which page. - * @param int $post_id Which id. - * - * @return void - */ - private function enqueue_stats_script( $page = 'listing', $post_id = 0 ) { - - $script_data = [ - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'ajaxNonce' => wp_create_nonce( 'ajax-nonce' ), - 'postId' => $post_id, - 'stats' => $this->get_stats_for_ajax( $post_id, $page ), - ]; - - wp_enqueue_script( 'wp-job-manager-stats' ); - wp_localize_script( - 'wp-job-manager-stats', - 'job_manager_stats', - $script_data - ); - - } - - /** - * Get all the registered stats. + * Delete all stats for a given job. * - * @return array + * @param int $post_id */ - private function get_registered_stats() { - return (array) apply_filters( - 'wpjm_get_registered_stats', - [ - Job_Listing_Stats::VIEW => [ - 'type' => 'action', - 'action' => 'page-load', - 'page' => 'listing', - ], - Job_Listing_Stats::VIEW_UNIQUE => [ - 'type' => 'action', - 'action' => 'page-load', - 'unique' => true, - 'page' => 'listing', - ], - Job_Listing_Stats::APPLY_CLICK => [ - 'type' => 'domEvent', - 'args' => [ - 'element' => 'input.application_button', - 'event' => 'click', - ], - 'unique' => true, - 'page' => 'listing', - ], - 'search_view' => [ - 'type' => 'action', - 'action' => 'page-load', - 'page' => 'jobs', - ], - 'search_view_unique' => [ - 'type' => 'action', - 'action' => 'page-load', - 'page' => 'jobs', - 'unique' => true, - ], - Job_Listing_Stats::SEARCH_IMPRESSION => [ - 'type' => 'impression', - 'args' => [ - 'container' => 'ul.job_listings', - 'item' => 'li.job_listing', - ], - 'page' => 'jobs', - ], - ] - ); + public function delete_stats( $post_id ) { + global $wpdb; + //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->wpjm_stats} WHERE post_id = %d", $post_id ) ); } /** - * Determine what stats should be added to the kind of page the user is viewing. + * Delete all stats for a given job. * - * @param int $post_id Optional post id. - * @param string $page The page in question. + * @param string $stat_name + * @param int $post_id + * @param null $date * * @return array */ - private function get_stats_for_ajax( $post_id = 0, $page = 'listing' ) { - $ajax_stats = []; - foreach ( $this->get_registered_stats() as $stat_name => $stat_data ) { - if ( $page !== $stat_data['page'] ) { - continue; - } - - $stat_ajax = [ - 'name' => $stat_name, - 'post_id' => $post_id, - 'type' => $stat_data['type'] ?? '', - 'action' => $stat_data['action'] ?? '', - 'args' => $stat_data['args'] ?? '', - ]; - - if ( ! empty( $stat_data['unique'] ) ) { - $unique_callback = $stat_data['unique_callback'] ?? [ $this, 'unique_by_post_id' ]; - $stat_ajax['unique_key'] = call_user_func( $unique_callback, $stat_name, $post_id ); - } - - $ajax_stats[] = $stat_ajax; + public function get_stats( $stat_name = '', $post_id = null, $date = null ) { + global $wpdb; + + $query = "SELECT * FROM {$wpdb->wpjm_stats} WHERE 1=1 "; + $params = []; + + if ( ! empty( $stat_name ) ) { + $query .= ' AND name = %s'; + $params[] = $stat_name; } - return $ajax_stats; - } + if ( ! empty( $post_id ) ) { + $query .= ' AND post_id = %d'; + $params[] = $post_id; + } - /** - * Derive unique key by post id. - * - * @access private - * - * @param string $stat_name Name. - * @param int $post_id Post id. - * - * @return string - */ - public function unique_by_post_id( $stat_name, $post_id ) { - return $stat_name . '_' . $post_id; - } + if ( ! empty( $date ) ) { + $query .= ' AND date = %s'; + $params[] = $date; + } - /** - * Any page containing a job shortcode is eligible. - * - * @param \WP_Post $post The post. - * - * @return bool - */ - public function page_has_jobs_shortcode( $post ) { - return $post && has_shortcode( $post->post_content, 'jobs' ); + if ( ! empty( $params ) ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query. + $query = $wpdb->prepare( $query, $params ); + } + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared above. + return $wpdb->get_results( $query ); } } diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index d6f4e47bd..dead66ca6 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -149,7 +149,6 @@ public function add_to_allowed_redirect_hosts( $hosts ) { * Performs plugin activation steps. */ public function activate() { - \WP_Job_Manager\Stats::instance()->activate(); WP_Job_Manager_Ajax::add_endpoint(); unregister_post_type( \WP_Job_Manager_Post_Types::PT_LISTING ); add_filter( 'pre_option_job_manager_enable_types', '__return_true' ); diff --git a/includes/ui/class-ui-elements.php b/includes/ui/class-ui-elements.php index 9af2220d5..a86cd4bd2 100644 --- a/includes/ui/class-ui-elements.php +++ b/includes/ui/class-ui-elements.php @@ -39,8 +39,10 @@ public static function icon( $icon, $label = '' ) { $html = ''; $is_classname = preg_match( '/^[\w-]+$/i', $icon ); + $is_url = preg_match( '/^url\(/i', $icon ); + $is_svg = preg_match( '/^' . esc_html( $label ) . ''; + echo '
' . esc_html( number_format_i18n( $label ) ) . '
'; } ?> @@ -56,16 +56,16 @@
- - + +
- - + +
- - + +
@@ -115,10 +115,12 @@
- - + + + + % + class="jm-stat-value-percent">%
diff --git a/tests/php/tests/includes/test_class.stats.php b/tests/php/tests/includes/test_class.stats.php new file mode 100644 index 000000000..2d2a62c4c --- /dev/null +++ b/tests/php/tests/includes/test_class.stats.php @@ -0,0 +1,171 @@ +migrate_db(); + } + + public function tearDown(): void { + parent::tearDown(); + + } + + public function test_log_stat_creates_record() { + $job_id = $this->factory->job_listing->create(); + + Stats::instance()->log_stat( 'test_stat', [ 'post_id' => $job_id, 'count' => 1 ] ); + + $stats = Stats::instance()->get_stats( null, $job_id ); + + $this->assertNotEmpty( $stats ); + $this->assertEquals( 'test_stat', $stats[0]->name ); + + } + + public function test_log_stat_exits_when_disabled() { + update_option( Stats::OPTION_ENABLE_STATS, false ); + Stats::instance()->log_stat( 'test_stat', [ 'post_id' => 1, 'count' => 1 ] ); + + $stats = Stats::instance()->get_stats( null, 1 ); + + $this->assertEmpty( $stats ); + + } + + public function test_log_stat_increases_count() { + + Stats::instance()->log_stat( 'test_stat', [ 'post_id' => 1, 'count' => 1 ] ); + Stats::instance()->log_stat( 'test_stat', [ 'post_id' => 1, 'count' => 1 ] ); + + $stats = Stats::instance()->get_stats( 'test_stat', 1 ); + + $this->assertNotEmpty( $stats ); + $this->assertEquals( 2, $stats[0]->count ); + + } + + public function test_batch_log_stats_creates_records() { + + $stats = [ + [ 'name' => 'test_stat', 'post_id' => 1, 'count' => 1 ], + [ 'name' => 'test_stat', 'post_id' => 2, 'count' => 2 ], + [ 'name' => 'test_stat_2', 'post_id' => 1, 'count' => 1 ], + ]; + + Stats::instance()->batch_log_stats( $stats ); + + $stats = Stats::instance()->get_stats(); + + $this->assertCount( 3, $stats ); + + } + + public function test_batch_log_stats_increases_counts() { + + $stats = [ + [ 'name' => 'test_stat', 'post_id' => 1, 'count' => 1 ], + [ 'name' => 'test_stat', 'post_id' => 1, 'count' => 1 ], + [ 'name' => 'test_stat', 'post_id' => 2, 'count' => 2 ], + ]; + + Stats::instance()->batch_log_stats( $stats ); + Stats::instance()->batch_log_stats( $stats ); + + $stats = Stats::instance()->get_stats(); + + $this->assertCount( 2, $stats ); + $this->assertEquals( 4, $stats[0]->count ); + $this->assertEquals( 4, $stats[1]->count ); + + } + + public function test_delete_stats_deletes_post_stats() { + + $stats = [ + [ 'name' => 'test_stat', 'post_id' => 1, 'count' => 1 ], + [ 'name' => 'test_stat', 'post_id' => 2, 'count' => 2 ], + [ 'name' => 'test_stat_2', 'post_id' => 1, 'count' => 1 ], + ]; + + Stats::instance()->batch_log_stats( $stats ); + + Stats::instance()->delete_stats( 1 ); + + $stats = Stats::instance()->get_stats( null, 1 ); + + $this->assertEmpty( $stats ); + + } + + public function test_job_listing_stats_counts_totals() { + + $job_id = $this->factory->job_listing->create(); + + Stats::instance()->batch_log_stats( [ + [ 'name' => 'test_stat', 'post_id' => $job_id, 'count' => 1 ], + [ 'name' => 'test_stat', 'post_id' => $job_id, 'count' => 1 ], + ] ); + + $job_stats = new Job_Listing_Stats( $job_id ); + + $total = $job_stats->get_event_total( 'test_stat' ); + + $this->assertEquals( 2, $total ); + } + + public function test_job_listing_stats_counts_daily_stats() { + + $job_id = $this->factory->job_listing->create(); + + Stats::instance()->batch_log_stats( [ + [ 'name' => 'test_stat', 'post_id' => $job_id, 'count' => 1, 'date' => '2020-01-01' ], + [ 'name' => 'test_stat', 'post_id' => $job_id, 'count' => 1, 'date' => '2020-01-01' ], + [ 'name' => 'test_stat', 'post_id' => $job_id, 'count' => 1, 'date' => '2020-01-02' ], + ] ); + + $job_stats = new Job_Listing_Stats( $job_id, [ + new \DateTime( '2020-01-01' ), + new \DateTime( '2020-01-02' ), + ] ); + + $daily = $job_stats->get_event_daily( 'test_stat' ); + + $this->assertEquals( [ '2020-01-01' => 2, '2020-01-02' => 1 ], $daily ); + + } + + public function test_ajax_stats_logged() { + + Stats_Script::instance(); + + $job_id = $this->factory->job_listing->create(); + + $_POST = [ + 'stats' => json_encode( [ + [ + 'post_id' => $job_id, + 'name' => 'job_view', + ], + [ + 'post_id' => $job_id, + 'name' => 'job_view_unique', + ], + ] ), + '_ajax_nonce' => wp_create_nonce( 'ajax-nonce' ), + ]; + + do_action( 'wp_ajax_job_manager_log_stat' ); + + $stats = Stats::instance()->get_stats( null, $job_id ); + + $this->assertEquals( [ 'job_view', 'job_view_unique' ], wp_list_pluck( $stats, 'name' ) ); + + } + +} From 658e0bd37e306df29fc9854d2909ea0a51290194 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 20 Mar 2024 17:26:25 +0100 Subject: [PATCH 40/50] Merge fixes --- .husky/pre-commit | 2 +- includes/class-job-dashboard-shortcode.php | 10 +++++----- templates/job-dashboard.php | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af21989..d5b5fd41c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged +#npx lint-staged diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php index e8a63d884..495f84ca0 100644 --- a/includes/class-job-dashboard-shortcode.php +++ b/includes/class-job-dashboard-shortcode.php @@ -212,11 +212,6 @@ public function get_job_actions( $job ) { 'nonce' => $base_nonce_action_name, ]; } - - $actions['duplicate'] = [ - 'label' => __( 'Duplicate', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; break; case 'expired': if ( job_manager_get_permalink( 'submit_job_form' ) ) { @@ -244,6 +239,11 @@ public function get_job_actions( $job ) { break; } + $actions['duplicate'] = [ + 'label' => __( 'Duplicate', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + $actions['delete'] = [ 'label' => __( 'Delete', 'wp-job-manager' ), 'nonce' => $base_nonce_action_name, diff --git a/templates/job-dashboard.php b/templates/job-dashboard.php index ac8a487a8..ea5e806b1 100644 --- a/templates/job-dashboard.php +++ b/templates/job-dashboard.php @@ -30,7 +30,6 @@ } $submit_job_form_page_id = get_option( 'job_manager_submit_job_form_page_id' ); -$wp_date_format = get_option( 'date_format' ) ?: 'F j, Y'; ?>
From 2d9b06f02c0cad8f273c148d7a5003adc4963e80 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 20 Mar 2024 17:37:07 +0100 Subject: [PATCH 41/50] Revert "Add wp cli stubs" This reverts commit c6cefbf33e6b9a0270b99f1f97461554b497697a. --- .psalm/psalm-loader.php | 1 - composer.json | 3 +-- composer.lock | 48 ++--------------------------------------- 3 files changed, 3 insertions(+), 49 deletions(-) diff --git a/.psalm/psalm-loader.php b/.psalm/psalm-loader.php index 70ab779bd..3dc311494 100644 --- a/.psalm/psalm-loader.php +++ b/.psalm/psalm-loader.php @@ -69,4 +69,3 @@ require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/php-stubs/wordpress-stubs/wordpress-stubs.php'; -require_once __DIR__ . '/../vendor/php-stubs/wordpress-cli-stubs/wordpress-cli-stubs.php'; diff --git a/composer.json b/composer.json index bfdcb87f8..edf7b33ec 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,7 @@ "sirbrillig/phpcs-variable-analysis": "^2.6", "yoast/phpunit-polyfills": "^1.0.2", "vimeo/psalm": "^5.13", - "php-stubs/wordpress-stubs": "^6.4", - "php-stubs/wp-cli-stubs": "^2.10" + "php-stubs/wordpress-stubs": "^6.4" }, "archive": { "exclude": [ diff --git a/composer.lock b/composer.lock index 84138d724..d54a5e2d9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c374870b045b55ca60f478f20abd2196", + "content-hash": "33fae2736293dd91fdb47d2118163002", "packages": [], "packages-dev": [ { @@ -1109,50 +1109,6 @@ }, "time": "2023-11-10T00:33:47+00:00" }, - { - "name": "php-stubs/wp-cli-stubs", - "version": "v2.10.0", - "source": { - "type": "git", - "url": "https://github.com/php-stubs/wp-cli-stubs.git", - "reference": "fbd7ff47393c9478e0f557d0b4caadaed20986fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wp-cli-stubs/zipball/fbd7ff47393c9478e0f557d0b4caadaed20986fb", - "reference": "fbd7ff47393c9478e0f557d0b4caadaed20986fb", - "shasum": "" - }, - "require": { - "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0" - }, - "require-dev": { - "php": "~7.3 || ~8.0", - "php-stubs/generator": "^0.8.0" - }, - "suggest": { - "symfony/polyfill-php73": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "WP-CLI function and class declaration stubs for static analysis.", - "homepage": "https://github.com/php-stubs/wp-cli-stubs", - "keywords": [ - "PHPStan", - "static analysis", - "wordpress", - "wp-cli" - ], - "support": { - "issues": "https://github.com/php-stubs/wp-cli-stubs/issues", - "source": "https://github.com/php-stubs/wp-cli-stubs/tree/v2.10.0" - }, - "time": "2024-02-09T02:10:10+00:00" - }, { "name": "phpcompatibility/php-compatibility", "version": "9.3.5", @@ -4433,5 +4389,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From 6446d4a5c9eecb46ef8096a1d1be2041a37184d9 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 20 Mar 2024 17:47:50 +0100 Subject: [PATCH 42/50] Fix psalm issues --- includes/class-wp-job-manager-recaptcha.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/includes/class-wp-job-manager-recaptcha.php b/includes/class-wp-job-manager-recaptcha.php index a16d4a83e..2a04d2865 100644 --- a/includes/class-wp-job-manager-recaptcha.php +++ b/includes/class-wp-job-manager-recaptcha.php @@ -15,7 +15,6 @@ * WP_Job_Manager_Recaptcha class. * * @since $$next-version$$ - * @internal */ class WP_Job_Manager_Recaptcha { @@ -142,7 +141,7 @@ public function display_recaptcha_field() { * * @param bool $success * - * @return bool|WP_Error + * @return bool|\WP_Error */ public function validate_recaptcha_field( $success ) { $recaptcha_field_label = get_option( 'job_manager_recaptcha_label' ); @@ -209,7 +208,7 @@ public function validate_recaptcha_field( $success ) { * * @since $$next-version$$ * - * @param float The score tolerance value. Default is 0.5. + * @param float $score_tolerance The score tolerance value. Default is 0.5. */ $score_tolerance = apply_filters( 'job_manager_recaptcha_v3_score_tolerance', 0.5 ); From 0c094e6f8c1e5d34dc453b485da0a82bc2224746 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 20 Mar 2024 17:51:34 +0100 Subject: [PATCH 43/50] Uncomment --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index d5b5fd41c..36af21989 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -#npx lint-staged +npx lint-staged From efb0b7f390160ae6227aeec98c52e1f2a3fb0be4 Mon Sep 17 00:00:00 2001 From: Mikey Arce Date: Wed, 20 Mar 2024 13:35:07 -0700 Subject: [PATCH 44/50] Fix stat display date (#2792) --- includes/class-stats-dashboard.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/class-stats-dashboard.php b/includes/class-stats-dashboard.php index c7457cfd6..d45c7023a 100644 --- a/includes/class-stats-dashboard.php +++ b/includes/class-stats-dashboard.php @@ -179,10 +179,12 @@ private function get_daily_stats_chart( \WP_Post $job ): array { ksort( $by_day ); $date_format = apply_filters( 'job_manager_get_dashboard_date_format', 'M d, Y' ); + $timezone = new \DateTimeZone( wp_timezone_string() ); $by_day_formatted = []; foreach ( $by_day as $date => $data ) { - $date_fmt = wp_date( $date_format, strtotime( $date ) ); + $date_obj = new \DateTime( $date, $timezone ); + $date_fmt = wp_date( $date_format, $date_obj->getTimestamp(), $timezone ); $by_day_formatted[ $date_fmt ] = $by_day[ $date ]; $by_day_formatted[ $date_fmt ]['date'] = $date_fmt; } @@ -192,7 +194,6 @@ private function get_daily_stats_chart( \WP_Post $job ): array { 'max' => $max, 'y-labels' => [ $max / 2, $max ], ]; - /** * Filter the job daily stat data, displayed as a chart in the job overlay. * From 85ef64ec98f489725ec4278b264774a6060361f1 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Thu, 28 Mar 2024 17:04:25 +0100 Subject: [PATCH 45/50] Add usage tracking for settings data --- includes/admin/class-wp-job-manager-admin.php | 1 - .../admin/class-wp-job-manager-settings.php | 56 +++++++++++++- ...ass-wp-job-manager-email-notifications.php | 1 + ...ass-wp-job-manager-usage-tracking-data.php | 73 ++++++++++++++++++- includes/class-wp-job-manager.php | 1 + ...ass.wp-job-manager-usage-tracking-data.php | 23 ++++++ 6 files changed, 149 insertions(+), 6 deletions(-) diff --git a/includes/admin/class-wp-job-manager-admin.php b/includes/admin/class-wp-job-manager-admin.php index 41e31497a..7a0c749aa 100644 --- a/includes/admin/class-wp-job-manager-admin.php +++ b/includes/admin/class-wp-job-manager-admin.php @@ -57,7 +57,6 @@ public function __construct() { WP_Job_Manager_CPT::instance(); include_once dirname( __FILE__ ) . '/class-wp-job-manager-promoted-jobs-admin.php'; - include_once dirname( __FILE__ ) . '/class-wp-job-manager-settings.php'; include_once dirname( __FILE__ ) . '/class-wp-job-manager-writepanels.php'; include_once dirname( __FILE__ ) . '/class-wp-job-manager-setup.php'; include_once dirname( __FILE__ ) . '/class-wp-job-manager-addons-landing-page.php'; diff --git a/includes/admin/class-wp-job-manager-settings.php b/includes/admin/class-wp-job-manager-settings.php index 0b5103740..7aa858ed3 100644 --- a/includes/admin/class-wp-job-manager-settings.php +++ b/includes/admin/class-wp-job-manager-settings.php @@ -82,6 +82,16 @@ public function get_settings() { return $this->settings; } + /** + * Set a single setting. + * + * @param string $option Option name. + * @param mixed $value Value. + */ + public function set_setting( $option, $value ) { + update_option( $option, $value ); + } + /** * Initializes the configuration for the plugin's setting fields. * @@ -89,7 +99,7 @@ public function get_settings() { */ protected function init_settings() { // Prepare roles option. - $roles = get_editable_roles(); + $roles = function_exists( 'get_editable_roles' ) ? get_editable_roles() : []; $account_roles = []; foreach ( $roles as $key => $role ) { @@ -115,6 +125,7 @@ protected function init_settings() { 'relative' => __( 'Relative to the current date (e.g., 1 day, 1 week, 1 month ago)', 'wp-job-manager' ), 'default' => __( 'Default date format as defined in Settings', 'wp-job-manager' ), ], + 'track' => 'value', ], [ 'name' => \WP_Job_Manager\Stats::OPTION_ENABLE_STATS, @@ -124,6 +135,7 @@ protected function init_settings() { 'desc' => __( 'Collect anonymous visitor data about job listings (page views, search impressions), and show them in the job dashboard.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_google_maps_api_key', @@ -141,6 +153,7 @@ protected function init_settings() { 'desc' => '', 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_bypass_trash_on_uninstall', @@ -150,6 +163,7 @@ protected function init_settings() { 'desc' => '', 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], ], ], @@ -163,6 +177,7 @@ protected function init_settings() { 'label' => __( 'Listings Per Page', 'wp-job-manager' ), 'desc' => __( 'Number of job listings to display per page.', 'wp-job-manager' ), 'attributes' => [], + 'track' => 'value', ], [ 'name' => 'job_manager_job_listing_pagination_type', @@ -174,6 +189,7 @@ protected function init_settings() { 'load_more' => __( 'Load More Listings button', 'wp-job-manager' ), 'pagination' => __( 'Page numbered links', 'wp-job-manager' ), ], + 'track' => 'value', ], [ 'name' => 'job_manager_hide_filled_positions', @@ -183,6 +199,7 @@ protected function init_settings() { 'desc' => __( 'Filled job listings will not be included in search results, sitemap and feeds.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_hide_expired', @@ -192,6 +209,7 @@ protected function init_settings() { 'desc' => __( 'Expired job listings will not be shown to the users on the job board.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_hide_expired_content', @@ -201,6 +219,7 @@ protected function init_settings() { 'desc' => __( 'Your site will display the titles of expired listings, but not the content of the listings. Otherwise, expired listings display their full content minus the application area.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_enable_categories', @@ -210,6 +229,7 @@ protected function init_settings() { 'desc' => __( 'This lets users select from a list of categories when submitting a job. Note: an admin has to create categories before site users can select them.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_enable_default_category_multiselect', @@ -219,6 +239,7 @@ protected function init_settings() { 'desc' => __( 'The category selection box will default to allowing multiple selections on the [jobs] shortcode. Without this, visitors will only be able to select a single category when filtering jobs.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_category_filter_type', @@ -230,6 +251,7 @@ protected function init_settings() { 'any' => __( 'Jobs will be shown if within ANY selected category', 'wp-job-manager' ), 'all' => __( 'Jobs will be shown if within ALL selected categories', 'wp-job-manager' ), ], + 'track' => 'bool', ], [ 'name' => 'job_manager_enable_types', @@ -239,6 +261,7 @@ protected function init_settings() { 'desc' => __( 'This lets users select from a list of types when submitting a job. Note: an admin has to create types before site users can select them.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_multi_job_type', @@ -248,6 +271,7 @@ protected function init_settings() { 'desc' => __( 'This allows users to select more than one type when submitting a job. The metabox on the post editor and the selection box on the front-end job submission form will both reflect this.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_enable_remote_position', @@ -257,6 +281,7 @@ protected function init_settings() { 'desc' => __( 'This lets users select if the listing is a remote position when submitting a job.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_enable_salary', @@ -266,6 +291,7 @@ protected function init_settings() { 'desc' => __( 'This lets users add a salary when submitting a job.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_enable_salary_currency', @@ -275,6 +301,7 @@ protected function init_settings() { 'desc' => __( 'This lets users add a salary currency when submitting a job.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_default_salary_currency', @@ -285,6 +312,7 @@ protected function init_settings() { 'type' => 'text', 'placeholder' => __( 'e.g. USD', 'wp-job-manager' ), 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_enable_salary_unit', @@ -294,6 +322,7 @@ protected function init_settings() { 'desc' => __( 'This lets users add a salary unit when submitting a job.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_default_salary_unit', @@ -304,6 +333,7 @@ protected function init_settings() { 'type' => 'select', 'options' => job_manager_get_salary_unit_options(), 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_display_location_address', @@ -312,6 +342,7 @@ protected function init_settings() { 'cb_label' => __( 'Display Location Address', 'wp-job-manager' ), 'desc' => __( 'Display the full address of the job listing location if it is detected by Google Maps Geocoding API. If full address is not available then it will display whatever text the user submitted for the location.', 'wp-job-manager' ), 'type' => 'checkbox', + 'track' => 'bool', ], [ 'name' => 'job_manager_strip_job_description_shortcodes', @@ -320,6 +351,7 @@ protected function init_settings() { 'cb_label' => __( 'Strip shortcodes from Job description', 'wp-job-manager' ), 'desc' => __( 'If enabled, shortcodes will be stripped from the job description on the single job listing page.', 'wp-job-manager' ), 'type' => 'checkbox', + 'track' => 'bool', ], ], ], @@ -334,6 +366,7 @@ protected function init_settings() { 'desc' => __( 'Limits job listing submissions to registered, logged-in users.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_enable_registration', @@ -343,6 +376,7 @@ protected function init_settings() { 'desc' => __( 'Includes account creation on the listing submission form, to allow non-registered users to create an account and submit a job listing simultaneously.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ @@ -353,6 +387,7 @@ protected function init_settings() { 'desc' => __( 'Allow employers to set a date in the future for the listing to publish.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_generate_username_from_email', @@ -362,6 +397,7 @@ protected function init_settings() { 'desc' => __( 'Automatically generates usernames for new accounts from the registrant\'s email address. If this is not enabled, a "username" field will display instead.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_use_standard_password_setup_email', @@ -371,6 +407,7 @@ protected function init_settings() { 'desc' => __( 'Sends an email to the user with their username and a link to set their password. If this is not enabled, a "password" field will display instead, and their email address won\'t be verified.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_registration_role', @@ -379,6 +416,7 @@ protected function init_settings() { 'desc' => __( 'Any new accounts created during submission will have this role. If you haven\'t enabled account creation during submission in the options above, your own method of assigning roles will apply.', 'wp-job-manager' ), 'type' => 'select', 'options' => $account_roles, + 'track' => 'value', ], [ 'name' => 'job_manager_submission_requires_approval', @@ -388,6 +426,7 @@ protected function init_settings() { 'desc' => __( 'Sets all new submissions to "pending." They will not appear on your site until an admin approves them.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_user_can_edit_pending_submissions', @@ -397,6 +436,7 @@ protected function init_settings() { 'desc' => __( 'Users can continue to edit pending listings until they are approved by an admin.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], [ 'name' => 'job_manager_user_edit_published_submissions', @@ -411,6 +451,7 @@ protected function init_settings() { 'yes_moderated' => __( 'Users can edit, but edits require admin approval', 'wp-job-manager' ), ], 'attributes' => [], + 'track' => 'value', ], [ 'name' => 'job_manager_submission_duration', @@ -421,6 +462,7 @@ protected function init_settings() { 'attributes' => [], 'sanitize_callback' => [ $this, 'sanitize_submission_duration' ], 'placeholder' => __( 'No limit', 'wp-job-manager' ), + 'track' => 'value', ], [ 'name' => 'job_manager_renewal_days', @@ -430,6 +472,7 @@ protected function init_settings() { 'type' => 'number', 'attributes' => [], 'sanitize_callback' => [ $this, 'sanitize_renewal_days' ], + 'track' => 'value', ], [ 'name' => 'job_manager_submission_limit', @@ -440,6 +483,7 @@ protected function init_settings() { 'attributes' => [], 'sanitize_callback' => [ $this, 'sanitize_submission_limit' ], 'placeholder' => __( 'No limit', 'wp-job-manager' ), + 'track' => 'value', ], [ 'name' => 'job_manager_allowed_application_method', @@ -452,6 +496,7 @@ protected function init_settings() { 'email' => __( 'Email addresses only', 'wp-job-manager' ), 'url' => __( 'Website URLs only', 'wp-job-manager' ), ], + 'track' => 'value', ], [ 'name' => 'job_manager_show_agreement_job_submission', @@ -461,6 +506,7 @@ protected function init_settings() { 'desc' => __( 'Require a Terms and Conditions checkbox to be marked before a job can be submitted. The linked page can be set from the Pages settings tab.', 'wp-job-manager' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], ], ], @@ -486,6 +532,7 @@ protected function init_settings() { 'v2' => __( 'reCaptcha v2', 'wp-job-manager' ), 'v3' => __( 'reCaptcha v3', 'wp-job-manager' ), ], + 'track' => 'value', ], [ 'name' => 'job_manager_recaptcha_site_key', @@ -513,6 +560,7 @@ protected function init_settings() { 'desc' => sprintf( __( 'This will help prevent bots from submitting job listings. You must have entered a valid site key and secret key above.', 'wp-job-manager' ), 'https://www.google.com/recaptcha/admin#list' ), 'type' => 'checkbox', 'attributes' => [], + 'track' => 'bool', ], ], ], @@ -525,6 +573,7 @@ protected function init_settings() { 'label' => __( 'Submit Job Form Page', 'wp-job-manager' ), 'desc' => __( 'Select the page where you\'ve used the [submit_job_form] shortcode. This lets the plugin know the location of the form.', 'wp-job-manager' ), 'type' => 'page', + 'track' => 'bool', ], [ 'name' => 'job_manager_job_dashboard_page_id', @@ -532,6 +581,7 @@ protected function init_settings() { 'label' => __( 'Job Dashboard Page', 'wp-job-manager' ), 'desc' => __( 'Select the page where you\'ve used the [job_dashboard] shortcode. This lets the plugin know the location of the dashboard.', 'wp-job-manager' ), 'type' => 'page', + 'track' => 'bool', ], [ 'name' => 'job_manager_jobs_page_id', @@ -539,6 +589,7 @@ protected function init_settings() { 'label' => __( 'Job Listings Page', 'wp-job-manager' ), 'desc' => __( 'Select the page where you\'ve used the [jobs] shortcode. This lets the plugin know the location of the job listings page.', 'wp-job-manager' ), 'type' => 'page', + 'track' => 'bool', ], [ 'name' => 'job_manager_terms_and_conditions_page_id', @@ -546,6 +597,7 @@ protected function init_settings() { 'label' => __( 'Terms and Conditions Page', 'wp-job-manager' ), 'desc' => __( 'Select the page to link when "Terms and Conditions Checkbox" is enabled. See setting in "Job Submission" tab.', 'wp-job-manager' ), 'type' => 'page', + 'track' => 'bool', ], ], ], @@ -560,6 +612,7 @@ protected function init_settings() { 'sanitize_callback' => [ $this, 'sanitize_capabilities' ], // translators: Placeholder %s is the url to the WordPress core documentation for capabilities and roles. 'desc' => sprintf( __( 'Enter which roles or capabilities allow visitors to browse job listings. If no value is selected, everyone (including logged out guests) will be able to browse job listings.', 'wp-job-manager' ), 'http://codex.wordpress.org/Roles_and_Capabilities' ), + 'track' => 'bool', ], [ 'name' => 'job_manager_view_job_listing_capability', @@ -569,6 +622,7 @@ protected function init_settings() { 'sanitize_callback' => [ $this, 'sanitize_capabilities' ], // translators: Placeholder %s is the url to the WordPress core documentation for capabilities and roles. 'desc' => sprintf( __( 'Enter which roles or capabilities allow visitors to view a single job listing. If no value is selected, everyone (including logged out guests) will be able to view job listings.', 'wp-job-manager' ), 'http://codex.wordpress.org/Roles_and_Capabilities' ), + 'track' => 'bool', ], ], ], diff --git a/includes/class-wp-job-manager-email-notifications.php b/includes/class-wp-job-manager-email-notifications.php index 2f8a3e570..6fced260d 100644 --- a/includes/class-wp-job-manager-email-notifications.php +++ b/includes/class-wp-job-manager-email-notifications.php @@ -470,6 +470,7 @@ public static function add_email_settings( $settings, $context ) { 'label' => false, 'std' => self::get_email_setting_defaults( $email_notification_key ), 'settings' => self::get_email_setting_fields( $email_notification_key ), + 'track' => 'bool', ]; } diff --git a/includes/class-wp-job-manager-usage-tracking-data.php b/includes/class-wp-job-manager-usage-tracking-data.php index acc932ae3..533ddd07d 100644 --- a/includes/class-wp-job-manager-usage-tracking-data.php +++ b/includes/class-wp-job-manager-usage-tracking-data.php @@ -62,14 +62,23 @@ public static function get_usage_data() { 'jobs_by_guests' => self::get_jobs_by_guests(), ]; - $all_extenstions = self::get_official_extensions( false ); + $settings = self::get_settings_data(); + + foreach ( $settings as $name => $value ) { + $name = preg_replace( '/[^a-z0-9]/', '_', $name ); + $usage_data[ 'settings_' . $name ] = preg_replace( '/[^a-z0-9]/', '_', $value ); + } + + $all_extensions = self::get_official_extensions( false ); $licensed_extensions = self::get_official_extensions( true ); - $usage_data['official_extensions'] = count( $all_extenstions ); + $usage_data['official_extensions'] = count( $all_extensions ); $usage_data['licensed_extensions'] = count( $licensed_extensions ); - foreach ( array_keys( $all_extenstions ) as $installed_plugin ) { - $usage_data[ $installed_plugin ] = isset( $licensed_extensions[ $installed_plugin ] ) ? 'licensed' : 'unlicensed'; + foreach ( array_keys( $all_extensions ) as $installed_plugin ) { + $name = preg_replace( '/[^a-z0-9]/', '_', $installed_plugin ); + $name = preg_replace( '/^wp-job-manager/', 'license_', $name ); + $usage_data[ $name ] = isset( $licensed_extensions[ $installed_plugin ] ) ? 'licensed' : 'unlicensed'; } return $usage_data; @@ -366,6 +375,62 @@ private static function has_paid_extensions() { return self::get_official_extensions_count() > 0; } + /** + * Get usage data for some settings. + * + * @return array + */ + public static function get_settings_data() { + $settings = WP_Job_Manager_Settings::instance()->get_settings(); + + $settings_data = []; + + foreach ( $settings as $group ) { + + foreach ( $group[1] as $option ) { + + $name = $option['name']; + $value = get_option( $name ); + + if ( empty( $option['track'] ) ) { + continue; + } + + switch ( $option['track'] ) { + case 'bool': + $value = $value ? '1' : '0'; + break; + case 'value': + if ( isset( $option['options'] ) && ! in_array( $value, array_keys( $option['options'] ), true ) ) { + unset( $value ); + } + break; + case 'is-default': + $value = $value === $option['std'] ? '1' : '0'; + break; + default: + unset( $value ); + break; + } + + if ( ! isset( $value ) || ! is_scalar( $value ) || strlen( $value ) > 50 ) { + continue; + } + + $name = preg_replace( '/^job_manager_/', '', $name ); + + $settings_data[ $name ] = $value; + } + } + + /** + * Filter the settings fields that are sent for usage tracking. + * + * @param array $settings_data The default settings data. + */ + return apply_filters( 'job_manager_logged_settings', $settings_data ); + } + /** * Get the base fields to be sent for event logging. * diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index dead66ca6..14bff7de6 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -82,6 +82,7 @@ public function __construct() { include_once JOB_MANAGER_PLUGIN_DIR . '/includes/abstracts/abstract-wp-job-manager-email-template.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-wp-job-manager-email-notifications.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-wp-job-manager-data-exporter.php'; + include_once JOB_MANAGER_PLUGIN_DIR . '/includes/admin/class-wp-job-manager-settings.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-wp-job-manager-com-api.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/promoted-jobs/class-wp-job-manager-promoted-jobs.php'; diff --git a/tests/php/tests/includes/test_class.wp-job-manager-usage-tracking-data.php b/tests/php/tests/includes/test_class.wp-job-manager-usage-tracking-data.php index b446e07fc..e186b6c6a 100644 --- a/tests/php/tests/includes/test_class.wp-job-manager-usage-tracking-data.php +++ b/tests/php/tests/includes/test_class.wp-job-manager-usage-tracking-data.php @@ -776,6 +776,29 @@ public function test_get_event_logging_base_fields_paid_with_extensions() { $this->assertEquals( 1, $base_fields['paid'] ); } + + /** + * Tests that get_usage_data() returns the plugin settings. + * + * @since 1.30.0 + * @covers WP_Job_Manager_Usage_Tracking_Data::get_usage_data + */ + public function test_get_settings() { + $published = 3; + $expired = 2; + + update_option( 'job_manager_enable_types', '1' ); + update_option( 'job_manager_hide_filled_positions', '1' ); + update_option( 'job_manager_google_maps_api_key', '000000000' ); + + $this->create_job_listings_with_meta( '_company_name', 'Automattic', $published, $expired ); + + $data = WP_Job_Manager_Usage_Tracking_Data::get_usage_data(); + $this->assertEquals( $data['settings_enable_types'], '1' ); + $this->assertEquals( $data['settings_hide_filled_positions'], '1' ); + $this->assertTrue( empty( $data['settings_google_maps_api_key'] ) ); + } + /** * Adds fake license to one of the products. */ From 2d6906d9ac77da777a19716e5c324cd75a301421 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Thu, 28 Mar 2024 17:04:54 +0100 Subject: [PATCH 46/50] Remove deprecated mark from active function --- includes/abstracts/abstract-wp-job-manager-form.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/abstracts/abstract-wp-job-manager-form.php b/includes/abstracts/abstract-wp-job-manager-form.php index 9dca801fe..fb0c87d7c 100644 --- a/includes/abstracts/abstract-wp-job-manager-form.php +++ b/includes/abstracts/abstract-wp-job-manager-form.php @@ -322,7 +322,6 @@ public function clear_fields() { * Enqueue the scripts for the form. */ public function enqueue_scripts() { - _deprecated_function( __METHOD__, '$$next-version$$', 'WP_Job_Manager\WP_Job_Manager_Form::enqueue_scripts' ); WP_Job_Manager\WP_Job_Manager_Recaptcha::enqueue_scripts(); } From b20578acdfd7a79b96a7061537f13ce6e377d7d9 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Thu, 28 Mar 2024 17:37:39 +0100 Subject: [PATCH 47/50] Update psalm-baseline.xml --- .psalm/psalm-baseline.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.psalm/psalm-baseline.xml b/.psalm/psalm-baseline.xml index dc350445a..a9cfbfd90 100644 --- a/.psalm/psalm-baseline.xml +++ b/.psalm/psalm-baseline.xml @@ -110,6 +110,9 @@ null + + + stirng|int From 20d4d78c18def330d414ae224b989b06998f79e7 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Thu, 4 Apr 2024 17:16:29 +0200 Subject: [PATCH 48/50] Fix data types --- includes/admin/class-wp-job-manager-settings.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/admin/class-wp-job-manager-settings.php b/includes/admin/class-wp-job-manager-settings.php index 7aa858ed3..9a38516f4 100644 --- a/includes/admin/class-wp-job-manager-settings.php +++ b/includes/admin/class-wp-job-manager-settings.php @@ -251,7 +251,7 @@ protected function init_settings() { 'any' => __( 'Jobs will be shown if within ANY selected category', 'wp-job-manager' ), 'all' => __( 'Jobs will be shown if within ALL selected categories', 'wp-job-manager' ), ], - 'track' => 'bool', + 'track' => 'value', ], [ 'name' => 'job_manager_enable_types', @@ -312,7 +312,7 @@ protected function init_settings() { 'type' => 'text', 'placeholder' => __( 'e.g. USD', 'wp-job-manager' ), 'attributes' => [], - 'track' => 'bool', + 'track' => 'value', ], [ 'name' => 'job_manager_enable_salary_unit', @@ -333,7 +333,7 @@ protected function init_settings() { 'type' => 'select', 'options' => job_manager_get_salary_unit_options(), 'attributes' => [], - 'track' => 'bool', + 'track' => 'value', ], [ 'name' => 'job_manager_display_location_address', From 4175f4ff4071e4dd2a961dd5671fff5e17d9852a Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Mon, 15 Apr 2024 14:00:34 +0200 Subject: [PATCH 49/50] Add primary action to job dashboard and overlay (#2800) --- assets/css/job-dashboard.scss | 14 +++- assets/css/job-overlay.scss | 22 ++++++ assets/css/ui.elements.scss | 10 +++ includes/class-job-dashboard-shortcode.php | 82 +++++++++++++++++++++- templates/job-dashboard-overlay.php | 32 +++++---- templates/job-dashboard.php | 12 +--- templates/job-stats.php | 2 +- 7 files changed, 149 insertions(+), 25 deletions(-) diff --git a/assets/css/job-dashboard.scss b/assets/css/job-dashboard.scss index 589c89c5b..c042434ea 100644 --- a/assets/css/job-dashboard.scss +++ b/assets/css/job-dashboard.scss @@ -96,6 +96,10 @@ .jm-dashboard-job-column.actions { flex: 0.5 1 100%; text-align: right; + display: flex; + justify-content: flex-end; + align-items: center; + gap: var(--jm-ui-space-m); } .jm-dashboard-job-column a.job-title { @@ -112,8 +116,12 @@ .jm-dashboard-action { display: block; text-decoration: none; - } +.jm-dashboard-action--primary { + flex-basis: fit-content; + white-space: nowrap; +} + .jm-dashboard-action:where(:not(:hover):not(:focus)) { color: inherit; } @@ -126,12 +134,16 @@ { .jm-dashboard-job { flex-wrap: wrap; + align-items: flex-start; } .jm-dashboard-header { display: none; } + .jm-dashboard-job-column.actions { + justify-content: space-between; + } .jm-dashboard-job-column.company ~ .jm-dashboard-job-column.job_title { flex-basis: calc( 100% - var(--jm-dashboard-company-logo-size) - var(--jm-ui-space-sm) ); diff --git a/assets/css/job-overlay.scss b/assets/css/job-overlay.scss index 94350e893..b074fb7e3 100644 --- a/assets/css/job-overlay.scss +++ b/assets/css/job-overlay.scss @@ -76,6 +76,7 @@ .jm-ui-actions-row { gap: var(--jm-ui-space-sm); + flex-wrap: wrap; } } @@ -89,7 +90,28 @@ @media (max-width: 600px) { .jm-dashboard__overlay { height: 100%; + max-width: 100%; + margin-bottom: 0; } + + .jm-job-overlay-footer { + .jm-ui-actions-row { + gap: var(--jm-ui-space-s); + + * { + font-size: var(--jm-ui-font-size-s); + } + } + + .jm-ui-button--outline { + padding: var(--jm-ui-space-xxs) var(--jm-ui-space-s2); + } + + .jm-ui-button--link { + padding: var(--jm-ui-space-xxs) var(--jm-ui-space-xs); + } + } + } @keyframes jm-job-overlay-fade-in { diff --git a/assets/css/ui.elements.scss b/assets/css/ui.elements.scss index 1d2bf31e7..341029714 100644 --- a/assets/css/ui.elements.scss +++ b/assets/css/ui.elements.scss @@ -37,6 +37,7 @@ font-size: var(--jm-ui-button-font-size); font-weight: 400; letter-spacing: -0.1px; + white-space: nowrap; transition: color 0.2s ease-out, background 0.2s ease-out; @@ -66,6 +67,15 @@ } } +.jm-ui-button--small, +{ + padding: var(--jm-ui-space-xxs) var(--jm-ui-space-s); + gap: var(--jm-ui-space-xs); + font-size: var(--jm-ui-font-size-s); + font-weight: 400; + letter-spacing: -0.1px; +} + .jm-ui-button__a { text-decoration: none; } diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php index 495f84ca0..a388d07fc 100644 --- a/includes/class-job-dashboard-shortcode.php +++ b/includes/class-job-dashboard-shortcode.php @@ -60,6 +60,7 @@ public function __construct() { add_action( 'job_manager_job_dashboard_columns', [ $this, 'maybe_display_company_column' ], 8 ); add_action( 'job_manager_job_dashboard_column_job_title', [ self::class, 'the_job_title' ], 10 ); add_action( 'job_manager_job_dashboard_column_job_title', [ self::class, 'the_status' ], 12 ); + add_action( 'job_manager_job_dashboard_column_actions', [ self::class, 'the_primary_action' ], 10, 2 ); Job_Overlay::instance(); } @@ -260,15 +261,67 @@ public function get_job_actions( $job ) { $actions = apply_filters( 'job_manager_my_job_actions', $actions, $job ); // For backwards compatibility, convert `nonce => true` to the nonce action name. - foreach ( $actions as $key => $action ) { + foreach ( $actions as $key => &$action ) { if ( true === $action['nonce'] ) { - $actions[ $key ]['nonce'] = $base_nonce_action_name; + $action['nonce'] = $base_nonce_action_name; } + + $action_url = add_query_arg( + [ + 'action' => $key, + 'job_id' => $job->ID, + ], + '?' + ); + + if ( $action['nonce'] ) { + $action_url = wp_nonce_url( $action_url, $action['nonce'] ); + } + + $action['name'] = $key; + $action['url'] = $action_url; } return $actions; } + /** + * Determine the highlighted primary action for a job. + * + * @param \WP_Post $job The job. + * @param array $actions Available job action. + * + * @return array|false + */ + public static function get_primary_action( $job, $actions ) { + + $action_order = [ + 'mark_filled', + 'renew', + 'relist', + 'continue', + 'edit', + 'delete', + 'duplicate', + 'mark_not_filled', + ]; + + $primary_action = false; + + foreach ( $action_order as $action ) { + if ( isset( $actions[ $action ] ) ) { + $primary_action = $actions[ $action ]; + break; + } + } + + /** + * Filter the highlighted primary action for a job on the job dashboard page. + */ + return apply_filters( 'job_manager_my_job_primary_action', $primary_action, $job, $actions ); + + } + /** * Filters the url from paginate_links to avoid multiple calls for same action in job dashboard * @@ -531,6 +584,31 @@ public static function the_date( $job ) { echo '
' . esc_html( wp_date( apply_filters( 'job_manager_get_dashboard_date_format', 'M d, Y' ), get_post_datetime( $job )->getTimestamp() ) ) . '
'; } + /** + * Show the primary action as a button. + * + * @param \WP_Post $job The job post. + * @param array $actions Available actions. + * + * @output string + */ + public static function the_primary_action( $job, $actions ) { + $action = self::get_primary_action( $job, $actions ); + + if ( ! $action ) { + return; + } + + echo UI_Elements::button( + [ + 'label' => $action['label'], + 'url' => $action['url'], + 'class' => 'job-dashboard-action-' . esc_attr( $action['name'] ) . ' jm-dashboard-action jm-dashboard-action--primary jm-ui-button--small', + ], + 'jm-ui-button--outline' + ); + } + /** * Add job status to the job dashboard title column. * diff --git a/templates/job-dashboard-overlay.php b/templates/job-dashboard-overlay.php index d46175a47..6587f8b7b 100644 --- a/templates/job-dashboard-overlay.php +++ b/templates/job-dashboard-overlay.php @@ -60,25 +60,33 @@
diff --git a/templates/job-dashboard.php b/templates/job-dashboard.php index ea5e806b1..5256b3679 100644 --- a/templates/job-dashboard.php +++ b/templates/job-dashboard.php @@ -82,18 +82,12 @@ class="jm-dashboard-job-column ">
+ ID ] ?? [] ); ?> ID ] ) ) { - foreach ( $job_actions[ $job->ID ] as $action => $value ) { - $action_url = add_query_arg( [ - 'action' => $action, - 'job_id' => $job->ID, - ] ); - if ( $value['nonce'] ) { - $action_url = wp_nonce_url( $action_url, $value['nonce'] ); - } - $actions_html .= '' . esc_html( $value['label'] ) . '' . "\n"; + foreach ( $job_actions[ $job->ID ] as $action ) { + $actions_html .= '' . esc_html( $action['label'] ) . '' . "\n"; } } diff --git a/templates/job-stats.php b/templates/job-stats.php index 4daf49f39..1969ea0de 100644 --- a/templates/job-stats.php +++ b/templates/job-stats.php @@ -108,7 +108,7 @@
-
From 452dc2f4379ba17ddd983be17a09cb5b221823fb Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Mon, 15 Apr 2024 21:19:45 +0200 Subject: [PATCH 50/50] Add job overlay to admin (#2804) --- .psalm/psalm-baseline.xml | 4 - assets/css/admin.scss | 6 + assets/css/job-overlay.scss | 4 + assets/js/job-dashboard.js | 11 +- includes/admin/class-wp-job-manager-admin.php | 3 + includes/admin/class-wp-job-manager-cpt.php | 79 ++++++----- includes/class-job-dashboard-shortcode.php | 49 +++++-- includes/class-job-overlay.php | 129 ++++++++++++++++-- includes/class-stats-dashboard.php | 10 +- includes/class-wp-job-manager.php | 13 +- psalm.xml | 4 +- templates/job-dashboard-overlay.php | 33 +---- templates/job-dashboard.php | 2 - 13 files changed, 236 insertions(+), 111 deletions(-) diff --git a/.psalm/psalm-baseline.xml b/.psalm/psalm-baseline.xml index a9cfbfd90..fbc28baec 100644 --- a/.psalm/psalm-baseline.xml +++ b/.psalm/psalm-baseline.xml @@ -74,9 +74,6 @@ WP_Job_manager WP_Job_manager - - - @@ -219,7 +216,6 @@ $job_data - $pending_jobs '; try { - const response = await fetch( `${ overlayEndpoint }?job_id=${ id }` ); + const response = await fetch( `${ overlayEndpoint }&job_id=${ id }` ); if ( ! response.ok ) { throw new Error( response.statusText ); @@ -56,7 +59,7 @@ async function showOverlay( eventOrId ) { } const clearHash = () => { - history.replaceState( null, '', window.location.pathname ); + history.replaceState( null, '', window.location.pathname + window.location.search ); overlayDialog.removeEventListener( 'close', clearHash ); }; @@ -67,7 +70,7 @@ async function showOverlay( eventOrId ) { function setupStatsOverlay() { document - .querySelectorAll( '.jm-dashboard-job .job-title' ) + .querySelectorAll( '.jm-dashboard-job .job-title, tr.job_listing td.column-stats' ) .forEach( el => el.addEventListener( 'click', showOverlay ) ); const urlHash = window.location.hash?.substring( 1 ); diff --git a/includes/admin/class-wp-job-manager-admin.php b/includes/admin/class-wp-job-manager-admin.php index 7a0c749aa..5d2255415 100644 --- a/includes/admin/class-wp-job-manager-admin.php +++ b/includes/admin/class-wp-job-manager-admin.php @@ -5,6 +5,8 @@ * @package wp-job-manager */ +use WP_Job_Manager\Job_Overlay; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -64,6 +66,7 @@ public function __construct() { $this->settings_page = WP_Job_Manager_Settings::instance(); WP_Job_Manager_Addons_Landing_Page::instance(); + Job_Overlay::instance(); add_action( 'admin_init', [ $this, 'admin_init' ] ); add_action( 'current_screen', [ $this, 'conditional_includes' ] ); diff --git a/includes/admin/class-wp-job-manager-cpt.php b/includes/admin/class-wp-job-manager-cpt.php index b03d0d3e9..9e4bbf07b 100644 --- a/includes/admin/class-wp-job-manager-cpt.php +++ b/includes/admin/class-wp-job-manager-cpt.php @@ -263,7 +263,7 @@ public function approve_job() { 'post_status' => 'publish', ]; wp_update_post( $job_data ); - wp_safe_redirect( remove_query_arg( 'approve_job', add_query_arg( 'handled_jobs', $post_id, add_query_arg( 'action_performed', 'approve_jobs', admin_url( 'edit.php?post_type=job_listing' ) ) ) ) ); + wp_safe_redirect( remove_query_arg( 'approve_job', add_query_arg( [ 'handled_jobs' => [ $post_id ] ], add_query_arg( 'action_performed', 'approve_jobs', admin_url( 'edit.php?post_type=job_listing' ) ) ) ) ); exit; } } @@ -545,39 +545,7 @@ public function row_actions( $actions, $post ) { unset( $actions['inline hide-if-no-js'] ); unset( $actions['trash'] ); - $admin_actions = []; - - if ( in_array( $post->post_status, [ 'pending', 'pending_payment' ], true ) && current_user_can( 'publish_post', $post->ID ) ) { - $admin_actions['approve'] = [ - 'action' => 'approved', - 'name' => __( 'Approve', 'wp-job-manager' ), - 'url' => wp_nonce_url( add_query_arg( 'approve_job', $post->ID ), 'approve_job' ), - ]; - } - if ( 'trash' !== $post->post_status ) { - if ( current_user_can( 'read_post', $post->ID ) ) { - $admin_actions['view'] = [ - 'action' => 'view', - 'name' => __( 'View', 'wp-job-manager' ), - 'url' => get_permalink( $post->ID ), - ]; - } - if ( current_user_can( 'edit_post', $post->ID ) ) { - $admin_actions['edit'] = [ - 'action' => 'edit', - 'name' => __( 'Edit', 'wp-job-manager' ), - 'url' => get_edit_post_link( $post->ID ), - ]; - } - if ( current_user_can( 'delete_post', $post->ID ) ) { - $admin_actions['delete'] = [ - 'action' => 'delete', - 'name' => __( 'Delete', 'wp-job-manager' ), - 'url' => get_delete_post_link( $post->ID ), - ]; - } - } - $admin_actions = apply_filters( 'job_manager_admin_actions', $admin_actions, $post ); + $admin_actions = $this->get_admin_actions( $post ); foreach ( $admin_actions as $action ) { $actions[ $action['action'] ] = '' . esc_html( $action['name'] ) . ''; @@ -587,6 +555,49 @@ public function row_actions( $actions, $post ) { return $actions; } + /** + * Get the admin actions for a job listing. + * + * @param \WP_Post $post + * + * @return array + */ + public function get_admin_actions( $post ) { + $admin_actions = []; + + if ( in_array( $post->post_status, [ 'pending', 'pending_payment' ], true ) && current_user_can( 'publish_post', $post->ID ) ) { + $admin_actions['approve'] = [ + 'action' => 'approved', + 'name' => __( 'Approve', 'wp-job-manager' ), + 'url' => wp_nonce_url( add_query_arg( 'approve_job', $post->ID ), 'approve_job' ), + ]; + } + if ( 'trash' !== $post->post_status ) { + if ( current_user_can( 'read_post', $post->ID ) ) { + $admin_actions['view'] = [ + 'action' => 'view', + 'name' => __( 'View', 'wp-job-manager' ), + 'url' => get_permalink( $post->ID ), + ]; + } + if ( current_user_can( 'edit_post', $post->ID ) ) { + $admin_actions['edit'] = [ + 'action' => 'edit', + 'name' => __( 'Edit', 'wp-job-manager' ), + 'url' => get_edit_post_link( $post->ID ), + ]; + } + if ( current_user_can( 'delete_post', $post->ID ) ) { + $admin_actions['delete'] = [ + 'action' => 'delete', + 'name' => __( 'Delete', 'wp-job-manager' ), + 'url' => get_delete_post_link( $post->ID ), + ]; + } + } + return apply_filters( 'job_manager_admin_actions', $admin_actions, $post ); + } + /** * Displays the content for each custom column on the admin list for Job Listings. * diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php index a388d07fc..defba535b 100644 --- a/includes/class-job-dashboard-shortcode.php +++ b/includes/class-job-dashboard-shortcode.php @@ -8,6 +8,7 @@ namespace WP_Job_Manager; use WP_Job_Manager\UI\Notice; +use WP_Job_Manager\UI\Redirect_Message; use WP_Job_Manager\UI\UI_Elements; if ( ! defined( 'ABSPATH' ) ) { @@ -100,9 +101,7 @@ public function output_job_dashboard( $attrs ) { ); $posts_per_page = $attrs['posts_per_page']; - \WP_Job_Manager::register_style( 'wp-job-manager-job-dashboard', 'css/job-dashboard.css', [ 'wp-job-manager-ui' ] ); - wp_enqueue_style( 'wp-job-manager-job-dashboard' ); - wp_enqueue_script( 'wp-job-manager-job-dashboard' ); + Job_Overlay::instance()->init_dashboard_overlay(); ob_start(); @@ -134,7 +133,12 @@ public function output_job_dashboard( $attrs ) { // Cache IDs for access check later on. $this->job_dashboard_job_ids = wp_list_pluck( $jobs->posts, 'ID' ); - echo '
' . wp_kses_post( $this->job_dashboard_message ) . '
'; + $message = Redirect_Message::get_message( 'updated' ); + + if ( ! empty( $message ) ) { + //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in the notice class. + echo '
' . $message . '
'; + } $job_dashboard_columns = apply_filters( 'job_manager_job_dashboard_columns', @@ -160,6 +164,8 @@ public function output_job_dashboard( $attrs ) { ] ); + do_action( 'job_manager_job_dashboard', $jobs ); + return ob_get_clean(); } @@ -171,18 +177,16 @@ public function output_job_dashboard( $attrs ) { * @return array */ public function get_job_actions( $job ) { - if ( - ! get_current_user_id() - || ! $job instanceof \WP_Post - || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type - || ! $this->is_job_available_on_dashboard( $job ) - ) { + if ( ! $this->can_manage_job( $job ) ) { return []; } + $base_url = self::get_job_dashboard_page_url(); + $base_nonce_action_name = 'job_manager_my_job_actions'; $actions = []; + switch ( $job->post_status ) { case 'publish': if ( \WP_Job_Manager_Post_Types::job_is_editable( $job->ID ) ) { @@ -262,7 +266,7 @@ public function get_job_actions( $job ) { // For backwards compatibility, convert `nonce => true` to the nonce action name. foreach ( $actions as $key => &$action ) { - if ( true === $action['nonce'] ) { + if ( isset( $action['nonce'] ) && true === $action['nonce'] ) { $action['nonce'] = $base_nonce_action_name; } @@ -510,6 +514,8 @@ public function handle_actions() { $this->job_dashboard_message = Notice::error( $e->getMessage() ); } + Redirect_Message::redirect( remove_query_arg( [ 'action', 'job_id', '_wpnonce' ] ), $this->job_dashboard_message, 'updated' ); + } /** @@ -665,6 +671,27 @@ public static function get_job_dashboard_page_url() { } } + /** + * Check if the current user can manage this job listing. + * + * @param \WP_Post|null $job + * + * @return bool + */ + public function can_manage_job( $job ) { + + if ( ! get_current_user_id() + || empty( $job ) + || ! $job instanceof \WP_Post + || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type ) { + return false; + } + + return is_admin() + ? current_user_can( \WP_Job_Manager_Post_Types::CAP_MANAGE_LISTINGS, $job->ID ) + : $this->is_job_available_on_dashboard( $job ); + } + /** * Check if a job is listed on the current user's job dashboard page. * diff --git a/includes/class-job-overlay.php b/includes/class-job-overlay.php index 317ce0db9..cd3b22f42 100644 --- a/includes/class-job-overlay.php +++ b/includes/class-job-overlay.php @@ -9,6 +9,8 @@ use WP_Job_Manager\UI\Modal_Dialog; use WP_Job_Manager\UI\Notice; +use WP_Job_Manager\UI\UI; +use WP_Job_Manager\UI\UI_Elements; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -28,6 +30,10 @@ class Job_Overlay { */ public function __construct() { add_action( 'job_manager_ajax_job_dashboard_overlay', [ $this, 'ajax_job_overlay' ] ); + add_action( 'wp_ajax_job_dashboard_overlay', [ $this, 'ajax_job_overlay' ] ); + add_action( 'admin_footer', [ $this, 'init_admin_dashboard_overlay' ], 10 ); + add_action( 'job_manager_job_dashboard', [ $this, 'init_dashboard_overlay' ], 10 ); + add_action( 'job_manager_job_overlay_footer', [ $this, 'output_footer_actions' ], 10 ); } @@ -35,14 +41,28 @@ public function __construct() { * Render the job dashboard overlay content for an AJAX request. */ public function ajax_job_overlay() { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce check. + if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'job_dashboard_overlay' ) ) { + wp_send_json_error( + Notice::error( + [ + 'message' => __( 'Invalid request.', 'wp-job-manager' ), + 'classes' => [ 'type-dialog' ], + ] + ) + ); + + return; + } + $job_id = isset( $_REQUEST['job_id'] ) ? absint( $_REQUEST['job_id'] ) : null; $job = $job_id ? get_post( $job_id ) : null; $shortcode = Job_Dashboard_Shortcode::instance(); - if ( empty( $job ) || ! $shortcode->is_job_available_on_dashboard( $job ) ) { + if ( ! $shortcode->can_manage_job( $job ) ) { wp_send_json_error( Notice::error( [ @@ -55,7 +75,7 @@ public function ajax_job_overlay() { return; } - $content = $this->get_job_overlay( $job ); + $content = $this->render_job_overlay( $job ); wp_send_json_success( $content ); @@ -65,6 +85,11 @@ public function ajax_job_overlay() { * Output the modal element. */ public function output_modal_element() { + + UI::instance()->enqueue_styles(); + wp_enqueue_style( 'wp-job-manager-job-dashboard' ); + wp_enqueue_script( 'wp-job-manager-job-dashboard' ); + $overlay = new Modal_Dialog( [ 'id' => 'jmDashboardOverlay', @@ -83,17 +108,14 @@ public function output_modal_element() { * * @return string */ - private function get_job_overlay( $job ) { - - $job_actions = Job_Dashboard_Shortcode::instance()->get_job_actions( $job ); + private function render_job_overlay( $job ) { ob_start(); get_job_manager_template( 'job-dashboard-overlay.php', [ - 'job' => $job, - 'job_actions' => $job_actions, + 'job' => $job, ] ); @@ -102,5 +124,96 @@ private function get_job_overlay( $job ) { return $content; } + /** + * Load and output overlay dependencies on the job listings screen. + * + * @output Modal HTML. + * + * @return void + */ + public function init_admin_dashboard_overlay() { + $screen = get_current_screen(); + + if ( ! $screen || 'edit-job_listing' !== $screen->id ) { + return; + } + + $this->init_dashboard_overlay(); + } + + /** + * Load scripts and output HTML skeleton for the job dashboard overlay. + * + * @output Modal HTML. + * + * @return void + */ + public function init_dashboard_overlay() { + + \WP_Job_Manager::register_script( 'wp-job-manager-job-dashboard', 'js/job-dashboard.js', null, true ); + \WP_Job_Manager::register_style( 'wp-job-manager-job-dashboard', 'css/job-dashboard.css', [ 'wp-job-manager-ui' ] ); + + $endpoint = is_admin() + ? add_query_arg( [ 'action' => 'job_dashboard_overlay' ], admin_url( 'admin-ajax.php' ) ) + : \WP_Job_Manager_Ajax::get_endpoint( 'job_dashboard_overlay' ); + $endpoint = wp_nonce_url( $endpoint, 'job_dashboard_overlay' ); + + wp_localize_script( + 'wp-job-manager-job-dashboard', + 'job_manager_job_dashboard', + [ + 'i18nConfirmDelete' => esc_html__( 'Are you sure you want to delete this listing?', 'wp-job-manager' ), + 'overlayEndpoint' => $endpoint, + 'statsEnabled' => \WP_Job_Manager\Stats::is_enabled(), + ] + ); + + $this->output_modal_element(); + } + + /** + * Output the job actions in the overlay footer. + * + * @param \WP_Post $job + */ + public function output_footer_actions( $job ) { + + if ( is_admin() ) { + return; + } + + $job_actions = Job_Dashboard_Shortcode::instance()->get_job_actions( $job ); + + $buttons = []; + $actions = []; + if ( ! empty( $job_actions ) ) { + $primary = Job_Dashboard_Shortcode::get_primary_action( $job, $job_actions ); + + if ( $primary ) { + $buttons[] = [ + 'label' => $primary['label'], + 'url' => $primary['url'], + 'class' => 'job-dashboard-action-' . esc_attr( $primary['name'] ), + 'primary' => false, + ]; + } + + foreach ( $job_actions as $action ) { + if ( ! empty( $primary ) && $primary['name'] === $action['name'] ) { + continue; + } + $actions[] = [ + 'label' => $action['label'], + 'url' => $action['url'], + 'class' => 'job-dashboard-action-' . esc_attr( $action['name'] ), + ]; + } + } + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in UI classes. + echo UI_Elements::actions( $buttons, $actions ); + + } + } diff --git a/includes/class-stats-dashboard.php b/includes/class-stats-dashboard.php index d45c7023a..8f1dc2bdd 100644 --- a/includes/class-stats-dashboard.php +++ b/includes/class-stats-dashboard.php @@ -35,8 +35,8 @@ private function __construct() { add_filter( 'job_manager_job_dashboard_columns', [ $this, 'add_stats_column' ] ); add_action( 'job_manager_job_dashboard_column_' . self::COLUMN_NAME, [ $this, 'render_stats_column' ] ); - add_filter( 'manage_edit-job_listing_columns', [ $this, 'add_stats_column' ], 20 ); - add_action( 'manage_job_listing_posts_custom_column', [ $this, 'maybe_render_job_listing_posts_custom_column' ], 2 ); + add_filter( 'manage_edit-job_listing_columns', [ $this, 'add_stats_column' ] ); + add_action( 'manage_job_listing_posts_custom_column', [ $this, 'maybe_render_admin_stats_column' ], 2 ); add_action( 'job_manager_job_overlay_content', [ $this, 'output_job_stats' ], 12 ); } @@ -77,11 +77,13 @@ public function render_stats_column( $job ) { * * @param string $column */ - public function maybe_render_job_listing_posts_custom_column( $column ) { + public function maybe_render_admin_stats_column( $column ) { global $post; - if ( self::COLUMN_NAME === $column ) { + if ( self::COLUMN_NAME === $column && ! empty( $post->ID ) ) { + echo ''; $this->render_stats_column( $post ); + echo ''; } } diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index 14bff7de6..553206f0e 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -6,6 +6,8 @@ * @since 1.33.0 */ +use WP_Job_Manager\Job_Overlay; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -540,7 +542,6 @@ public function frontend_scripts() { wp_register_script( 'jquery-deserialize', JOB_MANAGER_PLUGIN_URL . '/assets/lib/jquery-deserialize/jquery.deserialize.js', [ 'jquery' ], '1.2.1', true ); self::register_script( 'wp-job-manager-ajax-filters', 'js/ajax-filters.js', $ajax_filter_deps, true ); - self::register_script( 'wp-job-manager-job-dashboard', 'js/job-dashboard.js', null, true ); self::register_script( 'wp-job-manager-job-application', 'js/job-application.js', [ 'jquery' ], true ); self::register_script( 'wp-job-manager-job-submission', 'js/job-submission.js', [ 'jquery' ], true ); wp_localize_script( 'wp-job-manager-ajax-filters', 'job_manager_ajax_filters', $ajax_data ); @@ -571,15 +572,7 @@ public function frontend_scripts() { ] ); - wp_localize_script( - 'wp-job-manager-job-dashboard', - 'job_manager_job_dashboard', - [ - 'i18nConfirmDelete' => esc_html__( 'Are you sure you want to delete this listing?', 'wp-job-manager' ), - 'overlayEndpoint' => WP_Job_Manager_Ajax::get_endpoint( 'job_dashboard_overlay' ), - 'statsEnabled' => \WP_Job_Manager\Stats::is_enabled(), - ] - ); + Job_Overlay::instance()->init_dashboard_overlay(); wp_localize_script( 'wp-job-manager-job-submission', diff --git a/psalm.xml b/psalm.xml index 045f2ceec..43d54c8d9 100644 --- a/psalm.xml +++ b/psalm.xml @@ -29,11 +29,11 @@ - + - +
diff --git a/templates/job-dashboard-overlay.php b/templates/job-dashboard-overlay.php index 6587f8b7b..6a50c5ddb 100644 --- a/templates/job-dashboard-overlay.php +++ b/templates/job-dashboard-overlay.php @@ -9,7 +9,6 @@ * @version $$next-version$$ * * @var WP_Post $job Array of job post results. - * @var array $job_actions */ use WP_Job_Manager\Job_Dashboard_Shortcode; @@ -58,35 +57,5 @@
- +
diff --git a/templates/job-dashboard.php b/templates/job-dashboard.php index 5256b3679..d271a6e37 100644 --- a/templates/job-dashboard.php +++ b/templates/job-dashboard.php @@ -100,6 +100,4 @@ class="jm-dashboard-job-column "> $max_num_pages ] ); ?> - - output_modal_element(); ?>