google = $google; } /** * Source slugs, in display order. * * @return string[] */ public static function sources() { return array( self::SEARCH_CONSOLE, self::ANALYTICS, self::BUSINESS_PROFILE ); } /** * Whether a connection is able to list a source at all. * * @param Connection $connection Connection. * @param string $source Source slug. * @return string Empty when it can, otherwise a message explaining why not. */ public static function unavailable( Connection $connection, $source ) { if ( ! $connection->is_connected() ) { return __( 'This connection isn\'t finished. Complete it on the Connections screen.', 'turtlereport' ); } if ( self::BUSINESS_PROFILE !== $source ) { return ''; } if ( 'oauth' !== $connection->type ) { return __( 'Service-account connections can\'t read Business Profile. Use a Google sign-in connection for this site to add it.', 'turtlereport' ); } if ( ! in_array( Scopes::BUSINESS_PROFILE, $connection->scopes, true ) ) { return __( 'This connection wasn\'t given Business Profile access. Reconnect it on the Connections screen with "Also read Business Profile" ticked.', 'turtlereport' ); } return ''; } /** * Items for one source. * * @param Connection $connection Connection. * @param string $source Source slug. * @param int $ttl Cache lifetime; 0 forces a fresh fetch. * @return array[] Items with value, label and optional group. * @throws ApiException When Google refuses or the connection can't read the source. */ public function items( Connection $connection, $source, $ttl = self::TTL ) { $services = $this->google->for_connection( $connection ); switch ( $source ) { case self::SEARCH_CONSOLE: $items = array(); foreach ( $services->search_console()->sites( $ttl ) as $url ) { $items[] = array( 'value' => $url, 'label' => self::site_label( $url ), ); } usort( $items, static function ( $a, $b ) { return strcasecmp( $a['label'], $b['label'] ); } ); return $items; case self::ANALYTICS: $items = array(); foreach ( $services->analytics()->properties( $ttl ) as $p ) { $items[] = array( 'value' => $p['id'], 'label' => $p['name'], 'group' => $p['account'], ); } return $items; case self::BUSINESS_PROFILE: $items = array(); foreach ( $services->business_profile()->locations( $ttl ) as $l ) { $items[] = array( 'value' => $l['id'], 'label' => $l['name'], ); } return $items; } throw new ApiException( __( 'Unknown data source.', 'turtlereport' ), ApiException::INVALID ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Caught by the plugin; escaped where the message is shown. } /** * Every source for a connection, with errors caught per source so one * failing API doesn't hide the others. * * @param Connection $connection Connection. * @return array Source slug => array{items: array[], error: string, loaded: bool}. */ public function all( Connection $connection ) { $out = array(); foreach ( self::sources() as $source ) { $result = array( 'items' => array(), 'error' => '', 'loaded' => false, ); $problem = self::unavailable( $connection, $source ); if ( '' !== $problem ) { $result['error'] = $problem; } else { try { $result['items'] = $this->items( $connection, $source ); $result['loaded'] = true; } catch ( ApiException $e ) { $result['error'] = self::explain( $e ); } } $out[ $source ] = $result; } return $out; } /** * Message for an error, with setup advice where Google's own text is unhelpful. * * @param ApiException $e Error. * @return string */ public static function explain( ApiException $e ) { if ( ApiException::QUOTA === $e->reason() ) { return __( 'Google hasn\'t approved Business Profile API access for this Cloud project yet. See "Business Profile" in the setup guide.', 'turtlereport' ); } return $e->getMessage(); } /** * Readable label for a Search Console site. * * @param string $url Site URL. * @return string */ public static function site_label( $url ) { if ( 0 === strpos( $url, 'sc-domain:' ) ) { /* translators: %s: domain name. */ return sprintf( __( '%s (domain)', 'turtlereport' ), substr( $url, 10 ) ); } return rtrim( (string) preg_replace( '#^https?://#', '', $url ), '/' ); } }