',
esc_html__( 'Tourtify is active.', 'tourtify' ),
esc_url( self::url() ),
esc_html__( 'Connect your Tourtify website', 'tourtify' )
);
}
/**
* Suggested text for the site's privacy policy (Settings › Privacy › Policy guide).
*/
public static function privacy_policy() {
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
return;
}
$text = '
' . esc_html__( 'Tourtify takes the bookings made on this site. Add something like this to your privacy policy, and link to your own booking terms.', 'tourtify' ) . '
' . esc_html__( 'Bookings on this website are handled by Tourtify, our booking system. When you click Book now, the booking window loads from our Tourtify website, and the details you enter there (such as your name, email address, phone number, travellers and payment) are sent to Tourtify to make and manage your booking. The booking also records the page you booked from and any campaign tags in its address. Payments are processed by our payment provider on a secure checkout page.', 'tourtify' ) . '
'
. '
' . esc_html__( 'Pages that show our reviews load them from our Tourtify website. Tour lists are loaded by this website’s server and send no information about you.', 'tourtify' ) . '
';
wp_add_privacy_policy_content( __( 'Tourtify', 'tourtify' ), wp_kses_post( $text ) );
}
/**
* "Settings" next to Deactivate on the Plugins screen.
*
* @param array $links Existing links.
* @return array
*/
public static function action_links( $links ) {
array_unshift( $links, '' . esc_html__( 'Settings', 'tourtify' ) . '' );
return $links;
}
/**
* The Tourtify menu.
*/
public static function menu() {
// The Tourtify mark (apps/api/public/favicon.svg) in one colour, so WordPress tints it like the other menu
// icons: the tile's outline, the near peak, and the far peak a shade lighter (fill-opacity survives the tint).
$svg = '';
$icon = 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- a menu icon as a data URI.
add_menu_page( __( 'Tourtify', 'tourtify' ), __( 'Tourtify', 'tourtify' ), 'manage_options', self::PAGE, array( __CLASS__, 'page' ), $icon, 58 );
}
/**
* Styles and the copy buttons, only on the Tourtify screen.
*
* @param string $hook The admin page's hook name.
*/
public static function assets( $hook ) {
if ( 'toplevel_page_' . self::PAGE !== $hook ) {
return;
}
// The card styles too, for the Display tab's preview.
wp_enqueue_style( 'tourtify-admin', TOURTIFY_URL . 'assets/css/admin.css', array( 'tourtify' ), TOURTIFY_VERSION );
wp_enqueue_script( 'tourtify-admin', TOURTIFY_URL . 'assets/js/admin.js', array( 'wp-i18n' ), TOURTIFY_VERSION, true );
wp_set_script_translations( 'tourtify-admin', 'tourtify' );
}
/**
* The settings, saved through the Settings API.
*/
public static function settings() {
register_setting(
self::PAGE,
Tourtify_Client::OPTION,
array(
'type' => 'array',
'sanitize_callback' => array( __CLASS__, 'sanitize' ),
'default' => array(),
)
);
}
/**
* Check a new address against the Tourtify website before keeping it.
*
* @param mixed $input The submitted settings.
* @return array
*/
public static function sanitize( $input ) {
// The plugin itself saving the connection (Connect with Tourtify, Disconnect): keep it as it is.
if ( Tourtify_Client::$writing ) {
return array_intersect_key( wp_parse_args( is_array( $input ) ? $input : array(), Tourtify_Client::defaults() ), Tourtify_Client::defaults() );
}
$old = Tourtify_Client::settings();
$input = is_array( $input ) ? $input : array();
// The address and key never come from a form: only Connect with Tourtify proves the business is yours.
$out = array_merge(
$old,
array(
'currency' => strtoupper( substr( sanitize_key( (string) ( $input['currency'] ?? $old['currency'] ) ), 0, 3 ) ),
'button_label' => sanitize_text_field( wp_unslash( (string) ( $input['button_label'] ?? $old['button_label'] ) ) ),
)
);
// The Display tab: unticked boxes aren't sent, so the form says it was there.
if ( ! empty( $input['display'] ) ) {
$out['layout'] = 'list' === ( $input['layout'] ?? '' ) ? 'list' : 'grid';
$out['columns'] = max( 1, min( 4, (int) ( $input['columns'] ?? 3 ) ) );
foreach ( array( 'show_image', 'show_meta', 'show_summary', 'show_price', 'show_button', 'load_styles' ) as $key ) {
$out[ $key ] = ! empty( $input[ $key ] );
}
}
return array_intersect_key( $out, Tourtify_Client::defaults() );
}
/**
* "Connect with Tourtify": send the person to Tourtify to pick their business, with a one-time state to check on return.
*/
public static function connect_start() {
self::guard( 'tourtify_connect' );
$state = wp_generate_password( 32, false, false );
$verifier = wp_generate_password( 64, false, false );
set_transient(
'tourtify_connect_' . get_current_user_id(),
array(
'state' => $state,
'verifier' => $verifier,
),
15 * MINUTE_IN_SECONDS
);
// PKCE: Tourtify gets only the challenge; the code it sends back is useless without this site's verifier.
$challenge = rtrim( strtr( base64_encode( hash( 'sha256', $verifier, true ) ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- base64url of a hash (PKCE).
// The plugin's own admin page on this site, so a redirect to it is never an open redirect.
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Tourtify's console is another site by design.
wp_redirect(
Tourtify_Client::app_link(
'/connect/wordpress',
'connect',
array(
'return' => self::url(),
'state' => $state,
'site_name' => wp_strip_all_tags( get_bloginfo( 'name' ) ),
'code_challenge' => $challenge,
'code_challenge_method' => 'S256',
)
)
);
exit;
}
/**
* Back from Tourtify with a one-time code: check the state, swap the code (with the verifier) for this site's key
* and the business's website address, then save them. Nothing in the address bar is trusted.
*/
public static function connect_return() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- the one-time state stands in for a nonce here.
if ( ! isset( $_GET['page'], $_GET['tourtify_state'] ) || self::PAGE !== $_GET['page'] || ! current_user_can( 'manage_options' ) ) {
return;
}
// Cancelled in Tourtify: say so, whatever the state (nothing is exchanged). An older tab's Cancel must not
// read as an expired link, nor clear the connection started in a newer tab.
if ( isset( $_GET['tourtify_error'] ) || ! isset( $_GET['tourtify_code'] ) ) {
self::back( 'cancelled' );
}
$key = 'tourtify_connect_' . get_current_user_id();
$started = get_transient( $key );
$state = sanitize_text_field( wp_unslash( (string) $_GET['tourtify_state'] ) );
if ( ! is_array( $started ) || ! hash_equals( (string) ( $started['state'] ?? '' ), $state ) ) {
self::back( 'expired' );
}
delete_transient( $key );
$code = sanitize_text_field( wp_unslash( (string) $_GET['tourtify_code'] ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$connection = Tourtify_Client::exchange( $code, (string) $started['verifier'] );
if ( is_wp_error( $connection ) ) {
self::back( 'failed' );
}
Tourtify_Client::save_connection( $connection['site_url'], $connection['key'] );
self::back( 'connected' );
}
/**
* Disconnect: revoke this site's key in Tourtify, then forget it (display settings are kept).
*/
public static function disconnect() {
self::guard( 'tourtify_disconnect' );
Tourtify_Client::revoke();
Tourtify_Client::save_connection( '', '' );
self::back( 'disconnected' );
}
/**
* Refresh tours now (instead of within ten minutes).
*/
public static function refresh() {
self::guard( 'tourtify_refresh' );
Tourtify_Client::flush();
self::back( 'refreshed' );
}
/**
* "Create a Tours page": a draft page with the tours and reviews, opened in the editor.
*/
public static function create_page() {
self::guard( 'tourtify_create_page', 'edit_pages' );
$content = '
' . esc_html__( 'Choose a date and book online in a couple of minutes.', 'tourtify' ) . '
1, 'columns' => 1 ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by the renderer. ?>
__( 'A tour list’s options after defaults (layout, columns, sort, parts to show…).', 'tourtify' ),
'tourtify_tours' => __( 'The tours a list shows: add, remove or reorder them.', 'tourtify' ),
'tourtify_card_html' => __( 'One card’s finished HTML.', 'tourtify' ),
'tourtify_template' => __( 'Which template file renders a card.', 'tourtify' ),
'tourtify_button_attributes' => __( 'A Book now button’s attributes (classes, data attributes).', 'tourtify' ),
'tourtify_button_label' => __( 'A Book now button’s text.', 'tourtify' ),
'tourtify_button_html' => __( 'A Book now button’s finished HTML.', 'tourtify' ),
'tourtify_load_styles' => __( 'Whether the plugin’s card styles load (return false to use only your own).', 'tourtify' ),
'tourtify_feed' => __( 'The tours feed as received, before it is cached.', 'tourtify' ),
'tourtify_cache_ttl' => __( 'How long tours are cached, in seconds (default 600).', 'tourtify' ),
'tourtify_request_args' => __( 'The request to your Tourtify website (timeout, headers, proxy).', 'tourtify' ),
);
$actions = array(
'tourtify_before_tours' => __( 'Before a list of cards (receives the tours and the options).', 'tourtify' ),
'tourtify_after_tours' => __( 'After a list of cards.', 'tourtify' ),
'tourtify_card_start' => __( 'At the start of each card (receives the tour).', 'tourtify' ),
'tourtify_card_end' => __( 'At the end of each card.', 'tourtify' ),
);
$functions = array(
'tourtify_get_tours( $args )' => __( 'Your published tours as arrays, filtered and sorted like a tour list.', 'tourtify' ),
'tourtify_get_tour( $slug )' => __( 'One tour, or null.', 'tourtify' ),
'tourtify_book_button( $slug, $args )' => __( 'A Book now button (args: label, title).', 'tourtify' ),
'tourtify_tours( $args )' => __( 'Tour cards, as the block renders them.', 'tourtify' ),
'tourtify_site_url()' => __( 'Your Tourtify website’s address.', 'tourtify' ),
);
?>