sites = $sites; $this->connections = $connections; $this->access = $access; $this->email_log = $email_log; $this->lists = $lists; $this->settings = $settings; } /** * {@inheritDoc} * * @return string */ public function menu_title() { return __( 'Sites & access', 'turtlereport' ); } /** * {@inheritDoc} * * @return string */ public function capability() { return Capabilities::MANAGE; } /** * {@inheritDoc} * * @return void */ public function render() { $site = $this->editing(); if ( null !== $site ) { View::render( 'sites/edit', array( 'site' => $site, 'connections' => $this->connections->all(), 'roles' => $this->access->grantable_roles(), 'users' => $this->granted_users( $site ), 'metrics' => TrafficMetrics::all(), 'viewer_role' => ViewerRole::ROLE, 'client_logo' => Settings::attachment_url( $site->client_logo_id ), 'email_log' => '' !== $site->id ? $this->email_log->get( $site->id ) : null, 'timezone' => wp_timezone_string(), 'sources' => $this->source_lists( $site ), 'viewers' => $this->people( ( new Recipients( $this->access ) )->viewer_ids( $site ) ), 'email_users' => $this->email_users( $site ), 'audience' => '' !== $site->id ? ( new Recipients( $this->access ) )->resolve( $site ) : array(), 'me' => (string) wp_get_current_user()->user_email, ) ); return; } $rows = array(); foreach ( $this->sites->all() as $s ) { $rows[] = array( 'site' => $s, 'connection' => $this->connections->find( $s->connection_id ), 'extra' => array_values( array_filter( array_map( function ( $id ) use ( $s ) { return $id !== $s->connection_id ? $this->connections->find( $id ) : null; }, $s->connection_ids() ) ) ), 'user_count' => count( $this->access->users_for_site( $s->id ) ), 'emails' => Schedule::is_active( $s->email ) ? $this->email_log->get( $s->id ) : null, ); } View::render( 'sites/list', array( 'rows' => $rows, 'roles' => $this->access->grantable_roles(), 'has_connections' => array() !== $this->connections->all(), ) ); } /** * {@inheritDoc} * * @return void */ public function enqueue() { $this->enqueue_admin_style(); $site = $this->editing(); if ( null !== $site ) { wp_enqueue_media(); } AdminAssets::enqueue( 'admin', 'turtlereport-admin' ); if ( null === $site ) { return; } AdminAssets::enqueue( 'clients', 'turtlereport-site-editor' ); wp_add_inline_script( 'turtlereport-site-editor', 'window.turtlereportSiteEditor = ' . wp_json_encode( array( 'gscSite' => $site->gsc_site, 'gaProperty' => $site->ga_property, 'gbpLocation' => $site->gbp_location, 'basis' => $site->lead_basis, 'presets' => array_values( ConversionPresets::all() ), 'segPresets' => array_values( Segments::presets() ), 'maxSegments' => Segments::MAX_SEGMENTS, 'maxGroups' => Profile::MAX_GROUPS, 'maxMetrics' => Profile::MAX_TRAFFIC_METRICS, 'sourceConnections' => $site->source_connections, 'connectionsUrl' => ConnectionsPage::url(), 'connections' => array_values( array_map( static function ( $c ) { return array( 'id' => $c->id, 'isOAuth' => 'oauth' === $c->type, 'hasBusiness' => in_array( \TurtleReport\Auth\Scopes::BUSINESS_PROFILE, $c->scopes, true ), 'connected' => $c->is_connected(), 'email' => $c->account_email, ); }, $this->connections->all() ) ), ) ) . ';', 'before' ); } /** * Lists for each source's connection, fetched server-side so the editor * works even if its script is blocked. Cached for ten minutes, shared with * the REST route. * * @param Profile $site Site. * @return array Source slug => array{items: array[], error: string, loaded: bool}; a source without a connection is left out. */ private function source_lists( Profile $site ) { $out = array(); $cache = array(); foreach ( self::source_map() as $slug => $source ) { $id = $site->connection_for( $source ); $connection = '' !== $id ? $this->connections->find( $id ) : null; if ( ! $connection ) { continue; } if ( ! isset( $cache[ $connection->id ] ) ) { $cache[ $connection->id ] = $this->lists->all( $connection ); } $out[ $slug ] = $cache[ $connection->id ][ $slug ]; } return $out; } /** * SourceLists slug => Profile source key. * * @return array */ public static function source_map() { return array( SourceLists::SEARCH_CONSOLE => Profile::SOURCE_SEARCH, SourceLists::ANALYTICS => Profile::SOURCE_ANALYTICS, SourceLists::BUSINESS_PROFILE => Profile::SOURCE_BUSINESS, ); } /** * Name and email of users, for display. * * @param int[] $user_ids User IDs. * @return array[] Items with id, name and email. */ private function people( array $user_ids ) { $out = array(); foreach ( $user_ids as $user_id ) { $user = get_userdata( (int) $user_id ); if ( $user ) { $out[] = array( 'id' => (int) $user->ID, 'name' => (string) $user->display_name, 'email' => (string) $user->user_email, ); } } return $out; } /** * Users chosen for the scheduled email, flagged when they can't open the report. * * @param Profile $site Site. * @return array[] Items with id, name, email and can_view. */ private function email_users( Profile $site ) { $out = array(); foreach ( $this->people( isset( $site->email['user_ids'] ) ? (array) $site->email['user_ids'] : array() ) as $person ) { $person['can_view'] = '' === $site->id || $this->access->can_manage( $person['id'] ) || $this->access->can_view( $person['id'], $site->id ); $out[] = $person; } return $out; } /** * The site being edited, a new blank site, or null on the list view. * * @return Profile|null */ private function editing() { if ( false === $this->editing ) { $this->editing = $this->resolve_editing(); $draft = $this->editing ? get_transient( SiteActions::draft_key() ) : false; if ( is_array( $draft ) && isset( $draft['id'], $draft['site'] ) && $draft['id'] === $this->editing->id ) { $this->editing = Profile::from_array( $draft['site'] ); $this->draft_users = array_map( 'intval', (array) $draft['users'] ); delete_transient( SiteActions::draft_key() ); } if ( $this->editing ) { $this->editing = $this->apply_new_account( $this->editing ); } } return $this->editing; } /** * Selects an account just connected from this screen for the source it was added for. * * The admin came back from Google with the form restored above; this puts * the new account where they asked for it, so its properties load straight * away. Every other source is left alone. * * @param Profile $site Site being edited. * @return Profile The site to show, unchanged when there is no new account. */ private function apply_new_account( Profile $site ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only; the value is only used if it names a finished connection. $id = isset( $_GET['connected'] ) ? sanitize_key( wp_unslash( $_GET['connected'] ) ) : ''; $source = isset( $_GET['source'] ) ? sanitize_key( wp_unslash( $_GET['source'] ) ) : ''; // phpcs:enable if ( '' === $id || ! in_array( $source, Profile::sources(), true ) ) { return $site; } $connection = $this->connections->find( $id ); if ( ! $connection || ! $connection->is_connected() ) { return $site; } // A copy: the repository hands out shared objects, and this choice isn't saved until the admin saves the form. $shown = clone $site; $shown->source_connections[ $source ] = $connection->id; if ( '' === $shown->connection_id ) { $shown->connection_id = $connection->id; } return $shown; } /** * Works out which site the screen is for. * * @return Profile|null */ private function resolve_editing() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only screen routing. $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; if ( 'new' === $action ) { $site = new Profile(); if ( $this->settings ) { $site->email = $this->settings->email_defaults(); } $all = $this->connections->all(); $requested = isset( $_GET['connection'] ) ? sanitize_key( wp_unslash( $_GET['connection'] ) ) : ''; if ( isset( $all[ $requested ] ) ) { $site->connection_id = $requested; } elseif ( 1 === count( $all ) ) { $site->connection_id = (string) key( $all ); } return $site; } if ( 'edit' === $action && isset( $_GET['site'] ) ) { return $this->sites->find( sanitize_key( wp_unslash( $_GET['site'] ) ) ); } // phpcs:enable return null; } /** * Users granted a site directly, for the access picker. * * @param Profile $site Site. * @return array[] Items with id, name and email. */ private function granted_users( Profile $site ) { if ( null !== $this->draft_users ) { $ids = $this->draft_users; } elseif ( '' === $site->id ) { return array(); } else { $ids = $this->access->users_for_site( $site->id ); } $out = array(); foreach ( $ids as $user_id ) { $user = get_userdata( $user_id ); if ( $user ) { $out[] = array( 'id' => $user->ID, 'name' => $user->display_name, 'email' => $user->user_email, ); } } return $out; } }