-
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 11 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_add() { | ||
$this->instance->add( 'foo', '/test-sw.js', array( 'bar' ), '1.0' ); | ||
|
||
$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->assertEquals( $default_scope, $registered_sw->args ); | ||
$this->assertEquals( array( 'bar' ), $registered_sw->deps ); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
<?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 = ''; | ||
|
||
/** | ||
* WP_Service_Workers constructor. | ||
*/ | ||
public function __construct() { | ||
parent::__construct(); | ||
global $wp_filesystem; | ||
|
||
if ( ! class_exists( 'WP_Filesystem' ) ) { | ||
require_once ABSPATH . '/wp-admin/includes/file.php'; | ||
} | ||
|
||
if ( null === $wp_filesystem ) { | ||
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. Would this be better in 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.
|
||
WP_Filesystem(); | ||
} | ||
} | ||
|
||
/** | ||
* 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 $path Path of the item relative to the WordPress root directory. | ||
* @param array $deps Optional. An array of registered item handles this item depends on. Default empty array. | ||
* @param bool $ver Always false for service worker. | ||
* @param mixed $scope Optional. Scope of the service worker. Default relative path. | ||
* @return bool Whether the item has been registered. True on success, false on failure. | ||
*/ | ||
public function add( $handle, $path, $deps = array(), $ver = false, $scope = null ) { | ||
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. Something that just came to mind: what if a script needs to be “enqueued” in multiple scopes? 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. Just pushed the changes for registering scopes as an array. |
||
|
||
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. 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 commentThe 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'. |
||
// Set default scope if missing. | ||
if ( ! $scope ) { | ||
$scope = site_url( '/', 'relative' ); | ||
} | ||
|
||
if ( false === parent::add( $handle, $path, $deps, false, $scope ) ) { | ||
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. Instead of passing Also, maybe a script should be able to added to multiple scopes as I mentioned above, so maybe it should be |
||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* 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 ( $scope === $item->args ) { | ||
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. Per above, I think if ( in_array( $item->args['scopes'], $scope, 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; | ||
} | ||
|
||
// @codingStandardsIgnoreLine | ||
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. Instead of this, let's do: echo $this->output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
||
echo $this->output; | ||
} | ||
|
||
/** | ||
* Get all scopes. | ||
* | ||
* @return array Array of scopes. | ||
*/ | ||
public function get_scopes() { | ||
|
||
$scopes = array(); | ||
foreach ( $this->registered as $handle => $item ) { | ||
$scopes[] = $item->args; | ||
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. Per above, I think we should consider other args being supplied and for multiple scopes to be registered for one script. So this could instead be: $scopes = array_merge( $scopes, $item->args['scopes'] ); |
||
} | ||
return $scopes; | ||
} | ||
|
||
/** | ||
* Process one registered script. | ||
* | ||
* @param string $handle Handle. | ||
* @param bool $group Group. | ||
* @return void | ||
*/ | ||
public function do_item( $handle, $group = false ) { | ||
global $wp_filesystem; | ||
|
||
$obj = $this->registered[ $handle ]; | ||
$this->output .= $wp_filesystem->get_contents( site_url() . $obj->src ) . "\n"; | ||
} | ||
|
||
/** | ||
* 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 ) { | ||
$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 ) ); | ||
|
||
$includes_url = $this->remove_url_scheme( includes_url( '/' ) ); | ||
$content_url = $this->remove_url_scheme( content_url( '/' ) ); | ||
$admin_url = $this->remove_url_scheme( get_admin_url( null, '/' ) ); | ||
|
||
$allowed_hosts = array( | ||
wp_parse_url( $includes_url, PHP_URL_HOST ), | ||
wp_parse_url( $content_url, PHP_URL_HOST ), | ||
wp_parse_url( $admin_url, PHP_URL_HOST ), | ||
); | ||
|
||
$url_host = wp_parse_url( $url, PHP_URL_HOST ); | ||
|
||
if ( ! in_array( $url_host, $allowed_hosts, true ) ) { | ||
/* 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 ); | ||
} elseif ( 0 === strpos( $url, $includes_url ) ) { | ||
$file_path = ABSPATH . WPINC . substr( $url, strlen( $includes_url ) - 1 ); | ||
} elseif ( 0 === strpos( $url, $admin_url ) ) { | ||
$file_path = ABSPATH . 'wp-admin' . substr( $url, strlen( $admin_url ) - 1 ); | ||
} else { | ||
$file_path = ABSPATH . substr( $url, strlen( $this->remove_url_scheme( $base_url ) ) ); | ||
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. Also added the Wondering if we should restrict the SW file path to validating against 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. Since plugins and themes would only enqueue their scripts that are inside |
||
} | ||
|
||
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_footer_scripts', 'admin_print_footer_scripts', 'customize_controls_print_footer_scripts' ) as $filter ) { | ||
add_filter( $filter, 'wp_print_service_workers' ); | ||
} | ||
|
||
add_action( 'template_redirect', 'service_worker_loaded' ); | ||
|
||
add_action( 'init', 'wp_add_sw_rewrite_rules' ); |
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.
This can be moved into
do_item
as well, and then the__construct
method can be eliminated.