Skip to content

Commit

Permalink
Merge pull request #14 from xwp/add/7-sw_api_prototype
Browse files Browse the repository at this point in the history
Prototype for Service Workers API
  • Loading branch information
westonruter committed Jul 9, 2018
2 parents 5de9db9 + e9e902e commit 11b04de
Show file tree
Hide file tree
Showing 8 changed files with 402 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,2 +1,3 @@
node_modules
build/
composer.lock
34 changes: 17 additions & 17 deletions pwa-wp.php
Expand Up @@ -26,22 +26,22 @@
define( 'PWAWP_PLUGIN_FILE', __FILE__ );
define( 'PWAWP_PLUGIN_DIR', dirname( __FILE__ ) );

pwawp_init();
/** WP_Web_App_Manifest Class */
require PWAWP_PLUGIN_DIR . '/wp-includes/class-wp-web-app-manifest.php';

/**
* Loads and instantiates the classes.
*/
function pwawp_init() {
$classes = array(
'web-app-manifest',
'https-detection',
);
foreach ( $classes as $class ) {
require PWAWP_PLUGIN_DIR . "/php/class-wp-{$class}.php";
}
/** WP_HTTPS_Detection Class */
require PWAWP_PLUGIN_DIR . '/wp-includes/class-wp-https-detection.php';

/** WP_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';

/** Amend default filters */
require PWAWP_PLUGIN_DIR . '/wp-includes/default-filters.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();
}
$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();
59 changes: 59 additions & 0 deletions tests/test-class-wp-service-workers.php
@@ -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 );
}
}
File renamed without changes.
205 changes: 205 additions & 0 deletions wp-includes/class-wp-service-workers.php
@@ -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 );
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;
}
}
File renamed without changes.
14 changes: 14 additions & 0 deletions wp-includes/default-filters.php
@@ -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' );

0 comments on commit 11b04de

Please sign in to comment.