register();
}
}
/**
* The plugin instance.
*
* @return self
*/
public static function instance() {
self::boot();
return self::$instance;
}
/**
* Wires services together.
*/
private function __construct() {
$this->cache = new Cache();
$this->connections = new ConnectionRepository( new Crypto() );
$this->sites = new ProfileRepository();
$this->access = new AccessPolicy( $this->sites );
$this->credentials = new CredentialsFactory( $this->connections );
$this->google = new ServicesFactory( $this->connections, $this->credentials, $this->cache );
$this->settings = new Settings();
$this->health = new HealthMonitor( $this->connections, $this->sites, $this->google, $this->settings );
$this->email_log = new EmailLog();
$this->runs = new RunRepository();
$this->deliveries = new DeliveryRepository();
}
/**
* Hooks into WordPress.
*
* @return void
*/
private function register() {
( new Capabilities( $this->access ) )->register();
( new ViewerRole() )->register();
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
add_action( Jobs::HOOK, array( $this, 'run_jobs' ) );
add_action( Dispatcher::SITE_HOOK, array( $this, 'send_site_report' ) );
add_action( 'wp_initialize_site', array( Installer::class, 'initialize_site' ), 200 );
( new ShareLinks( $this->sites, $this->runs, array( $this, 'builder' ) ) )->register();
if ( is_admin() ) {
add_action( 'admin_init', array( Installer::class, 'maybe_install' ) );
add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) );
add_filter( 'plugin_action_links_' . plugin_basename( TURTLEREPORT_FILE ), array( $this, 'action_links' ) );
add_action( 'admin_notices', array( $this->health, 'render_notice' ) );
$lists = new SourceLists( $this->google );
$pages = array(
new ReportsPage( $this->access, $this->connections, $this->settings ),
new OverviewPage( $this->sites, $this->connections, $this->health, $this->runs, $this->email_log ),
new SitesPage( $this->sites, $this->connections, $this->access, $this->email_log, $lists, $this->settings ),
new ConnectionsPage( $this->connections, $this->sites, $this->health ),
new DeliveriesPage( $this->sites, $this->runs, $this->deliveries ),
new SettingsPage( $this->settings, $this->sites, $this->email_log, $this->access ),
new GuidePage(),
);
( new Menu( ...$pages ) )->register();
( new Shell( $this->settings, $this->access, $this->health, ...$pages ) )->register();
( new ConnectionActions( $this->connections, $this->sites, $this->credentials, $this->cache, $this->health ) )->register();
( new SiteActions( $this->sites, $this->connections, $this->access, $this->cache, array( $this, 'dispatcher' ), $this->email_log, $lists ) )->register();
( new SettingsActions( $this->settings, array( $this, 'dispatcher' ), $this->sites, $this->email_log ) )->register();
( new DeliveryActions( $this->sites, $this->runs, $this->deliveries, array( $this, 'dispatcher' ) ) )->register();
}
}
/**
* Registers REST routes.
*
* @return void
*/
public function register_rest_routes() {
( new ReportController( $this->reports(), $this->sites, $this->cache, $this->health ) )->register_routes();
( new SetupController( $this->connections, $this->google ) )->register_routes();
( new PreferencesController() )->register_routes();
}
/**
* Reports exposed through the REST API.
*
* @return Reports\Report[]
*/
public function reports() {
if ( null === $this->reports ) {
$reports = array(
new Reports\SearchConsoleReport( $this->google ),
new Reports\ConversionReport( $this->google ),
new Reports\TrafficReport( $this->google ),
new Reports\AiTrafficReport( $this->google ),
new Reports\BusinessProfileReport( $this->google ),
new Reports\SegmentsReport( $this->google ),
);
/**
* Filters the reports exposed at /turtlereport/v1/sites/{site}/reports/{id}.
*
* Add your own by extending TurtleReport\Reports\Report. Access
* checks are applied automatically.
*
* @param Reports\Report[] $reports Reports.
* @param ServicesFactory $google Google services factory.
*/
$reports = apply_filters( 'turtlereport_reports', $reports, $this->google );
$this->reports = array_values(
array_filter(
(array) $reports,
array( __CLASS__, 'valid_report' )
)
);
}
return $this->reports;
}
/**
* Validates a filter-provided report before calling its methods.
*
* @param mixed $report Extension-provided value.
* @return bool
*/
private static function valid_report( $report ) {
return $report instanceof Reports\Report && 1 === preg_match( '/^[a-z0-9-]+$/', $report->id() );
}
/**
* E-mail dispatcher (built on first use; needs the reports).
*
* @return Dispatcher
*/
public function dispatcher() {
if ( null === $this->dispatcher ) {
$builder = $this->builder();
$mailer = new ReportMailer( new ReportSummary( $this->reports() ), $this->settings, $builder );
$this->dispatcher = new Dispatcher( $this->sites, $mailer, $this->email_log, new Recipients( $this->access ), $this->runs, $this->deliveries, new Notifier( $this->settings ) );
}
return $this->dispatcher;
}
/**
* Report builder: numbers, checks, insights and PDF (built on first use).
*
* @return ReportBuilder
*/
public function builder() {
if ( null === $this->builder ) {
$this->builder = new ReportBuilder( new ReportSummary( $this->reports() ), $this->settings );
}
return $this->builder;
}
/**
* Hourly cron handler: due e-mails, then daily health checks and cache warming.
*
* @return void
*/
public function run_jobs() {
Installer::maybe_install();
( new Jobs( $this->dispatcher(), $this->health, new CacheWarmer( $this->sites, $this->reports() ), $this->settings ) )->run();
}
/**
* Single cron event for one site's scheduled e-mail.
*
* @param string $site_id Site ID.
* @return void
*/
public function send_site_report( $site_id ) {
if ( function_exists( 'set_time_limit' ) ) {
set_time_limit( 300 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Background job talking to remote APIs and building a PDF.
}
Installer::ensure_schema();
Jobs::beat();
try {
$this->dispatcher()->send_site( (string) $site_id );
} catch ( \Throwable $e ) {
// Never let one report break WP-Cron for everything else; the hourly sweep tries again.
$this->email_log->update( (string) $site_id, array( 'last_error' => substr( $e->getMessage(), 0, 300 ) ) );
}
}
/**
* Install-wide settings, for extensions.
*
* @return Settings
*/
public function settings() {
return $this->settings;
}
/**
* Access rules, for extensions.
*
* @return AccessPolicy
*/
public function access() {
return $this->access;
}
/**
* Report runs, for extensions.
*
* @return RunRepository
*/
public function runs() {
return $this->runs;
}
/**
* Site storage, for extensions.
*
* @return ProfileRepository
*/
public function sites() {
return $this->sites;
}
/**
* Adds Reports and Settings links on the Plugins screen.
*
* @param string[] $links Existing links.
* @return string[]
*/
public function action_links( $links ) {
if ( current_user_can( Capabilities::MANAGE ) ) {
array_unshift(
$links,
sprintf( '%s', esc_url( ConnectionsPage::url() ), esc_html__( 'Connections', 'turtlereport' ) ),
sprintf( '%s', esc_url( ReportsPage::url() ), esc_html__( 'Reports', 'turtlereport' ) )
);
}
return $links;
}
/**
* Suggests privacy policy text.
*
* @return void
*/
public function add_privacy_policy_content() {
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
return;
}
wp_add_privacy_policy_content(
'TurtleReport',
wp_kses_post(
wpautop(
__( 'TurtleReport does not add tracking to your public website or collect data from your visitors. Logged-in administrators connect it to Google Search Console, Google Analytics and Google Business Profile, and it shows aggregated reports from those services inside the WordPress admin to the users they allow. Google credentials are stored encrypted in this site\'s database. Report data is fetched from Google\'s APIs and cached temporarily. When scheduled e-mail reports are used, a delivery history (recipient e-mail addresses, send times and results) and the report figures are kept for up to 400 days so failed e-mails can be resent; secure PDF links, if switched on, let anyone with the link download that one report until it expires.', 'turtlereport' )
)
)
);
}
}