diff --git a/_inc/client/components/module-settings/modules-per-tab-page.jsx b/_inc/client/components/module-settings/modules-per-tab-page.jsx index 2da34a10d2c56..126c221d28c91 100644 --- a/_inc/client/components/module-settings/modules-per-tab-page.jsx +++ b/_inc/client/components/module-settings/modules-per-tab-page.jsx @@ -128,6 +128,20 @@ const AllModuleSettingsComponent = React.createClass( { return ( ); case 'wordads': return ( ); + case 'google-analytics': + if ( 'inactive' === module.configure_url ) { + return ( +
+ { __( 'Activate this module to use Google Analytics.' ) } +
+ ); + } else { + return ( +
+ { __( 'Configure Google Analytics settings.' ) } +
+ ); + } case 'gravatar-hovercards': case 'contact-form': case 'latex': diff --git a/_inc/client/engagement/index.jsx b/_inc/client/engagement/index.jsx index cb173bfe1f054..6f46be7fffbc2 100644 --- a/_inc/client/engagement/index.jsx +++ b/_inc/client/engagement/index.jsx @@ -68,6 +68,7 @@ export const Engagement = ( props ) => { let cards = [ [ 'seo-tools', getModule( 'seo-tools' ).name, getModule( 'seo-tools' ).description, getModule( 'seo-tools' ).learn_more_button ], [ 'wordads', getModule( 'wordads' ).name, getModule( 'wordads' ).description, getModule( 'wordads' ).learn_more_button ], + [ 'google-analytics', getModule( 'google-analytics' ).name, getModule( 'google-analytics' ).description, getModule( 'google-analytics' ).learn_more_button ], [ 'stats', getModule( 'stats' ).name, getModule( 'stats' ).description, getModule( 'stats' ).learn_more_button ], [ 'sharedaddy', getModule( 'sharedaddy' ).name, getModule( 'sharedaddy' ).description, getModule( 'sharedaddy' ).learn_more_button ], [ 'publicize', getModule( 'publicize' ).name, getModule( 'publicize' ).description, getModule( 'publicize' ).learn_more_button ], @@ -100,7 +101,7 @@ export const Engagement = ( props ) => { customClasses = unavailableInDevMode ? 'devmode-disabled' : '', toggle = '', adminAndNonAdmin = isAdmin || includes( nonAdminAvailable, element[0] ), - isPro = 'seo-tools' === element[0] || 'wordads' === element[0], + isPro = includes( [ 'seo-tools', 'wordads', 'google-analytics' ], element[0] ), proProps = { module: element[0], configure_url: '' @@ -127,6 +128,7 @@ export const Engagement = ( props ) => { toggle = __( 'Unavailable in Dev Mode' ); } else if ( isAdmin ) { if ( ( 'seo-tools' === element[0] && ! hasBusiness ) || + ( 'google-analytics' === element[0] && ! hasBusiness ) || ( 'wordads' === element[0] && ! hasPremiumOrBusiness ) ) { toggle = ; } else { @@ -143,6 +145,10 @@ export const Engagement = ( props ) => { } } + if ( element[0] === 'google-analytics' && ! hasBusiness ) { + isModuleActive = false; + } + if ( isPro ) { // Add a "pro" button next to the header title element[1] = @@ -172,6 +178,12 @@ export const Engagement = ( props ) => { : 'inactive'; } + moduleDescription = ; + } else if ( element[0] === 'google-analytics' ) { + proProps.configure_url = isModuleActive + ? 'https://wordpress.com/settings/analytics/' + props.siteRawUrl + : 'inactive'; + moduleDescription = ; } diff --git a/_inc/client/plans/plan-body.jsx b/_inc/client/plans/plan-body.jsx index ba658d2fc95fd..175da922f5b90 100644 --- a/_inc/client/plans/plan-body.jsx +++ b/_inc/client/plans/plan-body.jsx @@ -28,10 +28,10 @@ const PlanBody = React.createClass( { render() { let planCard = ''; switch ( this.props.plan ) { - case 'jetpack_personal': - case 'jetpack_personal_monthly': - case 'jetpack_premium': - case 'jetpack_premium_monthly': + case 'jetpack_personal': + case 'jetpack_personal_monthly': + case 'jetpack_premium': + case 'jetpack_premium_monthly': case 'jetpack_business': case 'jetpack_business_monthly': planCard = ( @@ -226,6 +226,32 @@ const PlanBody = React.createClass( { : '' } + { + includes( [ 'jetpack_business', 'jetpack_business_monthly' ], this.props.plan ) ? +
+

{ __( 'Google Analytics' ) }

+

{ __( 'Track website statistics with Google Analytics for a deeper understanding of your website visitors and customers.' ) }

+ { + this.props.isFetchingPluginsData ? '' : + this.props.isModuleActivated( 'google-analytics' ) ? ( + + ) + : ( + + ) + } +
+ : '' + } + { includes( [ 'jetpack_personal', 'jetpack_personal_monthly' ], this.props.plan ) ?
diff --git a/_inc/client/pro-status/index.jsx b/_inc/client/pro-status/index.jsx index 85725426f2a3b..1667455de9675 100644 --- a/_inc/client/pro-status/index.jsx +++ b/_inc/client/pro-status/index.jsx @@ -4,6 +4,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { translate as __ } from 'i18n-calypso'; +import { includes } from 'lodash'; import Button from 'components/button'; import SimpleNotice from 'components/notice'; @@ -121,6 +122,22 @@ const ProStatus = React.createClass( { ); } + if ( 'google-analytics' === feature && ! includes( [ 'jetpack_business', 'jetpack_business_monthly' ], sitePlan.product_slug ) ) { + if ( this.props.fetchingSiteData ) { + return ''; + } + + return ( + + ); + } + if ( sitePlan.product_slug ) { let btnVals = {}; if ( 'jetpack_free' !== sitePlan.product_slug ) { diff --git a/_inc/client/search/index.jsx b/_inc/client/search/index.jsx index ba6d14486b6f0..208be32d1a56b 100644 --- a/_inc/client/search/index.jsx +++ b/_inc/client/search/index.jsx @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; import FoldableCard from 'components/foldable-card'; import { ModuleToggle } from 'components/module-toggle'; import forEach from 'lodash/forEach'; +import includes from 'lodash/includes'; import Button from 'components/button'; import Gridicon from 'components/gridicon'; import Collection from 'components/search/search-collection.jsx'; @@ -105,16 +106,14 @@ export const SearchResults = ( { } cards = moduleList.map( ( element ) => { - let isPro = 'scan' === element[0] - || 'akismet' === element[0] - || 'backups' === element[0] - || 'seo-tools' === element[0], - proProps = {}, + const isPro = includes( [ 'scan', 'akismet', 'backups', 'seo-tools', 'google-analytics' ], element[0] ); + let proProps = {}, + isModuleActive = isModuleActivated( element[0] ), unavailableDevMode = unavailableInDevMode( element[0] ), toggle = unavailableDevMode ? __( 'Unavailable in Dev Mode' ) : ( @@ -122,7 +121,7 @@ export const SearchResults = ( { customClasses = unavailableDevMode ? 'devmode-disabled' : '', wordAdsSubHeader = element[2]; - if ( 'wordads' === element[0] && ! isModuleActivated( element[0] ) ) { + if ( 'wordads' === element[0] && ! isModuleActive ) { wordAdsSubHeader = } @@ -132,13 +131,17 @@ export const SearchResults = ( { configure_url: '' }; - if ( + if ( ( 'videopress' !== element[0] || 'seo-tools' !== element[0] || ( 'seo-tools' === element[0] && ! hasBusiness + ) ) + && ( + 'google-analytics' !== element[0] + || ( 'google-analytics' === element[0] && ! hasBusiness ) ) ) { toggle = ; @@ -199,7 +202,7 @@ export const SearchResults = ( { ) } > { - isModuleActivated( element[0] ) || isPro ? + isModuleActive || isPro ? : // Render the long_description if module is deactivated
diff --git a/_inc/lib/admin-pages/class.jetpack-admin-page.php b/_inc/lib/admin-pages/class.jetpack-admin-page.php index f37c5bd8efba6..870b53ac9c552 100644 --- a/_inc/lib/admin-pages/class.jetpack-admin-page.php +++ b/_inc/lib/admin-pages/class.jetpack-admin-page.php @@ -216,15 +216,15 @@ function check_plan_deactivate_modules( $page ) { $active = Jetpack::get_active_modules(); switch ( $current->plan->product_slug ) { case 'jetpack_free': - $to_deactivate = array( 'seo-tools', 'videopress' ); + $to_deactivate = array( 'seo-tools', 'videopress', 'google-analytics' ); break; case 'jetpack_personal': case 'jetpack_personal_monthly': - $to_deactivate = array( 'seo-tools', 'videopress' ); + $to_deactivate = array( 'seo-tools', 'videopress', 'google-analytics' ); break; case 'jetpack_premium': case 'jetpack_premium_monthly': - $to_deactivate = array( 'seo-tools' ); + $to_deactivate = array( 'seo-tools', 'google-analytics' ); break; } $to_deactivate = array_intersect( $active, $to_deactivate ); diff --git a/_inc/lib/class.core-rest-api-endpoints.php b/_inc/lib/class.core-rest-api-endpoints.php index 29e2496fd97f9..84c7bf22e53fd 100644 --- a/_inc/lib/class.core-rest-api-endpoints.php +++ b/_inc/lib/class.core-rest-api-endpoints.php @@ -1546,6 +1546,16 @@ public static function get_updateable_data_list( $selector = '' ) { 'validate_callback' => __CLASS__ . '::validate_boolean', 'jp_group' => 'wordads', ), + + // Google Analytics + 'google_analytics_tracking_id' => array( + 'description' => esc_html__( 'Google Analytics', 'jetpack' ), + 'type' => 'string', + 'default' => '', + 'validate_callback' => __CLASS__ . '::validate_alphanum', + 'jp_group' => 'google-analytics', + ), + // Stats 'admin_bar' => array( 'description' => esc_html__( 'Put a chart showing 48 hours of views in the admin bar.', 'jetpack' ), @@ -2094,6 +2104,15 @@ public static function prepare_options_for_response( $module = '' ) { $options = self::split_options( $options, get_option( 'verification_services_codes' ) ); break; + case 'google-analytics': + $wga = get_option( 'jetpack_wga' ); + $code = ''; + if ( is_array( $wga ) && array_key_exists( 'code', $wga ) ) { + $code = $wga[ 'code' ]; + } + $options[ 'google_analytics_tracking_id' ][ 'current_value' ] = $code; + break; + case 'sharedaddy': // It's local, but it must be broken apart since it's saved as an array. if ( ! class_exists( 'Sharing_Service' ) && ! @include( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) { diff --git a/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php b/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php index a463bcde9e7b3..d5833ccd01d62 100644 --- a/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php +++ b/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php @@ -718,6 +718,14 @@ public function update_data( $data ) { $updated = get_option( $option ) != $value ? update_option( $option, (bool) $value ? 'letitsnow' : '' ) : true; break; + case 'google_analytics_tracking_id': + $grouped_options = $grouped_options_current = (array) get_option( 'jetpack_wga' ); + $grouped_options[ 'code' ] = $value; + + // If option value was the same, consider it done. + $updated = $grouped_options_current != $grouped_options ? update_option( 'jetpack_wga', $grouped_options ) : true; + break; + case 'wp_mobile_featured_images': case 'wp_mobile_excerpt': $value = ( 'enabled' === $value ) ? '1' : '0'; diff --git a/class.jetpack.php b/class.jetpack.php index 3e6656c8f12a2..7f9a871522129 100644 --- a/class.jetpack.php +++ b/class.jetpack.php @@ -1217,14 +1217,14 @@ public static function get_active_plan() { // Set the default options if ( ! $plan ) { - $plan = array( - 'product_slug' => 'jetpack_free', - 'supports' => array(), + $plan = array( + 'product_slug' => 'jetpack_free', + 'supports' => array(), ); } // Define what paid modules are supported by personal plans - $personal_plans = array( + $personal_plans = array( 'jetpack_personal', 'jetpack_personal_monthly', ); @@ -1261,6 +1261,7 @@ public static function get_active_plan() { 'akismet', 'vaultpress', 'seo-tools', + 'google-analytics', ); } @@ -2292,7 +2293,7 @@ public static function get_translated_modules( $modules ) { */ public static function get_active_modules() { $active = Jetpack_Options::get_option( 'active_modules' ); - + if ( ! is_array( $active ) ) { $active = array(); } diff --git a/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php b/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php index 589f7e8532072..0435d6c2d10ea 100644 --- a/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php +++ b/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php @@ -199,7 +199,7 @@ public function get_settings_response() { 'moderation_keys' => get_option( 'moderation_keys' ), 'blacklist_keys' => get_option( 'blacklist_keys' ), 'lang_id' => get_option( 'lang_id' ), - 'wga' => get_option( 'wga' ), + 'wga' => $this->get_google_analytics(), 'disabled_likes' => (bool) get_option( 'disabled_likes' ), 'disabled_reblogs' => (bool) get_option( 'disabled_reblogs' ), 'jetpack_comment_likes_enabled' => (bool) get_option( 'jetpack_comment_likes_enabled', false ), @@ -266,6 +266,10 @@ protected function get_locale( $key ) { return false; } + protected function get_google_analytics () { + $option_name = defined( 'IS_WPCOM' ) && IS_WPCOM ? 'wga' : 'jetpack_wga'; + return get_option( $option_name ); + } /** * Updates site settings for authorized users @@ -352,12 +356,18 @@ public function update_settings() { } break; case 'wga': + case 'jetpack_wga': if ( ! isset( $value['code'] ) || ! preg_match( '/^$|^UA-[\d-]+$/i', $value['code'] ) ) { return new WP_Error( 'invalid_code', 'Invalid UA ID' ); } - $wga = get_option( 'wga', array() ); + + $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM; + $option_name = $is_wpcom ? 'wga' : 'jetpack_wga'; + + $wga = get_option( $option_name, array() ); $wga['code'] = $value['code']; // maintain compatibility with wp-google-analytics - if ( update_option( 'wga', $wga ) ) { + + if ( update_option( $option_name, $wga ) ) { $updated[ $key ] = $value; } @@ -366,10 +376,11 @@ public function update_settings() { /** This action is documented in modules/widgets/social-media-icons.php */ do_action( 'jetpack_bump_stats_extras', 'google-analytics', $enabled_or_disabled ); - $business_plugins = WPCOM_Business_Plugins::instance(); - $business_plugins->activate_plugin( 'wp-google-analytics' ); + if ( $is_wpcom ) { + $business_plugins = WPCOM_Business_Plugins::instance(); + $business_plugins->activate_plugin( 'wp-google-analytics' ); + } break; - case 'jetpack_testimonial': case 'jetpack_portfolio': case 'jetpack_comment_likes_enabled': diff --git a/modules/google-analytics.php b/modules/google-analytics.php new file mode 100644 index 0000000000000..9705fe8e459d5 --- /dev/null +++ b/modules/google-analytics.php @@ -0,0 +1,14 @@ + $value ) { + if ( strpos( strtolower( $value ), strtolower( $site_url ) ) === 0 ) { + $track[ $k ] = substr( $track[ $k ], strlen( $site_url ) ); + } + if ( 'data' === $k ) { + $track[ $k ] = preg_replace( '/^https?:\/\/|^\/+/i', '', $track[ $k ] ); + } + + // This way we don't lose search data. + if ( 'data' === $k && 'search' === $track['code'] ) { + $track[ $k ] = rawurlencode( $track[ $k ] ); + } else { + $track[ $k ] = preg_replace( '/[^a-z0-9\.\/\+\?=-]+/i', '_', $track[ $k ] ); + } + + $track[ $k ] = trim( $track[ $k ], '_' ); + } + $char = ( strpos( $track['data'], '?' ) === false ) ? '?' : '&'; + return str_replace( "'", "\'", "/{$track['code']}/{$track['data']}{$char}referer=" . rawurlencode( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : '' ) ); // Input var okay. + } + + /** + * Maybe output or return, depending on the context + */ + private function _output_or_return( $val, $maybe ) { + if ( $maybe ) { + echo $val . "\r\n"; + } else { + return $val; + } + } + + /** + * This injects the Google Analytics code into the footer of the page. + * + * @param bool[optional] $output - defaults to true, false returns but does NOT echo the code. + */ + public function insert_code( $output = true ) { + // If $output is not a boolean false, set it to true (default). + $output = ( false !== $output); + + $tracking_id = $this->_get_tracking_code(); + if ( empty( $tracking_id ) ) { + return $this->_output_or_return( '', $output ); + } + + // If we're in the admin_area, return without inserting code. + if ( is_admin() ) { + return $this->_output_or_return( '', $output ); + } + + $custom_vars = array( + "_gaq.push(['_setAccount', '{$tracking_id}']);", + ); + + $track = array(); + if ( is_404() ) { + // This is a 404 and we are supposed to track them. + $custom_vars[] = "_gaq.push( [ '_trackEvent', '404', document.location.href, document.referrer ] );"; + } elseif ( is_search() ) { + // Set track for searches, if it's a search, and we are supposed to. + $track['data'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // Input var okay. + $track['code'] = 'search'; + } + + if ( ! empty( $track ) ) { + $track['url'] = $this->_get_url( $track ); + // adjust the code that we output, account for both types of tracking. + $track['url'] = esc_js( str_replace( '&', '&', $track['url'] ) ); + $custom_vars[] = "_gaq.push(['_trackPageview','{$track['url']}']);"; + } else { + $custom_vars[] = "_gaq.push(['_trackPageview']);"; + } + + $async_code = ""; + + $custom_vars_string = implode( "\r\n", $custom_vars ); + $async_code = str_replace( '%custom_vars%', $custom_vars_string, $async_code ); + + return $this->_output_or_return( $async_code, $output ); + } + + /** + * Used to get the tracking code option + * + * @return tracking code option value. + */ + private function _get_tracking_code() { + $o = get_option( 'jetpack_wga' ); + + if ( isset( $o['code'] ) && preg_match( '#UA-[\d-]+#', $o['code'], $matches ) ) { + return $o['code']; + } + + return ''; + } +} + +global $jetpack_google_analytics; +$jetpack_google_analytics = Jetpack_Google_Analytics::get_instance(); diff --git a/modules/module-headings.php b/modules/module-headings.php index cc2ceea33b2b7..f9bedd07fff8f 100644 --- a/modules/module-headings.php +++ b/modules/module-headings.php @@ -49,6 +49,11 @@ function jetpack_get_module_i18n( $key ) { 'description' => _x( 'Increase reach and traffic.', 'Module Description', 'jetpack' ), ), + 'google-analytics' => array( + 'name' => _x( 'Google Analytics', 'Module Name', 'jetpack' ), + 'description' => _x( 'Lets you use Google Analytics to track your WordPress site statistics.', 'Module Description', 'jetpack' ), + ), + 'gravatar-hovercards' => array( 'name' => _x( 'Gravatar Hovercards', 'Module Name', 'jetpack' ), 'description' => _x( 'Enable pop-up business cards over commenters’ Gravatars.', 'Module Description', 'jetpack' ), diff --git a/modules/module-info.php b/modules/module-info.php index 6b7d538a9b1ae..80af11299345f 100644 --- a/modules/module-info.php +++ b/modules/module-info.php @@ -641,3 +641,18 @@ function jetpack_wordads_more_info() { } add_action( 'jetpack_module_more_info_wordads', 'jetpack_wordads_more_info' ); // WordAds: STOP + +/** + * Google Analytics + */ +function jetpack_google_analytics_more_link() { + echo 'https://jetpack.com/support/google-analytics'; +} +add_action( 'jetpack_learn_more_button_google-analytics', 'jetpack_google_analytics_more_link' ); + +function jetpack_google_analytics_more_info() { + esc_html_e( + 'Track website statistics with Google Analytics for a deeper understanding of your website visitors and customers.' + , 'jetpack' ); +} +add_action( 'jetpack_module_more_info_google-analytics', 'jetpack_google_analytics_more_info' ); diff --git a/sync/class.jetpack-sync-defaults.php b/sync/class.jetpack-sync-defaults.php index 748bcc65bb009..e56d0e294ea13 100644 --- a/sync/class.jetpack-sync-defaults.php +++ b/sync/class.jetpack-sync-defaults.php @@ -67,7 +67,7 @@ class Jetpack_Sync_Defaults { 'comment_whitelist', 'comment_max_links', 'moderation_keys', - 'wga', + 'jetpack_wga', 'disabled_likes', 'disabled_reblogs', 'jetpack_comment_likes_enabled', @@ -175,22 +175,22 @@ class Jetpack_Sync_Defaults { static $default_post_checksum_columns = array( 'ID', 'post_modified', - ); + ); static $default_post_meta_checksum_columns = array( 'meta_id', 'meta_value' - ); + ); static $default_comment_checksum_columns = array( 'comment_ID', 'comment_content', - ); + ); static $default_comment_meta_checksum_columns = array( 'meta_id', 'meta_value' - ); + ); static $default_option_checksum_columns = array( 'option_name', diff --git a/tests/php/sync/test_class.jetpack-sync-options.php b/tests/php/sync/test_class.jetpack-sync-options.php index f3165fcc83b05..13eda3c514106 100644 --- a/tests/php/sync/test_class.jetpack-sync-options.php +++ b/tests/php/sync/test_class.jetpack-sync-options.php @@ -127,7 +127,7 @@ public function test_sync_default_options() { 'comment_whitelist' => 'pineapple', 'comment_max_links' => 99, 'moderation_keys' => 'pineapple', - 'wga' => 'pineapple', + 'jetpack_wga' => 'pineapple', 'disabled_likes' => 'pineapple', 'disabled_reblogs' => 'pineapple', 'jetpack_comment_likes_enabled' => 'pineapple', @@ -201,7 +201,7 @@ public function test_add_whitelisted_option_on_init_89() { do_action( 'init' ); $whitelist = $this->options_module->get_options_whitelist(); - + $this->assertTrue( in_array( 'foo_option_bar', $whitelist ) ); } @@ -215,7 +215,7 @@ public function add_jetpack_options_whitelist_filter( $options ) { } - + function add_option_on_89() { add_filter( 'jetpack_options_whitelist', array( $this, 'add_jetpack_options_whitelist_filter' ) ); }