$d ) { $code = sanitize_key( (string) $code ); if ( '' === $code || ! is_array( $d ) ) { continue; } $d = wp_parse_args( $d, array( 'label' => $code, 'icon' => '', 'sentence' => '', 'blurb' => '', 'core' => array(), 'likely' => array(), 'ids' => array(), 'aliases' => array(), 'kpis' => array(), 'first_win' => '', 'survey_roles' => array(), ) ); $d['core'] = self::valid_targets( $code, 'core', (array) $d['core'] ); $d['likely'] = self::valid_targets( $code, 'likely', (array) $d['likely'] ); $d['ids'] = array_values( array_filter( array_map( 'sanitize_key', (array) $d['ids'] ) ) ); $d['kpis'] = self::valid_kpis( $code, (array) $d['kpis'] ); $d['first_win'] = sanitize_key( (string) $d['first_win'] ); $d['survey_roles'] = array_values( array_filter( array_map( 'sanitize_key', (array) $d['survey_roles'] ) ) ); $out[ $code ] = $d; } self::$defs[ $locale ] = $out; return $out; } private static function valid_targets( $code, $key, array $list ) { $groups = self::group_ids(); $sections = self::section_ids(); $ok = array(); foreach ( $list as $t ) { $t = trim( (string) $t ); if ( '*' === $t ) { if ( 'likely' === $key ) { $ok[] = '*'; } else { self::$problems[] = sprintf( '%s.%s: "*" is only allowed in likely', $code, $key ); } continue; } $bits = explode( '/', $t, 2 ); $g = sanitize_key( $bits[0] ); $s = isset( $bits[1] ) ? sanitize_key( $bits[1] ) : ''; if ( ! empty( $groups ) && ! isset( $groups[ $g ] ) ) { self::$problems[] = sprintf( '%s.%s: unknown group "%s"', $code, $key, $t ); continue; } if ( '' !== $s && ! empty( $groups ) && empty( $sections[ $g ][ $s ] ) ) { self::$problems[] = sprintf( '%s.%s: unknown section "%s"', $code, $key, $t ); continue; } $ok[] = '' === $s ? $g : $g . '/' . $s; } return array_values( array_unique( $ok ) ); } private static function valid_kpis( $code, array $list ) { $ok = array(); foreach ( $list as $id ) { $id = sanitize_key( (string) $id ); if ( '' === $id ) { continue; } if ( class_exists( 'AIEO_Table_Registry' ) && method_exists( 'AIEO_Table_Registry', 'get' ) && ! AIEO_Table_Registry::get( $id ) ) { self::$problems[] = sprintf( '%s.kpis: unknown report "%s"', $code, $id ); continue; } $ok[] = $id; } return $ok; } private static function group_ids() { static $ids = null; if ( null !== $ids ) { return $ids; } $ids = array(); if ( class_exists( 'AIEO_DMM' ) ) { foreach ( (array) AIEO_DMM::instance()->get_group_definitions() as $g ) { if ( isset( $g['id'] ) ) { $ids[ (string) $g['id'] ] = true; } } } return $ids; } private static function section_ids() { static $ids = null; if ( null !== $ids ) { return $ids; } $ids = array(); if ( class_exists( 'AIEO_DMM' ) ) { foreach ( (array) AIEO_DMM::instance()->get_section_definitions() as $gid => $secs ) { foreach ( (array) $secs as $s ) { if ( isset( $s['id'] ) ) { $ids[ (string) $gid ][ (string) $s['id'] ] = true; } } } } return $ids; } public static function problems() { $defs = self::defs(); $out = self::$problems; if ( class_exists( 'AIEO_DMM' ) ) { foreach ( $defs as $code => $d ) { if ( '' !== $d['first_win'] && null === self::destination( $d['first_win'] ) ) { $out[] = sprintf( '%s.first_win: "%s" is not a destination on this build', $code, $d['first_win'] ); } } } return array_values( array_unique( $out ) ); } public static function codes() { return array_keys( self::defs() ); } public static function sanitize( array $codes ) { $codes = array_map( 'sanitize_key', array_map( 'strval', $codes ) ); $out = array(); foreach ( self::codes() as $c ) { if ( in_array( $c, $codes, true ) ) { $out[] = $c; } } return $out; } public static function labels( array $codes ) { $defs = self::defs(); $out = array(); foreach ( self::sanitize( $codes ) as $c ) { $out[] = (string) $defs[ $c ]['label']; } return $out; } public static function for_user( $uid ) { $uid = (int) $uid; if ( $uid > 0 && self::answered( $uid ) ) { $v = get_user_meta( $uid, self::META, true ); return is_array( $v ) ? self::sanitize( $v ) : array(); } return self::site_default(); } public static function answered( $uid ) { return (int) get_user_meta( (int) $uid, self::META_AT, true ) > 0; } public static function save( $uid, array $codes ) { $uid = (int) $uid; if ( $uid < 1 ) { return array(); } $codes = self::sanitize( $codes ); update_user_meta( $uid, self::META, $codes ); update_user_meta( $uid, self::META_AT, time() ); return $codes; } public static function clear( $uid ) { $uid = (int) $uid; if ( $uid < 1 ) { return; } delete_user_meta( $uid, self::META ); delete_user_meta( $uid, self::META_AT ); } public static function site_default() { $v = get_option( self::OPTION_DEFAULT ); return is_array( $v ) ? self::sanitize( $v ) : array(); } public static function set_site_default( array $codes ) { $codes = self::sanitize( $codes ); if ( empty( $codes ) ) { delete_option( self::OPTION_DEFAULT ); } else { update_option( self::OPTION_DEFAULT, $codes, false ); } } public static function record_survey_role( $uid, $role ) { $uid = (int) $uid; $role = sanitize_key( (string) $role ); if ( $uid > 0 && '' !== $role ) { update_user_meta( $uid, self::SURVEY_ROLE_META, $role ); } } public static function from_survey( $uid ) { $role = sanitize_key( (string) get_user_meta( (int) $uid, self::SURVEY_ROLE_META, true ) ); if ( '' === $role ) { return array(); } $out = array(); foreach ( self::defs() as $code => $d ) { if ( in_array( $role, (array) $d['survey_roles'], true ) ) { $out[] = $code; } } return $out; } public static function rank( $id, $group, $section, array $codes ) { $defs = self::defs(); $id = (string) $id; $group = (string) $group; $section = (string) $section; $gs = '' !== $section ? $group . '/' . $section : ''; $best = self::R_UNLIKELY; foreach ( self::sanitize( $codes ) as $c ) { $d = $defs[ $c ]; if ( ( '' !== $id && in_array( $id, $d['ids'], true ) ) || ( '' !== $group && in_array( $group, $d['core'], true ) ) || ( '' !== $gs && in_array( $gs, $d['core'], true ) ) ) { return self::R_CORE; } if ( in_array( '*', $d['likely'], true ) || ( '' !== $group && in_array( $group, $d['likely'], true ) ) || ( '' !== $gs && in_array( $gs, $d['likely'], true ) ) ) { $best = self::R_LIKELY; } } if ( self::R_UNLIKELY === $best ) { if ( in_array( $group, self::NEVER_DEMOTED, true ) || ! self::is_mapped( $id, $group, $section ) ) { $best = self::R_LIKELY; } } return $best; } private static function is_mapped( $id, $group, $section ) { if ( null === self::$mapped ) { self::$mapped = array(); foreach ( self::defs() as $d ) { foreach ( array_merge( (array) $d['core'], (array) $d['likely'] ) as $t ) { if ( '*' !== $t ) { self::$mapped[ $t ] = true; } } foreach ( (array) $d['ids'] as $t ) { self::$mapped[ 'id:' . $t ] = true; } } } if ( '' !== $id && isset( self::$mapped[ 'id:' . $id ] ) ) { return true; } if ( '' !== $group && isset( self::$mapped[ $group ] ) ) { return true; } return '' !== $section && isset( self::$mapped[ $group . '/' . $section ] ); } public static function combine( $shape, $persona ) { if ( 'not_for_you' === $shape ) { return 'not_for_you'; } if ( self::R_CORE === $shape || self::R_CORE === $persona ) { return self::R_CORE; } if ( self::R_UNLIKELY === $shape || self::R_UNLIKELY === $persona ) { return self::R_UNLIKELY; } return self::R_LIKELY; } private static function child_relevance( array $child, $group, array $codes ) { $id = isset( $child['id'] ) ? (string) $child['id'] : ''; $sec = isset( $child['section'] ) ? (string) $child['section'] : ''; if ( '' === $sec && class_exists( 'AIEO_DMM' ) && ( ! isset( $child['kind'] ) || 'link' !== $child['kind'] ) ) { $sec = (string) AIEO_DMM::instance()->resolve_module_section( $id ); } return class_exists( 'AIEO_Shop_Shape' ) ? AIEO_Shop_Shape::relevance( $id, $group, $sec, null, $codes ) : self::rank( $id, $group, $sec, $codes ); } public static function group_ranks( array $tree, array $codes ) { $order = array( self::R_CORE => 3, self::R_LIKELY => 2, self::R_UNLIKELY => 1, 'not_for_you' => 0 ); $out = array(); foreach ( $tree as $e ) { $eid = (string) $e['id']; if ( isset( $e['kind'] ) && 'group' === $e['kind'] ) { $best = self::R_UNLIKELY; foreach ( (array) $e['children'] as $ch ) { $r = self::child_relevance( $ch, $eid, $codes ); if ( $order[ $r ] > $order[ $best ] ) { $best = $r; } if ( self::R_CORE === $best ) { break; } } $out[ $eid ] = 'not_for_you' === $best ? self::R_UNLIKELY : $best; } else { $g = isset( $e['group'] ) ? (string) $e['group'] : ''; $r = self::child_relevance( $e, $g, $codes ); $out[ $eid ] = 'not_for_you' === $r ? self::R_UNLIKELY : $r; } } return $out; } public static function order_tree( array $tree, array $codes, $active_group = '' ) { $codes = self::sanitize( $codes ); if ( empty( $codes ) ) { $total = 0; foreach ( $tree as $e ) { $total += ( isset( $e['kind'] ) && 'group' === $e['kind'] ) ? count( (array) $e['children'] ) : 1; } return array( 'tree' => $tree, 'folded' => array(), 'pages_folded' => 0, 'pages_total' => $total, 'groups_first' => 0 ); } $ranks = self::group_ranks( $tree, $codes ); $core = array(); $likely = array(); $folded = array(); $total = 0; $fpages = 0; foreach ( $tree as $e ) { $eid = (string) $e['id']; $is_group = isset( $e['kind'] ) && 'group' === $e['kind']; $pages = $is_group ? count( (array) $e['children'] ) : 1; $total += $pages; $r = isset( $ranks[ $eid ] ) ? $ranks[ $eid ] : self::R_LIKELY; $gid = $is_group ? $eid : ( isset( $e['group'] ) ? (string) $e['group'] : '' ); $keep = ! empty( $e['has_active'] ) || ( '' !== $active_group && $gid === $active_group ) || in_array( $gid, self::NEVER_DEMOTED, true ); if ( self::R_CORE === $r ) { $core[] = $e; } elseif ( self::R_LIKELY === $r || $keep ) { $likely[] = $e; } else { $fpages += $pages; $folded[] = array( 'id' => $eid, 'label' => wp_specialchars_decode( (string) $e['label'], ENT_QUOTES ), 'pages' => $pages, 'url' => $is_group ? ( class_exists( 'AIEO_Chrome' ) && AIEO_Chrome::on() ? AIEO_Chrome::home_url() . '#rooster-admin-home-' . $eid : add_query_arg( array( 'page' => AIEO_DMM::MENU_SLUG, 'aieo_group' => $eid ), admin_url( 'admin.php' ) ) ) : AIEO_DMM::instance()->get_subtab_url( $eid ), ); } } return array( 'tree' => array_merge( $core, $likely ), 'folded' => $folded, 'pages_folded' => $fpages, 'pages_total' => $total, 'groups_first' => count( $core ), ); } public static function counts( array $codes ) { static $memo = array(); $key = implode( ',', self::sanitize( $codes ) ); if ( isset( $memo[ $key ] ) ) { return $memo[ $key ]; } $tree = class_exists( 'AIEO_DMM' ) ? (array) AIEO_DMM::instance()->get_subtab_tree( '' ) : array(); $fingerprint = md5( ( defined( 'AIEO_VERSION' ) ? AIEO_VERSION : '' ) . '|' . count( $tree ) . '|' . wp_json_encode( array_map( static function ( $e ) { return isset( $e['children'] ) ? count( (array) $e['children'] ) : 1; }, $tree ) ) . '|' . wp_json_encode( class_exists( 'AIEO_Shop_Shape' ) ? AIEO_Shop_Shape::get() : null ) . '|' . $key ); $tkey = 'aieo_persona_counts_' . $fingerprint; $cached = get_transient( $tkey ); if ( is_array( $cached ) && isset( $cached['groups_first'], $cached['pages'], $cached['pages_total'] ) ) { $memo[ $key ] = $cached; return $cached; } $ranks = self::group_ranks( $tree, self::sanitize( $codes ) ); $first = 0; $pages = 0; $total = 0; foreach ( $tree as $e ) { $n = ( isset( $e['kind'] ) && 'group' === $e['kind'] ) ? count( (array) $e['children'] ) : 1; $total += $n; if ( isset( $ranks[ (string) $e['id'] ] ) && self::R_CORE === $ranks[ (string) $e['id'] ] ) { $first++; $pages += $n; } } $memo[ $key ] = array( 'groups_first' => $first, 'pages' => $pages, 'pages_total' => $total ); set_transient( $tkey, $memo[ $key ], 12 * HOUR_IN_SECONDS ); return $memo[ $key ]; } public static function destination( $id ) { $id = (string) $id; if ( '' === $id || ! class_exists( 'AIEO_DMM' ) ) { return null; } static $index = null; if ( null === $index ) { $index = array(); $dmm = AIEO_DMM::instance(); foreach ( (array) $dmm->get_subtab_tree( '' ) as $e ) { if ( isset( $e['kind'] ) && 'group' === $e['kind'] ) { foreach ( (array) $e['children'] as $ch ) { $cid = (string) $ch['id']; $index[ $cid ] = array( 'id' => $cid, 'label' => wp_specialchars_decode( (string) $ch['label'], ENT_QUOTES ), 'url' => ( isset( $ch['kind'] ) && 'link' === $ch['kind'] && ! empty( $ch['url'] ) ) ? (string) $ch['url'] : $dmm->get_subtab_url( $cid ), 'group' => (string) $e['id'], ); } } elseif ( isset( $e['id'] ) ) { $index[ (string) $e['id'] ] = array( 'id' => (string) $e['id'], 'label' => wp_specialchars_decode( (string) $e['label'], ENT_QUOTES ), 'url' => $dmm->get_subtab_url( (string) $e['id'] ), 'group' => isset( $e['group'] ) ? (string) $e['group'] : '', ); } } } return isset( $index[ $id ] ) ? $index[ $id ] : null; } public static function first_win( array $codes ) { $defs = self::defs(); foreach ( self::sanitize( $codes ) as $c ) { $dest = self::destination( $defs[ $c ]['first_win'] ); if ( ! $dest ) { continue; } $why = ''; if ( class_exists( 'AIEO_Menu_Descriptions' ) && method_exists( 'AIEO_Menu_Descriptions', 'for_subtab' ) ) { $why = (string) AIEO_Menu_Descriptions::for_subtab( $dest['id'] ); } return array( 'persona' => $c, 'id' => $dest['id'], 'label' => $dest['label'], 'url' => $dest['url'], 'why' => $why, ); } return null; } public static function unmapped_sections() { self::defs(); self::is_mapped( '', '', '' ); $out = array(); foreach ( self::section_ids() as $g => $secs ) { if ( isset( self::$mapped[ $g ] ) ) { continue; } foreach ( array_keys( $secs ) as $s ) { if ( ! isset( self::$mapped[ $g . '/' . $s ] ) ) { $out[] = $g . '/' . $s; } } } foreach ( array_keys( self::group_ids() ) as $g ) { if ( ! isset( self::$mapped[ $g ] ) && empty( self::section_ids()[ $g ] ) ) { $out[] = $g; } } return $out; } public static function flush() { self::$defs = array(); self::$problems = array(); self::$mapped = null; } } if ( defined( 'WP_CLI' ) && WP_CLI && ! class_exists( 'AIEO_CLI_Personas' ) ) { class AIEO_CLI_Personas { /** * @subcommand report */ public function report( $args, $assoc ) { AIEO_Personas::flush(); $defs = AIEO_Personas::defs(); $prob = AIEO_Personas::problems(); WP_CLI::line( sprintf( '%d personas: %s', count( $defs ), implode( ', ', array_keys( $defs ) ) ) ); WP_CLI::line( '' ); foreach ( $defs as $code => $d ) { $c = AIEO_Personas::counts( array( $code ) ); $fw = AIEO_Personas::first_win( array( $code ) ); WP_CLI::line( sprintf( '%-11s %d groups first · %d of %d pages · core: %s · first win: %s', $code, $c['groups_first'], $c['pages'], $c['pages_total'], implode( ' ', $d['core'] ), $fw ? $fw['id'] : '(none on this build)' ) ); } WP_CLI::line( '' ); $gaps = AIEO_Personas::unmapped_sections(); if ( $gaps ) { WP_CLI::line( sprintf( '%d sections no persona mentions — likely for everyone, never hidden:', count( $gaps ) ) ); foreach ( $gaps as $g ) { WP_CLI::line( ' ' . $g ); } } else { WP_CLI::line( 'Every section is mentioned by at least one persona.' ); } if ( ! empty( $assoc['user'] ) ) { $uid = (int) $assoc['user']; $held = AIEO_Personas::for_user( $uid ); WP_CLI::line( '' ); WP_CLI::line( sprintf( 'user %d holds: %s%s', $uid, $held ? implode( ', ', $held ) : '(nothing)', AIEO_Personas::answered( $uid ) ? '' : ' (site default; never answered)' ) ); $fw = AIEO_Personas::first_win( $held ); if ( $fw ) { WP_CLI::line( sprintf( 'first win: %s — %s', $fw['label'], $fw['why'] ) ); } } if ( $prob ) { WP_CLI::line( '' ); foreach ( $prob as $p ) { WP_CLI::warning( $p ); } WP_CLI::error( sprintf( '%d problem(s) in includes/dmm/data/personas.php', count( $prob ) ) ); } WP_CLI::success( 'The persona map is valid.' ); } } WP_CLI::add_command( 'aieo personas', 'AIEO_CLI_Personas' ); }