Skip to content

Commit

Permalink
added changes from PR GoogleChromeLabs#14
Browse files Browse the repository at this point in the history
  • Loading branch information
nico-martin committed Jul 6, 2018
1 parent 5de9db9 commit 19f5827
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 4 deletions.
11 changes: 7 additions & 4 deletions pwa-wp.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,10 @@
* Requires PHP: 5.2
* Requires WP: 4.9
*/

define( 'PWAWP_VERSION', '0.1.0-alpha' );
define( 'PWAWP_PLUGIN_FILE', __FILE__ );
define( 'PWAWP_PLUGIN_DIR', dirname( __FILE__ ) );

pwawp_init();

/**
* Loads and instantiates the classes.
*/
Expand All @@ -39,9 +36,15 @@ function pwawp_init() {
foreach ( $classes as $class ) {
require PWAWP_PLUGIN_DIR . "/php/class-wp-{$class}.php";
}

$wp_web_app_manifest = new WP_Web_App_Manifest();
$wp_web_app_manifest->init();
$wp_https_detection = new WP_HTTPS_Detection();
$wp_https_detection->init();
// These could be in ABSPATH . WPINC . '/script-loader.php' file.
/** WordPress Service Workers Class */
require PWAWP_PLUGIN_DIR . '/wp-includes/class-wp-service-workers.php';
/** WordPress Service Worker Functions */
require PWAWP_PLUGIN_DIR . '/wp-includes/service-workers.php';
/** Hooks */
require PWAWP_PLUGIN_DIR . '/wp-includes/default-filters.php';
}
54 changes: 54 additions & 0 deletions tests/test-class-wp-service-workers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
/**
* Tests for class WP_Service_Workers.
*
* @package PWA
*/

/**
* Tests for class WP_Web_App_Manifest.
*/
class Test_WP_Service_Workers extends WP_UnitTestCase {
/**
* Tested instance.
*
* @var WP_Service_Workers
*/
public $instance;

/**
* Setup.
*
* @inheritdoc
*/
public function setUp() {
parent::setUp();
$this->instance = new WP_Service_Workers();
}

/**
* Test class constructor.
*
* @covers WP_Service_Workers::__construct()
*/
public function test_construct() {
$service_workers = new WP_Service_Workers();
$this->assertEquals( 'WP_Service_Workers', get_class( $service_workers ) );
}

/**
* Test adding new service worker.
*
* @covers WP_Service_Workers::add()
*/
public function test_register() {
$this->instance->register( 'foo', '/test-sw.js', array( 'bar' ) );
$default_scope = site_url( '/', 'relative' );
$this->assertTrue( in_array( $default_scope, $this->instance->get_scopes(), true ) );
$this->assertTrue( isset( $this->instance->registered['foo'] ) );
$registered_sw = $this->instance->registered['foo'];
$this->assertEquals( '/test-sw.js', $registered_sw->src );
$this->assertTrue( in_array( $default_scope, $registered_sw->args['scopes'], true ) );
$this->assertEquals( array( 'bar' ), $registered_sw->deps );
}
}
172 changes: 172 additions & 0 deletions wp-includes/class-wp-service-workers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php
/**
* Dependencies API: WP_Service_Workers class
*
* @since ?
*
* @package PWA
*/
/**
* Class used to register service workers.
*
* @since ?
*
* @see WP_Dependencies
*/
class WP_Service_Workers extends WP_Scripts {
/**
* Param for service workers.
*
* @var string
*/
public $query_var = 'wp_service_worker';
/**
* Output for service worker scope script.
*
* @var string
*/
public $output = '';
/**
* Initialize the class.
*/
public function init() {
/**
* Fires when the WP_Service_Workers instance is initialized.
*
* @param WP_Service_Workers $this WP_Service_Workers instance (passed by reference).
*/
do_action_ref_array( 'wp_default_service_workers', array( &$this ) );
}
/**
* Register service worker.
*
* Registers service worker if no item of that name already exists.
*
* @param string $handle Name of the item. Should be unique.
* @param string|callable $src URL to the source in the WordPress install, or a callback that returns the JS to include in the service worker.
* @param array $deps Optional. An array of registered item handles this item depends on. Default empty array.
* @param array $scopes Optional. Scopes of the service worker. Default relative path.
* @return bool Whether the item has been registered. True on success, false on failure.
*/
public function register( $handle, $src, $deps = array(), $scopes = array() ) {
// Set default scope if missing.
if ( empty( $scopes ) ) {
$scopes = array( site_url( '/', 'relative' ) );
}
return parent::add( $handle, $src, $deps, false, compact( 'scopes' ) );
}
/**
* Get service worker logic for scope.
*
* @param string $scope Scope of the Service Worker.
*/
public function serve_request( $scope ) {
header( 'Content-Type: text/javascript; charset=utf-8' );
nocache_headers();
if ( ! in_array( $scope, $this->get_scopes(), true ) ) {
status_header( 404 );
return;
}
$scope_items = array();
// Get handles from the relevant scope only.
foreach ( $this->registered as $handle => $item ) {
if ( in_array( $scope, $item->args['scopes'], true ) ) {
$scope_items[] = $handle;
}
}
$this->output = '';
$this->do_items( $scope_items );
$file_hash = md5( $this->output );
header( "Etag: $file_hash" );
$etag_header = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? trim( $_SERVER['HTTP_IF_NONE_MATCH'] ) : false;
if ( $file_hash === $etag_header ) {
status_header( 304 );
return;
}
echo $this->output; // phpcs:ignore WordPress.XSS.EscapeOutput, WordPress.Security.EscapeOutput
}
/**
* Get all scopes.
*
* @return array Array of scopes.
*/
public function get_scopes() {
$scopes = array();
foreach ( $this->registered as $handle => $item ) {
$scopes = array_merge( $scopes, $item->args['scopes'] );
}
return array_unique( $scopes );
}
/**
* Process one registered script.
*
* @param string $handle Handle.
* @param bool $group Group.
* @return void
*/
public function do_item( $handle, $group = false ) {
$obj = $this->registered[ $handle ];
if ( is_callable( $obj->src ) ) {
$this->output .= call_user_func( $obj->src ) . "\n";
} else {
$validated_path = $this->get_validated_file_path( $obj->src );
if ( is_wp_error( $validated_path ) ) {
_doing_it_wrong(
__FUNCTION__,
/* translators: %s is file URL */
sprintf( esc_html__( 'Service worker src is incorrect: %s', 'pwa' ), esc_html( $obj->src ) ),
'0.1'
);
/* translators: %s is file URL */
$this->output .= "console.warn( '" . sprintf( esc_html__( 'Service worker src is incorrect: %s', 'pwa' ), esc_html( $obj->src ) ) . "' );\n";
} else {
/* translators: %s is file URL */
$this->output .= sprintf( esc_html( "\n/* Source: %s */\n" ), esc_url( $obj->src ) );
$this->output .= @file_get_contents( $this->get_validated_file_path( $obj->src ) ) . "\n"; // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents
}
}
}
/**
* Remove URL scheme.
*
* @param string $schemed_url URL.
* @return string URL.
*/
public function remove_url_scheme( $schemed_url ) {
return preg_replace( '#^\w+:(?=//)#', '', $schemed_url );
}
/**
* Get validated path to file.
*
* @param string $url Relative path.
* @return null|string|WP_Error
*/
public function get_validated_file_path( $url ) {
if ( ! is_string( $url ) ) {
return new WP_Error( 'incorrect_path_format', esc_html__( 'URL has to be a string', 'pwa' ) );
}
$needs_base_url = ! preg_match( '|^(https?:)?//|', $url );
$base_url = site_url();
if ( $needs_base_url ) {
$url = $base_url . $url;
}
// Strip URL scheme, query, and fragment.
$url = $this->remove_url_scheme( preg_replace( ':[\?#].*$:', '', $url ) );
$content_url = $this->remove_url_scheme( content_url( '/' ) );
$allowed_host = wp_parse_url( $content_url, PHP_URL_HOST );
$url_host = wp_parse_url( $url, PHP_URL_HOST );
if ( $allowed_host !== $url_host ) {
/* translators: %s is file URL */
return new WP_Error( 'external_file_url', sprintf( __( 'URL is located on an external domain: %s.', 'pwa' ), $url_host ) );
}
$file_path = null;
if ( 0 === strpos( $url, $content_url ) ) {
$file_path = WP_CONTENT_DIR . substr( $url, strlen( $content_url ) - 1 );
}
if ( ! $file_path || false !== strpos( '../', $file_path ) || 0 !== validate_file( $file_path ) || ! file_exists( $file_path ) ) {
/* translators: %s is file URL */
return new WP_Error( 'file_path_not_found', sprintf( __( 'Unable to locate filesystem path for %s.', 'pwa' ), $url ) );
}
return $file_path;
}
}
11 changes: 11 additions & 0 deletions wp-includes/default-filters.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
/**
* Sets up the default filters and actions for PWA hooks.
*
* @package PWA
*/
foreach ( array( 'wp_print_scripts', 'admin_print_scripts', 'customize_controls_print_scripts' ) as $filter ) {
add_filter( $filter, 'wp_print_service_workers', 9 );
}
add_action( 'parse_request', 'wp_service_worker_loaded' );
add_filter( 'query_vars', 'wp_add_service_worker_query_var' );
93 changes: 93 additions & 0 deletions wp-includes/service-workers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/**
* Service Worker functions.
*
* @since ?
*
* @package PWA
*/
/**
* Initialize $wp_service_workers if it has not been set.
*
* @global WP_Service_Workers $wp_service_workers
* @return WP_Service_Workers WP_Service_Workers instance.
*/
function wp_service_workers() {
global $wp_service_workers;
if ( ! ( $wp_service_workers instanceof WP_Service_Workers ) ) {
$wp_service_workers = new WP_Service_Workers();
}
return $wp_service_workers;
}
/**
* Register a new service worker.
*
* @since ?
*
* @param string $handle Name of the service worker. Should be unique.
* @param string|callable $src Callback method or relative path of the service worker.
* @param array $deps Optional. An array of registered script handles this depends on. Default empty array.
* @param array $scopes Optional Scopes of the service worker.
* @return bool Whether the script has been registered. True on success, false on failure.
*/
function wp_register_service_worker( $handle, $src, $deps = array(), $scopes = array() ) {
$service_workers = wp_service_workers();
$registered = $service_workers->register( $handle, $src, $deps, $scopes );
return $registered;
}
/**
* Get service worker URL by scope.
*
* @param string $scope Scope, for example 'wp-admin'.
* @return string Service Worker URL.
*/
function wp_get_service_worker_url( $scope ) {
if ( ! $scope ) {
$scope = site_url( '/', 'relative' );
}
return add_query_arg( array(
'wp_service_worker' => $scope,
), site_url( '/', 'relative' ) );
}
/**
* Print service workers' scripts.
*/
function wp_print_service_workers() {
$scopes = wp_service_workers()->get_scopes();
if ( empty( $scopes ) ) {
return;
}
?>
<script>
if ( navigator.serviceWorker ) {
window.addEventListener('load', function() {
<?php foreach ( $scopes as $scope ) { ?>
navigator.serviceWorker.register(
<?php echo wp_json_encode( wp_get_service_worker_url( $scope ) ); ?>,
<?php echo wp_json_encode( compact( 'scope' ) ); ?>
);
<?php } ?>
} );
}
</script>
<?php
}
/**
* Register query var.
*
* @param array $query_vars Query vars.
* @return array Query vars.
*/
function wp_add_service_worker_query_var( $query_vars ) {
$query_vars[] = 'wp_service_worker';
return $query_vars;
}
/**
* If it's a service worker script page, display that.
*/
function wp_service_worker_loaded() {
if ( ! empty( $GLOBALS['wp']->query_vars['wp_service_worker'] ) ) {
wp_service_workers()->serve_request( $GLOBALS['wp']->query_vars['wp_service_worker'] );
exit;
}
}

0 comments on commit 19f5827

Please sign in to comment.