Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .wordpress-org/screenshot-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .wordpress-org/screenshot-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .wordpress-org/screenshot-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .wordpress-org/screenshot-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Choose the server version under OWA > Settings.
* The user name of logged-in users, if Track Named Users is on.
* WordPress admin pages, if Track WP Admin Pages is on (1.x only).

Previews, the Customizer and, if Don't Track Logged-in Users is on, logged-in users are not tracked.
Previews, the Customizer and logged-in users are not tracked. Turn off Don't Track Logged-in Users (on by default) to track logged-in users.

= Multisite =

Expand All @@ -46,11 +46,11 @@ A cached page keeps the tracking tag it was rendered with. When a site's tag cha

1. Install and activate the plugin.
2. Go to OWA > Settings.
3. Enter your OWA server's URL as the OWA Endpoint, ending with `/`.
3. Enter your OWA server's URL as the OWA Endpoint. Saving checks that OWA answers there and stores the URL it answers at, after any redirect. Without `https://` or `http://`, https is tried first, then http.
4. Enter the credentials:
* 1.x: the API Key (OWA: Settings > Users > Edit) and the Secret Auth Key (`OWA_AUTH_KEY` in the server's `owa-config.php`).
* 2.x: select 2.x and enter an Application Password (OWA: My Preferences > Application Passwords). A read-only password is enough.
5. Save, then select the Observation Profile to track and save again. The plugin stores its site ID; in 1.x the field is labeled Website ID.
5. Save. Once OWA accepts the credentials, the Observation Profile list appears: select the one to track and save again. The plugin stores its site ID; in 1.x the field is labeled Website ID.

On a multisite network, set up OWA 2.x once under Network Admin > OWA.

Expand Down
9 changes: 9 additions & 0 deletions js/lib/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ export function showTagSettingsLink( payload, values ) {
return Boolean( payload.tagSettingsUrl ) &&
[ 'owaVersion', 'owaEndpoint', 'siteId' ].every( ( key ) => values[ key ] === payload.values[ key ] );
}

/**
* The Website ID picker shows once OWA has returned its list with the saved
* credentials, and stays up while the list is fetched again after a save. A
* Website ID the network assigned is shown read-only whatever OWA says.
*/
export function showPicker( field, sites ) {
return Boolean( field.readonly || sites.loaded );
}
7 changes: 4 additions & 3 deletions js/network/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ function NetworkPage() {
...secretsPayload( { appPassword: password } ),
};

if ( payload.assignProfiles && assign ) {
// only what the table showed
if ( payload.profilesLoaded && payload.assignProfiles && assign ) {
data.assignedSiteIds = mappingPayload( mapping );
data.enabledSites = switchesPayload( payload.blogs, switches );
}
Expand Down Expand Up @@ -200,7 +201,7 @@ function NetworkPage() {
__next40pxDefaultSize
__nextHasNoMarginBottom
label="OWA Endpoint"
help="The URL of your OWA 2.x server, ending with a / (e.g. https://owa.example.com/)."
help="The URL of your OWA 2.x server (e.g. https://owa.example.com/). Without https:// or http://, https is tried first."
type="url"
value={ endpoint }
onChange={ setEndpoint }
Expand Down Expand Up @@ -242,7 +243,7 @@ function NetworkPage() {
onChange={ setAssign }
/>
</div>
{ payload.active && (
{ payload.active && payload.profilesLoaded && (
<SitesTable
payload={ payload }
mapping={ mapping }
Expand Down
28 changes: 15 additions & 13 deletions js/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';

import { fieldText, setupNotices, showTagSettingsLink, visibleFields, visibleSections } from '../lib/fields';
import { fieldText, setupNotices, showPicker, showTagSettingsLink, visibleFields, visibleSections } from '../lib/fields';
import { secretHint, secretsPayload } from '../lib/secrets';
import './style.css';

Expand All @@ -37,16 +37,18 @@ function WebsiteId( { field, value, onChange, sites, payload, values } ) {

return (
<>
<SelectControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ text.title }
value={ value }
options={ options }
onChange={ onChange }
disabled={ field.readonly || sites.loading }
help={ field.readonly ? '' : text.description }
/>
{ showPicker( field, sites ) && (
<SelectControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ text.title }
value={ value }
options={ options }
onChange={ onChange }
disabled={ field.readonly || sites.loading }
help={ field.readonly ? '' : text.description }
/>
) }
{ sites.notice && <p className="owa-wp-field-error">{ sites.notice }</p> }
{ payload.network.note && <p className="description">{ payload.network.note }</p> }
{ showTagSettingsLink( payload, values ) && (
Expand Down Expand Up @@ -119,7 +121,7 @@ function SettingsPage() {
const [ payload, setPayload ] = useState( null );
const [ values, setValues ] = useState( {} );
const [ secrets, setSecrets ] = useState( {} );
const [ sites, setSites ] = useState( { choices: [], notice: '', loading: true } );
const [ sites, setSites ] = useState( { choices: [], notice: '', loading: true, loaded: false } );
const [ errors, setErrors ] = useState( {} );
const [ notice, setNotice ] = useState( null );
const [ saving, setSaving ] = useState( false );
Expand All @@ -128,7 +130,7 @@ function SettingsPage() {
setSites( ( current ) => ( { ...current, loading: true } ) );
apiFetch( { path: SITES } )
.then( ( result ) => setSites( { ...result, loading: false } ) )
.catch( ( error ) => setSites( { choices: [], notice: error.message, loading: false } ) );
.catch( ( error ) => setSites( { choices: [], notice: error.message, loading: false, loaded: false } ) );
};

const apply = ( result ) => {
Expand Down
20 changes: 17 additions & 3 deletions owa-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class owaWp_plugin extends module {
// shown with the Website ID list: why it is empty or short
var $sitesNotice = '';

// OWA returned its list with the saved credentials
var $sitesLoaded = false;

// OWA 2.x TrackerBundle::SITE_ID_PATTERN: the IDs it publishes a bundle for
const BUNDLE_SITE_ID_PATTERN = '/^[A-Za-z0-9_-]{1,64}$/D';

Expand Down Expand Up @@ -728,7 +731,8 @@ function setupNotices( $version = null ) {
// where the network sets the Observation Profile, the site admin cannot, and the page says so
$assigned = owaWp\network::isActive() && owaWp\network::options()['assignProfiles'];

if ( ! $assigned && ! $this->isOwaReadyToTrack( $version ) ) {
// the picker shows once the credentials are in
if ( ! $assigned && $this->isOwaSdkReady( $version ) && ! $this->isOwaReadyToTrack( $version ) ) {

$notices[] = $version === 2
? 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.'
Expand Down Expand Up @@ -895,7 +899,7 @@ public function registerOptions() {
'title' => 'OWA Endpoint',
'page_name' => 'owa-wordpress',
'section' => 'general',
'description' => 'The URL of your OWA instance (i.e. http://www.mydomain.com/path/to/owa/). This should be the same as the OWA_PUBLIC_URL of your OWA server instance as defined in its owa-config.php file and MUST end with a "/".',
'description' => 'The URL of your OWA instance (e.g. https://owa.example.com/path/to/owa/), the OWA_PUBLIC_URL in its owa-config.php. Saving checks that OWA answers there. Without https:// or http://, https is tried first.',
'label_for' => 'OWA Endpoint',
'length' => 70,
'endWithSlash' => true,
Expand Down Expand Up @@ -970,7 +974,7 @@ public function registerOptions() {

'excludeLoggedInUsers' => array(

'default_value' => false,
'default_value' => true,
'field' => array(
'type' => 'boolean',
'title' => 'Don&#039;t Track Logged-in Users',
Expand Down Expand Up @@ -1071,6 +1075,14 @@ function getSitesNotice() {
return $this->sitesNotice;
}

/**
* Whether OWA returned the last site list: the credentials work.
*/
function sitesLoaded() {

return $this->sitesLoaded;
}

/**
* Options for the Website ID picker, from OWA's GET /base/v1/sites.
*
Expand All @@ -1084,11 +1096,13 @@ function getSitesFromOwa( $options = [] ) {

$sites = null;
$this->sitesNotice = '';
$this->sitesLoaded = false;

if ( $this->isOwaSdkReady() && ! empty( $this->owaSdk ) ) {

$sites_client = $this->owaSdk->createSites();
$sites = $sites_client->listSites();
$this->sitesLoaded = is_array( $sites );

if ( ! is_array( $sites ) ) {

Expand Down
39 changes: 32 additions & 7 deletions src/network.php
Original file line number Diff line number Diff line change
Expand Up @@ -427,15 +427,15 @@ public static function sanitize( array $input, array $stored ) {

if ( array_key_exists( 'owaEndpoint', $input ) ) {

$problem = rest::endpointProblem( (string) $input['owaEndpoint'] );
[ $url, $problem ] = rest::normalizeEndpoint( (string) $input['owaEndpoint'], (string) ( $stored['owaEndpoint'] ?? '' ) );

if ( $problem ) {

$errors['owaEndpoint'] = $problem;

} else {

$out['owaEndpoint'] = trim( (string) $input['owaEndpoint'] );
$out['owaEndpoint'] = $url;
}
}

Expand Down Expand Up @@ -547,10 +547,28 @@ public static function load( $plugin ) {
}

/**
* Stores the network settings. Invalid credentials store nothing. Saving
* while the network configures the Profiles stores each site's from
* assignedSiteIds, and a site not yet assigned one gets its exact match;
* the first such save fetches the Profiles if none are cached yet.
* @param array<string, mixed> $before
* @param array<string, mixed> $after
*/
public static function credentialsChanged( array $before, array $after ) {

foreach ( [ 'owaEndpoint', 'appPassword' ] as $key ) {

if ( (string) ( $before[ $key ] ?? '' ) !== (string) ( $after[ $key ] ?? '' ) ) {

return true;
}
}

return false;
}

/**
* Stores the network settings. Invalid credentials store nothing. New
* credentials drop the cached Profiles, and a save in network mode with
* none cached fetches them. Saving while the network configures the
* Profiles stores each site's from assignedSiteIds, and a site not yet
* assigned one gets its exact match.
*
* @param array<string, mixed> $input
* @return array{0: array<string, mixed>|null, 1: array<string, mixed>, 2: string[]} what was stored (null: nothing), problems, messages
Expand All @@ -574,10 +592,17 @@ public static function save( array $input, $plugin ) {
// saving marks everything seen
$options['newProfileIds'] = [];

// Profiles fetched with other credentials say nothing about these
if ( self::credentialsChanged( $stored, $options ) ) {

$options['sites'] = [];
$options['sitesFetchedAt'] = 0;
}

// stored first: the fetch below uses these credentials
update_site_option( self::OPTION, $options );

if ( self::isActive() && ! empty( $options['assignProfiles'] ) && empty( $options['sites'] ) ) {
if ( self::isActive() && empty( $options['sites'] ) ) {

$fetch = self::fetchProfiles( $plugin, $options );

Expand Down
86 changes: 75 additions & 11 deletions src/rest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class rest {
// a 1.x API key, which an application password never looks like
const LEGACY_KEY_PATTERN = '/^[a-f0-9]{32}$/D';

// under an endpoint, the tracker OWA 1.14 and 2.x both publish
const TRACKER_PATH = 'public/base/dist/owa.tracker.js';

public static function register( $plugin ) {

add_action( 'rest_api_init', fn() => self::registerRoutes( $plugin ) );
Expand Down Expand Up @@ -201,15 +204,15 @@ public static function sanitizeSettings( array $schema, array $input, array $sto

case 'url':

$error = self::endpointProblem( (string) $value );
[ $url, $error ] = self::normalizeEndpoint( (string) $value, (string) ( $stored[ $key ] ?? '' ) );

if ( $error ) {

$errors[ $key ] = $error;

} else {

$out[ $key ] = trim( (string) $value );
$out[ $key ] = $url;
}
break;

Expand Down Expand Up @@ -258,28 +261,86 @@ public static function sanitizeSettings( array $schema, array $input, array $sto
}

/**
* Why an OWA endpoint cannot be used, or '' ('' itself is allowed).
* The OWA endpoint to store for what was entered: where OWA's tracker
* loads from, after any redirect, with a trailing slash. Without a
* scheme, https:// is tried and then http://. An endpoint equal to the
* one stored is kept without asking OWA, so a save while OWA is down
* still goes through.
*
* @param string $current the stored endpoint
* @return array{0: string, 1: string} the URL ('' clears it), and why it cannot be used ('' when it can)
*/
public static function endpointProblem( $endpoint ) {
public static function normalizeEndpoint( $endpoint, $current = '' ) {

$endpoint = trim( (string) $endpoint );

if ( $endpoint === '' ) {

return '';
return [ '', '' ];
}

if ( preg_match( '#^[a-z][a-z0-9+.-]*://#i', $endpoint ) ) {

$candidates = [ rtrim( $endpoint, '/' ) . '/' ];

if ( ! preg_match( '#^https?://#i', $candidates[0] ) || ! filter_var( $candidates[0], FILTER_VALIDATE_URL ) ) {

return [ '', 'Not a valid http:// or https:// URL.' ];
}

} else {

$candidates = [ 'https://' . rtrim( $endpoint, '/' ) . '/', 'http://' . rtrim( $endpoint, '/' ) . '/' ];

if ( ! filter_var( $candidates[0], FILTER_VALIDATE_URL ) ) {

return [ '', 'Not a valid URL.' ];
}
}

if ( (string) $current !== '' && in_array( (string) $current, $candidates, true ) ) {

return [ (string) $current, '' ];
}

foreach ( $candidates as $url ) {

$found = self::owaAt( $url );

if ( $found !== '' ) {

return [ $found, '' ];
}
}

if ( ! preg_match( '#^https?://#i', $endpoint ) || ! filter_var( $endpoint, FILTER_VALIDATE_URL ) ) {
return [ '', sprintf( 'No OWA server found at %s.', implode( ' or ', $candidates ) ) ];
}

/**
* The endpoint OWA's tracker (1.14 and 2.x both publish it there) loads
* from, starting at $url and following redirects, or '' when it does not
* load.
*/
private static function owaAt( $url ) {

return 'Not a valid http:// or https:// URL.';
$response = wp_remote_get( $url . self::TRACKER_PATH, [ 'timeout' => 5, 'limit_response_size' => 1024 ] );

if ( is_wp_error( $response ) || (int) wp_remote_retrieve_response_code( $response ) !== 200 ) {

return '';
}

if ( substr( $endpoint, -1 ) !== '/' ) {
$final = isset( $response['http_response'] ) && is_object( $response['http_response'] ) && method_exists( $response['http_response'], 'get_response_object' )
? (string) $response['http_response']->get_response_object()->url
: '';

// a redirect to somewhere other than the tracker leaves the URL as entered
if ( ! preg_match( '#^https?://#i', $final ) || substr( $final, -strlen( self::TRACKER_PATH ) ) !== self::TRACKER_PATH ) {

return 'The URL must end with a slash (e.g. https://owa.example.com/).';
return $url;
}

return '';
return substr( $final, 0, -strlen( self::TRACKER_PATH ) );
}

/**
Expand Down Expand Up @@ -311,7 +372,8 @@ public static function sitesPayload( $plugin ) {
$choices[] = [ 'value' => (string) $id, 'label' => $site['label'] ];
}

return [ 'choices' => $choices, 'notice' => $plugin->getSitesNotice() ];
// the page shows the picker only once OWA has accepted the credentials
return [ 'choices' => $choices, 'notice' => $plugin->getSitesNotice(), 'loaded' => $plugin->sitesLoaded() ];
}

/**
Expand Down Expand Up @@ -384,6 +446,8 @@ public static function networkPayload() {
'assignProfiles' => $options['assignProfiles'],
'profiles' => $profiles,
'sitesFetchedAt' => $options['sitesFetchedAt'],
// the table shows once OWA has listed its Profiles for these credentials
'profilesLoaded' => $options['sitesFetchedAt'] > 0,
'blogs' => $blogs,
];
}
Expand Down
Loading
Loading