You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

524 lines
16 KiB
PHP

<?php
/**
* Class Editor
*
* @link https://github.com/googleforcreators/web-stories-wp
*
* @copyright 2020 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
*/
/**
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare(strict_types = 1);
namespace Google\Web_Stories\Admin;
use Google\Web_Stories\Assets;
use Google\Web_Stories\Context;
use Google\Web_Stories\Decoder;
use Google\Web_Stories\Experiments;
use Google\Web_Stories\Font_Post_Type;
use Google\Web_Stories\Infrastructure\HasRequirements;
use Google\Web_Stories\Locale;
use Google\Web_Stories\Media\Types;
use Google\Web_Stories\Model\Story;
use Google\Web_Stories\Page_Template_Post_Type;
use Google\Web_Stories\Service_Base;
use Google\Web_Stories\Settings;
use Google\Web_Stories\Story_Post_Type;
use Google\Web_Stories\Tracking;
use WP_Post;
/**
* Class Editor
*/
class Editor extends Service_Base implements HasRequirements {
/**
* Web Stories editor script handle.
*/
public const SCRIPT_HANDLE = 'web-stories-editor';
/**
* AMP validator script handle.
*/
public const AMP_VALIDATOR_SCRIPT_HANDLE = 'amp-validator';
/**
* The libheif script handle.
*/
public const LIBHEIF_SCRIPT_HANDLE = 'web-stories-libheif';
/**
* Experiments instance.
*
* @var Experiments Experiments instance.
*/
private Experiments $experiments;
/**
* Decoder instance.
*
* @var Decoder Decoder instance.
*/
private Decoder $decoder;
/**
* Meta boxes instance.
*
* @var Meta_Boxes Meta_Boxes instance.
*/
private Meta_Boxes $meta_boxes;
/**
* Locale instance.
*
* @var Locale Locale instance.
*/
private Locale $locale;
/**
* Google_Fonts instance.
*
* @var Google_Fonts Google_Fonts instance.
*/
private Google_Fonts $google_fonts;
/**
* Assets instance.
*
* @var Assets Assets instance.
*/
private Assets $assets;
/**
* Story_Post_Type instance.
*
* @var Story_Post_Type Story_Post_Type instance.
*/
private Story_Post_Type $story_post_type;
/**
* Page_Template_Post_Type instance.
*
* @var Page_Template_Post_Type Page_Template_Post_Type instance.
*/
private Page_Template_Post_Type $page_template_post_type;
/**
* Font_Post_Type instance.
*
* @var Font_Post_Type Font_Post_Type instance.
*/
private Font_Post_Type $font_post_type;
/**
* Context instance.
*
* @var Context Context instance.
*/
private Context $context;
/**
* Types instance.
*
* @var Types Types instance.
*/
private Types $types;
/**
* Settings instance.
*
* @var Settings Settings instance.
*/
private Settings $settings;
/**
* Dashboard constructor.
*
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*
* @since 1.0.0
*
* @param Experiments $experiments Experiments instance.
* @param Meta_Boxes $meta_boxes Meta_Boxes instance.
* @param Decoder $decoder Decoder instance.
* @param Locale $locale Locale instance.
* @param Google_Fonts $google_fonts Google_Fonts instance.
* @param Assets $assets Assets instance.
* @param Story_Post_Type $story_post_type Story_Post_Type instance.
* @param Page_Template_Post_Type $page_template_post_type Page_Template_Post_Type instance.
* @param Font_Post_Type $font_post_type Font_Post_Type instance.
* @param Context $context Context instance.
* @param Types $types Types instance.
* @param Settings $settings Settings instance.
*/
public function __construct(
Experiments $experiments,
Meta_Boxes $meta_boxes,
Decoder $decoder,
Locale $locale,
Google_Fonts $google_fonts,
Assets $assets,
Story_Post_Type $story_post_type,
Page_Template_Post_Type $page_template_post_type,
Font_Post_Type $font_post_type,
Context $context,
Types $types,
Settings $settings
) {
$this->experiments = $experiments;
$this->meta_boxes = $meta_boxes;
$this->decoder = $decoder;
$this->locale = $locale;
$this->google_fonts = $google_fonts;
$this->assets = $assets;
$this->story_post_type = $story_post_type;
$this->page_template_post_type = $page_template_post_type;
$this->font_post_type = $font_post_type;
$this->context = $context;
$this->types = $types;
$this->settings = $settings;
}
/**
* Initializes the Editor logic.
*
* @since 1.7.0
*/
public function register(): void {
add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
add_filter( 'replace_editor', [ $this, 'replace_editor' ], 10, 2 );
add_filter( 'use_block_editor_for_post_type', [ $this, 'filter_use_block_editor_for_post_type' ], 10, 2 );
}
/**
* Get the list of service IDs required for this service to be registered.
*
* Needed because the story and page template post types need to be registered first.
*
* @since 1.14.0
*
* @return string[] List of required services.
*/
public static function get_requirements(): array {
return [ 'page_template_post_type', 'story_post_type' ];
}
/**
* Replace default post editor with our own implementation.
*
* @codeCoverageIgnore
*
* @since 1.0.0
*
* @param bool|mixed $replace Bool if to replace editor or not.
* @param WP_Post $post Current post object.
* @return bool|mixed Whether the editor has been replaced.
*/
public function replace_editor( $replace, WP_Post $post ) {
if ( $this->story_post_type->get_slug() === get_post_type( $post ) ) {
$script_dependencies = [
Tracking::SCRIPT_HANDLE,
'postbox',
self::AMP_VALIDATOR_SCRIPT_HANDLE,
self::LIBHEIF_SCRIPT_HANDLE,
];
// Registering here because the script handle is required for wp_add_inline_script in edit-story.php.
$this->assets->register_script_asset( self::SCRIPT_HANDLE, $script_dependencies, false );
// Since the 'replace_editor' filter can be run multiple times, only load the
// custom editor after the 'current_screen' action and when we can be certain the
// $post_type, $post_type_object, $post globals are all set by WordPress.
if ( isset( $GLOBALS['post'] ) && $post === $GLOBALS['post'] && did_action( 'current_screen' ) ) {
require_once WEBSTORIES_PLUGIN_DIR_PATH . 'includes/templates/admin/edit-story.php';
}
return true;
}
return $replace;
}
/**
* Filters whether post type supports the block editor.
*
* Disables the block editor and associated logic (like enqueueing assets)
* for the story post type.
*
* @since 1.0.0
*
* @param bool|mixed $use_block_editor Whether the post type can be edited or not. Default true.
* @param string $post_type The post type being checked.
* @return false|mixed Whether to use the block editor.
*/
public function filter_use_block_editor_for_post_type( $use_block_editor, string $post_type ) {
if ( $this->story_post_type->get_slug() === $post_type ) {
return false;
}
return $use_block_editor;
}
/**
* Enqueue scripts for the element editor.
*
* @since 1.0.0
*
* @param string $hook The current admin page.
*/
public function admin_enqueue_scripts( string $hook ): void {
if ( ! $this->context->is_story_editor() ) {
return;
}
// Only output scripts and styles where in edit screens.
if ( ! \in_array( $hook, [ 'post.php', 'post-new.php' ], true ) ) {
return;
}
// Force media model to load.
wp_enqueue_media();
wp_enqueue_script(
self::AMP_VALIDATOR_SCRIPT_HANDLE,
'https://cdn.ampproject.org/v0/validator_wasm.js',
[],
WEBSTORIES_VERSION,
true
);
wp_enqueue_script(
self::LIBHEIF_SCRIPT_HANDLE,
trailingslashit( WEBSTORIES_CDN_URL ) . 'js/libheif-js@1.14.0/libheif.js',
[],
WEBSTORIES_VERSION,
true
);
wp_enqueue_script( self::SCRIPT_HANDLE );
$this->assets->enqueue_style_asset( self::SCRIPT_HANDLE, [ $this->google_fonts::SCRIPT_HANDLE ] );
wp_localize_script(
self::SCRIPT_HANDLE,
'webStories',
[
'publicPath' => $this->assets->get_base_url( 'assets/js/' ), // Required before the editor script is enqueued.
'localeData' => $this->assets->get_translations( self::SCRIPT_HANDLE ), // Required for i18n setLocaleData.
]
);
// Dequeue forms.css, see https://github.com/googleforcreators/web-stories-wp/issues/349 .
$this->assets->remove_admin_style( [ 'forms' ] );
}
/**
* Get editor settings as an array.
*
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*
* @since 1.0.0
*
* @return array<string,mixed> Editor settings.
*/
public function get_editor_settings(): array {
$post = get_post();
$story_id = $post->ID ?? null;
$general_settings_url = admin_url( 'options-general.php' );
if ( $story_id ) {
$this->setup_lock( $story_id );
}
// Media settings.
$max_upload_size = wp_max_upload_size();
if ( ! $max_upload_size ) {
$max_upload_size = 0;
}
$dashboard_url = add_query_arg(
[
'post_type' => $this->story_post_type->get_slug(),
'page' => 'stories-dashboard',
],
admin_url( 'edit.php' )
);
$revision_url = admin_url( 'revision.php' );
$dashboard_settings_url = add_query_arg(
[
'post_type' => $this->story_post_type->get_slug(),
'page' => 'stories-dashboard#/editor-settings',
],
admin_url( 'edit.php' )
);
$user = wp_get_current_user();
/** This filter is documented in wp-admin/includes/post.php */
$show_locked_dialog = apply_filters( 'show_post_locked_dialog', true, $post, $user );
$nonce = wp_create_nonce( 'wp_rest' );
$story = new Story();
$story->load_from_post( $post );
// Explicitly setting these flags which became the default in PHP 8.1.
// Needed for correct single quotes in the editor & output.
// See https://github.com/GoogleForCreators/web-stories-wp/issues/10809.
$publisher_name = html_entity_decode( $story->get_publisher_name(), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
$shopping_provider = $this->settings->get_setting( $this->settings::SETTING_NAME_SHOPPING_PROVIDER );
$auto_advance = $this->settings->get_setting( $this->settings::SETTING_NAME_AUTO_ADVANCE );
$page_duration = $this->settings->get_setting( $this->settings::SETTING_NAME_DEFAULT_PAGE_DURATION );
$auto_save_link = '';
if ( isset( $story_id ) ) {
$auto_save = wp_get_post_autosave( $story_id );
if ( $auto_save && $post ) {
if ( mysql2date( 'U', $auto_save->post_modified_gmt, false ) > mysql2date( 'U', $post->post_modified_gmt, false ) ) {
$auto_save_link = get_edit_post_link( $auto_save->ID );
} else {
wp_delete_post_revision( $auto_save->ID );
}
}
}
/**
* Revision.
*
* @var int $revision
*/
$revision = isset( $_GET['revision'] ) ? absint( $_GET['revision'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$revision_message = ! empty( $revision ) ?
sprintf(
/* translators: %s: Date and time of the revision. */
__( 'Story restored to revision from %s.', 'web-stories' ),
wp_post_revision_title( $revision, false )
)
: false;
$settings = [
'autoSaveInterval' => \defined( 'AUTOSAVE_INTERVAL' ) ? AUTOSAVE_INTERVAL : null,
'localAutoSaveInterval' => 15,
'autoSaveLink' => $auto_save_link,
'isRTL' => is_rtl(),
'locale' => $this->locale->get_locale_settings(),
'allowedMimeTypes' => $this->types->get_allowed_mime_types(),
'postType' => $this->story_post_type->get_slug(),
'storyId' => $story_id,
'dashboardLink' => $dashboard_url,
'revisionLink' => $revision_url,
'revisionMessage' => $revision_message,
'dashboardSettingsLink' => $dashboard_settings_url,
'generalSettingsLink' => $general_settings_url,
'cdnURL' => trailingslashit( WEBSTORIES_CDN_URL ),
'maxUpload' => $max_upload_size,
'editPostsCapabilityName' => $this->story_post_type->get_cap_name( 'edit_posts' ),
'capabilities' => [
'hasUploadMediaAction' => current_user_can( 'upload_files' ),
'canManageSettings' => current_user_can( 'manage_options' ),
],
'api' => [
'users' => '/web-stories/v1/users/',
'currentUser' => '/web-stories/v1/users/me/',
'stories' => trailingslashit( $this->story_post_type->get_rest_url() ),
'pageTemplates' => trailingslashit( $this->page_template_post_type->get_rest_url() ),
'media' => '/web-stories/v1/media/',
'hotlink' => '/web-stories/v1/hotlink/validate/',
'publisherLogos' => '/web-stories/v1/publisher-logos/',
'products' => '/web-stories/v1/products/',
'proxy' => rest_url( '/web-stories/v1/hotlink/proxy/' ),
'link' => '/web-stories/v1/link/',
'statusCheck' => '/web-stories/v1/status-check/',
'taxonomies' => '/web-stories/v1/taxonomies/',
'fonts' => trailingslashit( $this->font_post_type->get_rest_url() ),
'metaBoxes' => $this->meta_boxes->get_meta_box_url( (int) $story_id ),
'storyLocking' => rest_url( sprintf( '%s/%s/lock/', $this->story_post_type->get_rest_url(), $story_id ) ),
],
'metadata' => [
'publisher' => $publisher_name,
],
'postLock' => [
'interval' => 60,
'showLockedDialog' => $show_locked_dialog,
],
'canViewDefaultTemplates' => true,
'version' => WEBSTORIES_VERSION,
'nonce' => $nonce,
'showMedia3p' => true,
'globalAutoAdvance' => (bool) $auto_advance,
'globalPageDuration' => (float) $page_duration,
'shoppingProvider' => $shopping_provider,
'encodeMarkup' => $this->decoder->supports_decoding(),
'metaBoxes' => $this->meta_boxes->get_meta_boxes_per_location(),
'ffmpegCoreUrl' => trailingslashit( WEBSTORIES_CDN_URL ) . 'js/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js',
'mediainfoUrl' => trailingslashit( WEBSTORIES_CDN_URL ) . 'js/mediainfo.js@0.1.9/dist/MediaInfoModule.wasm',
'flags' => array_merge(
$this->experiments->get_experiment_statuses( 'general' ),
$this->experiments->get_experiment_statuses( 'editor' ),
),
];
/**
* Filters settings passed to the web stories editor.
*
* @since 1.0.0
*
* @param array $settings Array of settings passed to web stories editor.
*/
return apply_filters( 'web_stories_editor_settings', $settings );
}
/**
* Setup up post lock.
*
* @since 1.5.0
*
* @param int $story_id Post id of story.
*/
protected function setup_lock( int $story_id ): void {
if ( ! $this->story_post_type->has_cap( 'edit_posts' ) ) {
return;
}
// Make sure these functions are loaded.
if (
! \function_exists( '\wp_check_post_lock' ) ||
! \function_exists( '\wp_set_post_lock' )
) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}
// Check current lock.
$lock_user_id = wp_check_post_lock( $story_id );
if ( ! $lock_user_id ) {
// If no lock set, create new lock.
wp_set_post_lock( $story_id );
}
}
}