';
}
/**
* Open a panel: the white box with a heading. Same markup as Confetti and Address Autocomplete.
*
* @param string $id Panel ID.
* @param string $title Heading.
* @param array $args intro (a line under the heading), badge.
*/
public static function panel_open( $id, $title, $args = array() ) {
$args = wp_parse_args(
$args,
array(
'intro' => '',
'badge' => '',
)
);
?>
';
}
/**
* Open an option row: label on the left, control on the right.
*
* @param string $label Label.
* @param string $for ID of the control the label is for.
*/
public static function option_row_open( $label, $for = '' ) {
?>
' . wp_kses_post( $description ) . '';
}
echo '
';
}
/**
* A checkbox with its label, the way Confetti's settings show them.
*
* @param string $name Field name.
* @param bool $checked On.
* @param string $label Text beside it.
* @param string $id Input ID.
*/
public static function checkbox_field( $name, $checked, $label, $id = '' ) {
$id = $id ? $id : 'sunshine-utm-' . sanitize_key( $name );
?>
';
echo '';
wp_nonce_field( self::NONCE, '_sunshine_utm_nonce' );
}
/**
* "(opens in a new tab)" for screen readers and a small icon for everyone else.
*/
public static function new_tab_note() {
echo ' ' . esc_html__( '(opens in a new tab)', 'sunshine-utm-builder' ) . '';
}
/**
* A "Learn more" style link to a doc on wpsunshine.com, same as Address Autocomplete's.
*
* @param string $slug Doc slug.
* @param string $label Link text.
* @return string HTML.
*/
public static function doc_link( $slug, $label = '' ) {
if ( '' === $label ) {
$label = __( 'Learn more', 'sunshine-utm-builder' );
}
return '' . esc_html( $label ) . ' ' . esc_html__( '(opens in a new tab)', 'sunshine-utm-builder' ) . '';
}
public function handle_post() {
if ( empty( $_POST['sunshine_utm_action'] ) ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to change UTM Builder.', 'sunshine-utm-builder' ) );
}
check_admin_referer( self::NONCE, '_sunshine_utm_nonce' );
$post = wp_unslash( $_POST );
$action = sanitize_key( $post['sunshine_utm_action'] );
$next = self::url();
switch ( $action ) {
case 'save_settings':
sunshine_utm()->update_settings( $this->sanitize_settings( $post ) );
$this->add_notice( __( 'Settings saved.', 'sunshine-utm-builder' ) );
$next = self::url( array( 'tab' => 'settings' ) );
break;
case 'save_preset':
$id = isset( $post['preset_id'] ) ? absint( $post['preset_id'] ) : 0;
if ( '' === trim( isset( $post['name'] ) ? (string) $post['name'] : '' ) ) {
$this->add_notice( __( 'Give the preset a name.', 'sunshine-utm-builder' ), 'error' );
$next = self::url( array_filter( array(
'tab' => 'presets',
'preset' => $id
) ) );
break;
}
$saved = Sunshine_UTM_Presets::save( $post, $id );
if ( $saved ) {
$this->add_notice( $id ? __( 'Preset updated.', 'sunshine-utm-builder' ) : __( 'Preset added.', 'sunshine-utm-builder' ) );
} else {
$this->add_notice( __( 'The preset could not be saved.', 'sunshine-utm-builder' ), 'error' );
}
$next = self::url( array( 'tab' => 'presets' ) );
break;
case 'delete_preset':
Sunshine_UTM_Presets::delete( isset( $post['preset_id'] ) ? absint( $post['preset_id'] ) : 0 );
$this->add_notice( __( 'Preset deleted.', 'sunshine-utm-builder' ) );
$next = self::url( array( 'tab' => 'presets' ) );
break;
case 'save_batch_notes':
$batch_id = isset( $post['batch_id'] ) ? absint( $post['batch_id'] ) : 0;
Sunshine_UTM_Batches::update_notes( $batch_id, isset( $post['notes'] ) ? (string) $post['notes'] : '' );
$this->add_notice( __( 'Notes saved.', 'sunshine-utm-builder' ) );
$next = self::url(
array(
'tab' => 'batches',
'batch' => $batch_id,
)
);
break;
case 'update_short_link':
$code = isset( $post['code'] ) ? sanitize_title( $post['code'] ) : '';
$result = Sunshine_UTM_Short_Links::update(
$code,
isset( $post['new_code'] ) && is_string( $post['new_code'] ) ? $post['new_code'] : '',
// Not esc_url_raw or sanitize_text_field: they'd change an encoded link. update() checks it.
isset( $post['url'] ) && is_string( $post['url'] ) ? trim( $post['url'] ) : ''
);
if ( is_wp_error( $result ) ) {
$this->add_notice( $result->get_error_message(), 'error' );
} else {
$this->add_notice( __( 'Short link saved.', 'sunshine-utm-builder' ) );
$code = $result;
}
$next = self::url(
array(
'tab' => 'links',
'code' => $code,
)
);
break;
case 'delete_short_link':
$code = isset( $post['code'] ) ? sanitize_key( $post['code'] ) : '';
if ( Sunshine_UTM_Short_Links::delete( $code ) ) {
$this->add_notice( __( 'Short link deleted.', 'sunshine-utm-builder' ) );
}
$next = self::url( array( 'tab' => 'links' ) );
break;
case 'delete_batch':
Sunshine_UTM_Batches::delete( isset( $post['batch_id'] ) ? absint( $post['batch_id'] ) : 0 );
$this->add_notice( __( 'Batch deleted.', 'sunshine-utm-builder' ) );
$next = self::url( array( 'tab' => 'batches' ) );
break;
}
wp_safe_redirect( $next );
exit;
}
protected function sanitize_settings( $post ) {
$post_types = array();
$allowed = get_post_types( array( 'public' => true ) );
foreach ( isset( $post['post_types'] ) ? (array) $post['post_types'] : array() as $type ) {
$type = sanitize_key( $type );
if ( isset( $allowed[ $type ] ) ) {
$post_types[] = $type;
}
}
$shortener = isset( $post['shortener'] ) ? sanitize_key( $post['shortener'] ) : '';
$spaces = isset( $post['spaces'] ) ? (string) $post['spaces'] : '_';
$spaces = in_array( $spaces, array( '_', '-', 'keep' ), true ) ? $spaces : '_';
// Team lists are saved the way the values will look in links, using the rules saved with them.
$format = array(
'lowercase' => ! empty( $post['lowercase'] ),
'spaces' => $spaces,
'strip_symbols' => ! empty( $post['strip_symbols'] ),
);
$list = function ( $name ) use ( $post, $format ) {
$values = array();
foreach ( preg_split( '/\r\n|\r|\n|,/', isset( $post[ $name ] ) && is_string( $post[ $name ] ) ? $post[ $name ] : '' ) as $value ) {
$value = Sunshine_UTM_URL_Builder::format_value( sanitize_text_field( $value ), $format );
if ( '' !== $value ) {
$values[] = substr( $value, 0, 100 );
}
}
return array_slice( array_values( array_unique( $values ) ), 0, 200 );
};
return array(
'admin_bar' => ! empty( $post['admin_bar'] ),
'row_actions' => ! empty( $post['row_actions'] ),
'editor_panel' => ! empty( $post['editor_panel'] ),
'post_types' => $post_types,
'lowercase' => ! empty( $post['lowercase'] ),
'spaces' => $spaces,
'strip_symbols' => ! empty( $post['strip_symbols'] ),
'check_urls' => ! empty( $post['check_urls'] ),
'site_links' => ! empty( $post['site_links'] ),
'shortener' => isset( Sunshine_UTM_Shortener::services()[ $shortener ] ) ? $shortener : 'site',
'bitly_token' => isset( $post['bitly_token'] ) ? trim( sanitize_text_field( $post['bitly_token'] ) ) : '',
'rebrandly_key' => isset( $post['rebrandly_key'] ) ? trim( sanitize_text_field( $post['rebrandly_key'] ) ) : '',
'short_prefix' => Sunshine_UTM_Short_Links::clean_prefix( isset( $post['short_prefix'] ) ? $post['short_prefix'] : '' ),
'allowed_sources' => $list( 'allowed_sources' ),
'allowed_mediums' => $list( 'allowed_mediums' ),
'allowed_only' => ! empty( $post['allowed_only'] ),
'delete_data' => ! empty( $post['delete_data'] ),
);
}
/**
* Read the Bulk Builder's fields from a request.
*
* @param array $post Unslashed POST data.
* @return array Input for Sunshine_UTM_Bulk::rows().
*/
protected function bulk_input( $post ) {
$tags = array();
$variations = array();
foreach ( Sunshine_UTM_URL_Builder::KEYS as $key ) {
$tags[ $key ] = isset( $post['tags'][ $key ] ) ? (string) $post['tags'][ $key ] : '';
$variations[ $key ] = isset( $post['variations'][ $key ] ) ? (string) $post['variations'][ $key ] : '';
}
// Links, CSV and parameters aren't run through sanitize_text_field, which deletes %XX and
// breaks encoded links. Each value is checked or encoded where it's used.
return array(
'method' => isset( $post['method'] ) ? sanitize_key( $post['method'] ) : 'urls',
'urls' => isset( $post['urls'] ) ? (string) $post['urls'] : '',
'csv' => isset( $post['csv'] ) ? (string) $post['csv'] : '',
'variations' => $variations,
'networks' => isset( $post['networks'] ) ? array_map( 'sanitize_key', (array) $post['networks'] ) : array(),
'preset_id' => 0,
'tags' => $tags,
'extra' => Sunshine_UTM_URL_Builder::parse_extra( isset( $post['extra'] ) ? (string) $post['extra'] : '' ),
);
}
/**
* A preset's tags with tokens filled in for the link in the builder.
*/
public function ajax_preset() {
if ( ! check_ajax_referer( Sunshine_UTM::NONCE, 'nonce', false ) || ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( null, 403 );
}
$preset = Sunshine_UTM_Presets::get( isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked above.
if ( ! $preset ) {
wp_send_json_error( null, 404 );
}
$url = isset( $_POST['url'] ) ? (string) wp_unslash( $_POST['url'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only read for tokens.
wp_send_json_success( Sunshine_UTM_Presets::resolve( $preset, $url ) );
}
/**
* How many links the Bulk Builder will make, and the first 20 of them. No shortening or link
* checks here: those only run when the batch is created.
*/
public function ajax_bulk_preview() {
if ( ! check_ajax_referer( self::NONCE, 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'This page is out of date. Reload it and try again.', 'sunshine-utm-builder' ) ), 403 );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to do that.', 'sunshine-utm-builder' ) ), 403 );
}
$result = Sunshine_UTM_Bulk::rows( $this->bulk_input( wp_unslash( $_POST ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized in bulk_input().
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
$links = Sunshine_UTM_Bulk::generate(
array_slice( $result['rows'], 0, 20 ),
array(
'shorten' => false,
'check' => false,
)
);
wp_send_json_success(
array(
'total' => count( $result['rows'] ),
'links' => $links,
'skipped' => $result['skipped'],
)
);
}
public function ajax_bulk_create() {
if ( ! check_ajax_referer( self::NONCE, 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'This page is out of date. Reload it and try again.', 'sunshine-utm-builder' ) ), 403 );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to do that.', 'sunshine-utm-builder' ) ), 403 );
}
$post = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized in bulk_input().
$input = $this->bulk_input( $post );
$result = Sunshine_UTM_Bulk::rows( $input );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
$plugin = sunshine_utm();
$service = isset( $post['shortener'] ) ? sanitize_key( $post['shortener'] ) : '';
$shorten = '' !== $service;
$check = (bool) $plugin->get_setting( 'check_urls' );
$pages = count( array_unique( wp_list_pluck( $result['rows'], 'url' ) ) );
if ( $shorten && ! Sunshine_UTM_Shortener::is_ready( $service ) ) {
wp_send_json_error( array( 'message' => __( 'That link shortener isn\'t set up. Check Link shortening in Settings.', 'sunshine-utm-builder' ) ) );
}
if ( $shorten && Sunshine_UTM_Shortener::is_outside( $service ) && count( $result['rows'] ) > Sunshine_UTM_Bulk::SHORTEN_MAX ) {
wp_send_json_error(
array(
'message' => sprintf(
/* translators: 1: Bitly or Rebrandly, 2: the most links it can shorten at once. */
__( '%1$s works on batches of up to %2$s links. Choose short links on your own site, or make a smaller batch.', 'sunshine-utm-builder' ),
Sunshine_UTM_Shortener::label( $service ),
number_format_i18n( Sunshine_UTM_Bulk::SHORTEN_MAX )
),
)
);
}
if ( $check && $pages > Sunshine_UTM_Bulk::CHECK_MAX ) {
$check = false;
}
if ( function_exists( 'set_time_limit' ) ) {
@set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DiscouragedFunctions.Discouraged -- checking up to 50 links can take a few minutes; not allowed on every host.
}
$links = Sunshine_UTM_Bulk::generate(
$result['rows'],
array(
'shorten' => $shorten ? $service : false,
'check' => $check,
)
);
$batch_id = Sunshine_UTM_Batches::create(
array(
'name' => isset( $post['name'] ) ? (string) $post['name'] : '',
'method' => $input['method'],
'notes' => isset( $post['notes'] ) ? (string) $post['notes'] : '',
),
$links
);
if ( ! $batch_id ) {
wp_send_json_error( array( 'message' => __( 'The batch could not be saved to the database.', 'sunshine-utm-builder' ) ) );
}
$message = sprintf(
/* translators: %s: number of links. */
_n( '%s link created.', '%s links created.', count( $links ), 'sunshine-utm-builder' ),
number_format_i18n( count( $links ) )
);
if ( $shorten && count( array_filter( wp_list_pluck( $links, 'short_url' ) ) ) < count( $links ) ) {
$message .= ' ' . __( 'Some links could not be shortened. Check your link shortening settings.', 'sunshine-utm-builder' );
}
if ( $plugin->get_setting( 'check_urls' ) && ! $check ) {
/* translators: %s: the most pages that can be checked at once. */
$message .= ' ' . sprintf( __( 'Links weren\'t checked, since the batch goes to more than %s different pages.', 'sunshine-utm-builder' ), number_format_i18n( Sunshine_UTM_Bulk::CHECK_MAX ) );
}
$broken = count( $links ) - count( array_filter( wp_list_pluck( $links, 'url_ok' ) ) );
if ( $broken ) {
/* translators: %s: number of links. */
$message .= ' ' . sprintf( _n( '%s link goes to a page that didn\'t load. It\'s marked below.', '%s links go to pages that didn\'t load. They\'re marked below.', $broken, 'sunshine-utm-builder' ), number_format_i18n( $broken ) );
}
$this->add_notice( $message, $broken ? 'warning' : 'success' );
wp_send_json_success(
array(
'redirect' => self::url(
array(
'tab' => 'batches',
'batch' => $batch_id,
)
),
)
);
}
public function ajax_link_note() {
if ( ! check_ajax_referer( self::NONCE, 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'This page is out of date. Reload it and try again.', 'sunshine-utm-builder' ) ), 403 );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to do that.', 'sunshine-utm-builder' ) ), 403 );
}
$link_id = isset( $_POST['link_id'] ) ? absint( $_POST['link_id'] ) : 0;
$note = isset( $_POST['note'] ) ? sanitize_textarea_field( wp_unslash( $_POST['note'] ) ) : '';
if ( ! $link_id || ! Sunshine_UTM_Batches::update_link_note( $link_id, $note ) ) {
wp_send_json_error( array( 'message' => __( 'The note could not be saved.', 'sunshine-utm-builder' ) ) );
}
wp_send_json_success();
}
/**
* Download a batch as CSV.
*/
public function export() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to download this.', 'sunshine-utm-builder' ) );
}
check_admin_referer( 'sunshine_utm_export' );
$batch_id = isset( $_GET['batch'] ) ? absint( $_GET['batch'] ) : 0;
$batch = Sunshine_UTM_Batches::get( $batch_id );
if ( ! $batch ) {
wp_die( esc_html__( 'That batch no longer exists.', 'sunshine-utm-builder' ) );
}
$filename = sanitize_file_name( 'utm-links-' . sanitize_title( $batch['name'] ) . '.csv' );
nocache_headers();
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
echo Sunshine_UTM_Batches::to_csv( Sunshine_UTM_Batches::links( $batch_id ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV file.
exit;
}
public static function export_url( $batch_id ) {
return wp_nonce_url(
add_query_arg(
array(
'action' => 'sunshine_utm_export',
'batch' => $batch_id,
),
admin_url( 'admin-post.php' )
),
'sunshine_utm_export'
);
}
/**
* "UTM link" under published posts of the post types chosen in Settings.
*
* @param array $actions Row actions.
* @param WP_Post $post Post.
* @return array
*/
/**
* The "UTM link" panel in the block editor's post sidebar (assets/js/editor.js): a button that
* opens the Link Builder with this post filled in, and this post's short links with their
* clicks. Only for admins, on the post types chosen in Settings, and not in the site editor.
*/
public function editor_assets() {
$plugin = sunshine_utm();
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
$post = get_post();
if ( ! $plugin->get_setting( 'editor_panel' ) || ! $screen || 'post' !== $screen->base || ! $post || ! current_user_can( 'manage_options' ) ) {
return;
}
if ( ! in_array( $post->post_type, (array) $plugin->get_setting( 'post_types' ), true ) ) {
return;
}
$links = array();
foreach ( 'publish' === $post->post_status ? Sunshine_UTM_Short_Links::for_page( get_permalink( $post ) ) : array() as $row ) {
$links[] = array(
'short' => Sunshine_UTM_Short_Links::url( $row['code'] ),
'clicks' => (int) $row['clicks'],
'details' => self::url(
array(
'tab' => 'links',
'code' => $row['code'],
)
),
);
}
wp_enqueue_script( 'sunshine-utm-editor', SUNSHINE_UTM_URL . 'assets/js/editor.js', array( 'wp-plugins', 'wp-element', 'wp-data', 'wp-components', 'wp-edit-post', 'wp-editor' ), SUNSHINE_UTM_VERSION, true );
wp_localize_script(
'sunshine-utm-editor',
'SunshineUTMEditor',
array(
'builder' => Sunshine_UTM::builder_url(),
'links' => $links,
'texts' => array(
'title' => __( 'UTM link', 'sunshine-utm-builder' ),
'build' => __( 'Build a UTM link', 'sunshine-utm-builder' ),
'new_tab' => __( '(opens in a new tab)', 'sunshine-utm-builder' ),
'unpublished' => __( 'Publish this first, then build tagged links to it.', 'sunshine-utm-builder' ),
'short_links' => __( 'Short links to this page', 'sunshine-utm-builder' ),
/* translators: %s: number of clicks. */
'clicks' => __( '%s clicks', 'sunshine-utm-builder' ),
'click' => __( '1 click', 'sunshine-utm-builder' ),
),
)
);
}
public function row_action( $actions, $post ) {
$plugin = sunshine_utm();
if ( ! $plugin->get_setting( 'row_actions' ) || 'publish' !== $post->post_status || ! current_user_can( 'manage_options' ) ) {
return $actions;
}
if ( ! in_array( $post->post_type, (array) $plugin->get_setting( 'post_types' ), true ) ) {
return $actions;
}
$permalink = get_permalink( $post );
if ( $permalink ) {
$actions['sunshine_utm'] = sprintf(
'%3$s',
esc_url( Sunshine_UTM::builder_url( $permalink ) ),
/* translators: %s: post title. */
esc_attr( sprintf( __( 'Build a UTM link for ā%sā', 'sunshine-utm-builder' ), get_the_title( $post ) ) ),
esc_html__( 'UTM link', 'sunshine-utm-builder' )
);
}
return $actions;
}
public function add_notice( $message, $type = 'success' ) {
set_transient( 'sunshine_utm_notice_' . get_current_user_id(), array( $message, $type ), 60 );
}
public function notices() {
if ( ! $this->is_screen() || ! current_user_can( 'manage_options' ) ) {
return;
}
$notice = get_transient( 'sunshine_utm_notice_' . get_current_user_id() );
if ( ! $notice ) {
return;
}
delete_transient( 'sunshine_utm_notice_' . get_current_user_id() );
printf(
'
%2$s
',
esc_attr( $notice[1] ),
esc_html( $notice[0] )
);
}
/**
* Display a stored UTC time in the site's time zone.
*
* @param string $mysql_utc Y-m-d H:i:s in UTC.
* @return string
*/
public static function date( $mysql_utc ) {
$time = strtotime( $mysql_utc . ' UTC' );
return $time ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $time ) : '';
}
}