Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build: Server-side Render UpgradeNudge for use in PHP #13070

Merged
merged 56 commits into from
Aug 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
83d14a2
Build: Server-side render UpgradeNudge component
ockham Jul 17, 2019
64f45e7
[not verified] # Das ist eine Kombination aus 6 Commits.
ockham Jul 17, 2019
0b67391
[not verified] UpgradeNudge: import warning from '@wordpress/block-ed…
ockham Jul 17, 2019
f7854ec
[not verified] [not verified] Dummy props
ockham Jul 17, 2019
c311713
[not verified] [not verified] [not verified] Breakthrough
ockham Jul 19, 2019
46523bb
[not verified] Use unwrapped component
ockham Jul 19, 2019
27589b6
[not verified] Add components loading lib
ockham Jul 19, 2019
4e8c7e3
[not verified] Load UpgradeNudge
ockham Jul 19, 2019
76b47b0
[not verified] Use props
ockham Jul 19, 2019
63c7b44
[not verified] Whitespace :roll_eyes:
ockham Jul 19, 2019
c98b70c
[not verified] [not verified] Make static
ockham Jul 18, 2019
ac354e6
[not verified] i18n PoC
ockham Jul 23, 2019
f8ddf26
Assign a dummy value to upgradeUrl
ockham Jul 23, 2019
8101b73
Enqueue styling
ockham Jul 24, 2019
d4a8e7c
Move styling enqueue to better location
ockham Jul 24, 2019
fd71332
Add dummy prop for upgradeUrl
ockham Jul 24, 2019
94e160b
[not verified] Add explanatory comment
ockham Jul 24, 2019
134441e
Build: Re-arrange module override in extensions webpack config
ockham Jul 24, 2019
0a01325
Move to extensions webpack
ockham Jul 24, 2019
ccc85aa
Typo
ockham Jul 24, 2019
af6304f
Add comment about .html file extension
ockham Jul 24, 2019
45e660e
No React needed
ockham Jul 24, 2019
a66e33a
Add components styling
ockham Jul 24, 2019
60d9152
Move to components/ dir
ockham Jul 24, 2019
a7b2acd
Enqueue wp-components styling
ockham Jul 24, 2019
58e00d7
Moar style
ockham Jul 24, 2019
89b97df
Get plan name from endpoint
ockham Jul 25, 2019
18c79f1
Build $upgrade_url
ockham Jul 25, 2019
8c23e6a
Simplify
ockham Jul 25, 2019
328fc82
Plan name, not post type :facepalm:
ockham Jul 25, 2019
bbdd147
Move i18n-to-php
ockham Jul 25, 2019
9038846
Make the button work
ockham Jul 25, 2019
4ab808f
Whitespace
ockham Jul 25, 2019
13ac204
Reset webpack config
ockham Jul 25, 2019
f700e72
Add explanatory comment
ockham Jul 25, 2019
de9d7b4
Add more comments
ockham Jul 25, 2019
ccee667
Add basic error handling
ockham Jul 25, 2019
4a5e8de
WP.com fixes
ockham Jul 26, 2019
40b6b52
More WP.com fixing
ockham Jul 26, 2019
b204e46
s|@wordpress/block-editor|@wordpress/editor|
ockham Jul 29, 2019
bc0929f
Add missing @wordpress/server-side-render devDep
ockham Jul 29, 2019
1744e70
Remove explicit traverse devDep
ockham Jul 29, 2019
023679d
Remove react-autosize-textarea devDep
ockham Jul 29, 2019
6c91bf1
Fail gracefully
ockham Jul 29, 2019
b53ba8e
Fix all PHPCS warnings in new files and add them to the whitelist
jeherve Jul 31, 2019
ba9316f
Use `esc_html_e()` here instead of `echo esc_html__`
ockham Aug 1, 2019
0c82394
Declare `wp-components` as a dependency of `jetpack-components`
ockham Aug 1, 2019
ed1bffe
Remove extra `wp-components` dependency
ockham Aug 1, 2019
994030e
Add explanatory comments to Jetpack_Plan and Jetpack_Plans classes
ockham Aug 1, 2019
02db419
Another explanatory comment
ockham Aug 1, 2019
6fe8af9
Add unit test for i18n-to-php.js
ockham Aug 1, 2019
9ae24d1
Typo
ockham Aug 1, 2019
3f67b6c
More generic
ockham Aug 1, 2019
8b86719
Add unit test for _n() (currently failing)
ockham Aug 1, 2019
155d698
Add unit tests for _n() and _nx() (currently failing)
ockham Aug 1, 2019
ce8cccc
Fix translate functions implementations
ockham Aug 1, 2019
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
105 changes: 105 additions & 0 deletions _inc/lib/components.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Components Library
*
* Load and display a pre-rendered component
*/
class Jetpack_Components {
/**
* Load and display a pre-rendered component
*
* @since 7.7.0
*
* @param string $name Component name.
* @param array $props Component properties.
*
* @return string The component markup
*/
public static function render_component( $name, $props ) {

$rtl = is_rtl() ? '.rtl' : '';
wp_enqueue_style( 'jetpack-components', plugins_url( "_inc/blocks/components{$rtl}.css", JETPACK__PLUGIN_FILE ), array( 'wp-components' ), JETPACK__VERSION );

ob_start();
// `include` fails gracefully and throws a warning, but doesn't halt execution.
include JETPACK__PLUGIN_DIR . "_inc/blocks/$name.html";
$markup = ob_get_clean();
simison marked this conversation as resolved.
Show resolved Hide resolved

foreach ( $props as $key => $value ) {
$markup = str_replace(
"#$key#",
$value,
$markup
);

// Workaround, required to replace strings in `sprintf`-expressions.
// See extensions/i18n-to-php.js for more information.
$markup = str_replace(
"%($key)s",
$value,
$markup
);
}

return $markup;
}

/**
* Load and display a pre-rendered component
*
* @since 7.7.0
*
* @param array $props Component properties.
*
* @return string The component markup
*/
public static function render_upgrade_nudge( $props ) {
$plan_slug = $props['plan'];
jetpack_require_lib( 'plans' );
$plan = Jetpack_Plans::get_plan( $plan_slug );

if ( ! $plan ) {
return self::render_component(
'upgrade-nudge',
array(
'planName' => __( 'a paid plan', 'jetpack' ),
'upgradeUrl' => '',
)
);
}

// WP.com plan objects have a dedicated `path_slug` field, Jetpack plan objects don't
// For Jetpack, we thus use the plan slug with the 'jetpack_' prefix removed.
$plan_path_slug = wp_startswith( $plan_slug, 'jetpack_' )
? substr( $plan_slug, strlen( 'jetpack_' ) )
: $plan->path_slug;

$post_id = get_the_ID();
$post_type = get_post_type();

// The editor for CPTs has an `edit/` route fragment prefixed.
$post_type_editor_route_prefix = in_array( $post_type, array( 'page', 'post' ), true ) ? '' : 'edit';

if ( method_exists( 'Jetpack', 'build_raw_urls' ) ) {
$site_slug = Jetpack::build_raw_urls( home_url() );
} elseif ( class_exists( 'WPCOM_Masterbar' ) && method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
$site_slug = WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
}

$upgrade_url =
$plan_path_slug
? add_query_arg(
'redirect_to',
'/' . implode( '/', array_filter( array( $post_type_editor_route_prefix, $post_type, $site_slug, $post_id ) ) ),
"https://wordpress.com/checkout/${site_slug}/${plan_path_slug}"
) : '';

return self::render_component(
'upgrade-nudge',
array(
'planName' => $plan->product_name,
'upgradeUrl' => $upgrade_url,
)
);
}
}
75 changes: 75 additions & 0 deletions _inc/lib/plans.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Plans Library
*
* Fetch plans data from WordPress.com.
*
* Not to be confused with the `Jetpack_Plan` (singular)
* class, which stores and syncs data about the site's _current_ plan.
*
* @package Jetpack
*/
class Jetpack_Plans {
ockham marked this conversation as resolved.
Show resolved Hide resolved
/**
* Get a list of all available plans from WordPress.com
*
* @since 7.7.0
*
* @return array The plans list
*/
public static function get_plans() {
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
if ( ! class_exists( 'Store_Product_List' ) ) {
require WP_CONTENT_DIR . '/admin-plugins/wpcom-billing/store-product-list.php';
}

return Store_Product_List::get_active_plans_v1_5();
}

// We're on Jetpack, so it's safe to use this namespace.
$request = Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user(
'/plans?_locale=' . get_user_locale(),
// We're using version 1.5 of the endpoint rather than the default version 2
// since the latter only returns Jetpack Plans, but we're also interested in
// WordPress.com plans, for consumers of this method that run on WP.com.
'1.5',
ockham marked this conversation as resolved.
Show resolved Hide resolved
array(
'method' => 'GET',
'headers' => array(
'X-Forwarded-For' => Jetpack::current_user_ip( true ),
),
),
null,
'rest'
);

$body = wp_remote_retrieve_body( $request );
if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
return json_decode( $body );
} else {
return $body;
}
}

/**
* Get plan information for a plan given its slug
*
* @since 7.7.0
*
* @param string $plan_slug Plan slug.
*
* @return object The plan object
*/
public static function get_plan( $plan_slug ) {
$plans = self::get_plans();
if ( ! is_array( $plans ) ) {
return;
}

foreach ( $plans as $plan ) {
if ( $plan_slug === $plan->product_slug ) {
return $plan;
}
}
}
}
8 changes: 5 additions & 3 deletions bin/phpcs-whitelist.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ module.exports = [
'extensions/',
'functions.global.php',
'functions.opengraph.php',
'_inc/lib/debugger/',
'_inc/lib/core-api/wpcom-endpoints/memberships.php',
'_inc/lib/class.jetpack-password-checker.php',
'_inc/lib/admin-pages/class-jetpack-about-page.php',
'_inc/lib/class.jetpack-password-checker.php',
'_inc/lib/components.php',
'_inc/lib/core-api/wpcom-endpoints/memberships.php',
'_inc/lib/debugger/',
'_inc/lib/plans.php',
'load-jetpack.php',
'modules/masterbar/',
'modules/memberships/',
Expand Down
3 changes: 3 additions & 0 deletions class.jetpack-plan.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
/**
* Handles fetching of the site's plan from WordPress.com and caching the value locally.
*
* Not to be confused with the `Jetpack_Plans` class (in `_inc/lib/plans.php`), which
* fetches general information about all available plans from WordPress.com, side-effect free.
*
* @package Jetpack
*/

Expand Down
29 changes: 29 additions & 0 deletions extensions/shared/components/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* External dependencies
*/
import { renderToStaticMarkup } from 'react-dom/server';

/**
* Internal dependencies
*/
import { UpgradeNudge } from '../upgrade-nudge';

import './style.scss';

// Use dummy props that can be overwritten by a str_replace() on the server.
//
// Note that we're using the 'dumb' component exported from `upgrade-nudge.jsx` here,
// rather than the 'smart' one (which is wrapped in `withSelect` and `withDispatch` calls).
// This means putting the burden of props computation on PHP (`components.php`).
// If we wanted to use the 'smart' component instead, we'd need to provide sufficiently
// initialised Redux state when rendering ir (probably through globals set as arguments
// to the `StaticSiteGeneratorPlugin` call in `webpack.config.extensions.js`).
const upgradeNudge = renderToStaticMarkup(
<UpgradeNudge planName="#planName#" upgradeUrl="#upgradeUrl#" />
);

// StaticSiteGeneratorPlugin only supports `.html` extensions, even though
// our rendered components contain some PHP.
export default () => ( {
'upgrade-nudge.html': upgradeNudge,
} );
23 changes: 23 additions & 0 deletions extensions/shared/components/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@import '../styles/gutenberg-colors.scss';
@import '../styles/gutenberg-variables.scss';

// Styling copied from Gutenberg's `@wordpress/block-editor` styling for the Warning component
// so we can use it on the frontend.
.block-editor-warning {
border: $border-width solid $light-gray-500;
padding: 10px 14px;

.block-editor-warning__message {
line-height: $default-line-height;
font-family: $default-font;
font-size: $default-font-size;
}

.block-editor-warning__actions {
.components-button {
font-family: $default-font;
font-weight: inherit;
text-decoration: none;
ockham marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
67 changes: 67 additions & 0 deletions extensions/shared/i18n-to-php.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* External dependencies
*/
import React from 'react';

// How (and why) does this transform work?
ockham marked this conversation as resolved.
Show resolved Hide resolved
// The idea here is to replace every call to each of `@wordpress/i18n`'s translation functions
// (`__()`, `_n()`, `_x()`, `_nx()`) with their PHP counterpart, wrapped in a `<?php ... ?>`
// pseudo-tag, and using `echo` to output the var, plus some escaping for sanitization.
// The most puzzling part might be the `echo`: after all, we can't know _how_ the translated
// strings are used in the Javascript source -- e.g. they might be assigned to a variable
// for later usage, rather than rendered directly.
// The answer is that this happens during React's rendering (to static markup, in this case),
// where the entire component logic is essentially flattened to a component string. This means
// that even translated strings that have gone through any intermediate steps will end up in
// that rendered markup -- and thus, we'll have our `<?php echo esc_html__( ... ) ?>`
// statements right where they belong.
// A note on implementation:
// Ideally, our replaced translation function would simply return `<?php echo esc_html__( ... ) ?>`.
// However, React sanitizes strings by escaping chars like `<`. As a consequence, we need to use
// `dangerouslySetInnerHTML` to bypass the escaping. This also requires to be attached as a prop
// to a DOM element. I've chosen `<span />` since this likely has the smallest footprint for
// rendering strings (e.g. shouldn't normally get in the way of styling).
export const __ = ( text, domain ) => (
<span
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={ {
__html: `<?php esc_html_e( '${ text }', '${ domain }' ) ?>`,
} }
/>
);

export const _n = ( single, plural, number, domain ) => (
<span
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={ {
__html: `<?php echo esc_html( _n( '${ single }', '${ plural }', ${ number }, '${ domain }' ) ) ?>`,
} }
/>
);

export const _x = ( text, context, domain ) => (
<span
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={ {
__html: `<?php echo esc_html( _x( '${ text }', '${ context }', '${ domain }' ) ) ?>`,
} }
/>
);

export const _nx = ( single, plural, number, context, domain ) => (
<span
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={ {
__html: `<?php echo esc_html( _nx( '${ single }', '${ plural }', ${ number }, '${ context }', '${ domain }' ) ) ?>`,
} }
/>
);

// We have to stub `sprintf` with the identity function here, since the original
// `sprintf from '@wordpress/i18n'` only accepts strings as its first argument -- but
// our replaced translation functions return a React element (`<span />`, see above).
// This means that our rendered markup will contain `sprintf`-style `%(placeholder)s`
// for which we need to add an extra `str_replace()` step. This is done in `components.php`.
// TODO: Provide a wrapper around `@wordpress/i18n`'s `sprintf` that accepts React elements
// as first argument, and remove the `str_replace()` call in `components.php`.
export const sprintf = x => x;
45 changes: 45 additions & 0 deletions extensions/shared/test/i18n-to-php.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { renderToStaticMarkup } from 'react-dom/server';

/**
* Internal dependencies
*/
import { __, _n, _x, _nx } from '../i18n-to-php';

describe( 'i18n-to-php', () => {
test( 'renders __() to its PHP counterpart as expected', () => {
expect(
renderToStaticMarkup(
__( 'Upgrade to a paid plan to use this block on your site.', 'text-domain' )
)
).toBe(
"<span><?php esc_html_e( 'Upgrade to a paid plan to use this block on your site.', 'text-domain' ) ?></span>"
);
} );

test( 'renders _n() to its PHP counterpart as expected', () => {
expect( renderToStaticMarkup( _n( '%d person', '%d people', 1 + 2, 'text-domain' ) ) ).toBe(
"<span><?php echo esc_html( _n( '%d person', '%d people', 3, 'text-domain' ) ) ?></span>"
);
} );

test( 'renders _x() to its PHP counterpart as expected', () => {
expect(
renderToStaticMarkup( _x( 'Read', 'past participle: books I have read', 'text-domain' ) )
).toBe(
"<span><?php echo esc_html( _x( 'Read', 'past participle: books I have read', 'text-domain' ) ) ?></span>"
);
} );

test( 'renders _nx() to its PHP counterpart as expected', () => {
expect(
renderToStaticMarkup(
_nx( '%d group', '%d groups', 2 + 3, 'group of people', 'text-domain' )
)
).toBe(
"<span><?php echo esc_html( _nx( '%d group', '%d groups', 5, 'group of people', 'text-domain' ) ) ?></span>"
);
} );
} );
Loading