access = $access; $this->connections = $connections; $this->settings = $settings; } /** * {@inheritDoc} * * @return string */ public function menu_title() { return __( 'Reports', 'turtlereport' ); } /** * {@inheritDoc} * * @return string */ public function capability() { return Capabilities::VIEW_REPORTS; } /** * {@inheritDoc} * * @return void */ public function render() { $user_id = get_current_user_id(); $sites = $this->access->visible_sites( $user_id ); View::render( 'reports', array( 'sites' => $sites, 'can_manage' => current_user_can( Capabilities::MANAGE ), 'brand_logo' => $this->settings->logo_url(), 'brand_name' => $this->settings->brand_name(), 'has_connections' => array() !== $this->connections->all(), ) ); } /** * {@inheritDoc} * * @return void */ public function enqueue() { $sites = $this->access->visible_sites( get_current_user_id() ); if ( ! $sites ) { $this->enqueue_admin_style(); return; } wp_enqueue_style( 'turtlereport-reports', TURTLEREPORT_URL . 'assets/css/reports.css', array( 'turtlereport-shell' ), self::asset_version( 'assets/css/reports.css' ) ); wp_register_script( 'turtlereport-chart', TURTLEREPORT_URL . 'assets/vendor/chart.umd.min.js', array(), '4.5.1', true ); AdminAssets::enqueue( 'reports', 'turtlereport-reports', array( 'turtlereport-chart' ) ); wp_add_inline_script( 'turtlereport-reports', 'window.turtlereportReports = ' . wp_json_encode( $this->boot_data( $sites ) ) . ';', 'before' ); } /** * Data the report script needs. Contains no credentials or account details. * * @param Profile[] $sites Sites the user can view. * @return array */ private function boot_data( array $sites ) { $can_manage = current_user_can( Capabilities::MANAGE ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only choice of which visible site to show. $requested = isset( $_GET['site'] ) ? sanitize_key( wp_unslash( $_GET['site'] ) ) : ''; $selected = isset( $sites[ $requested ] ) ? $requested : (string) key( $sites ); $initial = null; $link_err = ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only: dates of a report link. if ( isset( $_GET['start'] ) || isset( $_GET['end'] ) ) { if ( '' !== $requested && ! isset( $sites[ $requested ] ) ) { // Same message whether the site doesn't exist or isn't granted: IDs aren't revealed. $link_err = __( 'You don\'t have access to the report in that link, so your own report is shown instead.', 'turtlereport' ); } else { $initial = self::link_period( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Validated in link_period(). if ( null === $initial ) { $link_err = __( 'That report link has invalid dates, so the latest period is shown instead.', 'turtlereport' ); } } } $metrics = TrafficMetrics::all(); $list = array(); foreach ( $sites as $site ) { $traffic = array(); foreach ( $site->traffic_metrics as $key ) { if ( isset( $metrics[ $key ] ) ) { $traffic[] = array( 'k' => $key, 'l' => $metrics[ $key ]['label'], 'f' => $metrics[ $key ]['format'], 'lower' => $metrics[ $key ]['lower'], ); } } $list[] = array( 'id' => $site->id, 'name' => $site->display_name(), 'siteLabel' => $site->site_label(), 'gscSite' => $site->gsc_site, 'searchType' => $site->search_type, 'hasGsc' => '' !== $site->gsc_site, 'hasGa' => '' !== $site->ga_property, 'sections' => array( 'search' => $site->wants( Profile::SECTION_SEARCH ), 'conversions' => $site->wants( Profile::SECTION_CONVERSIONS ), 'ai' => $site->wants( Profile::SECTION_AI ), 'traffic' => $site->wants( Profile::SECTION_TRAFFIC ) && array() !== $traffic, ), 'enabled' => array( 'search' => in_array( Profile::SECTION_SEARCH, $site->sections, true ), 'conversions' => in_array( Profile::SECTION_CONVERSIONS, $site->sections, true ), 'ai' => in_array( Profile::SECTION_AI, $site->sections, true ), 'traffic' => in_array( Profile::SECTION_TRAFFIC, $site->sections, true ), ), 'gbp' => $this->gbp_state( $site ), 'conversionLabel' => $site->conversion_label(), 'priorities' => \TurtleReport\Profiles\Priorities::for_report( $site ), 'groups' => $site->conversion_groups, 'leadBasis' => $site->lead_basis, 'traffic' => $traffic, 'segments' => array_map( static function ( $seg ) { return array( 'id' => $seg['id'], 'name' => $seg['name'], 'type' => $seg['type'], ); }, $site->segments ), 'clientLogo' => Settings::attachment_url( $site->client_logo_id ), 'trafficScope' => $site->traffic_scope, 'editUrl' => $can_manage ? SitesPage::url( array( 'action' => 'edit', 'site' => $site->id, ) ) : '', ); } return array( 'sites' => $list, 'selected' => $selected, 'canManage' => $can_manage, 'connectionsUrl' => $can_manage ? ConnectionsPage::url() : '', // Managers see this client's open Client Monitor issues in At a glance. 'monitorUrl' => $can_manage ? OverviewPage::url() : '', 'reportsUrl' => self::url(), 'brand' => array( 'name' => $this->settings->brand_name(), 'logo' => $this->settings->logo_url(), 'color' => $this->settings->brand_color(), 'footer' => (string) $this->settings->get( 'footer_text' ), 'credit' => $this->settings->show_credit(), ), 'today' => Periods::today(), // A report link's exact period (from an email), or null for the usual default. 'initial' => $initial, 'linkError' => $link_err, 'gbpFormUrl' => 'https://support.google.com/business/contact/api_default', 'vendor' => array( 'jspdf' => add_query_arg( 'ver', self::asset_version( 'assets/vendor/jspdf.umd.min.js' ), TURTLEREPORT_URL . 'assets/vendor/jspdf.umd.min.js' ), 'html2canvas' => add_query_arg( 'ver', self::asset_version( 'assets/vendor/html2canvas.min.js' ), TURTLEREPORT_URL . 'assets/vendor/html2canvas.min.js' ), ), ); } /** * Reads and checks the period in a report link: * ?start=Y-m-d&end=Y-m-d&compare=previous|year&cstart=Y-m-d&cend=Y-m-d. * * The dates must be real, in order, no longer than the REST API allows, * not in the future, and the comparison must end before the period * starts. Without cstart/cend the comparison is worked out from compare. * * @param array $query Query arguments ($_GET). * @return array{s:string,e:string,ps:string,pe:string,compare:string}|null Null when invalid. */ public static function link_period( array $query ) { $get = static function ( $key ) use ( $query ) { return isset( $query[ $key ] ) && is_scalar( $query[ $key ] ) ? sanitize_text_field( wp_unslash( (string) $query[ $key ] ) ) : ''; }; $date = static function ( $value ) { $d = \DateTimeImmutable::createFromFormat( '!Y-m-d', $value, new \DateTimeZone( 'UTC' ) ); return $d && $d->format( 'Y-m-d' ) === $value ? $d : null; }; $s = $date( $get( 'start' ) ); $e = $date( $get( 'end' ) ); if ( ! $s || ! $e || $s > $e ) { return null; } $today = $date( Periods::today() ); if ( $today && $e > $today ) { return null; } $days = Periods::inclusive_days( $s->format( 'Y-m-d' ), $e->format( 'Y-m-d' ) ); if ( $days > \TurtleReport\Rest\ReportController::MAX_RANGE_DAYS ) { return null; } $compare = 'year' === $get( 'compare' ) ? 'year' : 'previous'; $ps = $date( $get( 'cstart' ) ); $pe = $date( $get( 'cend' ) ); if ( ( $get( 'cstart' ) !== '' || $get( 'cend' ) !== '' ) && ( ! $ps || ! $pe || $ps > $pe || $pe >= $s || Periods::inclusive_days( $ps->format( 'Y-m-d' ), $pe->format( 'Y-m-d' ) ) > \TurtleReport\Rest\ReportController::MAX_RANGE_DAYS ) ) { return null; } $out = array( 's' => $s->format( 'Y-m-d' ), 'e' => $e->format( 'Y-m-d' ), 'compare' => $compare, ); if ( $ps && $pe ) { $out['ps'] = $ps->format( 'Y-m-d' ); $out['pe'] = $pe->format( 'Y-m-d' ); } elseif ( 'year' === $compare ) { $out['ps'] = Periods::shift_year( $out['s'], -1 ); $out['pe'] = Periods::shift_year( $out['e'], -1 ); } else { $out['pe'] = Periods::add_days( $out['s'], -1 ); $out['ps'] = Periods::add_days( $out['pe'], -( $days - 1 ) ); } return $out; } /** * Whether a site's Business Profile section can load, and why not. * * @param Profile $site Site. * @return string off, ready, no-location, no-scope or unsupported. */ private function gbp_state( Profile $site ) { if ( ! in_array( Profile::SECTION_BUSINESS, $site->sections, true ) ) { return 'off'; } $connection = $this->connections->find( $site->connection_for( Profile::SOURCE_BUSINESS ) ); if ( ! $connection || Connection::TYPE_SERVICE_ACCOUNT === $connection->type ) { return 'unsupported'; } if ( ! $connection->has_scope( Scopes::BUSINESS_PROFILE ) ) { return 'no-scope'; } return '' === $site->gbp_location ? 'no-location' : 'ready'; } }