-
Notifications
You must be signed in to change notification settings - Fork 99
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
Prototype for Service Workers API #14
Changes from all commits
357ee58
db2e496
531092c
076c3d0
e2cfd3e
296cf02
d6e8985
22658ed
ecc1431
4e8d750
2b585fa
a3ece90
7196301
922e8d6
7d63272
7c28ba4
ecdc6bf
20f037a
6a234f0
645dc19
02132c5
094a0d7
09506d1
4cc95ca
c920a65
7362ae3
f44146c
4869e0e
e9e902e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
node_modules | ||
build/ | ||
composer.lock |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?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 ); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
<?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 ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #26. This should perhaps not return a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (My suggestion won't work.) |
||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?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' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we check here for scope collisions? That is, what happens if two entities call register on the same scope?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is by design that two scripts can have the same scope. In fact this is the reason for this core API in the first place, to allow plugins and themes to each register their own scripts to run in the same scope as others'.