true, 'show_ui' => true ), 'names' );
unset( $types['attachment'] );
return array_values( array_filter( (array) apply_filters( 'ocssjs_supported_post_types', $types ), 'post_type_exists' ) );
}
public function add_meta_boxes(): void {
foreach ( $this->post_types() as $type ) {
add_meta_box(
'ocssjs-page-assets',
__( 'Page CSS & JavaScript', 'page-asset-manager' ),
array( $this, 'meta_box' ),
$type,
'advanced',
'default'
);
}
}
public function admin_assets( string $hook ): void {
if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) ) {
return;
}
$screen = get_current_screen();
if ( ! $screen || ! in_array( $screen->post_type, $this->post_types(), true ) ) {
return;
}
wp_enqueue_script( 'jquery-ui-sortable' );
wp_enqueue_script( 'ocssjs-admin', plugins_url( 'js/reorder-admin.js', __FILE__ ), array( 'jquery', 'jquery-ui-sortable' ), self::VERSION, true );
wp_enqueue_style( 'ocssjs-admin', plugins_url( 'css/reorder-admin.css', __FILE__ ), array( 'dashicons' ), self::VERSION );
wp_localize_script(
'ocssjs-admin',
'ocssjsAdmin',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( self::NONCE ),
'messages' => array(
'saving' => __( 'Saving…', 'page-asset-manager' ),
'scanning' => __( 'Scanning the front end…', 'page-asset-manager' ),
'reset' => __( 'Reset ordering and enable all discovered assets?', 'page-asset-manager' ),
'error' => __( 'The request failed. Please try again.', 'page-asset-manager' ),
),
)
);
}
public function meta_box( WP_Post $post ): void {
$settings = $this->settings( $post->ID );
$assets = $this->saved_assets( $post->ID, $settings );
?>
asset_column( 'css', __( 'Stylesheets', 'page-asset-manager' ), $assets['css'], $settings['css_disabled'] ); ?>
asset_column( 'js', __( 'JavaScript', 'page-asset-manager' ), $assets['js'], $settings['js_disabled'] ); ?>
$this->post_types(),
'post_status' => array( 'publish', 'draft', 'private' ),
'posts_per_page' => 100,
'orderby' => 'modified',
'order' => 'DESC',
'post__not_in' => array( $current ),
'meta_key' => self::META,
)
);
foreach ( $posts as $post ) {
printf( '', (int) $post->ID, esc_html( get_the_title( $post ) ?: __( '(no title)', 'page-asset-manager' ) ) );
}
}
private function asset_column( string $type, string $title, array $assets, array $disabled ): void {
?>
authorize();
$settings = array(
'css_order' => $this->handles( $this->posted_array( 'css_order' ) ),
'js_order' => $this->handles( $this->posted_array( 'js_order' ) ),
'css_disabled' => $this->handles( $this->posted_array( 'css_disabled' ) ),
'js_disabled' => array_values( array_diff( $this->handles( $this->posted_array( 'js_disabled' ) ), array( 'jquery', 'jquery-core', 'jquery-migrate' ) ) ),
);
$this->store_settings( $post_id, $settings );
wp_send_json_success( array( 'message' => __( 'Asset settings saved.', 'page-asset-manager' ) ) );
}
public function ajax_copy(): void {
$post_id = $this->authorize();
$source = isset( $_POST['source_id'] ) ? absint( wp_unslash( $_POST['source_id'] ) ) : 0;
if ( ! $source || ! current_user_can( 'edit_post', $source ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid source item.', 'page-asset-manager' ) ), 403 );
}
$this->store_settings( $post_id, $this->settings( $source ) );
wp_send_json_success( array( 'message' => __( 'Settings copied. Reloading…', 'page-asset-manager' ) ) );
}
public function ajax_scan(): void {
$post_id = $this->authorize();
$url = get_permalink( $post_id );
if ( ! $url ) {
wp_send_json_error( array( 'message' => __( 'This item has no front-end URL.', 'page-asset-manager' ) ), 400 );
}
$token = wp_generate_password( 32, false, false );
set_transient( self::SCAN_TOKEN . $post_id, hash( 'sha256', $token ), MINUTE_IN_SECONDS );
$cookies = array();
foreach ( $_COOKIE as $name => $value ) {
if ( is_string( $name ) && is_string( $value ) ) {
$cookies[] = new WP_Http_Cookie(
array(
'name' => sanitize_key( $name ),
'value' => $value,
)
);
}
}
$scan_url = add_query_arg( array( 'ocssjs_scan' => $post_id, 'ocssjs_token' => $token, 'cache_bust' => time() ), $url );
$request_args = array(
'timeout' => 20,
'redirection' => 3,
'headers' => array( 'Cache-Control' => 'no-cache' ),
'cookies' => $cookies,
'sslverify' => true,
);
$response = wp_remote_get( $scan_url, $request_args );
/*
* Local development tools commonly use a self-signed certificate.
* Retry only this specific failure and only for the site's own host.
*/
if ( is_wp_error( $response ) && $this->is_same_site_certificate_error( $response, $scan_url ) ) {
$request_args['sslverify'] = false;
$response = wp_remote_get( $scan_url, $request_args );
}
delete_transient( self::SCAN_TOKEN . $post_id );
if ( is_wp_error( $response ) ) {
wp_send_json_error( array( 'message' => sprintf( __( 'Front-end scan failed: %s', 'page-asset-manager' ), $response->get_error_message() ) ), 500 );
}
$assets = get_transient( self::SCAN_RESULT . $post_id );
delete_transient( self::SCAN_RESULT . $post_id );
if ( ! is_array( $assets ) ) {
wp_send_json_error( array( 'message' => __( 'No scan result was returned. A cache or loopback restriction may be blocking discovery.', 'page-asset-manager' ) ), 500 );
}
update_post_meta( $post_id, self::ASSETS, $assets );
wp_send_json_success( array( 'message' => __( 'Assets refreshed. Reloading…', 'page-asset-manager' ) ) );
}
private function is_same_site_certificate_error( WP_Error $error, string $url ): bool {
$message = strtolower( $error->get_error_message() );
if ( false === strpos( $message, 'curl error 60' ) && false === strpos( $message, 'certificate' ) ) {
return false;
}
$scan_host = wp_parse_url( $url, PHP_URL_HOST );
$site_host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
return is_string( $scan_host )
&& is_string( $site_host )
&& 0 === strcasecmp( $scan_host, $site_host );
}
public function capture_scan(): void {
if ( is_admin() || ! is_singular() || empty( $_GET['ocssjs_scan'] ) || empty( $_GET['ocssjs_token'] ) ) {
return;
}
$post_id = absint( wp_unslash( $_GET['ocssjs_scan'] ) );
$token = sanitize_text_field( wp_unslash( $_GET['ocssjs_token'] ) );
$expected = get_transient( self::SCAN_TOKEN . $post_id );
if ( get_queried_object_id() !== $post_id || ! is_string( $expected ) || ! hash_equals( $expected, hash( 'sha256', $token ) ) ) {
return;
}
set_transient( self::SCAN_RESULT . $post_id, array( 'css' => $this->collect( wp_styles() ), 'js' => $this->collect( wp_scripts() ) ), MINUTE_IN_SECONDS );
}
private function collect( WP_Dependencies $registry ): array {
$assets = array();
foreach ( array_values( array_unique( $registry->queue ) ) as $handle ) {
if ( ! isset( $registry->registered[ $handle ] ) ) {
continue;
}
$item = $registry->registered[ $handle ];
$assets[] = array( 'handle' => sanitize_key( $handle ), 'src' => is_string( $item->src ) ? $item->src : '', 'deps' => $this->handles( (array) $item->deps ) );
}
return $assets;
}
public function apply_settings(): void {
if ( is_admin() || ! is_singular() ) {
return;
}
$settings = $this->settings( get_queried_object_id() );
$this->reorder( wp_styles(), $settings['css_order'] );
$this->reorder( wp_scripts(), $settings['js_order'] );
foreach ( $settings['css_disabled'] as $handle ) {
wp_dequeue_style( $handle );
}
foreach ( $settings['js_disabled'] as $handle ) {
if ( ! in_array( $handle, array( 'jquery', 'jquery-core', 'jquery-migrate' ), true ) ) {
wp_dequeue_script( $handle );
}
}
}
private function reorder( WP_Dependencies $registry, array $preferred ): void {
$queue = array_values( array_unique( $registry->queue ) );
$front = array_values( array_intersect( $preferred, $queue ) );
$registry->queue = array_merge( $front, array_values( array_diff( $queue, $front ) ) );
}
private function authorize(): int {
check_ajax_referer( self::NONCE, 'nonce' );
$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;
if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to edit this item.', 'page-asset-manager' ) ), 403 );
}
if ( ! in_array( get_post_type( $post_id ), $this->post_types(), true ) ) {
wp_send_json_error( array( 'message' => __( 'This post type is not supported.', 'page-asset-manager' ) ), 400 );
}
return $post_id;
}
private function posted_array( string $key ): array {
return isset( $_POST[ $key ] ) && is_array( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : array();
}
private function handles( array $handles ): array {
return array_values( array_unique( array_filter( array_map( 'sanitize_key', $handles ) ) ) );
}
private function store_settings( int $post_id, array $settings ): void {
update_post_meta( $post_id, self::META, $settings );
update_post_meta( $post_id, self::LEGACY_CSS, $settings['css_order'] );
update_post_meta( $post_id, self::LEGACY_JS, $settings['js_order'] );
}
private function settings( int $post_id ): array {
$defaults = array( 'css_order' => array(), 'js_order' => array(), 'css_disabled' => array(), 'js_disabled' => array() );
$saved = get_post_meta( $post_id, self::META, true );
if ( ! is_array( $saved ) ) {
$saved = array( 'css_order' => (array) get_post_meta( $post_id, self::LEGACY_CSS, true ), 'js_order' => (array) get_post_meta( $post_id, self::LEGACY_JS, true ) );
}
foreach ( $defaults as $key => $default ) {
$defaults[ $key ] = $this->handles( isset( $saved[ $key ] ) && is_array( $saved[ $key ] ) ? $saved[ $key ] : $default );
}
return $defaults;
}
private function saved_assets( int $post_id, array $settings ): array {
$assets = get_post_meta( $post_id, self::ASSETS, true );
$assets = is_array( $assets ) ? $assets : array( 'css' => array(), 'js' => array() );
foreach ( array( 'css', 'js' ) as $type ) {
$assets[ $type ] = isset( $assets[ $type ] ) && is_array( $assets[ $type ] ) ? $assets[ $type ] : array();
$known = wp_list_pluck( $assets[ $type ], 'handle' );
foreach ( $settings[ $type . '_order' ] as $handle ) {
if ( ! in_array( $handle, $known, true ) ) {
$assets[ $type ][] = array( 'handle' => $handle, 'src' => '', 'deps' => array() );
}
}
$order = array_flip( $settings[ $type . '_order' ] );
usort( $assets[ $type ], static fn( array $a, array $b ): int => ( $order[ $a['handle'] ] ?? PHP_INT_MAX ) <=> ( $order[ $b['handle'] ] ?? PHP_INT_MAX ) );
}
return $assets;
}
}
OCSSJS_Page_Asset_Manager::instance();