From a415c29efee45520ae252d2aa28f1083a521cd7b Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:56:49 +0200 Subject: Adding upstream version 6.4.3+dfsg1. Signed-off-by: Daniel Baumann --- wp-admin/includes/admin-filters.php | 174 + wp-admin/includes/admin.php | 101 + wp-admin/includes/ajax-actions.php | 5612 +++++++++++++++++++ wp-admin/includes/bookmark.php | 379 ++ .../includes/class-automatic-upgrader-skin.php | 135 + .../includes/class-bulk-plugin-upgrader-skin.php | 87 + .../includes/class-bulk-theme-upgrader-skin.php | 88 + wp-admin/includes/class-bulk-upgrader-skin.php | 187 + wp-admin/includes/class-core-upgrader.php | 420 ++ wp-admin/includes/class-custom-background.php | 666 +++ wp-admin/includes/class-custom-image-header.php | 1617 ++++++ wp-admin/includes/class-file-upload-upgrader.php | 158 + wp-admin/includes/class-ftp-pure.php | 186 + wp-admin/includes/class-ftp-sockets.php | 246 + wp-admin/includes/class-ftp.php | 913 ++++ .../includes/class-language-pack-upgrader-skin.php | 97 + wp-admin/includes/class-language-pack-upgrader.php | 474 ++ wp-admin/includes/class-pclzip.php | 5732 ++++++++++++++++++++ wp-admin/includes/class-plugin-installer-skin.php | 349 ++ wp-admin/includes/class-plugin-upgrader-skin.php | 123 + wp-admin/includes/class-plugin-upgrader.php | 712 +++ wp-admin/includes/class-theme-installer-skin.php | 384 ++ wp-admin/includes/class-theme-upgrader-skin.php | 144 + wp-admin/includes/class-theme-upgrader.php | 771 +++ .../includes/class-walker-category-checklist.php | 138 + .../includes/class-walker-nav-menu-checklist.php | 126 + wp-admin/includes/class-walker-nav-menu-edit.php | 323 ++ wp-admin/includes/class-wp-ajax-upgrader-skin.php | 159 + .../class-wp-application-passwords-list-table.php | 267 + wp-admin/includes/class-wp-automatic-updater.php | 1554 ++++++ wp-admin/includes/class-wp-comments-list-table.php | 1120 ++++ wp-admin/includes/class-wp-community-events.php | 530 ++ wp-admin/includes/class-wp-debug-data.php | 1716 ++++++ wp-admin/includes/class-wp-filesystem-base.php | 864 +++ wp-admin/includes/class-wp-filesystem-direct.php | 688 +++ wp-admin/includes/class-wp-filesystem-ftpext.php | 830 +++ .../includes/class-wp-filesystem-ftpsockets.php | 718 +++ wp-admin/includes/class-wp-filesystem-ssh2.php | 834 +++ wp-admin/includes/class-wp-importer.php | 327 ++ wp-admin/includes/class-wp-internal-pointers.php | 175 + wp-admin/includes/class-wp-links-list-table.php | 354 ++ wp-admin/includes/class-wp-list-table-compat.php | 67 + wp-admin/includes/class-wp-list-table.php | 1874 +++++++ wp-admin/includes/class-wp-media-list-table.php | 891 +++ wp-admin/includes/class-wp-ms-sites-list-table.php | 861 +++ .../includes/class-wp-ms-themes-list-table.php | 1038 ++++ wp-admin/includes/class-wp-ms-users-list-table.php | 546 ++ .../class-wp-plugin-install-list-table.php | 831 +++ wp-admin/includes/class-wp-plugins-list-table.php | 1394 +++++ .../includes/class-wp-post-comments-list-table.php | 77 + wp-admin/includes/class-wp-posts-list-table.php | 2119 ++++++++ ...-wp-privacy-data-export-requests-list-table.php | 160 + ...wp-privacy-data-removal-requests-list-table.php | 167 + .../includes/class-wp-privacy-policy-content.php | 706 +++ .../includes/class-wp-privacy-requests-table.php | 565 ++ wp-admin/includes/class-wp-screen.php | 1359 +++++ .../includes/class-wp-site-health-auto-updates.php | 458 ++ wp-admin/includes/class-wp-site-health.php | 3635 +++++++++++++ wp-admin/includes/class-wp-site-icon.php | 234 + wp-admin/includes/class-wp-terms-list-table.php | 755 +++ .../includes/class-wp-theme-install-list-table.php | 570 ++ wp-admin/includes/class-wp-themes-list-table.php | 360 ++ wp-admin/includes/class-wp-upgrader-skin.php | 278 + wp-admin/includes/class-wp-upgrader-skins.php | 44 + wp-admin/includes/class-wp-upgrader.php | 1236 +++++ wp-admin/includes/class-wp-users-list-table.php | 683 +++ wp-admin/includes/comment.php | 219 + wp-admin/includes/continents-cities.php | 549 ++ wp-admin/includes/credits.php | 166 + wp-admin/includes/dashboard.php | 2137 ++++++++ wp-admin/includes/deprecated.php | 1591 ++++++ wp-admin/includes/edit-tag-messages.php | 59 + wp-admin/includes/export.php | 686 +++ wp-admin/includes/file.php | 2804 ++++++++++ wp-admin/includes/image-edit.php | 1132 ++++ wp-admin/includes/image.php | 1166 ++++ wp-admin/includes/import.php | 232 + wp-admin/includes/list-table.php | 108 + wp-admin/includes/media.php | 3866 +++++++++++++ wp-admin/includes/menu.php | 383 ++ wp-admin/includes/meta-boxes.php | 1751 ++++++ wp-admin/includes/misc.php | 1644 ++++++ wp-admin/includes/ms-admin-filters.php | 41 + wp-admin/includes/ms-deprecated.php | 140 + wp-admin/includes/ms.php | 1172 ++++ wp-admin/includes/nav-menu.php | 1552 ++++++ wp-admin/includes/network.php | 760 +++ wp-admin/includes/noop.php | 113 + wp-admin/includes/options.php | 136 + wp-admin/includes/plugin-install.php | 926 ++++ wp-admin/includes/plugin.php | 2597 +++++++++ wp-admin/includes/post.php | 2637 +++++++++ wp-admin/includes/privacy-tools.php | 968 ++++ wp-admin/includes/revision.php | 473 ++ wp-admin/includes/schema.php | 1382 +++++ wp-admin/includes/screen.php | 244 + wp-admin/includes/taxonomy.php | 316 ++ wp-admin/includes/template.php | 2821 ++++++++++ wp-admin/includes/theme-install.php | 271 + wp-admin/includes/theme.php | 1250 +++++ wp-admin/includes/translation-install.php | 277 + wp-admin/includes/update-core.php | 1870 +++++++ wp-admin/includes/update.php | 1165 ++++ wp-admin/includes/upgrade.php | 3671 +++++++++++++ wp-admin/includes/user.php | 756 +++ wp-admin/includes/widgets.php | 329 ++ 106 files changed, 95746 insertions(+) create mode 100644 wp-admin/includes/admin-filters.php create mode 100644 wp-admin/includes/admin.php create mode 100644 wp-admin/includes/ajax-actions.php create mode 100644 wp-admin/includes/bookmark.php create mode 100644 wp-admin/includes/class-automatic-upgrader-skin.php create mode 100644 wp-admin/includes/class-bulk-plugin-upgrader-skin.php create mode 100644 wp-admin/includes/class-bulk-theme-upgrader-skin.php create mode 100644 wp-admin/includes/class-bulk-upgrader-skin.php create mode 100644 wp-admin/includes/class-core-upgrader.php create mode 100644 wp-admin/includes/class-custom-background.php create mode 100644 wp-admin/includes/class-custom-image-header.php create mode 100644 wp-admin/includes/class-file-upload-upgrader.php create mode 100644 wp-admin/includes/class-ftp-pure.php create mode 100644 wp-admin/includes/class-ftp-sockets.php create mode 100644 wp-admin/includes/class-ftp.php create mode 100644 wp-admin/includes/class-language-pack-upgrader-skin.php create mode 100644 wp-admin/includes/class-language-pack-upgrader.php create mode 100644 wp-admin/includes/class-pclzip.php create mode 100644 wp-admin/includes/class-plugin-installer-skin.php create mode 100644 wp-admin/includes/class-plugin-upgrader-skin.php create mode 100644 wp-admin/includes/class-plugin-upgrader.php create mode 100644 wp-admin/includes/class-theme-installer-skin.php create mode 100644 wp-admin/includes/class-theme-upgrader-skin.php create mode 100644 wp-admin/includes/class-theme-upgrader.php create mode 100644 wp-admin/includes/class-walker-category-checklist.php create mode 100644 wp-admin/includes/class-walker-nav-menu-checklist.php create mode 100644 wp-admin/includes/class-walker-nav-menu-edit.php create mode 100644 wp-admin/includes/class-wp-ajax-upgrader-skin.php create mode 100644 wp-admin/includes/class-wp-application-passwords-list-table.php create mode 100644 wp-admin/includes/class-wp-automatic-updater.php create mode 100644 wp-admin/includes/class-wp-comments-list-table.php create mode 100644 wp-admin/includes/class-wp-community-events.php create mode 100644 wp-admin/includes/class-wp-debug-data.php create mode 100644 wp-admin/includes/class-wp-filesystem-base.php create mode 100644 wp-admin/includes/class-wp-filesystem-direct.php create mode 100644 wp-admin/includes/class-wp-filesystem-ftpext.php create mode 100644 wp-admin/includes/class-wp-filesystem-ftpsockets.php create mode 100644 wp-admin/includes/class-wp-filesystem-ssh2.php create mode 100644 wp-admin/includes/class-wp-importer.php create mode 100644 wp-admin/includes/class-wp-internal-pointers.php create mode 100644 wp-admin/includes/class-wp-links-list-table.php create mode 100644 wp-admin/includes/class-wp-list-table-compat.php create mode 100644 wp-admin/includes/class-wp-list-table.php create mode 100644 wp-admin/includes/class-wp-media-list-table.php create mode 100644 wp-admin/includes/class-wp-ms-sites-list-table.php create mode 100644 wp-admin/includes/class-wp-ms-themes-list-table.php create mode 100644 wp-admin/includes/class-wp-ms-users-list-table.php create mode 100644 wp-admin/includes/class-wp-plugin-install-list-table.php create mode 100644 wp-admin/includes/class-wp-plugins-list-table.php create mode 100644 wp-admin/includes/class-wp-post-comments-list-table.php create mode 100644 wp-admin/includes/class-wp-posts-list-table.php create mode 100644 wp-admin/includes/class-wp-privacy-data-export-requests-list-table.php create mode 100644 wp-admin/includes/class-wp-privacy-data-removal-requests-list-table.php create mode 100644 wp-admin/includes/class-wp-privacy-policy-content.php create mode 100644 wp-admin/includes/class-wp-privacy-requests-table.php create mode 100644 wp-admin/includes/class-wp-screen.php create mode 100644 wp-admin/includes/class-wp-site-health-auto-updates.php create mode 100644 wp-admin/includes/class-wp-site-health.php create mode 100644 wp-admin/includes/class-wp-site-icon.php create mode 100644 wp-admin/includes/class-wp-terms-list-table.php create mode 100644 wp-admin/includes/class-wp-theme-install-list-table.php create mode 100644 wp-admin/includes/class-wp-themes-list-table.php create mode 100644 wp-admin/includes/class-wp-upgrader-skin.php create mode 100644 wp-admin/includes/class-wp-upgrader-skins.php create mode 100644 wp-admin/includes/class-wp-upgrader.php create mode 100644 wp-admin/includes/class-wp-users-list-table.php create mode 100644 wp-admin/includes/comment.php create mode 100644 wp-admin/includes/continents-cities.php create mode 100644 wp-admin/includes/credits.php create mode 100644 wp-admin/includes/dashboard.php create mode 100644 wp-admin/includes/deprecated.php create mode 100644 wp-admin/includes/edit-tag-messages.php create mode 100644 wp-admin/includes/export.php create mode 100644 wp-admin/includes/file.php create mode 100644 wp-admin/includes/image-edit.php create mode 100644 wp-admin/includes/image.php create mode 100644 wp-admin/includes/import.php create mode 100644 wp-admin/includes/list-table.php create mode 100644 wp-admin/includes/media.php create mode 100644 wp-admin/includes/menu.php create mode 100644 wp-admin/includes/meta-boxes.php create mode 100644 wp-admin/includes/misc.php create mode 100644 wp-admin/includes/ms-admin-filters.php create mode 100644 wp-admin/includes/ms-deprecated.php create mode 100644 wp-admin/includes/ms.php create mode 100644 wp-admin/includes/nav-menu.php create mode 100644 wp-admin/includes/network.php create mode 100644 wp-admin/includes/noop.php create mode 100644 wp-admin/includes/options.php create mode 100644 wp-admin/includes/plugin-install.php create mode 100644 wp-admin/includes/plugin.php create mode 100644 wp-admin/includes/post.php create mode 100644 wp-admin/includes/privacy-tools.php create mode 100644 wp-admin/includes/revision.php create mode 100644 wp-admin/includes/schema.php create mode 100644 wp-admin/includes/screen.php create mode 100644 wp-admin/includes/taxonomy.php create mode 100644 wp-admin/includes/template.php create mode 100644 wp-admin/includes/theme-install.php create mode 100644 wp-admin/includes/theme.php create mode 100644 wp-admin/includes/translation-install.php create mode 100644 wp-admin/includes/update-core.php create mode 100644 wp-admin/includes/update.php create mode 100644 wp-admin/includes/upgrade.php create mode 100644 wp-admin/includes/user.php create mode 100644 wp-admin/includes/widgets.php (limited to 'wp-admin/includes') diff --git a/wp-admin/includes/admin-filters.php b/wp-admin/includes/admin-filters.php new file mode 100644 index 0000000..b5adb94 --- /dev/null +++ b/wp-admin/includes/admin-filters.php @@ -0,0 +1,174 @@ +id and the JS global 'pagenow'. + if ( ! empty( $_POST['screen_id'] ) ) { + $screen_id = sanitize_key( $_POST['screen_id'] ); + } else { + $screen_id = 'front'; + } + + if ( ! empty( $_POST['data'] ) ) { + $data = wp_unslash( (array) $_POST['data'] ); + + /** + * Filters Heartbeat Ajax response in no-privilege environments. + * + * @since 3.6.0 + * + * @param array $response The no-priv Heartbeat response. + * @param array $data The $_POST data sent. + * @param string $screen_id The screen ID. + */ + $response = apply_filters( 'heartbeat_nopriv_received', $response, $data, $screen_id ); + } + + /** + * Filters Heartbeat Ajax response in no-privilege environments when no data is passed. + * + * @since 3.6.0 + * + * @param array $response The no-priv Heartbeat response. + * @param string $screen_id The screen ID. + */ + $response = apply_filters( 'heartbeat_nopriv_send', $response, $screen_id ); + + /** + * Fires when Heartbeat ticks in no-privilege environments. + * + * Allows the transport to be easily replaced with long-polling. + * + * @since 3.6.0 + * + * @param array $response The no-priv Heartbeat response. + * @param string $screen_id The screen ID. + */ + do_action( 'heartbeat_nopriv_tick', $response, $screen_id ); + + // Send the current time according to the server. + $response['server_time'] = time(); + + wp_send_json( $response ); +} + +// +// GET-based Ajax handlers. +// + +/** + * Handles fetching a list table via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_fetch_list() { + $list_class = $_GET['list_args']['class']; + check_ajax_referer( "fetch-list-$list_class", '_ajax_fetch_list_nonce' ); + + $wp_list_table = _get_list_table( $list_class, array( 'screen' => $_GET['list_args']['screen']['id'] ) ); + if ( ! $wp_list_table ) { + wp_die( 0 ); + } + + if ( ! $wp_list_table->ajax_user_can() ) { + wp_die( -1 ); + } + + $wp_list_table->ajax_response(); + + wp_die( 0 ); +} + +/** + * Handles tag search via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_ajax_tag_search() { + if ( ! isset( $_GET['tax'] ) ) { + wp_die( 0 ); + } + + $taxonomy = sanitize_key( $_GET['tax'] ); + $taxonomy_object = get_taxonomy( $taxonomy ); + + if ( ! $taxonomy_object ) { + wp_die( 0 ); + } + + if ( ! current_user_can( $taxonomy_object->cap->assign_terms ) ) { + wp_die( -1 ); + } + + $search = wp_unslash( $_GET['q'] ); + + $comma = _x( ',', 'tag delimiter' ); + if ( ',' !== $comma ) { + $search = str_replace( $comma, ',', $search ); + } + + if ( str_contains( $search, ',' ) ) { + $search = explode( ',', $search ); + $search = $search[ count( $search ) - 1 ]; + } + + $search = trim( $search ); + + /** + * Filters the minimum number of characters required to fire a tag search via Ajax. + * + * @since 4.0.0 + * + * @param int $characters The minimum number of characters required. Default 2. + * @param WP_Taxonomy $taxonomy_object The taxonomy object. + * @param string $search The search term. + */ + $term_search_min_chars = (int) apply_filters( 'term_search_min_chars', 2, $taxonomy_object, $search ); + + /* + * Require $term_search_min_chars chars for matching (default: 2) + * ensure it's a non-negative, non-zero integer. + */ + if ( ( 0 == $term_search_min_chars ) || ( strlen( $search ) < $term_search_min_chars ) ) { + wp_die(); + } + + $results = get_terms( + array( + 'taxonomy' => $taxonomy, + 'name__like' => $search, + 'fields' => 'names', + 'hide_empty' => false, + 'number' => isset( $_GET['number'] ) ? (int) $_GET['number'] : 0, + ) + ); + + /** + * Filters the Ajax term search results. + * + * @since 6.1.0 + * + * @param string[] $results Array of term names. + * @param WP_Taxonomy $taxonomy_object The taxonomy object. + * @param string $search The search term. + */ + $results = apply_filters( 'ajax_term_search_results', $results, $taxonomy_object, $search ); + + echo implode( "\n", $results ); + wp_die(); +} + +/** + * Handles compression testing via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_wp_compression_test() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( -1 ); + } + + if ( ini_get( 'zlib.output_compression' ) || 'ob_gzhandler' === ini_get( 'output_handler' ) ) { + // Use `update_option()` on single site to mark the option for autoloading. + if ( is_multisite() ) { + update_site_option( 'can_compress_scripts', 0 ); + } else { + update_option( 'can_compress_scripts', 0, 'yes' ); + } + wp_die( 0 ); + } + + if ( isset( $_GET['test'] ) ) { + header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT' ); + header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' ); + header( 'Cache-Control: no-cache, must-revalidate, max-age=0' ); + header( 'Content-Type: application/javascript; charset=UTF-8' ); + $force_gzip = ( defined( 'ENFORCE_GZIP' ) && ENFORCE_GZIP ); + $test_str = '"wpCompressionTest Lorem ipsum dolor sit amet consectetuer mollis sapien urna ut a. Eu nonummy condimentum fringilla tempor pretium platea vel nibh netus Maecenas. Hac molestie amet justo quis pellentesque est ultrices interdum nibh Morbi. Cras mattis pretium Phasellus ante ipsum ipsum ut sociis Suspendisse Lorem. Ante et non molestie. Porta urna Vestibulum egestas id congue nibh eu risus gravida sit. Ac augue auctor Ut et non a elit massa id sodales. Elit eu Nulla at nibh adipiscing mattis lacus mauris at tempus. Netus nibh quis suscipit nec feugiat eget sed lorem et urna. Pellentesque lacus at ut massa consectetuer ligula ut auctor semper Pellentesque. Ut metus massa nibh quam Curabitur molestie nec mauris congue. Volutpat molestie elit justo facilisis neque ac risus Ut nascetur tristique. Vitae sit lorem tellus et quis Phasellus lacus tincidunt nunc Fusce. Pharetra wisi Suspendisse mus sagittis libero lacinia Integer consequat ac Phasellus. Et urna ac cursus tortor aliquam Aliquam amet tellus volutpat Vestibulum. Justo interdum condimentum In augue congue tellus sollicitudin Quisque quis nibh."'; + + if ( 1 == $_GET['test'] ) { + echo $test_str; + wp_die(); + } elseif ( 2 == $_GET['test'] ) { + if ( ! isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) { + wp_die( -1 ); + } + + if ( false !== stripos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate' ) && function_exists( 'gzdeflate' ) && ! $force_gzip ) { + header( 'Content-Encoding: deflate' ); + $out = gzdeflate( $test_str, 1 ); + } elseif ( false !== stripos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip' ) && function_exists( 'gzencode' ) ) { + header( 'Content-Encoding: gzip' ); + $out = gzencode( $test_str, 1 ); + } else { + wp_die( -1 ); + } + + echo $out; + wp_die(); + } elseif ( 'no' === $_GET['test'] ) { + check_ajax_referer( 'update_can_compress_scripts' ); + // Use `update_option()` on single site to mark the option for autoloading. + if ( is_multisite() ) { + update_site_option( 'can_compress_scripts', 0 ); + } else { + update_option( 'can_compress_scripts', 0, 'yes' ); + } + } elseif ( 'yes' === $_GET['test'] ) { + check_ajax_referer( 'update_can_compress_scripts' ); + // Use `update_option()` on single site to mark the option for autoloading. + if ( is_multisite() ) { + update_site_option( 'can_compress_scripts', 1 ); + } else { + update_option( 'can_compress_scripts', 1, 'yes' ); + } + } + } + + wp_die( 0 ); +} + +/** + * Handles image editor previews via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_imgedit_preview() { + $post_id = (int) $_GET['postid']; + if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { + wp_die( -1 ); + } + + check_ajax_referer( "image_editor-$post_id" ); + + require_once ABSPATH . 'wp-admin/includes/image-edit.php'; + + if ( ! stream_preview_image( $post_id ) ) { + wp_die( -1 ); + } + + wp_die(); +} + +/** + * Handles oEmbed caching via AJAX. + * + * @since 3.1.0 + * + * @global WP_Embed $wp_embed + */ +function wp_ajax_oembed_cache() { + $GLOBALS['wp_embed']->cache_oembed( $_GET['post'] ); + wp_die( 0 ); +} + +/** + * Handles user autocomplete via AJAX. + * + * @since 3.4.0 + */ +function wp_ajax_autocomplete_user() { + if ( ! is_multisite() || ! current_user_can( 'promote_users' ) || wp_is_large_network( 'users' ) ) { + wp_die( -1 ); + } + + /** This filter is documented in wp-admin/user-new.php */ + if ( ! current_user_can( 'manage_network_users' ) && ! apply_filters( 'autocomplete_users_for_site_admins', false ) ) { + wp_die( -1 ); + } + + $return = array(); + + /* + * Check the type of request. + * Current allowed values are `add` and `search`. + */ + if ( isset( $_REQUEST['autocomplete_type'] ) && 'search' === $_REQUEST['autocomplete_type'] ) { + $type = $_REQUEST['autocomplete_type']; + } else { + $type = 'add'; + } + + /* + * Check the desired field for value. + * Current allowed values are `user_email` and `user_login`. + */ + if ( isset( $_REQUEST['autocomplete_field'] ) && 'user_email' === $_REQUEST['autocomplete_field'] ) { + $field = $_REQUEST['autocomplete_field']; + } else { + $field = 'user_login'; + } + + // Exclude current users of this blog. + if ( isset( $_REQUEST['site_id'] ) ) { + $id = absint( $_REQUEST['site_id'] ); + } else { + $id = get_current_blog_id(); + } + + $include_blog_users = ( 'search' === $type ? get_users( + array( + 'blog_id' => $id, + 'fields' => 'ID', + ) + ) : array() ); + + $exclude_blog_users = ( 'add' === $type ? get_users( + array( + 'blog_id' => $id, + 'fields' => 'ID', + ) + ) : array() ); + + $users = get_users( + array( + 'blog_id' => false, + 'search' => '*' . $_REQUEST['term'] . '*', + 'include' => $include_blog_users, + 'exclude' => $exclude_blog_users, + 'search_columns' => array( 'user_login', 'user_nicename', 'user_email' ), + ) + ); + + foreach ( $users as $user ) { + $return[] = array( + /* translators: 1: User login, 2: User email address. */ + 'label' => sprintf( _x( '%1$s (%2$s)', 'user autocomplete result' ), $user->user_login, $user->user_email ), + 'value' => $user->$field, + ); + } + + wp_die( wp_json_encode( $return ) ); +} + +/** + * Handles Ajax requests for community events + * + * @since 4.8.0 + */ +function wp_ajax_get_community_events() { + require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php'; + + check_ajax_referer( 'community_events' ); + + $search = isset( $_POST['location'] ) ? wp_unslash( $_POST['location'] ) : ''; + $timezone = isset( $_POST['timezone'] ) ? wp_unslash( $_POST['timezone'] ) : ''; + $user_id = get_current_user_id(); + $saved_location = get_user_option( 'community-events-location', $user_id ); + $events_client = new WP_Community_Events( $user_id, $saved_location ); + $events = $events_client->get_events( $search, $timezone ); + $ip_changed = false; + + if ( is_wp_error( $events ) ) { + wp_send_json_error( + array( + 'error' => $events->get_error_message(), + ) + ); + } else { + if ( empty( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) ) { + $ip_changed = true; + } elseif ( isset( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) && $saved_location['ip'] !== $events['location']['ip'] ) { + $ip_changed = true; + } + + /* + * The location should only be updated when it changes. The API doesn't always return + * a full location; sometimes it's missing the description or country. The location + * that was saved during the initial request is known to be good and complete, though. + * It should be left intact until the user explicitly changes it (either by manually + * searching for a new location, or by changing their IP address). + * + * If the location was updated with an incomplete response from the API, then it could + * break assumptions that the UI makes (e.g., that there will always be a description + * that corresponds to a latitude/longitude location). + * + * The location is stored network-wide, so that the user doesn't have to set it on each site. + */ + if ( $ip_changed || $search ) { + update_user_meta( $user_id, 'community-events-location', $events['location'] ); + } + + wp_send_json_success( $events ); + } +} + +/** + * Handles dashboard widgets via AJAX. + * + * @since 3.4.0 + */ +function wp_ajax_dashboard_widgets() { + require_once ABSPATH . 'wp-admin/includes/dashboard.php'; + + $pagenow = $_GET['pagenow']; + if ( 'dashboard-user' === $pagenow || 'dashboard-network' === $pagenow || 'dashboard' === $pagenow ) { + set_current_screen( $pagenow ); + } + + switch ( $_GET['widget'] ) { + case 'dashboard_primary': + wp_dashboard_primary(); + break; + } + wp_die(); +} + +/** + * Handles Customizer preview logged-in status via AJAX. + * + * @since 3.4.0 + */ +function wp_ajax_logged_in() { + wp_die( 1 ); +} + +// +// Ajax helpers. +// + +/** + * Sends back current comment total and new page links if they need to be updated. + * + * Contrary to normal success Ajax response ("1"), die with time() on success. + * + * @since 2.7.0 + * @access private + * + * @param int $comment_id + * @param int $delta + */ +function _wp_ajax_delete_comment_response( $comment_id, $delta = -1 ) { + $total = isset( $_POST['_total'] ) ? (int) $_POST['_total'] : 0; + $per_page = isset( $_POST['_per_page'] ) ? (int) $_POST['_per_page'] : 0; + $page = isset( $_POST['_page'] ) ? (int) $_POST['_page'] : 0; + $url = isset( $_POST['_url'] ) ? sanitize_url( $_POST['_url'] ) : ''; + + // JS didn't send us everything we need to know. Just die with success message. + if ( ! $total || ! $per_page || ! $page || ! $url ) { + $time = time(); + $comment = get_comment( $comment_id ); + $comment_status = ''; + $comment_link = ''; + + if ( $comment ) { + $comment_status = $comment->comment_approved; + } + + if ( 1 === (int) $comment_status ) { + $comment_link = get_comment_link( $comment ); + } + + $counts = wp_count_comments(); + + $x = new WP_Ajax_Response( + array( + 'what' => 'comment', + // Here for completeness - not used. + 'id' => $comment_id, + 'supplemental' => array( + 'status' => $comment_status, + 'postId' => $comment ? $comment->comment_post_ID : '', + 'time' => $time, + 'in_moderation' => $counts->moderated, + 'i18n_comments_text' => sprintf( + /* translators: %s: Number of comments. */ + _n( '%s Comment', '%s Comments', $counts->approved ), + number_format_i18n( $counts->approved ) + ), + 'i18n_moderation_text' => sprintf( + /* translators: %s: Number of comments. */ + _n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ), + number_format_i18n( $counts->moderated ) + ), + 'comment_link' => $comment_link, + ), + ) + ); + $x->send(); + } + + $total += $delta; + if ( $total < 0 ) { + $total = 0; + } + + // Only do the expensive stuff on a page-break, and about 1 other time per page. + if ( 0 == $total % $per_page || 1 == mt_rand( 1, $per_page ) ) { + $post_id = 0; + // What type of comment count are we looking for? + $status = 'all'; + $parsed = parse_url( $url ); + + if ( isset( $parsed['query'] ) ) { + parse_str( $parsed['query'], $query_vars ); + + if ( ! empty( $query_vars['comment_status'] ) ) { + $status = $query_vars['comment_status']; + } + + if ( ! empty( $query_vars['p'] ) ) { + $post_id = (int) $query_vars['p']; + } + + if ( ! empty( $query_vars['comment_type'] ) ) { + $type = $query_vars['comment_type']; + } + } + + if ( empty( $type ) ) { + // Only use the comment count if not filtering by a comment_type. + $comment_count = wp_count_comments( $post_id ); + + // We're looking for a known type of comment count. + if ( isset( $comment_count->$status ) ) { + $total = $comment_count->$status; + } + } + // Else use the decremented value from above. + } + + // The time since the last comment count. + $time = time(); + $comment = get_comment( $comment_id ); + $counts = wp_count_comments(); + + $x = new WP_Ajax_Response( + array( + 'what' => 'comment', + 'id' => $comment_id, + 'supplemental' => array( + 'status' => $comment ? $comment->comment_approved : '', + 'postId' => $comment ? $comment->comment_post_ID : '', + /* translators: %s: Number of comments. */ + 'total_items_i18n' => sprintf( _n( '%s item', '%s items', $total ), number_format_i18n( $total ) ), + 'total_pages' => ceil( $total / $per_page ), + 'total_pages_i18n' => number_format_i18n( ceil( $total / $per_page ) ), + 'total' => $total, + 'time' => $time, + 'in_moderation' => $counts->moderated, + 'i18n_moderation_text' => sprintf( + /* translators: %s: Number of comments. */ + _n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ), + number_format_i18n( $counts->moderated ) + ), + ), + ) + ); + $x->send(); +} + +// +// POST-based Ajax handlers. +// + +/** + * Handles adding a hierarchical term via AJAX. + * + * @since 3.1.0 + * @access private + */ +function _wp_ajax_add_hierarchical_term() { + $action = $_POST['action']; + $taxonomy = get_taxonomy( substr( $action, 4 ) ); + check_ajax_referer( $action, '_ajax_nonce-add-' . $taxonomy->name ); + + if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) { + wp_die( -1 ); + } + + $names = explode( ',', $_POST[ 'new' . $taxonomy->name ] ); + $parent = isset( $_POST[ 'new' . $taxonomy->name . '_parent' ] ) ? (int) $_POST[ 'new' . $taxonomy->name . '_parent' ] : 0; + + if ( 0 > $parent ) { + $parent = 0; + } + + if ( 'category' === $taxonomy->name ) { + $post_category = isset( $_POST['post_category'] ) ? (array) $_POST['post_category'] : array(); + } else { + $post_category = ( isset( $_POST['tax_input'] ) && isset( $_POST['tax_input'][ $taxonomy->name ] ) ) ? (array) $_POST['tax_input'][ $taxonomy->name ] : array(); + } + + $checked_categories = array_map( 'absint', (array) $post_category ); + $popular_ids = wp_popular_terms_checklist( $taxonomy->name, 0, 10, false ); + + foreach ( $names as $cat_name ) { + $cat_name = trim( $cat_name ); + $category_nicename = sanitize_title( $cat_name ); + + if ( '' === $category_nicename ) { + continue; + } + + $cat_id = wp_insert_term( $cat_name, $taxonomy->name, array( 'parent' => $parent ) ); + + if ( ! $cat_id || is_wp_error( $cat_id ) ) { + continue; + } else { + $cat_id = $cat_id['term_id']; + } + + $checked_categories[] = $cat_id; + + if ( $parent ) { // Do these all at once in a second. + continue; + } + + ob_start(); + + wp_terms_checklist( + 0, + array( + 'taxonomy' => $taxonomy->name, + 'descendants_and_self' => $cat_id, + 'selected_cats' => $checked_categories, + 'popular_cats' => $popular_ids, + ) + ); + + $data = ob_get_clean(); + + $add = array( + 'what' => $taxonomy->name, + 'id' => $cat_id, + 'data' => str_replace( array( "\n", "\t" ), '', $data ), + 'position' => -1, + ); + } + + if ( $parent ) { // Foncy - replace the parent and all its children. + $parent = get_term( $parent, $taxonomy->name ); + $term_id = $parent->term_id; + + while ( $parent->parent ) { // Get the top parent. + $parent = get_term( $parent->parent, $taxonomy->name ); + if ( is_wp_error( $parent ) ) { + break; + } + $term_id = $parent->term_id; + } + + ob_start(); + + wp_terms_checklist( + 0, + array( + 'taxonomy' => $taxonomy->name, + 'descendants_and_self' => $term_id, + 'selected_cats' => $checked_categories, + 'popular_cats' => $popular_ids, + ) + ); + + $data = ob_get_clean(); + + $add = array( + 'what' => $taxonomy->name, + 'id' => $term_id, + 'data' => str_replace( array( "\n", "\t" ), '', $data ), + 'position' => -1, + ); + } + + ob_start(); + + wp_dropdown_categories( + array( + 'taxonomy' => $taxonomy->name, + 'hide_empty' => 0, + 'name' => 'new' . $taxonomy->name . '_parent', + 'orderby' => 'name', + 'hierarchical' => 1, + 'show_option_none' => '— ' . $taxonomy->labels->parent_item . ' —', + ) + ); + + $sup = ob_get_clean(); + + $add['supplemental'] = array( 'newcat_parent' => $sup ); + + $x = new WP_Ajax_Response( $add ); + $x->send(); +} + +/** + * Handles deleting a comment via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_delete_comment() { + $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; + + $comment = get_comment( $id ); + + if ( ! $comment ) { + wp_die( time() ); + } + + if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { + wp_die( -1 ); + } + + check_ajax_referer( "delete-comment_$id" ); + $status = wp_get_comment_status( $comment ); + $delta = -1; + + if ( isset( $_POST['trash'] ) && 1 == $_POST['trash'] ) { + if ( 'trash' === $status ) { + wp_die( time() ); + } + + $r = wp_trash_comment( $comment ); + } elseif ( isset( $_POST['untrash'] ) && 1 == $_POST['untrash'] ) { + if ( 'trash' !== $status ) { + wp_die( time() ); + } + + $r = wp_untrash_comment( $comment ); + + // Undo trash, not in Trash. + if ( ! isset( $_POST['comment_status'] ) || 'trash' !== $_POST['comment_status'] ) { + $delta = 1; + } + } elseif ( isset( $_POST['spam'] ) && 1 == $_POST['spam'] ) { + if ( 'spam' === $status ) { + wp_die( time() ); + } + + $r = wp_spam_comment( $comment ); + } elseif ( isset( $_POST['unspam'] ) && 1 == $_POST['unspam'] ) { + if ( 'spam' !== $status ) { + wp_die( time() ); + } + + $r = wp_unspam_comment( $comment ); + + // Undo spam, not in spam. + if ( ! isset( $_POST['comment_status'] ) || 'spam' !== $_POST['comment_status'] ) { + $delta = 1; + } + } elseif ( isset( $_POST['delete'] ) && 1 == $_POST['delete'] ) { + $r = wp_delete_comment( $comment ); + } else { + wp_die( -1 ); + } + + if ( $r ) { + // Decide if we need to send back '1' or a more complicated response including page links and comment counts. + _wp_ajax_delete_comment_response( $comment->comment_ID, $delta ); + } + + wp_die( 0 ); +} + +/** + * Handles deleting a tag via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_delete_tag() { + $tag_id = (int) $_POST['tag_ID']; + check_ajax_referer( "delete-tag_$tag_id" ); + + if ( ! current_user_can( 'delete_term', $tag_id ) ) { + wp_die( -1 ); + } + + $taxonomy = ! empty( $_POST['taxonomy'] ) ? $_POST['taxonomy'] : 'post_tag'; + $tag = get_term( $tag_id, $taxonomy ); + + if ( ! $tag || is_wp_error( $tag ) ) { + wp_die( 1 ); + } + + if ( wp_delete_term( $tag_id, $taxonomy ) ) { + wp_die( 1 ); + } else { + wp_die( 0 ); + } +} + +/** + * Handles deleting a link via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_delete_link() { + $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; + + check_ajax_referer( "delete-bookmark_$id" ); + + if ( ! current_user_can( 'manage_links' ) ) { + wp_die( -1 ); + } + + $link = get_bookmark( $id ); + if ( ! $link || is_wp_error( $link ) ) { + wp_die( 1 ); + } + + if ( wp_delete_link( $id ) ) { + wp_die( 1 ); + } else { + wp_die( 0 ); + } +} + +/** + * Handles deleting meta via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_delete_meta() { + $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; + + check_ajax_referer( "delete-meta_$id" ); + $meta = get_metadata_by_mid( 'post', $id ); + + if ( ! $meta ) { + wp_die( 1 ); + } + + if ( is_protected_meta( $meta->meta_key, 'post' ) || ! current_user_can( 'delete_post_meta', $meta->post_id, $meta->meta_key ) ) { + wp_die( -1 ); + } + + if ( delete_meta( $meta->meta_id ) ) { + wp_die( 1 ); + } + + wp_die( 0 ); +} + +/** + * Handles deleting a post via AJAX. + * + * @since 3.1.0 + * + * @param string $action Action to perform. + */ +function wp_ajax_delete_post( $action ) { + if ( empty( $action ) ) { + $action = 'delete-post'; + } + + $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; + check_ajax_referer( "{$action}_$id" ); + + if ( ! current_user_can( 'delete_post', $id ) ) { + wp_die( -1 ); + } + + if ( ! get_post( $id ) ) { + wp_die( 1 ); + } + + if ( wp_delete_post( $id ) ) { + wp_die( 1 ); + } else { + wp_die( 0 ); + } +} + +/** + * Handles sending a post to the Trash via AJAX. + * + * @since 3.1.0 + * + * @param string $action Action to perform. + */ +function wp_ajax_trash_post( $action ) { + if ( empty( $action ) ) { + $action = 'trash-post'; + } + + $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; + check_ajax_referer( "{$action}_$id" ); + + if ( ! current_user_can( 'delete_post', $id ) ) { + wp_die( -1 ); + } + + if ( ! get_post( $id ) ) { + wp_die( 1 ); + } + + if ( 'trash-post' === $action ) { + $done = wp_trash_post( $id ); + } else { + $done = wp_untrash_post( $id ); + } + + if ( $done ) { + wp_die( 1 ); + } + + wp_die( 0 ); +} + +/** + * Handles restoring a post from the Trash via AJAX. + * + * @since 3.1.0 + * + * @param string $action Action to perform. + */ +function wp_ajax_untrash_post( $action ) { + if ( empty( $action ) ) { + $action = 'untrash-post'; + } + + wp_ajax_trash_post( $action ); +} + +/** + * Handles deleting a page via AJAX. + * + * @since 3.1.0 + * + * @param string $action Action to perform. + */ +function wp_ajax_delete_page( $action ) { + if ( empty( $action ) ) { + $action = 'delete-page'; + } + + $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; + check_ajax_referer( "{$action}_$id" ); + + if ( ! current_user_can( 'delete_page', $id ) ) { + wp_die( -1 ); + } + + if ( ! get_post( $id ) ) { + wp_die( 1 ); + } + + if ( wp_delete_post( $id ) ) { + wp_die( 1 ); + } else { + wp_die( 0 ); + } +} + +/** + * Handles dimming a comment via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_dim_comment() { + $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; + $comment = get_comment( $id ); + + if ( ! $comment ) { + $x = new WP_Ajax_Response( + array( + 'what' => 'comment', + 'id' => new WP_Error( + 'invalid_comment', + /* translators: %d: Comment ID. */ + sprintf( __( 'Comment %d does not exist' ), $id ) + ), + ) + ); + $x->send(); + } + + if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) && ! current_user_can( 'moderate_comments' ) ) { + wp_die( -1 ); + } + + $current = wp_get_comment_status( $comment ); + + if ( isset( $_POST['new'] ) && $_POST['new'] == $current ) { + wp_die( time() ); + } + + check_ajax_referer( "approve-comment_$id" ); + + if ( in_array( $current, array( 'unapproved', 'spam' ), true ) ) { + $result = wp_set_comment_status( $comment, 'approve', true ); + } else { + $result = wp_set_comment_status( $comment, 'hold', true ); + } + + if ( is_wp_error( $result ) ) { + $x = new WP_Ajax_Response( + array( + 'what' => 'comment', + 'id' => $result, + ) + ); + $x->send(); + } + + // Decide if we need to send back '1' or a more complicated response including page links and comment counts. + _wp_ajax_delete_comment_response( $comment->comment_ID ); + wp_die( 0 ); +} + +/** + * Handles adding a link category via AJAX. + * + * @since 3.1.0 + * + * @param string $action Action to perform. + */ +function wp_ajax_add_link_category( $action ) { + if ( empty( $action ) ) { + $action = 'add-link-category'; + } + + check_ajax_referer( $action ); + + $taxonomy_object = get_taxonomy( 'link_category' ); + + if ( ! current_user_can( $taxonomy_object->cap->manage_terms ) ) { + wp_die( -1 ); + } + + $names = explode( ',', wp_unslash( $_POST['newcat'] ) ); + $x = new WP_Ajax_Response(); + + foreach ( $names as $cat_name ) { + $cat_name = trim( $cat_name ); + $slug = sanitize_title( $cat_name ); + + if ( '' === $slug ) { + continue; + } + + $cat_id = wp_insert_term( $cat_name, 'link_category' ); + + if ( ! $cat_id || is_wp_error( $cat_id ) ) { + continue; + } else { + $cat_id = $cat_id['term_id']; + } + + $cat_name = esc_html( $cat_name ); + + $x->add( + array( + 'what' => 'link-category', + 'id' => $cat_id, + 'data' => "", + 'position' => -1, + ) + ); + } + $x->send(); +} + +/** + * Handles adding a tag via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_add_tag() { + check_ajax_referer( 'add-tag', '_wpnonce_add-tag' ); + + $taxonomy = ! empty( $_POST['taxonomy'] ) ? $_POST['taxonomy'] : 'post_tag'; + $taxonomy_object = get_taxonomy( $taxonomy ); + + if ( ! current_user_can( $taxonomy_object->cap->edit_terms ) ) { + wp_die( -1 ); + } + + $x = new WP_Ajax_Response(); + + $tag = wp_insert_term( $_POST['tag-name'], $taxonomy, $_POST ); + + if ( $tag && ! is_wp_error( $tag ) ) { + $tag = get_term( $tag['term_id'], $taxonomy ); + } + + if ( ! $tag || is_wp_error( $tag ) ) { + $message = __( 'An error has occurred. Please reload the page and try again.' ); + $error_code = 'error'; + + if ( is_wp_error( $tag ) && $tag->get_error_message() ) { + $message = $tag->get_error_message(); + } + + if ( is_wp_error( $tag ) && $tag->get_error_code() ) { + $error_code = $tag->get_error_code(); + } + + $x->add( + array( + 'what' => 'taxonomy', + 'data' => new WP_Error( $error_code, $message ), + ) + ); + $x->send(); + } + + $wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => $_POST['screen'] ) ); + + $level = 0; + $noparents = ''; + + if ( is_taxonomy_hierarchical( $taxonomy ) ) { + $level = count( get_ancestors( $tag->term_id, $taxonomy, 'taxonomy' ) ); + ob_start(); + $wp_list_table->single_row( $tag, $level ); + $noparents = ob_get_clean(); + } + + ob_start(); + $wp_list_table->single_row( $tag ); + $parents = ob_get_clean(); + + require ABSPATH . 'wp-admin/includes/edit-tag-messages.php'; + + $message = ''; + if ( isset( $messages[ $taxonomy_object->name ][1] ) ) { + $message = $messages[ $taxonomy_object->name ][1]; + } elseif ( isset( $messages['_item'][1] ) ) { + $message = $messages['_item'][1]; + } + + $x->add( + array( + 'what' => 'taxonomy', + 'data' => $message, + 'supplemental' => array( + 'parents' => $parents, + 'noparents' => $noparents, + 'notice' => $message, + ), + ) + ); + + $x->add( + array( + 'what' => 'term', + 'position' => $level, + 'supplemental' => (array) $tag, + ) + ); + + $x->send(); +} + +/** + * Handles getting a tagcloud via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_get_tagcloud() { + if ( ! isset( $_POST['tax'] ) ) { + wp_die( 0 ); + } + + $taxonomy = sanitize_key( $_POST['tax'] ); + $taxonomy_object = get_taxonomy( $taxonomy ); + + if ( ! $taxonomy_object ) { + wp_die( 0 ); + } + + if ( ! current_user_can( $taxonomy_object->cap->assign_terms ) ) { + wp_die( -1 ); + } + + $tags = get_terms( + array( + 'taxonomy' => $taxonomy, + 'number' => 45, + 'orderby' => 'count', + 'order' => 'DESC', + ) + ); + + if ( empty( $tags ) ) { + wp_die( $taxonomy_object->labels->not_found ); + } + + if ( is_wp_error( $tags ) ) { + wp_die( $tags->get_error_message() ); + } + + foreach ( $tags as $key => $tag ) { + $tags[ $key ]->link = '#'; + $tags[ $key ]->id = $tag->term_id; + } + + // We need raw tag names here, so don't filter the output. + $return = wp_generate_tag_cloud( + $tags, + array( + 'filter' => 0, + 'format' => 'list', + ) + ); + + if ( empty( $return ) ) { + wp_die( 0 ); + } + + echo $return; + wp_die(); +} + +/** + * Handles getting comments via AJAX. + * + * @since 3.1.0 + * + * @global int $post_id + * + * @param string $action Action to perform. + */ +function wp_ajax_get_comments( $action ) { + global $post_id; + + if ( empty( $action ) ) { + $action = 'get-comments'; + } + + check_ajax_referer( $action ); + + if ( empty( $post_id ) && ! empty( $_REQUEST['p'] ) ) { + $id = absint( $_REQUEST['p'] ); + if ( ! empty( $id ) ) { + $post_id = $id; + } + } + + if ( empty( $post_id ) ) { + wp_die( -1 ); + } + + $wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) ); + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + wp_die( -1 ); + } + + $wp_list_table->prepare_items(); + + if ( ! $wp_list_table->has_items() ) { + wp_die( 1 ); + } + + $x = new WP_Ajax_Response(); + + ob_start(); + foreach ( $wp_list_table->items as $comment ) { + if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) && 0 === $comment->comment_approved ) { + continue; + } + get_comment( $comment ); + $wp_list_table->single_row( $comment ); + } + $comment_list_item = ob_get_clean(); + + $x->add( + array( + 'what' => 'comments', + 'data' => $comment_list_item, + ) + ); + + $x->send(); +} + +/** + * Handles replying to a comment via AJAX. + * + * @since 3.1.0 + * + * @param string $action Action to perform. + */ +function wp_ajax_replyto_comment( $action ) { + if ( empty( $action ) ) { + $action = 'replyto-comment'; + } + + check_ajax_referer( $action, '_ajax_nonce-replyto-comment' ); + + $comment_post_id = (int) $_POST['comment_post_ID']; + $post = get_post( $comment_post_id ); + + if ( ! $post ) { + wp_die( -1 ); + } + + if ( ! current_user_can( 'edit_post', $comment_post_id ) ) { + wp_die( -1 ); + } + + if ( empty( $post->post_status ) ) { + wp_die( 1 ); + } elseif ( in_array( $post->post_status, array( 'draft', 'pending', 'trash' ), true ) ) { + wp_die( __( 'You cannot reply to a comment on a draft post.' ) ); + } + + $user = wp_get_current_user(); + + if ( $user->exists() ) { + $comment_author = wp_slash( $user->display_name ); + $comment_author_email = wp_slash( $user->user_email ); + $comment_author_url = wp_slash( $user->user_url ); + $user_id = $user->ID; + + if ( current_user_can( 'unfiltered_html' ) ) { + if ( ! isset( $_POST['_wp_unfiltered_html_comment'] ) ) { + $_POST['_wp_unfiltered_html_comment'] = ''; + } + + if ( wp_create_nonce( 'unfiltered-html-comment' ) != $_POST['_wp_unfiltered_html_comment'] ) { + kses_remove_filters(); // Start with a clean slate. + kses_init_filters(); // Set up the filters. + remove_filter( 'pre_comment_content', 'wp_filter_post_kses' ); + add_filter( 'pre_comment_content', 'wp_filter_kses' ); + } + } + } else { + wp_die( __( 'Sorry, you must be logged in to reply to a comment.' ) ); + } + + $comment_content = trim( $_POST['content'] ); + + if ( '' === $comment_content ) { + wp_die( __( 'Please type your comment text.' ) ); + } + + $comment_type = isset( $_POST['comment_type'] ) ? trim( $_POST['comment_type'] ) : 'comment'; + + $comment_parent = 0; + + if ( isset( $_POST['comment_ID'] ) ) { + $comment_parent = absint( $_POST['comment_ID'] ); + } + + $comment_auto_approved = false; + + $commentdata = array( + 'comment_post_ID' => $comment_post_id, + ); + + $commentdata += compact( + 'comment_author', + 'comment_author_email', + 'comment_author_url', + 'comment_content', + 'comment_type', + 'comment_parent', + 'user_id' + ); + + // Automatically approve parent comment. + if ( ! empty( $_POST['approve_parent'] ) ) { + $parent = get_comment( $comment_parent ); + + if ( $parent && '0' === $parent->comment_approved && $parent->comment_post_ID == $comment_post_id ) { + if ( ! current_user_can( 'edit_comment', $parent->comment_ID ) ) { + wp_die( -1 ); + } + + if ( wp_set_comment_status( $parent, 'approve' ) ) { + $comment_auto_approved = true; + } + } + } + + $comment_id = wp_new_comment( $commentdata ); + + if ( is_wp_error( $comment_id ) ) { + wp_die( $comment_id->get_error_message() ); + } + + $comment = get_comment( $comment_id ); + + if ( ! $comment ) { + wp_die( 1 ); + } + + $position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1'; + + ob_start(); + if ( isset( $_REQUEST['mode'] ) && 'dashboard' === $_REQUEST['mode'] ) { + require_once ABSPATH . 'wp-admin/includes/dashboard.php'; + _wp_dashboard_recent_comments_row( $comment ); + } else { + if ( isset( $_REQUEST['mode'] ) && 'single' === $_REQUEST['mode'] ) { + $wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) ); + } else { + $wp_list_table = _get_list_table( 'WP_Comments_List_Table', array( 'screen' => 'edit-comments' ) ); + } + $wp_list_table->single_row( $comment ); + } + $comment_list_item = ob_get_clean(); + + $response = array( + 'what' => 'comment', + 'id' => $comment->comment_ID, + 'data' => $comment_list_item, + 'position' => $position, + ); + + $counts = wp_count_comments(); + $response['supplemental'] = array( + 'in_moderation' => $counts->moderated, + 'i18n_comments_text' => sprintf( + /* translators: %s: Number of comments. */ + _n( '%s Comment', '%s Comments', $counts->approved ), + number_format_i18n( $counts->approved ) + ), + 'i18n_moderation_text' => sprintf( + /* translators: %s: Number of comments. */ + _n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ), + number_format_i18n( $counts->moderated ) + ), + ); + + if ( $comment_auto_approved ) { + $response['supplemental']['parent_approved'] = $parent->comment_ID; + $response['supplemental']['parent_post_id'] = $parent->comment_post_ID; + } + + $x = new WP_Ajax_Response(); + $x->add( $response ); + $x->send(); +} + +/** + * Handles editing a comment via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_edit_comment() { + check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' ); + + $comment_id = (int) $_POST['comment_ID']; + + if ( ! current_user_can( 'edit_comment', $comment_id ) ) { + wp_die( -1 ); + } + + if ( '' === $_POST['content'] ) { + wp_die( __( 'Please type your comment text.' ) ); + } + + if ( isset( $_POST['status'] ) ) { + $_POST['comment_status'] = $_POST['status']; + } + + $updated = edit_comment(); + if ( is_wp_error( $updated ) ) { + wp_die( $updated->get_error_message() ); + } + + $position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1'; + $checkbox = ( isset( $_POST['checkbox'] ) && true == $_POST['checkbox'] ) ? 1 : 0; + $wp_list_table = _get_list_table( $checkbox ? 'WP_Comments_List_Table' : 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) ); + + $comment = get_comment( $comment_id ); + + if ( empty( $comment->comment_ID ) ) { + wp_die( -1 ); + } + + ob_start(); + $wp_list_table->single_row( $comment ); + $comment_list_item = ob_get_clean(); + + $x = new WP_Ajax_Response(); + + $x->add( + array( + 'what' => 'edit_comment', + 'id' => $comment->comment_ID, + 'data' => $comment_list_item, + 'position' => $position, + ) + ); + + $x->send(); +} + +/** + * Handles adding a menu item via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_add_menu_item() { + check_ajax_referer( 'add-menu_item', 'menu-settings-column-nonce' ); + + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( -1 ); + } + + require_once ABSPATH . 'wp-admin/includes/nav-menu.php'; + + /* + * For performance reasons, we omit some object properties from the checklist. + * The following is a hacky way to restore them when adding non-custom items. + */ + $menu_items_data = array(); + + foreach ( (array) $_POST['menu-item'] as $menu_item_data ) { + if ( + ! empty( $menu_item_data['menu-item-type'] ) && + 'custom' !== $menu_item_data['menu-item-type'] && + ! empty( $menu_item_data['menu-item-object-id'] ) + ) { + switch ( $menu_item_data['menu-item-type'] ) { + case 'post_type': + $_object = get_post( $menu_item_data['menu-item-object-id'] ); + break; + + case 'post_type_archive': + $_object = get_post_type_object( $menu_item_data['menu-item-object'] ); + break; + + case 'taxonomy': + $_object = get_term( $menu_item_data['menu-item-object-id'], $menu_item_data['menu-item-object'] ); + break; + } + + $_menu_items = array_map( 'wp_setup_nav_menu_item', array( $_object ) ); + $_menu_item = reset( $_menu_items ); + + // Restore the missing menu item properties. + $menu_item_data['menu-item-description'] = $_menu_item->description; + } + + $menu_items_data[] = $menu_item_data; + } + + $item_ids = wp_save_nav_menu_items( 0, $menu_items_data ); + if ( is_wp_error( $item_ids ) ) { + wp_die( 0 ); + } + + $menu_items = array(); + + foreach ( (array) $item_ids as $menu_item_id ) { + $menu_obj = get_post( $menu_item_id ); + + if ( ! empty( $menu_obj->ID ) ) { + $menu_obj = wp_setup_nav_menu_item( $menu_obj ); + $menu_obj->title = empty( $menu_obj->title ) ? __( 'Menu Item' ) : $menu_obj->title; + $menu_obj->label = $menu_obj->title; // Don't show "(pending)" in ajax-added items. + $menu_items[] = $menu_obj; + } + } + + /** This filter is documented in wp-admin/includes/nav-menu.php */ + $walker_class_name = apply_filters( 'wp_edit_nav_menu_walker', 'Walker_Nav_Menu_Edit', $_POST['menu'] ); + + if ( ! class_exists( $walker_class_name ) ) { + wp_die( 0 ); + } + + if ( ! empty( $menu_items ) ) { + $args = array( + 'after' => '', + 'before' => '', + 'link_after' => '', + 'link_before' => '', + 'walker' => new $walker_class_name(), + ); + + echo walk_nav_menu_tree( $menu_items, 0, (object) $args ); + } + + wp_die(); +} + +/** + * Handles adding meta via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_add_meta() { + check_ajax_referer( 'add-meta', '_ajax_nonce-add-meta' ); + $c = 0; + $pid = (int) $_POST['post_id']; + $post = get_post( $pid ); + + if ( isset( $_POST['metakeyselect'] ) || isset( $_POST['metakeyinput'] ) ) { + if ( ! current_user_can( 'edit_post', $pid ) ) { + wp_die( -1 ); + } + + if ( isset( $_POST['metakeyselect'] ) && '#NONE#' === $_POST['metakeyselect'] && empty( $_POST['metakeyinput'] ) ) { + wp_die( 1 ); + } + + // If the post is an autodraft, save the post as a draft and then attempt to save the meta. + if ( 'auto-draft' === $post->post_status ) { + $post_data = array(); + $post_data['action'] = 'draft'; // Warning fix. + $post_data['post_ID'] = $pid; + $post_data['post_type'] = $post->post_type; + $post_data['post_status'] = 'draft'; + $now = time(); + /* translators: 1: Post creation date, 2: Post creation time. */ + $post_data['post_title'] = sprintf( __( 'Draft created on %1$s at %2$s' ), gmdate( __( 'F j, Y' ), $now ), gmdate( __( 'g:i a' ), $now ) ); + + $pid = edit_post( $post_data ); + + if ( $pid ) { + if ( is_wp_error( $pid ) ) { + $x = new WP_Ajax_Response( + array( + 'what' => 'meta', + 'data' => $pid, + ) + ); + $x->send(); + } + + $mid = add_meta( $pid ); + if ( ! $mid ) { + wp_die( __( 'Please provide a custom field value.' ) ); + } + } else { + wp_die( 0 ); + } + } else { + $mid = add_meta( $pid ); + if ( ! $mid ) { + wp_die( __( 'Please provide a custom field value.' ) ); + } + } + + $meta = get_metadata_by_mid( 'post', $mid ); + $pid = (int) $meta->post_id; + $meta = get_object_vars( $meta ); + + $x = new WP_Ajax_Response( + array( + 'what' => 'meta', + 'id' => $mid, + 'data' => _list_meta_row( $meta, $c ), + 'position' => 1, + 'supplemental' => array( 'postid' => $pid ), + ) + ); + } else { // Update? + $mid = (int) key( $_POST['meta'] ); + $key = wp_unslash( $_POST['meta'][ $mid ]['key'] ); + $value = wp_unslash( $_POST['meta'][ $mid ]['value'] ); + + if ( '' === trim( $key ) ) { + wp_die( __( 'Please provide a custom field name.' ) ); + } + + $meta = get_metadata_by_mid( 'post', $mid ); + + if ( ! $meta ) { + wp_die( 0 ); // If meta doesn't exist. + } + + if ( + is_protected_meta( $meta->meta_key, 'post' ) || is_protected_meta( $key, 'post' ) || + ! current_user_can( 'edit_post_meta', $meta->post_id, $meta->meta_key ) || + ! current_user_can( 'edit_post_meta', $meta->post_id, $key ) + ) { + wp_die( -1 ); + } + + if ( $meta->meta_value != $value || $meta->meta_key != $key ) { + $u = update_metadata_by_mid( 'post', $mid, $value, $key ); + if ( ! $u ) { + wp_die( 0 ); // We know meta exists; we also know it's unchanged (or DB error, in which case there are bigger problems). + } + } + + $x = new WP_Ajax_Response( + array( + 'what' => 'meta', + 'id' => $mid, + 'old_id' => $mid, + 'data' => _list_meta_row( + array( + 'meta_key' => $key, + 'meta_value' => $value, + 'meta_id' => $mid, + ), + $c + ), + 'position' => 0, + 'supplemental' => array( 'postid' => $meta->post_id ), + ) + ); + } + $x->send(); +} + +/** + * Handles adding a user via AJAX. + * + * @since 3.1.0 + * + * @param string $action Action to perform. + */ +function wp_ajax_add_user( $action ) { + if ( empty( $action ) ) { + $action = 'add-user'; + } + + check_ajax_referer( $action ); + + if ( ! current_user_can( 'create_users' ) ) { + wp_die( -1 ); + } + + $user_id = edit_user(); + + if ( ! $user_id ) { + wp_die( 0 ); + } elseif ( is_wp_error( $user_id ) ) { + $x = new WP_Ajax_Response( + array( + 'what' => 'user', + 'id' => $user_id, + ) + ); + $x->send(); + } + + $user_object = get_userdata( $user_id ); + $wp_list_table = _get_list_table( 'WP_Users_List_Table' ); + + $role = current( $user_object->roles ); + + $x = new WP_Ajax_Response( + array( + 'what' => 'user', + 'id' => $user_id, + 'data' => $wp_list_table->single_row( $user_object, '', $role ), + 'supplemental' => array( + 'show-link' => sprintf( + /* translators: %s: The new user. */ + __( 'User %s added' ), + '' . $user_object->user_login . '' + ), + 'role' => $role, + ), + ) + ); + $x->send(); +} + +/** + * Handles closed post boxes via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_closed_postboxes() { + check_ajax_referer( 'closedpostboxes', 'closedpostboxesnonce' ); + $closed = isset( $_POST['closed'] ) ? explode( ',', $_POST['closed'] ) : array(); + $closed = array_filter( $closed ); + + $hidden = isset( $_POST['hidden'] ) ? explode( ',', $_POST['hidden'] ) : array(); + $hidden = array_filter( $hidden ); + + $page = isset( $_POST['page'] ) ? $_POST['page'] : ''; + + if ( sanitize_key( $page ) != $page ) { + wp_die( 0 ); + } + + $user = wp_get_current_user(); + if ( ! $user ) { + wp_die( -1 ); + } + + if ( is_array( $closed ) ) { + update_user_meta( $user->ID, "closedpostboxes_$page", $closed ); + } + + if ( is_array( $hidden ) ) { + // Postboxes that are always shown. + $hidden = array_diff( $hidden, array( 'submitdiv', 'linksubmitdiv', 'manage-menu', 'create-menu' ) ); + update_user_meta( $user->ID, "metaboxhidden_$page", $hidden ); + } + + wp_die( 1 ); +} + +/** + * Handles hidden columns via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_hidden_columns() { + check_ajax_referer( 'screen-options-nonce', 'screenoptionnonce' ); + $page = isset( $_POST['page'] ) ? $_POST['page'] : ''; + + if ( sanitize_key( $page ) != $page ) { + wp_die( 0 ); + } + + $user = wp_get_current_user(); + if ( ! $user ) { + wp_die( -1 ); + } + + $hidden = ! empty( $_POST['hidden'] ) ? explode( ',', $_POST['hidden'] ) : array(); + update_user_meta( $user->ID, "manage{$page}columnshidden", $hidden ); + + wp_die( 1 ); +} + +/** + * Handles updating whether to display the welcome panel via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_update_welcome_panel() { + check_ajax_referer( 'welcome-panel-nonce', 'welcomepanelnonce' ); + + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( -1 ); + } + + update_user_meta( get_current_user_id(), 'show_welcome_panel', empty( $_POST['visible'] ) ? 0 : 1 ); + + wp_die( 1 ); +} + +/** + * Handles for retrieving menu meta boxes via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_menu_get_metabox() { + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( -1 ); + } + + require_once ABSPATH . 'wp-admin/includes/nav-menu.php'; + + if ( isset( $_POST['item-type'] ) && 'post_type' === $_POST['item-type'] ) { + $type = 'posttype'; + $callback = 'wp_nav_menu_item_post_type_meta_box'; + $items = (array) get_post_types( array( 'show_in_nav_menus' => true ), 'object' ); + } elseif ( isset( $_POST['item-type'] ) && 'taxonomy' === $_POST['item-type'] ) { + $type = 'taxonomy'; + $callback = 'wp_nav_menu_item_taxonomy_meta_box'; + $items = (array) get_taxonomies( array( 'show_ui' => true ), 'object' ); + } + + if ( ! empty( $_POST['item-object'] ) && isset( $items[ $_POST['item-object'] ] ) ) { + $menus_meta_box_object = $items[ $_POST['item-object'] ]; + + /** This filter is documented in wp-admin/includes/nav-menu.php */ + $item = apply_filters( 'nav_menu_meta_box_object', $menus_meta_box_object ); + + $box_args = array( + 'id' => 'add-' . $item->name, + 'title' => $item->labels->name, + 'callback' => $callback, + 'args' => $item, + ); + + ob_start(); + $callback( null, $box_args ); + + $markup = ob_get_clean(); + + echo wp_json_encode( + array( + 'replace-id' => $type . '-' . $item->name, + 'markup' => $markup, + ) + ); + } + + wp_die(); +} + +/** + * Handles internal linking via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_wp_link_ajax() { + check_ajax_referer( 'internal-linking', '_ajax_linking_nonce' ); + + $args = array(); + + if ( isset( $_POST['search'] ) ) { + $args['s'] = wp_unslash( $_POST['search'] ); + } + + if ( isset( $_POST['term'] ) ) { + $args['s'] = wp_unslash( $_POST['term'] ); + } + + $args['pagenum'] = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 1; + + if ( ! class_exists( '_WP_Editors', false ) ) { + require ABSPATH . WPINC . '/class-wp-editor.php'; + } + + $results = _WP_Editors::wp_link_query( $args ); + + if ( ! isset( $results ) ) { + wp_die( 0 ); + } + + echo wp_json_encode( $results ); + echo "\n"; + + wp_die(); +} + +/** + * Handles saving menu locations via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_menu_locations_save() { + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( -1 ); + } + + check_ajax_referer( 'add-menu_item', 'menu-settings-column-nonce' ); + + if ( ! isset( $_POST['menu-locations'] ) ) { + wp_die( 0 ); + } + + set_theme_mod( 'nav_menu_locations', array_map( 'absint', $_POST['menu-locations'] ) ); + wp_die( 1 ); +} + +/** + * Handles saving the meta box order via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_meta_box_order() { + check_ajax_referer( 'meta-box-order' ); + $order = isset( $_POST['order'] ) ? (array) $_POST['order'] : false; + $page_columns = isset( $_POST['page_columns'] ) ? $_POST['page_columns'] : 'auto'; + + if ( 'auto' !== $page_columns ) { + $page_columns = (int) $page_columns; + } + + $page = isset( $_POST['page'] ) ? $_POST['page'] : ''; + + if ( sanitize_key( $page ) != $page ) { + wp_die( 0 ); + } + + $user = wp_get_current_user(); + if ( ! $user ) { + wp_die( -1 ); + } + + if ( $order ) { + update_user_meta( $user->ID, "meta-box-order_$page", $order ); + } + + if ( $page_columns ) { + update_user_meta( $user->ID, "screen_layout_$page", $page_columns ); + } + + wp_send_json_success(); +} + +/** + * Handles menu quick searching via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_menu_quick_search() { + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( -1 ); + } + + require_once ABSPATH . 'wp-admin/includes/nav-menu.php'; + + _wp_ajax_menu_quick_search( $_POST ); + + wp_die(); +} + +/** + * Handles retrieving a permalink via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_get_permalink() { + check_ajax_referer( 'getpermalink', 'getpermalinknonce' ); + $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0; + wp_die( get_preview_post_link( $post_id ) ); +} + +/** + * Handles retrieving a sample permalink via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_sample_permalink() { + check_ajax_referer( 'samplepermalink', 'samplepermalinknonce' ); + $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0; + $title = isset( $_POST['new_title'] ) ? $_POST['new_title'] : ''; + $slug = isset( $_POST['new_slug'] ) ? $_POST['new_slug'] : null; + wp_die( get_sample_permalink_html( $post_id, $title, $slug ) ); +} + +/** + * Handles Quick Edit saving a post from a list table via AJAX. + * + * @since 3.1.0 + * + * @global string $mode List table view mode. + */ +function wp_ajax_inline_save() { + global $mode; + + check_ajax_referer( 'inlineeditnonce', '_inline_edit' ); + + if ( ! isset( $_POST['post_ID'] ) || ! (int) $_POST['post_ID'] ) { + wp_die(); + } + + $post_id = (int) $_POST['post_ID']; + + if ( 'page' === $_POST['post_type'] ) { + if ( ! current_user_can( 'edit_page', $post_id ) ) { + wp_die( __( 'Sorry, you are not allowed to edit this page.' ) ); + } + } else { + if ( ! current_user_can( 'edit_post', $post_id ) ) { + wp_die( __( 'Sorry, you are not allowed to edit this post.' ) ); + } + } + + $last = wp_check_post_lock( $post_id ); + if ( $last ) { + $last_user = get_userdata( $last ); + $last_user_name = $last_user ? $last_user->display_name : __( 'Someone' ); + + /* translators: %s: User's display name. */ + $msg_template = __( 'Saving is disabled: %s is currently editing this post.' ); + + if ( 'page' === $_POST['post_type'] ) { + /* translators: %s: User's display name. */ + $msg_template = __( 'Saving is disabled: %s is currently editing this page.' ); + } + + printf( $msg_template, esc_html( $last_user_name ) ); + wp_die(); + } + + $data = &$_POST; + + $post = get_post( $post_id, ARRAY_A ); + + // Since it's coming from the database. + $post = wp_slash( $post ); + + $data['content'] = $post['post_content']; + $data['excerpt'] = $post['post_excerpt']; + + // Rename. + $data['user_ID'] = get_current_user_id(); + + if ( isset( $data['post_parent'] ) ) { + $data['parent_id'] = $data['post_parent']; + } + + // Status. + if ( isset( $data['keep_private'] ) && 'private' === $data['keep_private'] ) { + $data['visibility'] = 'private'; + $data['post_status'] = 'private'; + } else { + $data['post_status'] = $data['_status']; + } + + if ( empty( $data['comment_status'] ) ) { + $data['comment_status'] = 'closed'; + } + + if ( empty( $data['ping_status'] ) ) { + $data['ping_status'] = 'closed'; + } + + // Exclude terms from taxonomies that are not supposed to appear in Quick Edit. + if ( ! empty( $data['tax_input'] ) ) { + foreach ( $data['tax_input'] as $taxonomy => $terms ) { + $tax_object = get_taxonomy( $taxonomy ); + /** This filter is documented in wp-admin/includes/class-wp-posts-list-table.php */ + if ( ! apply_filters( 'quick_edit_show_taxonomy', $tax_object->show_in_quick_edit, $taxonomy, $post['post_type'] ) ) { + unset( $data['tax_input'][ $taxonomy ] ); + } + } + } + + // Hack: wp_unique_post_slug() doesn't work for drafts, so we will fake that our post is published. + if ( ! empty( $data['post_name'] ) && in_array( $post['post_status'], array( 'draft', 'pending' ), true ) ) { + $post['post_status'] = 'publish'; + $data['post_name'] = wp_unique_post_slug( $data['post_name'], $post['ID'], $post['post_status'], $post['post_type'], $post['post_parent'] ); + } + + // Update the post. + edit_post(); + + $wp_list_table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => $_POST['screen'] ) ); + + $mode = 'excerpt' === $_POST['post_view'] ? 'excerpt' : 'list'; + + $level = 0; + if ( is_post_type_hierarchical( $wp_list_table->screen->post_type ) ) { + $request_post = array( get_post( $_POST['post_ID'] ) ); + $parent = $request_post[0]->post_parent; + + while ( $parent > 0 ) { + $parent_post = get_post( $parent ); + $parent = $parent_post->post_parent; + ++$level; + } + } + + $wp_list_table->display_rows( array( get_post( $_POST['post_ID'] ) ), $level ); + + wp_die(); +} + +/** + * Handles Quick Edit saving for a term via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_inline_save_tax() { + check_ajax_referer( 'taxinlineeditnonce', '_inline_edit' ); + + $taxonomy = sanitize_key( $_POST['taxonomy'] ); + $taxonomy_object = get_taxonomy( $taxonomy ); + + if ( ! $taxonomy_object ) { + wp_die( 0 ); + } + + if ( ! isset( $_POST['tax_ID'] ) || ! (int) $_POST['tax_ID'] ) { + wp_die( -1 ); + } + + $id = (int) $_POST['tax_ID']; + + if ( ! current_user_can( 'edit_term', $id ) ) { + wp_die( -1 ); + } + + $wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => 'edit-' . $taxonomy ) ); + + $tag = get_term( $id, $taxonomy ); + $_POST['description'] = $tag->description; + + $updated = wp_update_term( $id, $taxonomy, $_POST ); + + if ( $updated && ! is_wp_error( $updated ) ) { + $tag = get_term( $updated['term_id'], $taxonomy ); + if ( ! $tag || is_wp_error( $tag ) ) { + if ( is_wp_error( $tag ) && $tag->get_error_message() ) { + wp_die( $tag->get_error_message() ); + } + wp_die( __( 'Item not updated.' ) ); + } + } else { + if ( is_wp_error( $updated ) && $updated->get_error_message() ) { + wp_die( $updated->get_error_message() ); + } + wp_die( __( 'Item not updated.' ) ); + } + + $level = 0; + $parent = $tag->parent; + + while ( $parent > 0 ) { + $parent_tag = get_term( $parent, $taxonomy ); + $parent = $parent_tag->parent; + ++$level; + } + + $wp_list_table->single_row( $tag, $level ); + wp_die(); +} + +/** + * Handles querying posts for the Find Posts modal via AJAX. + * + * @see window.findPosts + * + * @since 3.1.0 + */ +function wp_ajax_find_posts() { + check_ajax_referer( 'find-posts' ); + + $post_types = get_post_types( array( 'public' => true ), 'objects' ); + unset( $post_types['attachment'] ); + + $args = array( + 'post_type' => array_keys( $post_types ), + 'post_status' => 'any', + 'posts_per_page' => 50, + ); + + $search = wp_unslash( $_POST['ps'] ); + + if ( '' !== $search ) { + $args['s'] = $search; + } + + $posts = get_posts( $args ); + + if ( ! $posts ) { + wp_send_json_error( __( 'No items found.' ) ); + } + + $html = ''; + $alt = ''; + foreach ( $posts as $post ) { + $title = trim( $post->post_title ) ? $post->post_title : __( '(no title)' ); + $alt = ( 'alternate' === $alt ) ? '' : 'alternate'; + + switch ( $post->post_status ) { + case 'publish': + case 'private': + $stat = __( 'Published' ); + break; + case 'future': + $stat = __( 'Scheduled' ); + break; + case 'pending': + $stat = __( 'Pending Review' ); + break; + case 'draft': + $stat = __( 'Draft' ); + break; + } + + if ( '0000-00-00 00:00:00' === $post->post_date ) { + $time = ''; + } else { + /* translators: Date format in table columns, see https://www.php.net/manual/datetime.format.php */ + $time = mysql2date( __( 'Y/m/d' ), $post->post_date ); + } + + $html .= ''; + $html .= '' . "\n\n"; + } + + $html .= '

' . __( 'Title' ) . '' . __( 'Type' ) . '' . __( 'Date' ) . '' . __( 'Status' ) . '
' . esc_html( $post_types[ $post->post_type ]->labels->singular_name ) . '' . esc_html( $time ) . '' . esc_html( $stat ) . '
'; + + wp_send_json_success( $html ); +} + +/** + * Handles saving the widgets order via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_widgets_order() { + check_ajax_referer( 'save-sidebar-widgets', 'savewidgets' ); + + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( -1 ); + } + + unset( $_POST['savewidgets'], $_POST['action'] ); + + // Save widgets order for all sidebars. + if ( is_array( $_POST['sidebars'] ) ) { + $sidebars = array(); + + foreach ( wp_unslash( $_POST['sidebars'] ) as $key => $val ) { + $sb = array(); + + if ( ! empty( $val ) ) { + $val = explode( ',', $val ); + + foreach ( $val as $k => $v ) { + if ( ! str_contains( $v, 'widget-' ) ) { + continue; + } + + $sb[ $k ] = substr( $v, strpos( $v, '_' ) + 1 ); + } + } + $sidebars[ $key ] = $sb; + } + + wp_set_sidebars_widgets( $sidebars ); + wp_die( 1 ); + } + + wp_die( -1 ); +} + +/** + * Handles saving a widget via AJAX. + * + * @since 3.1.0 + * + * @global array $wp_registered_widgets + * @global array $wp_registered_widget_controls + * @global array $wp_registered_widget_updates + */ +function wp_ajax_save_widget() { + global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_widget_updates; + + check_ajax_referer( 'save-sidebar-widgets', 'savewidgets' ); + + if ( ! current_user_can( 'edit_theme_options' ) || ! isset( $_POST['id_base'] ) ) { + wp_die( -1 ); + } + + unset( $_POST['savewidgets'], $_POST['action'] ); + + /** + * Fires early when editing the widgets displayed in sidebars. + * + * @since 2.8.0 + */ + do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + + /** + * Fires early when editing the widgets displayed in sidebars. + * + * @since 2.8.0 + */ + do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + + /** This action is documented in wp-admin/widgets.php */ + do_action( 'sidebar_admin_setup' ); + + $id_base = wp_unslash( $_POST['id_base'] ); + $widget_id = wp_unslash( $_POST['widget-id'] ); + $sidebar_id = $_POST['sidebar']; + $multi_number = ! empty( $_POST['multi_number'] ) ? (int) $_POST['multi_number'] : 0; + $settings = isset( $_POST[ 'widget-' . $id_base ] ) && is_array( $_POST[ 'widget-' . $id_base ] ) ? $_POST[ 'widget-' . $id_base ] : false; + $error = '

' . __( 'An error has occurred. Please reload the page and try again.' ) . '

'; + + $sidebars = wp_get_sidebars_widgets(); + $sidebar = isset( $sidebars[ $sidebar_id ] ) ? $sidebars[ $sidebar_id ] : array(); + + // Delete. + if ( isset( $_POST['delete_widget'] ) && $_POST['delete_widget'] ) { + + if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) { + wp_die( $error ); + } + + $sidebar = array_diff( $sidebar, array( $widget_id ) ); + $_POST = array( + 'sidebar' => $sidebar_id, + 'widget-' . $id_base => array(), + 'the-widget-id' => $widget_id, + 'delete_widget' => '1', + ); + + /** This action is documented in wp-admin/widgets.php */ + do_action( 'delete_widget', $widget_id, $sidebar_id, $id_base ); + + } elseif ( $settings && preg_match( '/__i__|%i%/', key( $settings ) ) ) { + if ( ! $multi_number ) { + wp_die( $error ); + } + + $_POST[ 'widget-' . $id_base ] = array( $multi_number => reset( $settings ) ); + $widget_id = $id_base . '-' . $multi_number; + $sidebar[] = $widget_id; + } + $_POST['widget-id'] = $sidebar; + + foreach ( (array) $wp_registered_widget_updates as $name => $control ) { + + if ( $name == $id_base ) { + if ( ! is_callable( $control['callback'] ) ) { + continue; + } + + ob_start(); + call_user_func_array( $control['callback'], $control['params'] ); + ob_end_clean(); + break; + } + } + + if ( isset( $_POST['delete_widget'] ) && $_POST['delete_widget'] ) { + $sidebars[ $sidebar_id ] = $sidebar; + wp_set_sidebars_widgets( $sidebars ); + echo "deleted:$widget_id"; + wp_die(); + } + + if ( ! empty( $_POST['add_new'] ) ) { + wp_die(); + } + + $form = $wp_registered_widget_controls[ $widget_id ]; + if ( $form ) { + call_user_func_array( $form['callback'], $form['params'] ); + } + + wp_die(); +} + +/** + * Handles updating a widget via AJAX. + * + * @since 3.9.0 + * + * @global WP_Customize_Manager $wp_customize + */ +function wp_ajax_update_widget() { + global $wp_customize; + $wp_customize->widgets->wp_ajax_update_widget(); +} + +/** + * Handles removing inactive widgets via AJAX. + * + * @since 4.4.0 + */ +function wp_ajax_delete_inactive_widgets() { + check_ajax_referer( 'remove-inactive-widgets', 'removeinactivewidgets' ); + + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( -1 ); + } + + unset( $_POST['removeinactivewidgets'], $_POST['action'] ); + /** This action is documented in wp-admin/includes/ajax-actions.php */ + do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + /** This action is documented in wp-admin/includes/ajax-actions.php */ + do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + /** This action is documented in wp-admin/widgets.php */ + do_action( 'sidebar_admin_setup' ); + + $sidebars_widgets = wp_get_sidebars_widgets(); + + foreach ( $sidebars_widgets['wp_inactive_widgets'] as $key => $widget_id ) { + $pieces = explode( '-', $widget_id ); + $multi_number = array_pop( $pieces ); + $id_base = implode( '-', $pieces ); + $widget = get_option( 'widget_' . $id_base ); + unset( $widget[ $multi_number ] ); + update_option( 'widget_' . $id_base, $widget ); + unset( $sidebars_widgets['wp_inactive_widgets'][ $key ] ); + } + + wp_set_sidebars_widgets( $sidebars_widgets ); + + wp_die(); +} + +/** + * Handles creating missing image sub-sizes for just uploaded images via AJAX. + * + * @since 5.3.0 + */ +function wp_ajax_media_create_image_subsizes() { + check_ajax_referer( 'media-form' ); + + if ( ! current_user_can( 'upload_files' ) ) { + wp_send_json_error( array( 'message' => __( 'Sorry, you are not allowed to upload files.' ) ) ); + } + + if ( empty( $_POST['attachment_id'] ) ) { + wp_send_json_error( array( 'message' => __( 'Upload failed. Please reload and try again.' ) ) ); + } + + $attachment_id = (int) $_POST['attachment_id']; + + if ( ! empty( $_POST['_wp_upload_failed_cleanup'] ) ) { + // Upload failed. Cleanup. + if ( wp_attachment_is_image( $attachment_id ) && current_user_can( 'delete_post', $attachment_id ) ) { + $attachment = get_post( $attachment_id ); + + // Created at most 10 min ago. + if ( $attachment && ( time() - strtotime( $attachment->post_date_gmt ) < 600 ) ) { + wp_delete_attachment( $attachment_id, true ); + wp_send_json_success(); + } + } + } + + /* + * Set a custom header with the attachment_id. + * Used by the browser/client to resume creating image sub-sizes after a PHP fatal error. + */ + if ( ! headers_sent() ) { + header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id ); + } + + /* + * This can still be pretty slow and cause timeout or out of memory errors. + * The js that handles the response would need to also handle HTTP 500 errors. + */ + wp_update_image_subsizes( $attachment_id ); + + if ( ! empty( $_POST['_legacy_support'] ) ) { + // The old (inline) uploader. Only needs the attachment_id. + $response = array( 'id' => $attachment_id ); + } else { + // Media modal and Media Library grid view. + $response = wp_prepare_attachment_for_js( $attachment_id ); + + if ( ! $response ) { + wp_send_json_error( array( 'message' => __( 'Upload failed.' ) ) ); + } + } + + // At this point the image has been uploaded successfully. + wp_send_json_success( $response ); +} + +/** + * Handles uploading attachments via AJAX. + * + * @since 3.3.0 + */ +function wp_ajax_upload_attachment() { + check_ajax_referer( 'media-form' ); + /* + * This function does not use wp_send_json_success() / wp_send_json_error() + * as the html4 Plupload handler requires a text/html Content-Type for older IE. + * See https://core.trac.wordpress.org/ticket/31037 + */ + + if ( ! current_user_can( 'upload_files' ) ) { + echo wp_json_encode( + array( + 'success' => false, + 'data' => array( + 'message' => __( 'Sorry, you are not allowed to upload files.' ), + 'filename' => esc_html( $_FILES['async-upload']['name'] ), + ), + ) + ); + + wp_die(); + } + + if ( isset( $_REQUEST['post_id'] ) ) { + $post_id = $_REQUEST['post_id']; + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + echo wp_json_encode( + array( + 'success' => false, + 'data' => array( + 'message' => __( 'Sorry, you are not allowed to attach files to this post.' ), + 'filename' => esc_html( $_FILES['async-upload']['name'] ), + ), + ) + ); + + wp_die(); + } + } else { + $post_id = null; + } + + $post_data = ! empty( $_REQUEST['post_data'] ) ? _wp_get_allowed_postdata( _wp_translate_postdata( false, (array) $_REQUEST['post_data'] ) ) : array(); + + if ( is_wp_error( $post_data ) ) { + wp_die( $post_data->get_error_message() ); + } + + // If the context is custom header or background, make sure the uploaded file is an image. + if ( isset( $post_data['context'] ) && in_array( $post_data['context'], array( 'custom-header', 'custom-background' ), true ) ) { + $wp_filetype = wp_check_filetype_and_ext( $_FILES['async-upload']['tmp_name'], $_FILES['async-upload']['name'] ); + + if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) { + echo wp_json_encode( + array( + 'success' => false, + 'data' => array( + 'message' => __( 'The uploaded file is not a valid image. Please try again.' ), + 'filename' => esc_html( $_FILES['async-upload']['name'] ), + ), + ) + ); + + wp_die(); + } + } + + $attachment_id = media_handle_upload( 'async-upload', $post_id, $post_data ); + + if ( is_wp_error( $attachment_id ) ) { + echo wp_json_encode( + array( + 'success' => false, + 'data' => array( + 'message' => $attachment_id->get_error_message(), + 'filename' => esc_html( $_FILES['async-upload']['name'] ), + ), + ) + ); + + wp_die(); + } + + if ( isset( $post_data['context'] ) && isset( $post_data['theme'] ) ) { + if ( 'custom-background' === $post_data['context'] ) { + update_post_meta( $attachment_id, '_wp_attachment_is_custom_background', $post_data['theme'] ); + } + + if ( 'custom-header' === $post_data['context'] ) { + update_post_meta( $attachment_id, '_wp_attachment_is_custom_header', $post_data['theme'] ); + } + } + + $attachment = wp_prepare_attachment_for_js( $attachment_id ); + if ( ! $attachment ) { + wp_die(); + } + + echo wp_json_encode( + array( + 'success' => true, + 'data' => $attachment, + ) + ); + + wp_die(); +} + +/** + * Handles image editing via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_image_editor() { + $attachment_id = (int) $_POST['postid']; + + if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) { + wp_die( -1 ); + } + + check_ajax_referer( "image_editor-$attachment_id" ); + require_once ABSPATH . 'wp-admin/includes/image-edit.php'; + + $msg = false; + + switch ( $_POST['do'] ) { + case 'save': + $msg = wp_save_image( $attachment_id ); + if ( ! empty( $msg->error ) ) { + wp_send_json_error( $msg ); + } + + wp_send_json_success( $msg ); + break; + case 'scale': + $msg = wp_save_image( $attachment_id ); + break; + case 'restore': + $msg = wp_restore_image( $attachment_id ); + break; + } + + ob_start(); + wp_image_editor( $attachment_id, $msg ); + $html = ob_get_clean(); + + if ( ! empty( $msg->error ) ) { + wp_send_json_error( + array( + 'message' => $msg, + 'html' => $html, + ) + ); + } + + wp_send_json_success( + array( + 'message' => $msg, + 'html' => $html, + ) + ); +} + +/** + * Handles setting the featured image via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_set_post_thumbnail() { + $json = ! empty( $_REQUEST['json'] ); // New-style request. + + $post_id = (int) $_POST['post_id']; + if ( ! current_user_can( 'edit_post', $post_id ) ) { + wp_die( -1 ); + } + + $thumbnail_id = (int) $_POST['thumbnail_id']; + + if ( $json ) { + check_ajax_referer( "update-post_$post_id" ); + } else { + check_ajax_referer( "set_post_thumbnail-$post_id" ); + } + + if ( '-1' == $thumbnail_id ) { + if ( delete_post_thumbnail( $post_id ) ) { + $return = _wp_post_thumbnail_html( null, $post_id ); + $json ? wp_send_json_success( $return ) : wp_die( $return ); + } else { + wp_die( 0 ); + } + } + + if ( set_post_thumbnail( $post_id, $thumbnail_id ) ) { + $return = _wp_post_thumbnail_html( $thumbnail_id, $post_id ); + $json ? wp_send_json_success( $return ) : wp_die( $return ); + } + + wp_die( 0 ); +} + +/** + * Handles retrieving HTML for the featured image via AJAX. + * + * @since 4.6.0 + */ +function wp_ajax_get_post_thumbnail_html() { + $post_id = (int) $_POST['post_id']; + + check_ajax_referer( "update-post_$post_id" ); + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + wp_die( -1 ); + } + + $thumbnail_id = (int) $_POST['thumbnail_id']; + + // For backward compatibility, -1 refers to no featured image. + if ( -1 === $thumbnail_id ) { + $thumbnail_id = null; + } + + $return = _wp_post_thumbnail_html( $thumbnail_id, $post_id ); + wp_send_json_success( $return ); +} + +/** + * Handles setting the featured image for an attachment via AJAX. + * + * @since 4.0.0 + * + * @see set_post_thumbnail() + */ +function wp_ajax_set_attachment_thumbnail() { + if ( empty( $_POST['urls'] ) || ! is_array( $_POST['urls'] ) ) { + wp_send_json_error(); + } + + $thumbnail_id = (int) $_POST['thumbnail_id']; + if ( empty( $thumbnail_id ) ) { + wp_send_json_error(); + } + + if ( false === check_ajax_referer( 'set-attachment-thumbnail', '_ajax_nonce', false ) ) { + wp_send_json_error(); + } + + $post_ids = array(); + // For each URL, try to find its corresponding post ID. + foreach ( $_POST['urls'] as $url ) { + $post_id = attachment_url_to_postid( $url ); + if ( ! empty( $post_id ) ) { + $post_ids[] = $post_id; + } + } + + if ( empty( $post_ids ) ) { + wp_send_json_error(); + } + + $success = 0; + // For each found attachment, set its thumbnail. + foreach ( $post_ids as $post_id ) { + if ( ! current_user_can( 'edit_post', $post_id ) ) { + continue; + } + + if ( set_post_thumbnail( $post_id, $thumbnail_id ) ) { + ++$success; + } + } + + if ( 0 === $success ) { + wp_send_json_error(); + } else { + wp_send_json_success(); + } + + wp_send_json_error(); +} + +/** + * Handles formatting a date via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_date_format() { + wp_die( date_i18n( sanitize_option( 'date_format', wp_unslash( $_POST['date'] ) ) ) ); +} + +/** + * Handles formatting a time via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_time_format() { + wp_die( date_i18n( sanitize_option( 'time_format', wp_unslash( $_POST['date'] ) ) ) ); +} + +/** + * Handles saving posts from the fullscreen editor via AJAX. + * + * @since 3.1.0 + * @deprecated 4.3.0 + */ +function wp_ajax_wp_fullscreen_save_post() { + $post_id = isset( $_POST['post_ID'] ) ? (int) $_POST['post_ID'] : 0; + + $post = null; + + if ( $post_id ) { + $post = get_post( $post_id ); + } + + check_ajax_referer( 'update-post_' . $post_id, '_wpnonce' ); + + $post_id = edit_post(); + + if ( is_wp_error( $post_id ) ) { + wp_send_json_error(); + } + + if ( $post ) { + $last_date = mysql2date( __( 'F j, Y' ), $post->post_modified ); + $last_time = mysql2date( __( 'g:i a' ), $post->post_modified ); + } else { + $last_date = date_i18n( __( 'F j, Y' ) ); + $last_time = date_i18n( __( 'g:i a' ) ); + } + + $last_id = get_post_meta( $post_id, '_edit_last', true ); + if ( $last_id ) { + $last_user = get_userdata( $last_id ); + /* translators: 1: User's display name, 2: Date of last edit, 3: Time of last edit. */ + $last_edited = sprintf( __( 'Last edited by %1$s on %2$s at %3$s' ), esc_html( $last_user->display_name ), $last_date, $last_time ); + } else { + /* translators: 1: Date of last edit, 2: Time of last edit. */ + $last_edited = sprintf( __( 'Last edited on %1$s at %2$s' ), $last_date, $last_time ); + } + + wp_send_json_success( array( 'last_edited' => $last_edited ) ); +} + +/** + * Handles removing a post lock via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_wp_remove_post_lock() { + if ( empty( $_POST['post_ID'] ) || empty( $_POST['active_post_lock'] ) ) { + wp_die( 0 ); + } + + $post_id = (int) $_POST['post_ID']; + $post = get_post( $post_id ); + + if ( ! $post ) { + wp_die( 0 ); + } + + check_ajax_referer( 'update-post_' . $post_id ); + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + wp_die( -1 ); + } + + $active_lock = array_map( 'absint', explode( ':', $_POST['active_post_lock'] ) ); + + if ( get_current_user_id() != $active_lock[1] ) { + wp_die( 0 ); + } + + /** + * Filters the post lock window duration. + * + * @since 3.3.0 + * + * @param int $interval The interval in seconds the post lock duration + * should last, plus 5 seconds. Default 150. + */ + $new_lock = ( time() - apply_filters( 'wp_check_post_lock_window', 150 ) + 5 ) . ':' . $active_lock[1]; + update_post_meta( $post_id, '_edit_lock', $new_lock, implode( ':', $active_lock ) ); + wp_die( 1 ); +} + +/** + * Handles dismissing a WordPress pointer via AJAX. + * + * @since 3.1.0 + */ +function wp_ajax_dismiss_wp_pointer() { + $pointer = $_POST['pointer']; + + if ( sanitize_key( $pointer ) != $pointer ) { + wp_die( 0 ); + } + + // check_ajax_referer( 'dismiss-pointer_' . $pointer ); + + $dismissed = array_filter( explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ) ); + + if ( in_array( $pointer, $dismissed, true ) ) { + wp_die( 0 ); + } + + $dismissed[] = $pointer; + $dismissed = implode( ',', $dismissed ); + + update_user_meta( get_current_user_id(), 'dismissed_wp_pointers', $dismissed ); + wp_die( 1 ); +} + +/** + * Handles getting an attachment via AJAX. + * + * @since 3.5.0 + */ +function wp_ajax_get_attachment() { + if ( ! isset( $_REQUEST['id'] ) ) { + wp_send_json_error(); + } + + $id = absint( $_REQUEST['id'] ); + if ( ! $id ) { + wp_send_json_error(); + } + + $post = get_post( $id ); + if ( ! $post ) { + wp_send_json_error(); + } + + if ( 'attachment' !== $post->post_type ) { + wp_send_json_error(); + } + + if ( ! current_user_can( 'upload_files' ) ) { + wp_send_json_error(); + } + + $attachment = wp_prepare_attachment_for_js( $id ); + if ( ! $attachment ) { + wp_send_json_error(); + } + + wp_send_json_success( $attachment ); +} + +/** + * Handles querying attachments via AJAX. + * + * @since 3.5.0 + */ +function wp_ajax_query_attachments() { + if ( ! current_user_can( 'upload_files' ) ) { + wp_send_json_error(); + } + + $query = isset( $_REQUEST['query'] ) ? (array) $_REQUEST['query'] : array(); + $keys = array( + 's', + 'order', + 'orderby', + 'posts_per_page', + 'paged', + 'post_mime_type', + 'post_parent', + 'author', + 'post__in', + 'post__not_in', + 'year', + 'monthnum', + ); + + foreach ( get_taxonomies_for_attachments( 'objects' ) as $t ) { + if ( $t->query_var && isset( $query[ $t->query_var ] ) ) { + $keys[] = $t->query_var; + } + } + + $query = array_intersect_key( $query, array_flip( $keys ) ); + $query['post_type'] = 'attachment'; + + if ( + MEDIA_TRASH && + ! empty( $_REQUEST['query']['post_status'] ) && + 'trash' === $_REQUEST['query']['post_status'] + ) { + $query['post_status'] = 'trash'; + } else { + $query['post_status'] = 'inherit'; + } + + if ( current_user_can( get_post_type_object( 'attachment' )->cap->read_private_posts ) ) { + $query['post_status'] .= ',private'; + } + + // Filter query clauses to include filenames. + if ( isset( $query['s'] ) ) { + add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); + } + + /** + * Filters the arguments passed to WP_Query during an Ajax + * call for querying attachments. + * + * @since 3.7.0 + * + * @see WP_Query::parse_query() + * + * @param array $query An array of query variables. + */ + $query = apply_filters( 'ajax_query_attachments_args', $query ); + $attachments_query = new WP_Query( $query ); + update_post_parent_caches( $attachments_query->posts ); + + $posts = array_map( 'wp_prepare_attachment_for_js', $attachments_query->posts ); + $posts = array_filter( $posts ); + $total_posts = $attachments_query->found_posts; + + if ( $total_posts < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $query['paged'] ); + + $count_query = new WP_Query(); + $count_query->query( $query ); + $total_posts = $count_query->found_posts; + } + + $posts_per_page = (int) $attachments_query->get( 'posts_per_page' ); + + $max_pages = $posts_per_page ? ceil( $total_posts / $posts_per_page ) : 0; + + header( 'X-WP-Total: ' . (int) $total_posts ); + header( 'X-WP-TotalPages: ' . (int) $max_pages ); + + wp_send_json_success( $posts ); +} + +/** + * Handles updating attachment attributes via AJAX. + * + * @since 3.5.0 + */ +function wp_ajax_save_attachment() { + if ( ! isset( $_REQUEST['id'] ) || ! isset( $_REQUEST['changes'] ) ) { + wp_send_json_error(); + } + + $id = absint( $_REQUEST['id'] ); + if ( ! $id ) { + wp_send_json_error(); + } + + check_ajax_referer( 'update-post_' . $id, 'nonce' ); + + if ( ! current_user_can( 'edit_post', $id ) ) { + wp_send_json_error(); + } + + $changes = $_REQUEST['changes']; + $post = get_post( $id, ARRAY_A ); + + if ( 'attachment' !== $post['post_type'] ) { + wp_send_json_error(); + } + + if ( isset( $changes['parent'] ) ) { + $post['post_parent'] = $changes['parent']; + } + + if ( isset( $changes['title'] ) ) { + $post['post_title'] = $changes['title']; + } + + if ( isset( $changes['caption'] ) ) { + $post['post_excerpt'] = $changes['caption']; + } + + if ( isset( $changes['description'] ) ) { + $post['post_content'] = $changes['description']; + } + + if ( MEDIA_TRASH && isset( $changes['status'] ) ) { + $post['post_status'] = $changes['status']; + } + + if ( isset( $changes['alt'] ) ) { + $alt = wp_unslash( $changes['alt'] ); + if ( get_post_meta( $id, '_wp_attachment_image_alt', true ) !== $alt ) { + $alt = wp_strip_all_tags( $alt, true ); + update_post_meta( $id, '_wp_attachment_image_alt', wp_slash( $alt ) ); + } + } + + if ( wp_attachment_is( 'audio', $post['ID'] ) ) { + $changed = false; + $id3data = wp_get_attachment_metadata( $post['ID'] ); + + if ( ! is_array( $id3data ) ) { + $changed = true; + $id3data = array(); + } + + foreach ( wp_get_attachment_id3_keys( (object) $post, 'edit' ) as $key => $label ) { + if ( isset( $changes[ $key ] ) ) { + $changed = true; + $id3data[ $key ] = sanitize_text_field( wp_unslash( $changes[ $key ] ) ); + } + } + + if ( $changed ) { + wp_update_attachment_metadata( $id, $id3data ); + } + } + + if ( MEDIA_TRASH && isset( $changes['status'] ) && 'trash' === $changes['status'] ) { + wp_delete_post( $id ); + } else { + wp_update_post( $post ); + } + + wp_send_json_success(); +} + +/** + * Handles saving backward compatible attachment attributes via AJAX. + * + * @since 3.5.0 + */ +function wp_ajax_save_attachment_compat() { + if ( ! isset( $_REQUEST['id'] ) ) { + wp_send_json_error(); + } + + $id = absint( $_REQUEST['id'] ); + if ( ! $id ) { + wp_send_json_error(); + } + + if ( empty( $_REQUEST['attachments'] ) || empty( $_REQUEST['attachments'][ $id ] ) ) { + wp_send_json_error(); + } + + $attachment_data = $_REQUEST['attachments'][ $id ]; + + check_ajax_referer( 'update-post_' . $id, 'nonce' ); + + if ( ! current_user_can( 'edit_post', $id ) ) { + wp_send_json_error(); + } + + $post = get_post( $id, ARRAY_A ); + + if ( 'attachment' !== $post['post_type'] ) { + wp_send_json_error(); + } + + /** This filter is documented in wp-admin/includes/media.php */ + $post = apply_filters( 'attachment_fields_to_save', $post, $attachment_data ); + + if ( isset( $post['errors'] ) ) { + $errors = $post['errors']; // @todo return me and display me! + unset( $post['errors'] ); + } + + wp_update_post( $post ); + + foreach ( get_attachment_taxonomies( $post ) as $taxonomy ) { + if ( isset( $attachment_data[ $taxonomy ] ) ) { + wp_set_object_terms( $id, array_map( 'trim', preg_split( '/,+/', $attachment_data[ $taxonomy ] ) ), $taxonomy, false ); + } + } + + $attachment = wp_prepare_attachment_for_js( $id ); + + if ( ! $attachment ) { + wp_send_json_error(); + } + + wp_send_json_success( $attachment ); +} + +/** + * Handles saving the attachment order via AJAX. + * + * @since 3.5.0 + */ +function wp_ajax_save_attachment_order() { + if ( ! isset( $_REQUEST['post_id'] ) ) { + wp_send_json_error(); + } + + $post_id = absint( $_REQUEST['post_id'] ); + if ( ! $post_id ) { + wp_send_json_error(); + } + + if ( empty( $_REQUEST['attachments'] ) ) { + wp_send_json_error(); + } + + check_ajax_referer( 'update-post_' . $post_id, 'nonce' ); + + $attachments = $_REQUEST['attachments']; + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + wp_send_json_error(); + } + + foreach ( $attachments as $attachment_id => $menu_order ) { + if ( ! current_user_can( 'edit_post', $attachment_id ) ) { + continue; + } + + $attachment = get_post( $attachment_id ); + + if ( ! $attachment ) { + continue; + } + + if ( 'attachment' !== $attachment->post_type ) { + continue; + } + + wp_update_post( + array( + 'ID' => $attachment_id, + 'menu_order' => $menu_order, + ) + ); + } + + wp_send_json_success(); +} + +/** + * Handles sending an attachment to the editor via AJAX. + * + * Generates the HTML to send an attachment to the editor. + * Backward compatible with the {@see 'media_send_to_editor'} filter + * and the chain of filters that follow. + * + * @since 3.5.0 + */ +function wp_ajax_send_attachment_to_editor() { + check_ajax_referer( 'media-send-to-editor', 'nonce' ); + + $attachment = wp_unslash( $_POST['attachment'] ); + + $id = (int) $attachment['id']; + + $post = get_post( $id ); + if ( ! $post ) { + wp_send_json_error(); + } + + if ( 'attachment' !== $post->post_type ) { + wp_send_json_error(); + } + + if ( current_user_can( 'edit_post', $id ) ) { + // If this attachment is unattached, attach it. Primarily a back compat thing. + $insert_into_post_id = (int) $_POST['post_id']; + + if ( 0 == $post->post_parent && $insert_into_post_id ) { + wp_update_post( + array( + 'ID' => $id, + 'post_parent' => $insert_into_post_id, + ) + ); + } + } + + $url = empty( $attachment['url'] ) ? '' : $attachment['url']; + $rel = ( str_contains( $url, 'attachment_id' ) || get_attachment_link( $id ) === $url ); + + remove_filter( 'media_send_to_editor', 'image_media_send_to_editor' ); + + if ( str_starts_with( $post->post_mime_type, 'image' ) ) { + $align = isset( $attachment['align'] ) ? $attachment['align'] : 'none'; + $size = isset( $attachment['image-size'] ) ? $attachment['image-size'] : 'medium'; + $alt = isset( $attachment['image_alt'] ) ? $attachment['image_alt'] : ''; + + // No whitespace-only captions. + $caption = isset( $attachment['post_excerpt'] ) ? $attachment['post_excerpt'] : ''; + if ( '' === trim( $caption ) ) { + $caption = ''; + } + + $title = ''; // We no longer insert title tags into tags, as they are redundant. + $html = get_image_send_to_editor( $id, $caption, $title, $align, $url, $rel, $size, $alt ); + } elseif ( wp_attachment_is( 'video', $post ) || wp_attachment_is( 'audio', $post ) ) { + $html = stripslashes_deep( $_POST['html'] ); + } else { + $html = isset( $attachment['post_title'] ) ? $attachment['post_title'] : ''; + $rel = $rel ? ' rel="attachment wp-att-' . $id . '"' : ''; // Hard-coded string, $id is already sanitized. + + if ( ! empty( $url ) ) { + $html = '' . $html . ''; + } + } + + /** This filter is documented in wp-admin/includes/media.php */ + $html = apply_filters( 'media_send_to_editor', $html, $id, $attachment ); + + wp_send_json_success( $html ); +} + +/** + * Handles sending a link to the editor via AJAX. + * + * Generates the HTML to send a non-image embed link to the editor. + * + * Backward compatible with the following filters: + * - file_send_to_editor_url + * - audio_send_to_editor_url + * - video_send_to_editor_url + * + * @since 3.5.0 + * + * @global WP_Post $post Global post object. + * @global WP_Embed $wp_embed + */ +function wp_ajax_send_link_to_editor() { + global $post, $wp_embed; + + check_ajax_referer( 'media-send-to-editor', 'nonce' ); + + $src = wp_unslash( $_POST['src'] ); + if ( ! $src ) { + wp_send_json_error(); + } + + if ( ! strpos( $src, '://' ) ) { + $src = 'http://' . $src; + } + + $src = sanitize_url( $src ); + if ( ! $src ) { + wp_send_json_error(); + } + + $link_text = trim( wp_unslash( $_POST['link_text'] ) ); + if ( ! $link_text ) { + $link_text = wp_basename( $src ); + } + + $post = get_post( isset( $_POST['post_id'] ) ? $_POST['post_id'] : 0 ); + + // Ping WordPress for an embed. + $check_embed = $wp_embed->run_shortcode( '[embed]' . $src . '[/embed]' ); + + // Fallback that WordPress creates when no oEmbed was found. + $fallback = $wp_embed->maybe_make_link( $src ); + + if ( $check_embed !== $fallback ) { + // TinyMCE view for [embed] will parse this. + $html = '[embed]' . $src . '[/embed]'; + } elseif ( $link_text ) { + $html = '' . $link_text . ''; + } else { + $html = ''; + } + + // Figure out what filter to run: + $type = 'file'; + $ext = preg_replace( '/^.+?\.([^.]+)$/', '$1', $src ); + if ( $ext ) { + $ext_type = wp_ext2type( $ext ); + if ( 'audio' === $ext_type || 'video' === $ext_type ) { + $type = $ext_type; + } + } + + /** This filter is documented in wp-admin/includes/media.php */ + $html = apply_filters( "{$type}_send_to_editor_url", $html, $src, $link_text ); + + wp_send_json_success( $html ); +} + +/** + * Handles the Heartbeat API via AJAX. + * + * Runs when the user is logged in. + * + * @since 3.6.0 + */ +function wp_ajax_heartbeat() { + if ( empty( $_POST['_nonce'] ) ) { + wp_send_json_error(); + } + + $response = array(); + $data = array(); + $nonce_state = wp_verify_nonce( $_POST['_nonce'], 'heartbeat-nonce' ); + + // 'screen_id' is the same as $current_screen->id and the JS global 'pagenow'. + if ( ! empty( $_POST['screen_id'] ) ) { + $screen_id = sanitize_key( $_POST['screen_id'] ); + } else { + $screen_id = 'front'; + } + + if ( ! empty( $_POST['data'] ) ) { + $data = wp_unslash( (array) $_POST['data'] ); + } + + if ( 1 !== $nonce_state ) { + /** + * Filters the nonces to send to the New/Edit Post screen. + * + * @since 4.3.0 + * + * @param array $response The Heartbeat response. + * @param array $data The $_POST data sent. + * @param string $screen_id The screen ID. + */ + $response = apply_filters( 'wp_refresh_nonces', $response, $data, $screen_id ); + + if ( false === $nonce_state ) { + // User is logged in but nonces have expired. + $response['nonces_expired'] = true; + wp_send_json( $response ); + } + } + + if ( ! empty( $data ) ) { + /** + * Filters the Heartbeat response received. + * + * @since 3.6.0 + * + * @param array $response The Heartbeat response. + * @param array $data The $_POST data sent. + * @param string $screen_id The screen ID. + */ + $response = apply_filters( 'heartbeat_received', $response, $data, $screen_id ); + } + + /** + * Filters the Heartbeat response sent. + * + * @since 3.6.0 + * + * @param array $response The Heartbeat response. + * @param string $screen_id The screen ID. + */ + $response = apply_filters( 'heartbeat_send', $response, $screen_id ); + + /** + * Fires when Heartbeat ticks in logged-in environments. + * + * Allows the transport to be easily replaced with long-polling. + * + * @since 3.6.0 + * + * @param array $response The Heartbeat response. + * @param string $screen_id The screen ID. + */ + do_action( 'heartbeat_tick', $response, $screen_id ); + + // Send the current time according to the server. + $response['server_time'] = time(); + + wp_send_json( $response ); +} + +/** + * Handles getting revision diffs via AJAX. + * + * @since 3.6.0 + */ +function wp_ajax_get_revision_diffs() { + require ABSPATH . 'wp-admin/includes/revision.php'; + + $post = get_post( (int) $_REQUEST['post_id'] ); + if ( ! $post ) { + wp_send_json_error(); + } + + if ( ! current_user_can( 'edit_post', $post->ID ) ) { + wp_send_json_error(); + } + + // Really just pre-loading the cache here. + $revisions = wp_get_post_revisions( $post->ID, array( 'check_enabled' => false ) ); + if ( ! $revisions ) { + wp_send_json_error(); + } + + $return = array(); + + if ( function_exists( 'set_time_limit' ) ) { + set_time_limit( 0 ); + } + + foreach ( $_REQUEST['compare'] as $compare_key ) { + list( $compare_from, $compare_to ) = explode( ':', $compare_key ); // from:to + + $return[] = array( + 'id' => $compare_key, + 'fields' => wp_get_revision_ui_diff( $post, $compare_from, $compare_to ), + ); + } + wp_send_json_success( $return ); +} + +/** + * Handles auto-saving the selected color scheme for + * a user's own profile via AJAX. + * + * @since 3.8.0 + * + * @global array $_wp_admin_css_colors + */ +function wp_ajax_save_user_color_scheme() { + global $_wp_admin_css_colors; + + check_ajax_referer( 'save-color-scheme', 'nonce' ); + + $color_scheme = sanitize_key( $_POST['color_scheme'] ); + + if ( ! isset( $_wp_admin_css_colors[ $color_scheme ] ) ) { + wp_send_json_error(); + } + + $previous_color_scheme = get_user_meta( get_current_user_id(), 'admin_color', true ); + update_user_meta( get_current_user_id(), 'admin_color', $color_scheme ); + + wp_send_json_success( + array( + 'previousScheme' => 'admin-color-' . $previous_color_scheme, + 'currentScheme' => 'admin-color-' . $color_scheme, + ) + ); +} + +/** + * Handles getting themes from themes_api() via AJAX. + * + * @since 3.9.0 + * + * @global array $themes_allowedtags + * @global array $theme_field_defaults + */ +function wp_ajax_query_themes() { + global $themes_allowedtags, $theme_field_defaults; + + if ( ! current_user_can( 'install_themes' ) ) { + wp_send_json_error(); + } + + $args = wp_parse_args( + wp_unslash( $_REQUEST['request'] ), + array( + 'per_page' => 20, + 'fields' => array_merge( + (array) $theme_field_defaults, + array( + 'reviews_url' => true, // Explicitly request the reviews URL to be linked from the Add Themes screen. + ) + ), + ) + ); + + if ( isset( $args['browse'] ) && 'favorites' === $args['browse'] && ! isset( $args['user'] ) ) { + $user = get_user_option( 'wporg_favorites' ); + if ( $user ) { + $args['user'] = $user; + } + } + + $old_filter = isset( $args['browse'] ) ? $args['browse'] : 'search'; + + /** This filter is documented in wp-admin/includes/class-wp-theme-install-list-table.php */ + $args = apply_filters( 'install_themes_table_api_args_' . $old_filter, $args ); + + $api = themes_api( 'query_themes', $args ); + + if ( is_wp_error( $api ) ) { + wp_send_json_error(); + } + + $update_php = network_admin_url( 'update.php?action=install-theme' ); + + $installed_themes = search_theme_directories(); + + if ( false === $installed_themes ) { + $installed_themes = array(); + } + + foreach ( $installed_themes as $theme_slug => $theme_data ) { + // Ignore child themes. + if ( str_contains( $theme_slug, '/' ) ) { + unset( $installed_themes[ $theme_slug ] ); + } + } + + foreach ( $api->themes as &$theme ) { + $theme->install_url = add_query_arg( + array( + 'theme' => $theme->slug, + '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ), + ), + $update_php + ); + + if ( current_user_can( 'switch_themes' ) ) { + if ( is_multisite() ) { + $theme->activate_url = add_query_arg( + array( + 'action' => 'enable', + '_wpnonce' => wp_create_nonce( 'enable-theme_' . $theme->slug ), + 'theme' => $theme->slug, + ), + network_admin_url( 'themes.php' ) + ); + } else { + $theme->activate_url = add_query_arg( + array( + 'action' => 'activate', + '_wpnonce' => wp_create_nonce( 'switch-theme_' . $theme->slug ), + 'stylesheet' => $theme->slug, + ), + admin_url( 'themes.php' ) + ); + } + } + + $is_theme_installed = array_key_exists( $theme->slug, $installed_themes ); + + // We only care about installed themes. + $theme->block_theme = $is_theme_installed && wp_get_theme( $theme->slug )->is_block_theme(); + + if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) { + $customize_url = $theme->block_theme ? admin_url( 'site-editor.php' ) : wp_customize_url( $theme->slug ); + + $theme->customize_url = add_query_arg( + array( + 'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ), + ), + $customize_url + ); + } + + $theme->name = wp_kses( $theme->name, $themes_allowedtags ); + $theme->author = wp_kses( $theme->author['display_name'], $themes_allowedtags ); + $theme->version = wp_kses( $theme->version, $themes_allowedtags ); + $theme->description = wp_kses( $theme->description, $themes_allowedtags ); + + $theme->stars = wp_star_rating( + array( + 'rating' => $theme->rating, + 'type' => 'percent', + 'number' => $theme->num_ratings, + 'echo' => false, + ) + ); + + $theme->num_ratings = number_format_i18n( $theme->num_ratings ); + $theme->preview_url = set_url_scheme( $theme->preview_url ); + $theme->compatible_wp = is_wp_version_compatible( $theme->requires ); + $theme->compatible_php = is_php_version_compatible( $theme->requires_php ); + } + + wp_send_json_success( $api ); +} + +/** + * Applies [embed] Ajax handlers to a string. + * + * @since 4.0.0 + * + * @global WP_Post $post Global post object. + * @global WP_Embed $wp_embed Embed API instance. + * @global WP_Scripts $wp_scripts + * @global int $content_width + */ +function wp_ajax_parse_embed() { + global $post, $wp_embed, $content_width; + + if ( empty( $_POST['shortcode'] ) ) { + wp_send_json_error(); + } + + $post_id = isset( $_POST['post_ID'] ) ? (int) $_POST['post_ID'] : 0; + + if ( $post_id > 0 ) { + $post = get_post( $post_id ); + + if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) { + wp_send_json_error(); + } + setup_postdata( $post ); + } elseif ( ! current_user_can( 'edit_posts' ) ) { // See WP_oEmbed_Controller::get_proxy_item_permissions_check(). + wp_send_json_error(); + } + + $shortcode = wp_unslash( $_POST['shortcode'] ); + + preg_match( '/' . get_shortcode_regex() . '/s', $shortcode, $matches ); + $atts = shortcode_parse_atts( $matches[3] ); + + if ( ! empty( $matches[5] ) ) { + $url = $matches[5]; + } elseif ( ! empty( $atts['src'] ) ) { + $url = $atts['src']; + } else { + $url = ''; + } + + $parsed = false; + $wp_embed->return_false_on_fail = true; + + if ( 0 === $post_id ) { + /* + * Refresh oEmbeds cached outside of posts that are past their TTL. + * Posts are excluded because they have separate logic for refreshing + * their post meta caches. See WP_Embed::cache_oembed(). + */ + $wp_embed->usecache = false; + } + + if ( is_ssl() && str_starts_with( $url, 'http://' ) ) { + /* + * Admin is ssl and the user pasted non-ssl URL. + * Check if the provider supports ssl embeds and use that for the preview. + */ + $ssl_shortcode = preg_replace( '%^(\\[embed[^\\]]*\\])http://%i', '$1https://', $shortcode ); + $parsed = $wp_embed->run_shortcode( $ssl_shortcode ); + + if ( ! $parsed ) { + $no_ssl_support = true; + } + } + + // Set $content_width so any embeds fit in the destination iframe. + if ( isset( $_POST['maxwidth'] ) && is_numeric( $_POST['maxwidth'] ) && $_POST['maxwidth'] > 0 ) { + if ( ! isset( $content_width ) ) { + $content_width = (int) $_POST['maxwidth']; + } else { + $content_width = min( $content_width, (int) $_POST['maxwidth'] ); + } + } + + if ( $url && ! $parsed ) { + $parsed = $wp_embed->run_shortcode( $shortcode ); + } + + if ( ! $parsed ) { + wp_send_json_error( + array( + 'type' => 'not-embeddable', + /* translators: %s: URL that could not be embedded. */ + 'message' => sprintf( __( '%s failed to embed.' ), '' . esc_html( $url ) . '' ), + ) + ); + } + + if ( has_shortcode( $parsed, 'audio' ) || has_shortcode( $parsed, 'video' ) ) { + $styles = ''; + $mce_styles = wpview_media_sandbox_styles(); + + foreach ( $mce_styles as $style ) { + $styles .= sprintf( '', $style ); + } + + $html = do_shortcode( $parsed ); + + global $wp_scripts; + + if ( ! empty( $wp_scripts ) ) { + $wp_scripts->done = array(); + } + + ob_start(); + wp_print_scripts( array( 'mediaelement-vimeo', 'wp-mediaelement' ) ); + $scripts = ob_get_clean(); + + $parsed = $styles . $html . $scripts; + } + + if ( ! empty( $no_ssl_support ) || ( is_ssl() && ( preg_match( '%<(iframe|script|embed) [^>]*src="http://%', $parsed ) || + preg_match( '%]*href="http://%', $parsed ) ) ) ) { + // Admin is ssl and the embed is not. Iframes, scripts, and other "active content" will be blocked. + wp_send_json_error( + array( + 'type' => 'not-ssl', + 'message' => __( 'This preview is unavailable in the editor.' ), + ) + ); + } + + $return = array( + 'body' => $parsed, + 'attr' => $wp_embed->last_attr, + ); + + if ( str_contains( $parsed, 'class="wp-embedded-content' ) ) { + if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { + $script_src = includes_url( 'js/wp-embed.js' ); + } else { + $script_src = includes_url( 'js/wp-embed.min.js' ); + } + + $return['head'] = ''; + $return['sandbox'] = true; + } + + wp_send_json_success( $return ); +} + +/** + * @since 4.0.0 + * + * @global WP_Post $post Global post object. + * @global WP_Scripts $wp_scripts + */ +function wp_ajax_parse_media_shortcode() { + global $post, $wp_scripts; + + if ( empty( $_POST['shortcode'] ) ) { + wp_send_json_error(); + } + + $shortcode = wp_unslash( $_POST['shortcode'] ); + + // Only process previews for media related shortcodes: + $found_shortcodes = get_shortcode_tags_in_content( $shortcode ); + $media_shortcodes = array( + 'audio', + 'embed', + 'playlist', + 'video', + 'gallery', + ); + + $other_shortcodes = array_diff( $found_shortcodes, $media_shortcodes ); + + if ( ! empty( $other_shortcodes ) ) { + wp_send_json_error(); + } + + if ( ! empty( $_POST['post_ID'] ) ) { + $post = get_post( (int) $_POST['post_ID'] ); + } + + // The embed shortcode requires a post. + if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) { + if ( in_array( 'embed', $found_shortcodes, true ) ) { + wp_send_json_error(); + } + } else { + setup_postdata( $post ); + } + + $parsed = do_shortcode( $shortcode ); + + if ( empty( $parsed ) ) { + wp_send_json_error( + array( + 'type' => 'no-items', + 'message' => __( 'No items found.' ), + ) + ); + } + + $head = ''; + $styles = wpview_media_sandbox_styles(); + + foreach ( $styles as $style ) { + $head .= ''; + } + + if ( ! empty( $wp_scripts ) ) { + $wp_scripts->done = array(); + } + + ob_start(); + + echo $parsed; + + if ( 'playlist' === $_REQUEST['type'] ) { + wp_underscore_playlist_templates(); + + wp_print_scripts( 'wp-playlist' ); + } else { + wp_print_scripts( array( 'mediaelement-vimeo', 'wp-mediaelement' ) ); + } + + wp_send_json_success( + array( + 'head' => $head, + 'body' => ob_get_clean(), + ) + ); +} + +/** + * Handles destroying multiple open sessions for a user via AJAX. + * + * @since 4.1.0 + */ +function wp_ajax_destroy_sessions() { + $user = get_userdata( (int) $_POST['user_id'] ); + + if ( $user ) { + if ( ! current_user_can( 'edit_user', $user->ID ) ) { + $user = false; + } elseif ( ! wp_verify_nonce( $_POST['nonce'], 'update-user_' . $user->ID ) ) { + $user = false; + } + } + + if ( ! $user ) { + wp_send_json_error( + array( + 'message' => __( 'Could not log out user sessions. Please try again.' ), + ) + ); + } + + $sessions = WP_Session_Tokens::get_instance( $user->ID ); + + if ( get_current_user_id() === $user->ID ) { + $sessions->destroy_others( wp_get_session_token() ); + $message = __( 'You are now logged out everywhere else.' ); + } else { + $sessions->destroy_all(); + /* translators: %s: User's display name. */ + $message = sprintf( __( '%s has been logged out.' ), $user->display_name ); + } + + wp_send_json_success( array( 'message' => $message ) ); +} + +/** + * Handles cropping an image via AJAX. + * + * @since 4.3.0 + */ +function wp_ajax_crop_image() { + $attachment_id = absint( $_POST['id'] ); + + check_ajax_referer( 'image_editor-' . $attachment_id, 'nonce' ); + + if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) { + wp_send_json_error(); + } + + $context = str_replace( '_', '-', $_POST['context'] ); + $data = array_map( 'absint', $_POST['cropDetails'] ); + $cropped = wp_crop_image( $attachment_id, $data['x1'], $data['y1'], $data['width'], $data['height'], $data['dst_width'], $data['dst_height'] ); + + if ( ! $cropped || is_wp_error( $cropped ) ) { + wp_send_json_error( array( 'message' => __( 'Image could not be processed.' ) ) ); + } + + switch ( $context ) { + case 'site-icon': + require_once ABSPATH . 'wp-admin/includes/class-wp-site-icon.php'; + $wp_site_icon = new WP_Site_Icon(); + + // Skip creating a new attachment if the attachment is a Site Icon. + if ( get_post_meta( $attachment_id, '_wp_attachment_context', true ) == $context ) { + + // Delete the temporary cropped file, we don't need it. + wp_delete_file( $cropped ); + + // Additional sizes in wp_prepare_attachment_for_js(). + add_filter( 'image_size_names_choose', array( $wp_site_icon, 'additional_sizes' ) ); + break; + } + + /** This filter is documented in wp-admin/includes/class-custom-image-header.php */ + $cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication. + $attachment = $wp_site_icon->create_attachment_object( $cropped, $attachment_id ); + unset( $attachment['ID'] ); + + // Update the attachment. + add_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) ); + $attachment_id = $wp_site_icon->insert_attachment( $attachment, $cropped ); + remove_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) ); + + // Additional sizes in wp_prepare_attachment_for_js(). + add_filter( 'image_size_names_choose', array( $wp_site_icon, 'additional_sizes' ) ); + break; + + default: + /** + * Fires before a cropped image is saved. + * + * Allows to add filters to modify the way a cropped image is saved. + * + * @since 4.3.0 + * + * @param string $context The Customizer control requesting the cropped image. + * @param int $attachment_id The attachment ID of the original image. + * @param string $cropped Path to the cropped image file. + */ + do_action( 'wp_ajax_crop_image_pre_save', $context, $attachment_id, $cropped ); + + /** This filter is documented in wp-admin/includes/class-custom-image-header.php */ + $cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication. + + $parent_url = wp_get_attachment_url( $attachment_id ); + $parent_basename = wp_basename( $parent_url ); + $url = str_replace( $parent_basename, wp_basename( $cropped ), $parent_url ); + + $size = wp_getimagesize( $cropped ); + $image_type = ( $size ) ? $size['mime'] : 'image/jpeg'; + + // Get the original image's post to pre-populate the cropped image. + $original_attachment = get_post( $attachment_id ); + $sanitized_post_title = sanitize_file_name( $original_attachment->post_title ); + $use_original_title = ( + ( '' !== trim( $original_attachment->post_title ) ) && + /* + * Check if the original image has a title other than the "filename" default, + * meaning the image had a title when originally uploaded or its title was edited. + */ + ( $parent_basename !== $sanitized_post_title ) && + ( pathinfo( $parent_basename, PATHINFO_FILENAME ) !== $sanitized_post_title ) + ); + $use_original_description = ( '' !== trim( $original_attachment->post_content ) ); + + $attachment = array( + 'post_title' => $use_original_title ? $original_attachment->post_title : wp_basename( $cropped ), + 'post_content' => $use_original_description ? $original_attachment->post_content : $url, + 'post_mime_type' => $image_type, + 'guid' => $url, + 'context' => $context, + ); + + // Copy the image caption attribute (post_excerpt field) from the original image. + if ( '' !== trim( $original_attachment->post_excerpt ) ) { + $attachment['post_excerpt'] = $original_attachment->post_excerpt; + } + + // Copy the image alt text attribute from the original image. + if ( '' !== trim( $original_attachment->_wp_attachment_image_alt ) ) { + $attachment['meta_input'] = array( + '_wp_attachment_image_alt' => wp_slash( $original_attachment->_wp_attachment_image_alt ), + ); + } + + $attachment_id = wp_insert_attachment( $attachment, $cropped ); + $metadata = wp_generate_attachment_metadata( $attachment_id, $cropped ); + + /** + * Filters the cropped image attachment metadata. + * + * @since 4.3.0 + * + * @see wp_generate_attachment_metadata() + * + * @param array $metadata Attachment metadata. + */ + $metadata = apply_filters( 'wp_ajax_cropped_attachment_metadata', $metadata ); + wp_update_attachment_metadata( $attachment_id, $metadata ); + + /** + * Filters the attachment ID for a cropped image. + * + * @since 4.3.0 + * + * @param int $attachment_id The attachment ID of the cropped image. + * @param string $context The Customizer control requesting the cropped image. + */ + $attachment_id = apply_filters( 'wp_ajax_cropped_attachment_id', $attachment_id, $context ); + } + + wp_send_json_success( wp_prepare_attachment_for_js( $attachment_id ) ); +} + +/** + * Handles generating a password via AJAX. + * + * @since 4.4.0 + */ +function wp_ajax_generate_password() { + wp_send_json_success( wp_generate_password( 24 ) ); +} + +/** + * Handles generating a password in the no-privilege context via AJAX. + * + * @since 5.7.0 + */ +function wp_ajax_nopriv_generate_password() { + wp_send_json_success( wp_generate_password( 24 ) ); +} + +/** + * Handles saving the user's WordPress.org username via AJAX. + * + * @since 4.4.0 + */ +function wp_ajax_save_wporg_username() { + if ( ! current_user_can( 'install_themes' ) && ! current_user_can( 'install_plugins' ) ) { + wp_send_json_error(); + } + + check_ajax_referer( 'save_wporg_username_' . get_current_user_id() ); + + $username = isset( $_REQUEST['username'] ) ? wp_unslash( $_REQUEST['username'] ) : false; + + if ( ! $username ) { + wp_send_json_error(); + } + + wp_send_json_success( update_user_meta( get_current_user_id(), 'wporg_favorites', $username ) ); +} + +/** + * Handles installing a theme via AJAX. + * + * @since 4.6.0 + * + * @see Theme_Upgrader + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + */ +function wp_ajax_install_theme() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) ) { + wp_send_json_error( + array( + 'slug' => '', + 'errorCode' => 'no_theme_specified', + 'errorMessage' => __( 'No theme specified.' ), + ) + ); + } + + $slug = sanitize_key( wp_unslash( $_POST['slug'] ) ); + + $status = array( + 'install' => 'theme', + 'slug' => $slug, + ); + + if ( ! current_user_can( 'install_themes' ) ) { + $status['errorMessage'] = __( 'Sorry, you are not allowed to install themes on this site.' ); + wp_send_json_error( $status ); + } + + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + require_once ABSPATH . 'wp-admin/includes/theme.php'; + + $api = themes_api( + 'theme_information', + array( + 'slug' => $slug, + 'fields' => array( 'sections' => false ), + ) + ); + + if ( is_wp_error( $api ) ) { + $status['errorMessage'] = $api->get_error_message(); + wp_send_json_error( $status ); + } + + $skin = new WP_Ajax_Upgrader_Skin(); + $upgrader = new Theme_Upgrader( $skin ); + $result = $upgrader->install( $api->download_link ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $status['debug'] = $skin->get_upgrade_messages(); + } + + if ( is_wp_error( $result ) ) { + $status['errorCode'] = $result->get_error_code(); + $status['errorMessage'] = $result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( is_wp_error( $skin->result ) ) { + $status['errorCode'] = $skin->result->get_error_code(); + $status['errorMessage'] = $skin->result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( $skin->get_errors()->has_errors() ) { + $status['errorMessage'] = $skin->get_error_messages(); + wp_send_json_error( $status ); + } elseif ( is_null( $result ) ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + $status['themeName'] = wp_get_theme( $slug )->get( 'Name' ); + + if ( current_user_can( 'switch_themes' ) ) { + if ( is_multisite() ) { + $status['activateUrl'] = add_query_arg( + array( + 'action' => 'enable', + '_wpnonce' => wp_create_nonce( 'enable-theme_' . $slug ), + 'theme' => $slug, + ), + network_admin_url( 'themes.php' ) + ); + } else { + $status['activateUrl'] = add_query_arg( + array( + 'action' => 'activate', + '_wpnonce' => wp_create_nonce( 'switch-theme_' . $slug ), + 'stylesheet' => $slug, + ), + admin_url( 'themes.php' ) + ); + } + } + + $theme = wp_get_theme( $slug ); + $status['blockTheme'] = $theme->is_block_theme(); + + if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) { + $status['customizeUrl'] = add_query_arg( + array( + 'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ), + ), + wp_customize_url( $slug ) + ); + } + + /* + * See WP_Theme_Install_List_Table::_get_theme_status() if we wanted to check + * on post-installation status. + */ + wp_send_json_success( $status ); +} + +/** + * Handles updating a theme via AJAX. + * + * @since 4.6.0 + * + * @see Theme_Upgrader + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + */ +function wp_ajax_update_theme() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) ) { + wp_send_json_error( + array( + 'slug' => '', + 'errorCode' => 'no_theme_specified', + 'errorMessage' => __( 'No theme specified.' ), + ) + ); + } + + $stylesheet = preg_replace( '/[^A-z0-9_\-]/', '', wp_unslash( $_POST['slug'] ) ); + $status = array( + 'update' => 'theme', + 'slug' => $stylesheet, + 'oldVersion' => '', + 'newVersion' => '', + ); + + if ( ! current_user_can( 'update_themes' ) ) { + $status['errorMessage'] = __( 'Sorry, you are not allowed to update themes for this site.' ); + wp_send_json_error( $status ); + } + + $theme = wp_get_theme( $stylesheet ); + if ( $theme->exists() ) { + $status['oldVersion'] = $theme->get( 'Version' ); + } + + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + + $current = get_site_transient( 'update_themes' ); + if ( empty( $current ) ) { + wp_update_themes(); + } + + $skin = new WP_Ajax_Upgrader_Skin(); + $upgrader = new Theme_Upgrader( $skin ); + $result = $upgrader->bulk_upgrade( array( $stylesheet ) ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $status['debug'] = $skin->get_upgrade_messages(); + } + + if ( is_wp_error( $skin->result ) ) { + $status['errorCode'] = $skin->result->get_error_code(); + $status['errorMessage'] = $skin->result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( $skin->get_errors()->has_errors() ) { + $status['errorMessage'] = $skin->get_error_messages(); + wp_send_json_error( $status ); + } elseif ( is_array( $result ) && ! empty( $result[ $stylesheet ] ) ) { + + // Theme is already at the latest version. + if ( true === $result[ $stylesheet ] ) { + $status['errorMessage'] = $upgrader->strings['up_to_date']; + wp_send_json_error( $status ); + } + + $theme = wp_get_theme( $stylesheet ); + if ( $theme->exists() ) { + $status['newVersion'] = $theme->get( 'Version' ); + } + + wp_send_json_success( $status ); + } elseif ( false === $result ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + // An unhandled error occurred. + $status['errorMessage'] = __( 'Theme update failed.' ); + wp_send_json_error( $status ); +} + +/** + * Handles deleting a theme via AJAX. + * + * @since 4.6.0 + * + * @see delete_theme() + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + */ +function wp_ajax_delete_theme() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) ) { + wp_send_json_error( + array( + 'slug' => '', + 'errorCode' => 'no_theme_specified', + 'errorMessage' => __( 'No theme specified.' ), + ) + ); + } + + $stylesheet = preg_replace( '/[^A-z0-9_\-]/', '', wp_unslash( $_POST['slug'] ) ); + $status = array( + 'delete' => 'theme', + 'slug' => $stylesheet, + ); + + if ( ! current_user_can( 'delete_themes' ) ) { + $status['errorMessage'] = __( 'Sorry, you are not allowed to delete themes on this site.' ); + wp_send_json_error( $status ); + } + + if ( ! wp_get_theme( $stylesheet )->exists() ) { + $status['errorMessage'] = __( 'The requested theme does not exist.' ); + wp_send_json_error( $status ); + } + + // Check filesystem credentials. `delete_theme()` will bail otherwise. + $url = wp_nonce_url( 'themes.php?action=delete&stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet ); + + ob_start(); + $credentials = request_filesystem_credentials( $url ); + ob_end_clean(); + + if ( false === $credentials || ! WP_Filesystem( $credentials ) ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + require_once ABSPATH . 'wp-admin/includes/theme.php'; + + $result = delete_theme( $stylesheet ); + + if ( is_wp_error( $result ) ) { + $status['errorMessage'] = $result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( false === $result ) { + $status['errorMessage'] = __( 'Theme could not be deleted.' ); + wp_send_json_error( $status ); + } + + wp_send_json_success( $status ); +} + +/** + * Handles installing a plugin via AJAX. + * + * @since 4.6.0 + * + * @see Plugin_Upgrader + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + */ +function wp_ajax_install_plugin() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) ) { + wp_send_json_error( + array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => __( 'No plugin specified.' ), + ) + ); + } + + $status = array( + 'install' => 'plugin', + 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), + ); + + if ( ! current_user_can( 'install_plugins' ) ) { + $status['errorMessage'] = __( 'Sorry, you are not allowed to install plugins on this site.' ); + wp_send_json_error( $status ); + } + + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + + $api = plugins_api( + 'plugin_information', + array( + 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), + 'fields' => array( + 'sections' => false, + ), + ) + ); + + if ( is_wp_error( $api ) ) { + $status['errorMessage'] = $api->get_error_message(); + wp_send_json_error( $status ); + } + + $status['pluginName'] = $api->name; + + $skin = new WP_Ajax_Upgrader_Skin(); + $upgrader = new Plugin_Upgrader( $skin ); + $result = $upgrader->install( $api->download_link ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $status['debug'] = $skin->get_upgrade_messages(); + } + + if ( is_wp_error( $result ) ) { + $status['errorCode'] = $result->get_error_code(); + $status['errorMessage'] = $result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( is_wp_error( $skin->result ) ) { + $status['errorCode'] = $skin->result->get_error_code(); + $status['errorMessage'] = $skin->result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( $skin->get_errors()->has_errors() ) { + $status['errorMessage'] = $skin->get_error_messages(); + wp_send_json_error( $status ); + } elseif ( is_null( $result ) ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + $install_status = install_plugin_install_status( $api ); + $pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : ''; + + // If installation request is coming from import page, do not return network activation link. + $plugins_url = ( 'import' === $pagenow ) ? admin_url( 'plugins.php' ) : network_admin_url( 'plugins.php' ); + + if ( current_user_can( 'activate_plugin', $install_status['file'] ) && is_plugin_inactive( $install_status['file'] ) ) { + $status['activateUrl'] = add_query_arg( + array( + '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $install_status['file'] ), + 'action' => 'activate', + 'plugin' => $install_status['file'], + ), + $plugins_url + ); + } + + if ( is_multisite() && current_user_can( 'manage_network_plugins' ) && 'import' !== $pagenow ) { + $status['activateUrl'] = add_query_arg( array( 'networkwide' => 1 ), $status['activateUrl'] ); + } + + wp_send_json_success( $status ); +} + +/** + * Handles updating a plugin via AJAX. + * + * @since 4.2.0 + * + * @see Plugin_Upgrader + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + */ +function wp_ajax_update_plugin() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['plugin'] ) || empty( $_POST['slug'] ) ) { + wp_send_json_error( + array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => __( 'No plugin specified.' ), + ) + ); + } + + $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) ); + + $status = array( + 'update' => 'plugin', + 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), + 'oldVersion' => '', + 'newVersion' => '', + ); + + if ( ! current_user_can( 'update_plugins' ) || 0 !== validate_file( $plugin ) ) { + $status['errorMessage'] = __( 'Sorry, you are not allowed to update plugins for this site.' ); + wp_send_json_error( $status ); + } + + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + $status['plugin'] = $plugin; + $status['pluginName'] = $plugin_data['Name']; + + if ( $plugin_data['Version'] ) { + /* translators: %s: Plugin version. */ + $status['oldVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] ); + } + + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + + wp_update_plugins(); + + $skin = new WP_Ajax_Upgrader_Skin(); + $upgrader = new Plugin_Upgrader( $skin ); + $result = $upgrader->bulk_upgrade( array( $plugin ) ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $status['debug'] = $skin->get_upgrade_messages(); + } + + if ( is_wp_error( $skin->result ) ) { + $status['errorCode'] = $skin->result->get_error_code(); + $status['errorMessage'] = $skin->result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( $skin->get_errors()->has_errors() ) { + $status['errorMessage'] = $skin->get_error_messages(); + wp_send_json_error( $status ); + } elseif ( is_array( $result ) && ! empty( $result[ $plugin ] ) ) { + + /* + * Plugin is already at the latest version. + * + * This may also be the return value if the `update_plugins` site transient is empty, + * e.g. when you update two plugins in quick succession before the transient repopulates. + * + * Preferably something can be done to ensure `update_plugins` isn't empty. + * For now, surface some sort of error here. + */ + if ( true === $result[ $plugin ] ) { + $status['errorMessage'] = $upgrader->strings['up_to_date']; + wp_send_json_error( $status ); + } + + $plugin_data = get_plugins( '/' . $result[ $plugin ]['destination_name'] ); + $plugin_data = reset( $plugin_data ); + + if ( $plugin_data['Version'] ) { + /* translators: %s: Plugin version. */ + $status['newVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] ); + } + + wp_send_json_success( $status ); + } elseif ( false === $result ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + // An unhandled error occurred. + $status['errorMessage'] = __( 'Plugin update failed.' ); + wp_send_json_error( $status ); +} + +/** + * Handles deleting a plugin via AJAX. + * + * @since 4.6.0 + * + * @see delete_plugins() + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + */ +function wp_ajax_delete_plugin() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) { + wp_send_json_error( + array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => __( 'No plugin specified.' ), + ) + ); + } + + $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) ); + + $status = array( + 'delete' => 'plugin', + 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), + ); + + if ( ! current_user_can( 'delete_plugins' ) || 0 !== validate_file( $plugin ) ) { + $status['errorMessage'] = __( 'Sorry, you are not allowed to delete plugins for this site.' ); + wp_send_json_error( $status ); + } + + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + $status['plugin'] = $plugin; + $status['pluginName'] = $plugin_data['Name']; + + if ( is_plugin_active( $plugin ) ) { + $status['errorMessage'] = __( 'You cannot delete a plugin while it is active on the main site.' ); + wp_send_json_error( $status ); + } + + // Check filesystem credentials. `delete_plugins()` will bail otherwise. + $url = wp_nonce_url( 'plugins.php?action=delete-selected&verify-delete=1&checked[]=' . $plugin, 'bulk-plugins' ); + + ob_start(); + $credentials = request_filesystem_credentials( $url ); + ob_end_clean(); + + if ( false === $credentials || ! WP_Filesystem( $credentials ) ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + $result = delete_plugins( array( $plugin ) ); + + if ( is_wp_error( $result ) ) { + $status['errorMessage'] = $result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( false === $result ) { + $status['errorMessage'] = __( 'Plugin could not be deleted.' ); + wp_send_json_error( $status ); + } + + wp_send_json_success( $status ); +} + +/** + * Handles searching plugins via AJAX. + * + * @since 4.6.0 + * + * @global string $s Search term. + */ +function wp_ajax_search_plugins() { + check_ajax_referer( 'updates' ); + + // Ensure after_plugin_row_{$plugin_file} gets hooked. + wp_plugin_update_rows(); + + $pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : ''; + if ( 'plugins-network' === $pagenow || 'plugins' === $pagenow ) { + set_current_screen( $pagenow ); + } + + /** @var WP_Plugins_List_Table $wp_list_table */ + $wp_list_table = _get_list_table( + 'WP_Plugins_List_Table', + array( + 'screen' => get_current_screen(), + ) + ); + + $status = array(); + + if ( ! $wp_list_table->ajax_user_can() ) { + $status['errorMessage'] = __( 'Sorry, you are not allowed to manage plugins for this site.' ); + wp_send_json_error( $status ); + } + + // Set the correct requester, so pagination works. + $_SERVER['REQUEST_URI'] = add_query_arg( + array_diff_key( + $_POST, + array( + '_ajax_nonce' => null, + 'action' => null, + ) + ), + network_admin_url( 'plugins.php', 'relative' ) + ); + + $GLOBALS['s'] = wp_unslash( $_POST['s'] ); + + $wp_list_table->prepare_items(); + + ob_start(); + $wp_list_table->display(); + $status['count'] = count( $wp_list_table->items ); + $status['items'] = ob_get_clean(); + + wp_send_json_success( $status ); +} + +/** + * Handles searching plugins to install via AJAX. + * + * @since 4.6.0 + */ +function wp_ajax_search_install_plugins() { + check_ajax_referer( 'updates' ); + + $pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : ''; + if ( 'plugin-install-network' === $pagenow || 'plugin-install' === $pagenow ) { + set_current_screen( $pagenow ); + } + + /** @var WP_Plugin_Install_List_Table $wp_list_table */ + $wp_list_table = _get_list_table( + 'WP_Plugin_Install_List_Table', + array( + 'screen' => get_current_screen(), + ) + ); + + $status = array(); + + if ( ! $wp_list_table->ajax_user_can() ) { + $status['errorMessage'] = __( 'Sorry, you are not allowed to manage plugins for this site.' ); + wp_send_json_error( $status ); + } + + // Set the correct requester, so pagination works. + $_SERVER['REQUEST_URI'] = add_query_arg( + array_diff_key( + $_POST, + array( + '_ajax_nonce' => null, + 'action' => null, + ) + ), + network_admin_url( 'plugin-install.php', 'relative' ) + ); + + $wp_list_table->prepare_items(); + + ob_start(); + $wp_list_table->display(); + $status['count'] = (int) $wp_list_table->get_pagination_arg( 'total_items' ); + $status['items'] = ob_get_clean(); + + wp_send_json_success( $status ); +} + +/** + * Handles editing a theme or plugin file via AJAX. + * + * @since 4.9.0 + * + * @see wp_edit_theme_plugin_file() + */ +function wp_ajax_edit_theme_plugin_file() { + $r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) ); // Validation of args is done in wp_edit_theme_plugin_file(). + + if ( is_wp_error( $r ) ) { + wp_send_json_error( + array_merge( + array( + 'code' => $r->get_error_code(), + 'message' => $r->get_error_message(), + ), + (array) $r->get_error_data() + ) + ); + } else { + wp_send_json_success( + array( + 'message' => __( 'File edited successfully.' ), + ) + ); + } +} + +/** + * Handles exporting a user's personal data via AJAX. + * + * @since 4.9.6 + */ +function wp_ajax_wp_privacy_export_personal_data() { + + if ( empty( $_POST['id'] ) ) { + wp_send_json_error( __( 'Missing request ID.' ) ); + } + + $request_id = (int) $_POST['id']; + + if ( $request_id < 1 ) { + wp_send_json_error( __( 'Invalid request ID.' ) ); + } + + if ( ! current_user_can( 'export_others_personal_data' ) ) { + wp_send_json_error( __( 'Sorry, you are not allowed to perform this action.' ) ); + } + + check_ajax_referer( 'wp-privacy-export-personal-data-' . $request_id, 'security' ); + + // Get the request. + $request = wp_get_user_request( $request_id ); + + if ( ! $request || 'export_personal_data' !== $request->action_name ) { + wp_send_json_error( __( 'Invalid request type.' ) ); + } + + $email_address = $request->email; + if ( ! is_email( $email_address ) ) { + wp_send_json_error( __( 'A valid email address must be given.' ) ); + } + + if ( ! isset( $_POST['exporter'] ) ) { + wp_send_json_error( __( 'Missing exporter index.' ) ); + } + + $exporter_index = (int) $_POST['exporter']; + + if ( ! isset( $_POST['page'] ) ) { + wp_send_json_error( __( 'Missing page index.' ) ); + } + + $page = (int) $_POST['page']; + + $send_as_email = isset( $_POST['sendAsEmail'] ) ? ( 'true' === $_POST['sendAsEmail'] ) : false; + + /** + * Filters the array of exporter callbacks. + * + * @since 4.9.6 + * + * @param array $args { + * An array of callable exporters of personal data. Default empty array. + * + * @type array ...$0 { + * Array of personal data exporters. + * + * @type callable $callback Callable exporter function that accepts an + * email address and a page number and returns an + * array of name => value pairs of personal data. + * @type string $exporter_friendly_name Translated user facing friendly name for the + * exporter. + * } + * } + */ + $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); + + if ( ! is_array( $exporters ) ) { + wp_send_json_error( __( 'An exporter has improperly used the registration filter.' ) ); + } + + // Do we have any registered exporters? + if ( 0 < count( $exporters ) ) { + if ( $exporter_index < 1 ) { + wp_send_json_error( __( 'Exporter index cannot be negative.' ) ); + } + + if ( $exporter_index > count( $exporters ) ) { + wp_send_json_error( __( 'Exporter index is out of range.' ) ); + } + + if ( $page < 1 ) { + wp_send_json_error( __( 'Page index cannot be less than one.' ) ); + } + + $exporter_keys = array_keys( $exporters ); + $exporter_key = $exporter_keys[ $exporter_index - 1 ]; + $exporter = $exporters[ $exporter_key ]; + + if ( ! is_array( $exporter ) ) { + wp_send_json_error( + /* translators: %s: Exporter array index. */ + sprintf( __( 'Expected an array describing the exporter at index %s.' ), $exporter_key ) + ); + } + + if ( ! array_key_exists( 'exporter_friendly_name', $exporter ) ) { + wp_send_json_error( + /* translators: %s: Exporter array index. */ + sprintf( __( 'Exporter array at index %s does not include a friendly name.' ), $exporter_key ) + ); + } + + $exporter_friendly_name = $exporter['exporter_friendly_name']; + + if ( ! array_key_exists( 'callback', $exporter ) ) { + wp_send_json_error( + /* translators: %s: Exporter friendly name. */ + sprintf( __( 'Exporter does not include a callback: %s.' ), esc_html( $exporter_friendly_name ) ) + ); + } + + if ( ! is_callable( $exporter['callback'] ) ) { + wp_send_json_error( + /* translators: %s: Exporter friendly name. */ + sprintf( __( 'Exporter callback is not a valid callback: %s.' ), esc_html( $exporter_friendly_name ) ) + ); + } + + $callback = $exporter['callback']; + $response = call_user_func( $callback, $email_address, $page ); + + if ( is_wp_error( $response ) ) { + wp_send_json_error( $response ); + } + + if ( ! is_array( $response ) ) { + wp_send_json_error( + /* translators: %s: Exporter friendly name. */ + sprintf( __( 'Expected response as an array from exporter: %s.' ), esc_html( $exporter_friendly_name ) ) + ); + } + + if ( ! array_key_exists( 'data', $response ) ) { + wp_send_json_error( + /* translators: %s: Exporter friendly name. */ + sprintf( __( 'Expected data in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) ) + ); + } + + if ( ! is_array( $response['data'] ) ) { + wp_send_json_error( + /* translators: %s: Exporter friendly name. */ + sprintf( __( 'Expected data array in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) ) + ); + } + + if ( ! array_key_exists( 'done', $response ) ) { + wp_send_json_error( + /* translators: %s: Exporter friendly name. */ + sprintf( __( 'Expected done (boolean) in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) ) + ); + } + } else { + // No exporters, so we're done. + $exporter_key = ''; + + $response = array( + 'data' => array(), + 'done' => true, + ); + } + + /** + * Filters a page of personal data exporter data. Used to build the export report. + * + * Allows the export response to be consumed by destinations in addition to Ajax. + * + * @since 4.9.6 + * + * @param array $response The personal data for the given exporter and page number. + * @param int $exporter_index The index of the exporter that provided this data. + * @param string $email_address The email address associated with this personal data. + * @param int $page The page number for this response. + * @param int $request_id The privacy request post ID associated with this request. + * @param bool $send_as_email Whether the final results of the export should be emailed to the user. + * @param string $exporter_key The key (slug) of the exporter that provided this data. + */ + $response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ); + + if ( is_wp_error( $response ) ) { + wp_send_json_error( $response ); + } + + wp_send_json_success( $response ); +} + +/** + * Handles erasing personal data via AJAX. + * + * @since 4.9.6 + */ +function wp_ajax_wp_privacy_erase_personal_data() { + + if ( empty( $_POST['id'] ) ) { + wp_send_json_error( __( 'Missing request ID.' ) ); + } + + $request_id = (int) $_POST['id']; + + if ( $request_id < 1 ) { + wp_send_json_error( __( 'Invalid request ID.' ) ); + } + + // Both capabilities are required to avoid confusion, see `_wp_personal_data_removal_page()`. + if ( ! current_user_can( 'erase_others_personal_data' ) || ! current_user_can( 'delete_users' ) ) { + wp_send_json_error( __( 'Sorry, you are not allowed to perform this action.' ) ); + } + + check_ajax_referer( 'wp-privacy-erase-personal-data-' . $request_id, 'security' ); + + // Get the request. + $request = wp_get_user_request( $request_id ); + + if ( ! $request || 'remove_personal_data' !== $request->action_name ) { + wp_send_json_error( __( 'Invalid request type.' ) ); + } + + $email_address = $request->email; + + if ( ! is_email( $email_address ) ) { + wp_send_json_error( __( 'Invalid email address in request.' ) ); + } + + if ( ! isset( $_POST['eraser'] ) ) { + wp_send_json_error( __( 'Missing eraser index.' ) ); + } + + $eraser_index = (int) $_POST['eraser']; + + if ( ! isset( $_POST['page'] ) ) { + wp_send_json_error( __( 'Missing page index.' ) ); + } + + $page = (int) $_POST['page']; + + /** + * Filters the array of personal data eraser callbacks. + * + * @since 4.9.6 + * + * @param array $args { + * An array of callable erasers of personal data. Default empty array. + * + * @type array ...$0 { + * Array of personal data exporters. + * + * @type callable $callback Callable eraser that accepts an email address and a page + * number, and returns an array with boolean values for + * whether items were removed or retained and any messages + * from the eraser, as well as if additional pages are + * available. + * @type string $exporter_friendly_name Translated user facing friendly name for the eraser. + * } + * } + */ + $erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() ); + + // Do we have any registered erasers? + if ( 0 < count( $erasers ) ) { + + if ( $eraser_index < 1 ) { + wp_send_json_error( __( 'Eraser index cannot be less than one.' ) ); + } + + if ( $eraser_index > count( $erasers ) ) { + wp_send_json_error( __( 'Eraser index is out of range.' ) ); + } + + if ( $page < 1 ) { + wp_send_json_error( __( 'Page index cannot be less than one.' ) ); + } + + $eraser_keys = array_keys( $erasers ); + $eraser_key = $eraser_keys[ $eraser_index - 1 ]; + $eraser = $erasers[ $eraser_key ]; + + if ( ! is_array( $eraser ) ) { + /* translators: %d: Eraser array index. */ + wp_send_json_error( sprintf( __( 'Expected an array describing the eraser at index %d.' ), $eraser_index ) ); + } + + if ( ! array_key_exists( 'eraser_friendly_name', $eraser ) ) { + /* translators: %d: Eraser array index. */ + wp_send_json_error( sprintf( __( 'Eraser array at index %d does not include a friendly name.' ), $eraser_index ) ); + } + + $eraser_friendly_name = $eraser['eraser_friendly_name']; + + if ( ! array_key_exists( 'callback', $eraser ) ) { + wp_send_json_error( + sprintf( + /* translators: %s: Eraser friendly name. */ + __( 'Eraser does not include a callback: %s.' ), + esc_html( $eraser_friendly_name ) + ) + ); + } + + if ( ! is_callable( $eraser['callback'] ) ) { + wp_send_json_error( + sprintf( + /* translators: %s: Eraser friendly name. */ + __( 'Eraser callback is not valid: %s.' ), + esc_html( $eraser_friendly_name ) + ) + ); + } + + $callback = $eraser['callback']; + $response = call_user_func( $callback, $email_address, $page ); + + if ( is_wp_error( $response ) ) { + wp_send_json_error( $response ); + } + + if ( ! is_array( $response ) ) { + wp_send_json_error( + sprintf( + /* translators: 1: Eraser friendly name, 2: Eraser array index. */ + __( 'Did not receive array from %1$s eraser (index %2$d).' ), + esc_html( $eraser_friendly_name ), + $eraser_index + ) + ); + } + + if ( ! array_key_exists( 'items_removed', $response ) ) { + wp_send_json_error( + sprintf( + /* translators: 1: Eraser friendly name, 2: Eraser array index. */ + __( 'Expected items_removed key in response array from %1$s eraser (index %2$d).' ), + esc_html( $eraser_friendly_name ), + $eraser_index + ) + ); + } + + if ( ! array_key_exists( 'items_retained', $response ) ) { + wp_send_json_error( + sprintf( + /* translators: 1: Eraser friendly name, 2: Eraser array index. */ + __( 'Expected items_retained key in response array from %1$s eraser (index %2$d).' ), + esc_html( $eraser_friendly_name ), + $eraser_index + ) + ); + } + + if ( ! array_key_exists( 'messages', $response ) ) { + wp_send_json_error( + sprintf( + /* translators: 1: Eraser friendly name, 2: Eraser array index. */ + __( 'Expected messages key in response array from %1$s eraser (index %2$d).' ), + esc_html( $eraser_friendly_name ), + $eraser_index + ) + ); + } + + if ( ! is_array( $response['messages'] ) ) { + wp_send_json_error( + sprintf( + /* translators: 1: Eraser friendly name, 2: Eraser array index. */ + __( 'Expected messages key to reference an array in response array from %1$s eraser (index %2$d).' ), + esc_html( $eraser_friendly_name ), + $eraser_index + ) + ); + } + + if ( ! array_key_exists( 'done', $response ) ) { + wp_send_json_error( + sprintf( + /* translators: 1: Eraser friendly name, 2: Eraser array index. */ + __( 'Expected done flag in response array from %1$s eraser (index %2$d).' ), + esc_html( $eraser_friendly_name ), + $eraser_index + ) + ); + } + } else { + // No erasers, so we're done. + $eraser_key = ''; + + $response = array( + 'items_removed' => false, + 'items_retained' => false, + 'messages' => array(), + 'done' => true, + ); + } + + /** + * Filters a page of personal data eraser data. + * + * Allows the erasure response to be consumed by destinations in addition to Ajax. + * + * @since 4.9.6 + * + * @param array $response { + * The personal data for the given exporter and page number. + * + * @type bool $items_removed Whether items were actually removed or not. + * @type bool $items_retained Whether items were retained or not. + * @type string[] $messages An array of messages to add to the personal data export file. + * @type bool $done Whether the eraser is finished or not. + * } + * @param int $eraser_index The index of the eraser that provided this data. + * @param string $email_address The email address associated with this personal data. + * @param int $page The page number for this response. + * @param int $request_id The privacy request post ID associated with this request. + * @param string $eraser_key The key (slug) of the eraser that provided this data. + */ + $response = apply_filters( 'wp_privacy_personal_data_erasure_page', $response, $eraser_index, $email_address, $page, $request_id, $eraser_key ); + + if ( is_wp_error( $response ) ) { + wp_send_json_error( $response ); + } + + wp_send_json_success( $response ); +} + +/** + * Handles site health checks on server communication via AJAX. + * + * @since 5.2.0 + * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_dotorg_communication() + * @see WP_REST_Site_Health_Controller::test_dotorg_communication() + */ +function wp_ajax_health_check_dotorg_communication() { + _doing_it_wrong( + 'wp_ajax_health_check_dotorg_communication', + sprintf( + // translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it. + __( 'The Site Health check for %1$s has been replaced with %2$s.' ), + 'wp_ajax_health_check_dotorg_communication', + 'WP_REST_Site_Health_Controller::test_dotorg_communication' + ), + '5.6.0' + ); + + check_ajax_referer( 'health-check-site-status' ); + + if ( ! current_user_can( 'view_site_health_checks' ) ) { + wp_send_json_error(); + } + + if ( ! class_exists( 'WP_Site_Health' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php'; + } + + $site_health = WP_Site_Health::get_instance(); + wp_send_json_success( $site_health->get_test_dotorg_communication() ); +} + +/** + * Handles site health checks on background updates via AJAX. + * + * @since 5.2.0 + * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_background_updates() + * @see WP_REST_Site_Health_Controller::test_background_updates() + */ +function wp_ajax_health_check_background_updates() { + _doing_it_wrong( + 'wp_ajax_health_check_background_updates', + sprintf( + // translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it. + __( 'The Site Health check for %1$s has been replaced with %2$s.' ), + 'wp_ajax_health_check_background_updates', + 'WP_REST_Site_Health_Controller::test_background_updates' + ), + '5.6.0' + ); + + check_ajax_referer( 'health-check-site-status' ); + + if ( ! current_user_can( 'view_site_health_checks' ) ) { + wp_send_json_error(); + } + + if ( ! class_exists( 'WP_Site_Health' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php'; + } + + $site_health = WP_Site_Health::get_instance(); + wp_send_json_success( $site_health->get_test_background_updates() ); +} + +/** + * Handles site health checks on loopback requests via AJAX. + * + * @since 5.2.0 + * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_loopback_requests() + * @see WP_REST_Site_Health_Controller::test_loopback_requests() + */ +function wp_ajax_health_check_loopback_requests() { + _doing_it_wrong( + 'wp_ajax_health_check_loopback_requests', + sprintf( + // translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it. + __( 'The Site Health check for %1$s has been replaced with %2$s.' ), + 'wp_ajax_health_check_loopback_requests', + 'WP_REST_Site_Health_Controller::test_loopback_requests' + ), + '5.6.0' + ); + + check_ajax_referer( 'health-check-site-status' ); + + if ( ! current_user_can( 'view_site_health_checks' ) ) { + wp_send_json_error(); + } + + if ( ! class_exists( 'WP_Site_Health' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php'; + } + + $site_health = WP_Site_Health::get_instance(); + wp_send_json_success( $site_health->get_test_loopback_requests() ); +} + +/** + * Handles site health check to update the result status via AJAX. + * + * @since 5.2.0 + */ +function wp_ajax_health_check_site_status_result() { + check_ajax_referer( 'health-check-site-status-result' ); + + if ( ! current_user_can( 'view_site_health_checks' ) ) { + wp_send_json_error(); + } + + set_transient( 'health-check-site-status-result', wp_json_encode( $_POST['counts'] ) ); + + wp_send_json_success(); +} + +/** + * Handles site health check to get directories and database sizes via AJAX. + * + * @since 5.2.0 + * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::get_directory_sizes() + * @see WP_REST_Site_Health_Controller::get_directory_sizes() + */ +function wp_ajax_health_check_get_sizes() { + _doing_it_wrong( + 'wp_ajax_health_check_get_sizes', + sprintf( + // translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it. + __( 'The Site Health check for %1$s has been replaced with %2$s.' ), + 'wp_ajax_health_check_get_sizes', + 'WP_REST_Site_Health_Controller::get_directory_sizes' + ), + '5.6.0' + ); + + check_ajax_referer( 'health-check-site-status-result' ); + + if ( ! current_user_can( 'view_site_health_checks' ) || is_multisite() ) { + wp_send_json_error(); + } + + if ( ! class_exists( 'WP_Debug_Data' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-debug-data.php'; + } + + $sizes_data = WP_Debug_Data::get_sizes(); + $all_sizes = array( 'raw' => 0 ); + + foreach ( $sizes_data as $name => $value ) { + $name = sanitize_text_field( $name ); + $data = array(); + + if ( isset( $value['size'] ) ) { + if ( is_string( $value['size'] ) ) { + $data['size'] = sanitize_text_field( $value['size'] ); + } else { + $data['size'] = (int) $value['size']; + } + } + + if ( isset( $value['debug'] ) ) { + if ( is_string( $value['debug'] ) ) { + $data['debug'] = sanitize_text_field( $value['debug'] ); + } else { + $data['debug'] = (int) $value['debug']; + } + } + + if ( ! empty( $value['raw'] ) ) { + $data['raw'] = (int) $value['raw']; + } + + $all_sizes[ $name ] = $data; + } + + if ( isset( $all_sizes['total_size']['debug'] ) && 'not available' === $all_sizes['total_size']['debug'] ) { + wp_send_json_error( $all_sizes ); + } + + wp_send_json_success( $all_sizes ); +} + +/** + * Handles renewing the REST API nonce via AJAX. + * + * @since 5.3.0 + */ +function wp_ajax_rest_nonce() { + exit( wp_create_nonce( 'wp_rest' ) ); +} + +/** + * Handles enabling or disable plugin and theme auto-updates via AJAX. + * + * @since 5.5.0 + */ +function wp_ajax_toggle_auto_updates() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['type'] ) || empty( $_POST['asset'] ) || empty( $_POST['state'] ) ) { + wp_send_json_error( array( 'error' => __( 'Invalid data. No selected item.' ) ) ); + } + + $asset = sanitize_text_field( urldecode( $_POST['asset'] ) ); + + if ( 'enable' !== $_POST['state'] && 'disable' !== $_POST['state'] ) { + wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown state.' ) ) ); + } + $state = $_POST['state']; + + if ( 'plugin' !== $_POST['type'] && 'theme' !== $_POST['type'] ) { + wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) ); + } + $type = $_POST['type']; + + switch ( $type ) { + case 'plugin': + if ( ! current_user_can( 'update_plugins' ) ) { + $error_message = __( 'Sorry, you are not allowed to modify plugins.' ); + wp_send_json_error( array( 'error' => $error_message ) ); + } + + $option = 'auto_update_plugins'; + /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ + $all_items = apply_filters( 'all_plugins', get_plugins() ); + break; + case 'theme': + if ( ! current_user_can( 'update_themes' ) ) { + $error_message = __( 'Sorry, you are not allowed to modify themes.' ); + wp_send_json_error( array( 'error' => $error_message ) ); + } + + $option = 'auto_update_themes'; + $all_items = wp_get_themes(); + break; + default: + wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) ); + } + + if ( ! array_key_exists( $asset, $all_items ) ) { + $error_message = __( 'Invalid data. The item does not exist.' ); + wp_send_json_error( array( 'error' => $error_message ) ); + } + + $auto_updates = (array) get_site_option( $option, array() ); + + if ( 'disable' === $state ) { + $auto_updates = array_diff( $auto_updates, array( $asset ) ); + } else { + $auto_updates[] = $asset; + $auto_updates = array_unique( $auto_updates ); + } + + // Remove items that have been deleted since the site option was last updated. + $auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) ); + + update_site_option( $option, $auto_updates ); + + wp_send_json_success(); +} + +/** + * Handles sending a password reset link via AJAX. + * + * @since 5.7.0 + */ +function wp_ajax_send_password_reset() { + + // Validate the nonce for this action. + $user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0; + check_ajax_referer( 'reset-password-for-' . $user_id, 'nonce' ); + + // Verify user capabilities. + if ( ! current_user_can( 'edit_user', $user_id ) ) { + wp_send_json_error( __( 'Cannot send password reset, permission denied.' ) ); + } + + // Send the password reset link. + $user = get_userdata( $user_id ); + $results = retrieve_password( $user->user_login ); + + if ( true === $results ) { + wp_send_json_success( + /* translators: %s: User's display name. */ + sprintf( __( 'A password reset link was emailed to %s.' ), $user->display_name ) + ); + } else { + wp_send_json_error( $results->get_error_message() ); + } +} diff --git a/wp-admin/includes/bookmark.php b/wp-admin/includes/bookmark.php new file mode 100644 index 0000000..c5600bf --- /dev/null +++ b/wp-admin/includes/bookmark.php @@ -0,0 +1,379 @@ +' . __( 'You need a higher level of permission.' ) . '' . + '

' . __( 'Sorry, you are not allowed to edit the links for this site.' ) . '

', + 403 + ); + } + + $_POST['link_url'] = esc_url( $_POST['link_url'] ); + $_POST['link_name'] = esc_html( $_POST['link_name'] ); + $_POST['link_image'] = esc_html( $_POST['link_image'] ); + $_POST['link_rss'] = esc_url( $_POST['link_rss'] ); + if ( ! isset( $_POST['link_visible'] ) || 'N' !== $_POST['link_visible'] ) { + $_POST['link_visible'] = 'Y'; + } + + if ( ! empty( $link_id ) ) { + $_POST['link_id'] = $link_id; + return wp_update_link( $_POST ); + } else { + return wp_insert_link( $_POST ); + } +} + +/** + * Retrieves the default link for editing. + * + * @since 2.0.0 + * + * @return stdClass Default link object. + */ +function get_default_link_to_edit() { + $link = new stdClass(); + if ( isset( $_GET['linkurl'] ) ) { + $link->link_url = esc_url( wp_unslash( $_GET['linkurl'] ) ); + } else { + $link->link_url = ''; + } + + if ( isset( $_GET['name'] ) ) { + $link->link_name = esc_attr( wp_unslash( $_GET['name'] ) ); + } else { + $link->link_name = ''; + } + + $link->link_visible = 'Y'; + + return $link; +} + +/** + * Deletes a specified link from the database. + * + * @since 2.0.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param int $link_id ID of the link to delete. + * @return true Always true. + */ +function wp_delete_link( $link_id ) { + global $wpdb; + /** + * Fires before a link is deleted. + * + * @since 2.0.0 + * + * @param int $link_id ID of the link to delete. + */ + do_action( 'delete_link', $link_id ); + + wp_delete_object_term_relationships( $link_id, 'link_category' ); + + $wpdb->delete( $wpdb->links, array( 'link_id' => $link_id ) ); + + /** + * Fires after a link has been deleted. + * + * @since 2.2.0 + * + * @param int $link_id ID of the deleted link. + */ + do_action( 'deleted_link', $link_id ); + + clean_bookmark_cache( $link_id ); + + return true; +} + +/** + * Retrieves the link category IDs associated with the link specified. + * + * @since 2.1.0 + * + * @param int $link_id Link ID to look up. + * @return int[] The IDs of the requested link's categories. + */ +function wp_get_link_cats( $link_id = 0 ) { + $cats = wp_get_object_terms( $link_id, 'link_category', array( 'fields' => 'ids' ) ); + return array_unique( $cats ); +} + +/** + * Retrieves link data based on its ID. + * + * @since 2.0.0 + * + * @param int|stdClass $link Link ID or object to retrieve. + * @return object Link object for editing. + */ +function get_link_to_edit( $link ) { + return get_bookmark( $link, OBJECT, 'edit' ); +} + +/** + * Inserts a link into the database, or updates an existing link. + * + * Runs all the necessary sanitizing, provides default values if arguments are missing, + * and finally saves the link. + * + * @since 2.0.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param array $linkdata { + * Elements that make up the link to insert. + * + * @type int $link_id Optional. The ID of the existing link if updating. + * @type string $link_url The URL the link points to. + * @type string $link_name The title of the link. + * @type string $link_image Optional. A URL of an image. + * @type string $link_target Optional. The target element for the anchor tag. + * @type string $link_description Optional. A short description of the link. + * @type string $link_visible Optional. 'Y' means visible, anything else means not. + * @type int $link_owner Optional. A user ID. + * @type int $link_rating Optional. A rating for the link. + * @type string $link_rel Optional. A relationship of the link to you. + * @type string $link_notes Optional. An extended description of or notes on the link. + * @type string $link_rss Optional. A URL of an associated RSS feed. + * @type int $link_category Optional. The term ID of the link category. + * If empty, uses default link category. + * } + * @param bool $wp_error Optional. Whether to return a WP_Error object on failure. Default false. + * @return int|WP_Error Value 0 or WP_Error on failure. The link ID on success. + */ +function wp_insert_link( $linkdata, $wp_error = false ) { + global $wpdb; + + $defaults = array( + 'link_id' => 0, + 'link_name' => '', + 'link_url' => '', + 'link_rating' => 0, + ); + + $parsed_args = wp_parse_args( $linkdata, $defaults ); + $parsed_args = wp_unslash( sanitize_bookmark( $parsed_args, 'db' ) ); + + $link_id = $parsed_args['link_id']; + $link_name = $parsed_args['link_name']; + $link_url = $parsed_args['link_url']; + + $update = false; + if ( ! empty( $link_id ) ) { + $update = true; + } + + if ( '' === trim( $link_name ) ) { + if ( '' !== trim( $link_url ) ) { + $link_name = $link_url; + } else { + return 0; + } + } + + if ( '' === trim( $link_url ) ) { + return 0; + } + + $link_rating = ( ! empty( $parsed_args['link_rating'] ) ) ? $parsed_args['link_rating'] : 0; + $link_image = ( ! empty( $parsed_args['link_image'] ) ) ? $parsed_args['link_image'] : ''; + $link_target = ( ! empty( $parsed_args['link_target'] ) ) ? $parsed_args['link_target'] : ''; + $link_visible = ( ! empty( $parsed_args['link_visible'] ) ) ? $parsed_args['link_visible'] : 'Y'; + $link_owner = ( ! empty( $parsed_args['link_owner'] ) ) ? $parsed_args['link_owner'] : get_current_user_id(); + $link_notes = ( ! empty( $parsed_args['link_notes'] ) ) ? $parsed_args['link_notes'] : ''; + $link_description = ( ! empty( $parsed_args['link_description'] ) ) ? $parsed_args['link_description'] : ''; + $link_rss = ( ! empty( $parsed_args['link_rss'] ) ) ? $parsed_args['link_rss'] : ''; + $link_rel = ( ! empty( $parsed_args['link_rel'] ) ) ? $parsed_args['link_rel'] : ''; + $link_category = ( ! empty( $parsed_args['link_category'] ) ) ? $parsed_args['link_category'] : array(); + + // Make sure we set a valid category. + if ( ! is_array( $link_category ) || 0 === count( $link_category ) ) { + $link_category = array( get_option( 'default_link_category' ) ); + } + + if ( $update ) { + if ( false === $wpdb->update( $wpdb->links, compact( 'link_url', 'link_name', 'link_image', 'link_target', 'link_description', 'link_visible', 'link_owner', 'link_rating', 'link_rel', 'link_notes', 'link_rss' ), compact( 'link_id' ) ) ) { + if ( $wp_error ) { + return new WP_Error( 'db_update_error', __( 'Could not update link in the database.' ), $wpdb->last_error ); + } else { + return 0; + } + } + } else { + if ( false === $wpdb->insert( $wpdb->links, compact( 'link_url', 'link_name', 'link_image', 'link_target', 'link_description', 'link_visible', 'link_owner', 'link_rating', 'link_rel', 'link_notes', 'link_rss' ) ) ) { + if ( $wp_error ) { + return new WP_Error( 'db_insert_error', __( 'Could not insert link into the database.' ), $wpdb->last_error ); + } else { + return 0; + } + } + $link_id = (int) $wpdb->insert_id; + } + + wp_set_link_cats( $link_id, $link_category ); + + if ( $update ) { + /** + * Fires after a link was updated in the database. + * + * @since 2.0.0 + * + * @param int $link_id ID of the link that was updated. + */ + do_action( 'edit_link', $link_id ); + } else { + /** + * Fires after a link was added to the database. + * + * @since 2.0.0 + * + * @param int $link_id ID of the link that was added. + */ + do_action( 'add_link', $link_id ); + } + clean_bookmark_cache( $link_id ); + + return $link_id; +} + +/** + * Updates link with the specified link categories. + * + * @since 2.1.0 + * + * @param int $link_id ID of the link to update. + * @param int[] $link_categories Array of link category IDs to add the link to. + */ +function wp_set_link_cats( $link_id = 0, $link_categories = array() ) { + // If $link_categories isn't already an array, make it one: + if ( ! is_array( $link_categories ) || 0 === count( $link_categories ) ) { + $link_categories = array( get_option( 'default_link_category' ) ); + } + + $link_categories = array_map( 'intval', $link_categories ); + $link_categories = array_unique( $link_categories ); + + wp_set_object_terms( $link_id, $link_categories, 'link_category' ); + + clean_bookmark_cache( $link_id ); +} + +/** + * Updates a link in the database. + * + * @since 2.0.0 + * + * @param array $linkdata Link data to update. See wp_insert_link() for accepted arguments. + * @return int|WP_Error Value 0 or WP_Error on failure. The updated link ID on success. + */ +function wp_update_link( $linkdata ) { + $link_id = (int) $linkdata['link_id']; + + $link = get_bookmark( $link_id, ARRAY_A ); + + // Escape data pulled from DB. + $link = wp_slash( $link ); + + // Passed link category list overwrites existing category list if not empty. + if ( isset( $linkdata['link_category'] ) && is_array( $linkdata['link_category'] ) + && count( $linkdata['link_category'] ) > 0 + ) { + $link_cats = $linkdata['link_category']; + } else { + $link_cats = $link['link_category']; + } + + // Merge old and new fields with new fields overwriting old ones. + $linkdata = array_merge( $link, $linkdata ); + $linkdata['link_category'] = $link_cats; + + return wp_insert_link( $linkdata ); +} + +/** + * Outputs the 'disabled' message for the WordPress Link Manager. + * + * @since 3.5.0 + * @access private + * + * @global string $pagenow The filename of the current screen. + */ +function wp_link_manager_disabled_message() { + global $pagenow; + + if ( ! in_array( $pagenow, array( 'link-manager.php', 'link-add.php', 'link.php' ), true ) ) { + return; + } + + add_filter( 'pre_option_link_manager_enabled', '__return_true', 100 ); + $really_can_manage_links = current_user_can( 'manage_links' ); + remove_filter( 'pre_option_link_manager_enabled', '__return_true', 100 ); + + if ( $really_can_manage_links ) { + $plugins = get_plugins(); + + if ( empty( $plugins['link-manager/link-manager.php'] ) ) { + if ( current_user_can( 'install_plugins' ) ) { + $install_url = wp_nonce_url( + self_admin_url( 'update.php?action=install-plugin&plugin=link-manager' ), + 'install-plugin_link-manager' + ); + + wp_die( + sprintf( + /* translators: %s: A link to install the Link Manager plugin. */ + __( 'If you are looking to use the link manager, please install the Link Manager plugin.' ), + esc_url( $install_url ) + ) + ); + } + } elseif ( is_plugin_inactive( 'link-manager/link-manager.php' ) ) { + if ( current_user_can( 'activate_plugins' ) ) { + $activate_url = wp_nonce_url( + self_admin_url( 'plugins.php?action=activate&plugin=link-manager/link-manager.php' ), + 'activate-plugin_link-manager/link-manager.php' + ); + + wp_die( + sprintf( + /* translators: %s: A link to activate the Link Manager plugin. */ + __( 'Please activate the Link Manager plugin to use the link manager.' ), + esc_url( $activate_url ) + ) + ); + } + } + } + + wp_die( __( 'Sorry, you are not allowed to edit the links for this site.' ) ); +} diff --git a/wp-admin/includes/class-automatic-upgrader-skin.php b/wp-admin/includes/class-automatic-upgrader-skin.php new file mode 100644 index 0000000..4fee620 --- /dev/null +++ b/wp-admin/includes/class-automatic-upgrader-skin.php @@ -0,0 +1,135 @@ +options['context'] = $context; + } + /* + * TODO: Fix up request_filesystem_credentials(), or split it, to allow us to request a no-output version. + * This will output a credentials form in event of failure. We don't want that, so just hide with a buffer. + */ + ob_start(); + $result = parent::request_filesystem_credentials( $error, $context, $allow_relaxed_file_ownership ); + ob_end_clean(); + return $result; + } + + /** + * Retrieves the upgrade messages. + * + * @since 3.7.0 + * + * @return string[] Messages during an upgrade. + */ + public function get_upgrade_messages() { + return $this->messages; + } + + /** + * Stores a message about the upgrade. + * + * @since 3.7.0 + * @since 5.9.0 Renamed `$data` to `$feedback` for PHP 8 named parameter support. + * + * @param string|array|WP_Error $feedback Message data. + * @param mixed ...$args Optional text replacements. + */ + public function feedback( $feedback, ...$args ) { + if ( is_wp_error( $feedback ) ) { + $string = $feedback->get_error_message(); + } elseif ( is_array( $feedback ) ) { + return; + } else { + $string = $feedback; + } + + if ( ! empty( $this->upgrader->strings[ $string ] ) ) { + $string = $this->upgrader->strings[ $string ]; + } + + if ( str_contains( $string, '%' ) ) { + if ( ! empty( $args ) ) { + $string = vsprintf( $string, $args ); + } + } + + $string = trim( $string ); + + // Only allow basic HTML in the messages, as it'll be used in emails/logs rather than direct browser output. + $string = wp_kses( + $string, + array( + 'a' => array( + 'href' => true, + ), + 'br' => true, + 'em' => true, + 'strong' => true, + ) + ); + + if ( empty( $string ) ) { + return; + } + + $this->messages[] = $string; + } + + /** + * Creates a new output buffer. + * + * @since 3.7.0 + */ + public function header() { + ob_start(); + } + + /** + * Retrieves the buffered content, deletes the buffer, and processes the output. + * + * @since 3.7.0 + */ + public function footer() { + $output = ob_get_clean(); + if ( ! empty( $output ) ) { + $this->feedback( $output ); + } + } +} diff --git a/wp-admin/includes/class-bulk-plugin-upgrader-skin.php b/wp-admin/includes/class-bulk-plugin-upgrader-skin.php new file mode 100644 index 0000000..7cbf334 --- /dev/null +++ b/wp-admin/includes/class-bulk-plugin-upgrader-skin.php @@ -0,0 +1,87 @@ +upgrader->strings['skin_before_update_header'] = __( 'Updating Plugin %1$s (%2$d/%3$d)' ); + } + + /** + * @param string $title + */ + public function before( $title = '' ) { + parent::before( $this->plugin_info['Title'] ); + } + + /** + * @param string $title + */ + public function after( $title = '' ) { + parent::after( $this->plugin_info['Title'] ); + $this->decrement_update_count( 'plugin' ); + } + + /** + */ + public function bulk_footer() { + parent::bulk_footer(); + + $update_actions = array( + 'plugins_page' => sprintf( + '%s', + self_admin_url( 'plugins.php' ), + __( 'Go to Plugins page' ) + ), + 'updates_page' => sprintf( + '%s', + self_admin_url( 'update-core.php' ), + __( 'Go to WordPress Updates page' ) + ), + ); + + if ( ! current_user_can( 'activate_plugins' ) ) { + unset( $update_actions['plugins_page'] ); + } + + /** + * Filters the list of action links available following bulk plugin updates. + * + * @since 3.0.0 + * + * @param string[] $update_actions Array of plugin action links. + * @param array $plugin_info Array of information for the last-updated plugin. + */ + $update_actions = apply_filters( 'update_bulk_plugins_complete_actions', $update_actions, $this->plugin_info ); + + if ( ! empty( $update_actions ) ) { + $this->feedback( implode( ' | ', (array) $update_actions ) ); + } + } +} diff --git a/wp-admin/includes/class-bulk-theme-upgrader-skin.php b/wp-admin/includes/class-bulk-theme-upgrader-skin.php new file mode 100644 index 0000000..8ec3bbf --- /dev/null +++ b/wp-admin/includes/class-bulk-theme-upgrader-skin.php @@ -0,0 +1,88 @@ +upgrader->strings['skin_before_update_header'] = __( 'Updating Theme %1$s (%2$d/%3$d)' ); + } + + /** + * @param string $title + */ + public function before( $title = '' ) { + parent::before( $this->theme_info->display( 'Name' ) ); + } + + /** + * @param string $title + */ + public function after( $title = '' ) { + parent::after( $this->theme_info->display( 'Name' ) ); + $this->decrement_update_count( 'theme' ); + } + + /** + */ + public function bulk_footer() { + parent::bulk_footer(); + + $update_actions = array( + 'themes_page' => sprintf( + '%s', + self_admin_url( 'themes.php' ), + __( 'Go to Themes page' ) + ), + 'updates_page' => sprintf( + '%s', + self_admin_url( 'update-core.php' ), + __( 'Go to WordPress Updates page' ) + ), + ); + + if ( ! current_user_can( 'switch_themes' ) && ! current_user_can( 'edit_theme_options' ) ) { + unset( $update_actions['themes_page'] ); + } + + /** + * Filters the list of action links available following bulk theme updates. + * + * @since 3.0.0 + * + * @param string[] $update_actions Array of theme action links. + * @param WP_Theme $theme_info Theme object for the last-updated theme. + */ + $update_actions = apply_filters( 'update_bulk_theme_complete_actions', $update_actions, $this->theme_info ); + + if ( ! empty( $update_actions ) ) { + $this->feedback( implode( ' | ', (array) $update_actions ) ); + } + } +} diff --git a/wp-admin/includes/class-bulk-upgrader-skin.php b/wp-admin/includes/class-bulk-upgrader-skin.php new file mode 100644 index 0000000..4613119 --- /dev/null +++ b/wp-admin/includes/class-bulk-upgrader-skin.php @@ -0,0 +1,187 @@ + '', + 'nonce' => '', + ); + $args = wp_parse_args( $args, $defaults ); + + parent::__construct( $args ); + } + + /** + */ + public function add_strings() { + $this->upgrader->strings['skin_upgrade_start'] = __( 'The update process is starting. This process may take a while on some hosts, so please be patient.' ); + /* translators: 1: Title of an update, 2: Error message. */ + $this->upgrader->strings['skin_update_failed_error'] = __( 'An error occurred while updating %1$s: %2$s' ); + /* translators: %s: Title of an update. */ + $this->upgrader->strings['skin_update_failed'] = __( 'The update of %s failed.' ); + /* translators: %s: Title of an update. */ + $this->upgrader->strings['skin_update_successful'] = __( '%s updated successfully.' ); + $this->upgrader->strings['skin_upgrade_end'] = __( 'All updates have been completed.' ); + } + + /** + * @since 5.9.0 Renamed `$string` (a PHP reserved keyword) to `$feedback` for PHP 8 named parameter support. + * + * @param string $feedback Message data. + * @param mixed ...$args Optional text replacements. + */ + public function feedback( $feedback, ...$args ) { + if ( isset( $this->upgrader->strings[ $feedback ] ) ) { + $feedback = $this->upgrader->strings[ $feedback ]; + } + + if ( str_contains( $feedback, '%' ) ) { + if ( $args ) { + $args = array_map( 'strip_tags', $args ); + $args = array_map( 'esc_html', $args ); + $feedback = vsprintf( $feedback, $args ); + } + } + if ( empty( $feedback ) ) { + return; + } + if ( $this->in_loop ) { + echo "$feedback
\n"; + } else { + echo "

$feedback

\n"; + } + } + + /** + */ + public function header() { + // Nothing. This will be displayed within an iframe. + } + + /** + */ + public function footer() { + // Nothing. This will be displayed within an iframe. + } + + /** + * @since 5.9.0 Renamed `$error` to `$errors` for PHP 8 named parameter support. + * + * @param string|WP_Error $errors Errors. + */ + public function error( $errors ) { + if ( is_string( $errors ) && isset( $this->upgrader->strings[ $errors ] ) ) { + $this->error = $this->upgrader->strings[ $errors ]; + } + + if ( is_wp_error( $errors ) ) { + $messages = array(); + foreach ( $errors->get_error_messages() as $emessage ) { + if ( $errors->get_error_data() && is_string( $errors->get_error_data() ) ) { + $messages[] = $emessage . ' ' . esc_html( strip_tags( $errors->get_error_data() ) ); + } else { + $messages[] = $emessage; + } + } + $this->error = implode( ', ', $messages ); + } + echo ''; + } + + /** + */ + public function bulk_header() { + $this->feedback( 'skin_upgrade_start' ); + } + + /** + */ + public function bulk_footer() { + $this->feedback( 'skin_upgrade_end' ); + } + + /** + * @param string $title + */ + public function before( $title = '' ) { + $this->in_loop = true; + printf( '

' . $this->upgrader->strings['skin_before_update_header'] . '

', $title, $this->upgrader->update_current, $this->upgrader->update_count ); + echo ''; + // This progress messages div gets moved via JavaScript when clicking on "More details.". + echo '

'; + $this->flush_output(); + } + + /** + * @param string $title + */ + public function after( $title = '' ) { + echo '

'; + if ( $this->error || ! $this->result ) { + if ( $this->error ) { + $after_error_message = sprintf( $this->upgrader->strings['skin_update_failed_error'], $title, '' . $this->error . '' ); + } else { + $after_error_message = sprintf( $this->upgrader->strings['skin_update_failed'], $title ); + } + wp_admin_notice( + $after_error_message, + array( + 'additional_classes' => array( 'error' ), + ) + ); + + echo ''; + } + if ( $this->result && ! is_wp_error( $this->result ) ) { + if ( ! $this->error ) { + echo '
' . + '

' . sprintf( $this->upgrader->strings['skin_update_successful'], $title ) . + ' ' . + '

'; + } + + echo ''; + } + + $this->reset(); + $this->flush_output(); + } + + /** + */ + public function reset() { + $this->in_loop = false; + $this->error = false; + } + + /** + */ + public function flush_output() { + wp_ob_end_flush_all(); + flush(); + } +} diff --git a/wp-admin/includes/class-core-upgrader.php b/wp-admin/includes/class-core-upgrader.php new file mode 100644 index 0000000..165e1f7 --- /dev/null +++ b/wp-admin/includes/class-core-upgrader.php @@ -0,0 +1,420 @@ +strings['up_to_date'] = __( 'WordPress is at the latest version.' ); + $this->strings['locked'] = __( 'Another update is currently in progress.' ); + $this->strings['no_package'] = __( 'Update package not available.' ); + /* translators: %s: Package URL. */ + $this->strings['downloading_package'] = sprintf( __( 'Downloading update from %s…' ), '%s' ); + $this->strings['unpack_package'] = __( 'Unpacking the update…' ); + $this->strings['copy_failed'] = __( 'Could not copy files.' ); + $this->strings['copy_failed_space'] = __( 'Could not copy files. You may have run out of disk space.' ); + $this->strings['start_rollback'] = __( 'Attempting to restore the previous version.' ); + $this->strings['rollback_was_required'] = __( 'Due to an error during updating, WordPress has been restored to your previous version.' ); + } + + /** + * Upgrades WordPress core. + * + * @since 2.8.0 + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * @global callable $_wp_filesystem_direct_method + * + * @param object $current Response object for whether WordPress is current. + * @param array $args { + * Optional. Arguments for upgrading WordPress core. Default empty array. + * + * @type bool $pre_check_md5 Whether to check the file checksums before + * attempting the upgrade. Default true. + * @type bool $attempt_rollback Whether to attempt to rollback the chances if + * there is a problem. Default false. + * @type bool $do_rollback Whether to perform this "upgrade" as a rollback. + * Default false. + * } + * @return string|false|WP_Error New WordPress version on success, false or WP_Error on failure. + */ + public function upgrade( $current, $args = array() ) { + global $wp_filesystem; + + require ABSPATH . WPINC . '/version.php'; // $wp_version; + + $start_time = time(); + + $defaults = array( + 'pre_check_md5' => true, + 'attempt_rollback' => false, + 'do_rollback' => false, + 'allow_relaxed_file_ownership' => false, + ); + $parsed_args = wp_parse_args( $args, $defaults ); + + $this->init(); + $this->upgrade_strings(); + + // Is an update available? + if ( ! isset( $current->response ) || 'latest' === $current->response ) { + return new WP_Error( 'up_to_date', $this->strings['up_to_date'] ); + } + + $res = $this->fs_connect( array( ABSPATH, WP_CONTENT_DIR ), $parsed_args['allow_relaxed_file_ownership'] ); + if ( ! $res || is_wp_error( $res ) ) { + return $res; + } + + $wp_dir = trailingslashit( $wp_filesystem->abspath() ); + + $partial = true; + if ( $parsed_args['do_rollback'] ) { + $partial = false; + } elseif ( $parsed_args['pre_check_md5'] && ! $this->check_files() ) { + $partial = false; + } + + /* + * If partial update is returned from the API, use that, unless we're doing + * a reinstallation. If we cross the new_bundled version number, then use + * the new_bundled zip. Don't though if the constant is set to skip bundled items. + * If the API returns a no_content zip, go with it. Finally, default to the full zip. + */ + if ( $parsed_args['do_rollback'] && $current->packages->rollback ) { + $to_download = 'rollback'; + } elseif ( $current->packages->partial && 'reinstall' !== $current->response && $wp_version === $current->partial_version && $partial ) { + $to_download = 'partial'; + } elseif ( $current->packages->new_bundled && version_compare( $wp_version, $current->new_bundled, '<' ) + && ( ! defined( 'CORE_UPGRADE_SKIP_NEW_BUNDLED' ) || ! CORE_UPGRADE_SKIP_NEW_BUNDLED ) ) { + $to_download = 'new_bundled'; + } elseif ( $current->packages->no_content ) { + $to_download = 'no_content'; + } else { + $to_download = 'full'; + } + + // Lock to prevent multiple Core Updates occurring. + $lock = WP_Upgrader::create_lock( 'core_updater', 15 * MINUTE_IN_SECONDS ); + if ( ! $lock ) { + return new WP_Error( 'locked', $this->strings['locked'] ); + } + + $download = $this->download_package( $current->packages->$to_download, true ); + + /* + * Allow for signature soft-fail. + * WARNING: This may be removed in the future. + */ + if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) { + // Output the failure error as a normal feedback, and not as an error: + /** This filter is documented in wp-admin/includes/update-core.php */ + apply_filters( 'update_feedback', $download->get_error_message() ); + + // Report this failure back to WordPress.org for debugging purposes. + wp_version_check( + array( + 'signature_failure_code' => $download->get_error_code(), + 'signature_failure_data' => $download->get_error_data(), + ) + ); + + // Pretend this error didn't happen. + $download = $download->get_error_data( 'softfail-filename' ); + } + + if ( is_wp_error( $download ) ) { + WP_Upgrader::release_lock( 'core_updater' ); + return $download; + } + + $working_dir = $this->unpack_package( $download ); + if ( is_wp_error( $working_dir ) ) { + WP_Upgrader::release_lock( 'core_updater' ); + return $working_dir; + } + + // Copy update-core.php from the new version into place. + if ( ! $wp_filesystem->copy( $working_dir . '/wordpress/wp-admin/includes/update-core.php', $wp_dir . 'wp-admin/includes/update-core.php', true ) ) { + $wp_filesystem->delete( $working_dir, true ); + WP_Upgrader::release_lock( 'core_updater' ); + return new WP_Error( 'copy_failed_for_update_core_file', __( 'The update cannot be installed because some files could not be copied. This is usually due to inconsistent file permissions.' ), 'wp-admin/includes/update-core.php' ); + } + $wp_filesystem->chmod( $wp_dir . 'wp-admin/includes/update-core.php', FS_CHMOD_FILE ); + + wp_opcache_invalidate( ABSPATH . 'wp-admin/includes/update-core.php' ); + require_once ABSPATH . 'wp-admin/includes/update-core.php'; + + if ( ! function_exists( 'update_core' ) ) { + WP_Upgrader::release_lock( 'core_updater' ); + return new WP_Error( 'copy_failed_space', $this->strings['copy_failed_space'] ); + } + + $result = update_core( $working_dir, $wp_dir ); + + // In the event of an issue, we may be able to roll back. + if ( $parsed_args['attempt_rollback'] && $current->packages->rollback && ! $parsed_args['do_rollback'] ) { + $try_rollback = false; + if ( is_wp_error( $result ) ) { + $error_code = $result->get_error_code(); + /* + * Not all errors are equal. These codes are critical: copy_failed__copy_dir, + * mkdir_failed__copy_dir, copy_failed__copy_dir_retry, and disk_full. + * do_rollback allows for update_core() to trigger a rollback if needed. + */ + if ( str_contains( $error_code, 'do_rollback' ) ) { + $try_rollback = true; + } elseif ( str_contains( $error_code, '__copy_dir' ) ) { + $try_rollback = true; + } elseif ( 'disk_full' === $error_code ) { + $try_rollback = true; + } + } + + if ( $try_rollback ) { + /** This filter is documented in wp-admin/includes/update-core.php */ + apply_filters( 'update_feedback', $result ); + + /** This filter is documented in wp-admin/includes/update-core.php */ + apply_filters( 'update_feedback', $this->strings['start_rollback'] ); + + $rollback_result = $this->upgrade( $current, array_merge( $parsed_args, array( 'do_rollback' => true ) ) ); + + $original_result = $result; + $result = new WP_Error( + 'rollback_was_required', + $this->strings['rollback_was_required'], + (object) array( + 'update' => $original_result, + 'rollback' => $rollback_result, + ) + ); + } + } + + /** This action is documented in wp-admin/includes/class-wp-upgrader.php */ + do_action( + 'upgrader_process_complete', + $this, + array( + 'action' => 'update', + 'type' => 'core', + ) + ); + + // Clear the current updates. + delete_site_transient( 'update_core' ); + + if ( ! $parsed_args['do_rollback'] ) { + $stats = array( + 'update_type' => $current->response, + 'success' => true, + 'fs_method' => $wp_filesystem->method, + 'fs_method_forced' => defined( 'FS_METHOD' ) || has_filter( 'filesystem_method' ), + 'fs_method_direct' => ! empty( $GLOBALS['_wp_filesystem_direct_method'] ) ? $GLOBALS['_wp_filesystem_direct_method'] : '', + 'time_taken' => time() - $start_time, + 'reported' => $wp_version, + 'attempted' => $current->version, + ); + + if ( is_wp_error( $result ) ) { + $stats['success'] = false; + // Did a rollback occur? + if ( ! empty( $try_rollback ) ) { + $stats['error_code'] = $original_result->get_error_code(); + $stats['error_data'] = $original_result->get_error_data(); + // Was the rollback successful? If not, collect its error too. + $stats['rollback'] = ! is_wp_error( $rollback_result ); + if ( is_wp_error( $rollback_result ) ) { + $stats['rollback_code'] = $rollback_result->get_error_code(); + $stats['rollback_data'] = $rollback_result->get_error_data(); + } + } else { + $stats['error_code'] = $result->get_error_code(); + $stats['error_data'] = $result->get_error_data(); + } + } + + wp_version_check( $stats ); + } + + WP_Upgrader::release_lock( 'core_updater' ); + + return $result; + } + + /** + * Determines if this WordPress Core version should update to an offered version or not. + * + * @since 3.7.0 + * + * @param string $offered_ver The offered version, of the format x.y.z. + * @return bool True if we should update to the offered version, otherwise false. + */ + public static function should_update_to_version( $offered_ver ) { + require ABSPATH . WPINC . '/version.php'; // $wp_version; // x.y.z + + $current_branch = implode( '.', array_slice( preg_split( '/[.-]/', $wp_version ), 0, 2 ) ); // x.y + $new_branch = implode( '.', array_slice( preg_split( '/[.-]/', $offered_ver ), 0, 2 ) ); // x.y + + $current_is_development_version = (bool) strpos( $wp_version, '-' ); + + // Defaults: + $upgrade_dev = get_site_option( 'auto_update_core_dev', 'enabled' ) === 'enabled'; + $upgrade_minor = get_site_option( 'auto_update_core_minor', 'enabled' ) === 'enabled'; + $upgrade_major = get_site_option( 'auto_update_core_major', 'unset' ) === 'enabled'; + + // WP_AUTO_UPDATE_CORE = true (all), 'beta', 'rc', 'development', 'branch-development', 'minor', false. + if ( defined( 'WP_AUTO_UPDATE_CORE' ) ) { + if ( false === WP_AUTO_UPDATE_CORE ) { + // Defaults to turned off, unless a filter allows it. + $upgrade_dev = false; + $upgrade_minor = false; + $upgrade_major = false; + } elseif ( true === WP_AUTO_UPDATE_CORE + || in_array( WP_AUTO_UPDATE_CORE, array( 'beta', 'rc', 'development', 'branch-development' ), true ) + ) { + // ALL updates for core. + $upgrade_dev = true; + $upgrade_minor = true; + $upgrade_major = true; + } elseif ( 'minor' === WP_AUTO_UPDATE_CORE ) { + // Only minor updates for core. + $upgrade_dev = false; + $upgrade_minor = true; + $upgrade_major = false; + } + } + + // 1: If we're already on that version, not much point in updating? + if ( $offered_ver === $wp_version ) { + return false; + } + + // 2: If we're running a newer version, that's a nope. + if ( version_compare( $wp_version, $offered_ver, '>' ) ) { + return false; + } + + $failure_data = get_site_option( 'auto_core_update_failed' ); + if ( $failure_data ) { + // If this was a critical update failure, cannot update. + if ( ! empty( $failure_data['critical'] ) ) { + return false; + } + + // Don't claim we can update on update-core.php if we have a non-critical failure logged. + if ( $wp_version === $failure_data['current'] && str_contains( $offered_ver, '.1.next.minor' ) ) { + return false; + } + + /* + * Cannot update if we're retrying the same A to B update that caused a non-critical failure. + * Some non-critical failures do allow retries, like download_failed. + * 3.7.1 => 3.7.2 resulted in files_not_writable, if we are still on 3.7.1 and still trying to update to 3.7.2. + */ + if ( empty( $failure_data['retry'] ) && $wp_version === $failure_data['current'] && $offered_ver === $failure_data['attempted'] ) { + return false; + } + } + + // 3: 3.7-alpha-25000 -> 3.7-alpha-25678 -> 3.7-beta1 -> 3.7-beta2. + if ( $current_is_development_version ) { + + /** + * Filters whether to enable automatic core updates for development versions. + * + * @since 3.7.0 + * + * @param bool $upgrade_dev Whether to enable automatic updates for + * development versions. + */ + if ( ! apply_filters( 'allow_dev_auto_core_updates', $upgrade_dev ) ) { + return false; + } + // Else fall through to minor + major branches below. + } + + // 4: Minor in-branch updates (3.7.0 -> 3.7.1 -> 3.7.2 -> 3.7.4). + if ( $current_branch === $new_branch ) { + + /** + * Filters whether to enable minor automatic core updates. + * + * @since 3.7.0 + * + * @param bool $upgrade_minor Whether to enable minor automatic core updates. + */ + return apply_filters( 'allow_minor_auto_core_updates', $upgrade_minor ); + } + + // 5: Major version updates (3.7.0 -> 3.8.0 -> 3.9.1). + if ( version_compare( $new_branch, $current_branch, '>' ) ) { + + /** + * Filters whether to enable major automatic core updates. + * + * @since 3.7.0 + * + * @param bool $upgrade_major Whether to enable major automatic core updates. + */ + return apply_filters( 'allow_major_auto_core_updates', $upgrade_major ); + } + + // If we're not sure, we don't want it. + return false; + } + + /** + * Compares the disk file checksums against the expected checksums. + * + * @since 3.7.0 + * + * @global string $wp_version The WordPress version string. + * @global string $wp_local_package Locale code of the package. + * + * @return bool True if the checksums match, otherwise false. + */ + public function check_files() { + global $wp_version, $wp_local_package; + + $checksums = get_core_checksums( $wp_version, isset( $wp_local_package ) ? $wp_local_package : 'en_US' ); + + if ( ! is_array( $checksums ) ) { + return false; + } + + foreach ( $checksums as $file => $checksum ) { + // Skip files which get updated. + if ( str_starts_with( $file, 'wp-content' ) ) { + continue; + } + if ( ! file_exists( ABSPATH . $file ) || md5_file( ABSPATH . $file ) !== $checksum ) { + return false; + } + } + + return true; + } +} diff --git a/wp-admin/includes/class-custom-background.php b/wp-admin/includes/class-custom-background.php new file mode 100644 index 0000000..2eb3ccf --- /dev/null +++ b/wp-admin/includes/class-custom-background.php @@ -0,0 +1,666 @@ +admin_header_callback = $admin_header_callback; + $this->admin_image_div_callback = $admin_image_div_callback; + + add_action( 'admin_menu', array( $this, 'init' ) ); + + add_action( 'wp_ajax_custom-background-add', array( $this, 'ajax_background_add' ) ); + + // Unused since 3.5.0. + add_action( 'wp_ajax_set-background-image', array( $this, 'wp_set_background_image' ) ); + } + + /** + * Sets up the hooks for the Custom Background admin page. + * + * @since 3.0.0 + */ + public function init() { + $page = add_theme_page( + _x( 'Background', 'custom background' ), + _x( 'Background', 'custom background' ), + 'edit_theme_options', + 'custom-background', + array( $this, 'admin_page' ) + ); + + if ( ! $page ) { + return; + } + + add_action( "load-{$page}", array( $this, 'admin_load' ) ); + add_action( "load-{$page}", array( $this, 'take_action' ), 49 ); + add_action( "load-{$page}", array( $this, 'handle_upload' ), 49 ); + + if ( $this->admin_header_callback ) { + add_action( "admin_head-{$page}", $this->admin_header_callback, 51 ); + } + } + + /** + * Sets up the enqueue for the CSS & JavaScript files. + * + * @since 3.0.0 + */ + public function admin_load() { + get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => + '

' . __( 'You can customize the look of your site without touching any of your theme’s code by using a custom background. Your background can be an image or a color.' ) . '

' . + '

' . __( 'To use a background image, simply upload it or choose an image that has already been uploaded to your Media Library by clicking the “Choose Image” button. You can display a single instance of your image, or tile it to fill the screen. You can have your background fixed in place, so your site content moves on top of it, or you can have it scroll with your site.' ) . '

' . + '

' . __( 'You can also choose a background color by clicking the Select Color button and either typing in a legitimate HTML hex value, e.g. “#ff0000” for red, or by choosing a color using the color picker.' ) . '

' . + '

' . __( 'Do not forget to click on the Save Changes button when you are finished.' ) . '

', + ) + ); + + get_current_screen()->set_help_sidebar( + '

' . __( 'For more information:' ) . '

' . + '

' . __( 'Documentation on Custom Background' ) . '

' . + '

' . __( 'Support forums' ) . '

' + ); + + wp_enqueue_media(); + wp_enqueue_script( 'custom-background' ); + wp_enqueue_style( 'wp-color-picker' ); + } + + /** + * Executes custom background modification. + * + * @since 3.0.0 + */ + public function take_action() { + if ( empty( $_POST ) ) { + return; + } + + if ( isset( $_POST['reset-background'] ) ) { + check_admin_referer( 'custom-background-reset', '_wpnonce-custom-background-reset' ); + + remove_theme_mod( 'background_image' ); + remove_theme_mod( 'background_image_thumb' ); + + $this->updated = true; + return; + } + + if ( isset( $_POST['remove-background'] ) ) { + // @todo Uploaded files are not removed here. + check_admin_referer( 'custom-background-remove', '_wpnonce-custom-background-remove' ); + + set_theme_mod( 'background_image', '' ); + set_theme_mod( 'background_image_thumb', '' ); + + $this->updated = true; + wp_safe_redirect( $_POST['_wp_http_referer'] ); + return; + } + + if ( isset( $_POST['background-preset'] ) ) { + check_admin_referer( 'custom-background' ); + + if ( in_array( $_POST['background-preset'], array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) { + $preset = $_POST['background-preset']; + } else { + $preset = 'default'; + } + + set_theme_mod( 'background_preset', $preset ); + } + + if ( isset( $_POST['background-position'] ) ) { + check_admin_referer( 'custom-background' ); + + $position = explode( ' ', $_POST['background-position'] ); + + if ( in_array( $position[0], array( 'left', 'center', 'right' ), true ) ) { + $position_x = $position[0]; + } else { + $position_x = 'left'; + } + + if ( in_array( $position[1], array( 'top', 'center', 'bottom' ), true ) ) { + $position_y = $position[1]; + } else { + $position_y = 'top'; + } + + set_theme_mod( 'background_position_x', $position_x ); + set_theme_mod( 'background_position_y', $position_y ); + } + + if ( isset( $_POST['background-size'] ) ) { + check_admin_referer( 'custom-background' ); + + if ( in_array( $_POST['background-size'], array( 'auto', 'contain', 'cover' ), true ) ) { + $size = $_POST['background-size']; + } else { + $size = 'auto'; + } + + set_theme_mod( 'background_size', $size ); + } + + if ( isset( $_POST['background-repeat'] ) ) { + check_admin_referer( 'custom-background' ); + + $repeat = $_POST['background-repeat']; + + if ( 'no-repeat' !== $repeat ) { + $repeat = 'repeat'; + } + + set_theme_mod( 'background_repeat', $repeat ); + } + + if ( isset( $_POST['background-attachment'] ) ) { + check_admin_referer( 'custom-background' ); + + $attachment = $_POST['background-attachment']; + + if ( 'fixed' !== $attachment ) { + $attachment = 'scroll'; + } + + set_theme_mod( 'background_attachment', $attachment ); + } + + if ( isset( $_POST['background-color'] ) ) { + check_admin_referer( 'custom-background' ); + + $color = preg_replace( '/[^0-9a-fA-F]/', '', $_POST['background-color'] ); + + if ( strlen( $color ) === 6 || strlen( $color ) === 3 ) { + set_theme_mod( 'background_color', $color ); + } else { + set_theme_mod( 'background_color', '' ); + } + } + + $this->updated = true; + } + + /** + * Displays the custom background page. + * + * @since 3.0.0 + */ + public function admin_page() { + ?> +
+

+ + Customizer.' ), + admin_url( 'customize.php?autofocus[control]=background_image' ) + ); + wp_admin_notice( + $message, + array( + 'type' => 'info', + 'additional_classes' => array( 'hide-if-no-customize' ), + ) + ); + } + + if ( ! empty( $this->updated ) ) { + $updated_message = sprintf( + /* translators: %s: Home URL. */ + __( 'Background updated. Visit your site to see how it looks.' ), + esc_url( home_url( '/' ) ) + ); + wp_admin_notice( + $updated_message, + array( + 'id' => 'message', + 'additional_classes' => array( 'updated' ), + ) + ); + } + ?> + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+
+ + + + + + array( + 'label' => __( 'Top Left' ), + 'icon' => 'dashicons dashicons-arrow-left-alt', + ), + 'center top' => array( + 'label' => __( 'Top' ), + 'icon' => 'dashicons dashicons-arrow-up-alt', + ), + 'right top' => array( + 'label' => __( 'Top Right' ), + 'icon' => 'dashicons dashicons-arrow-right-alt', + ), + ), + array( + 'left center' => array( + 'label' => __( 'Left' ), + 'icon' => 'dashicons dashicons-arrow-left-alt', + ), + 'center center' => array( + 'label' => __( 'Center' ), + 'icon' => 'background-position-center-icon', + ), + 'right center' => array( + 'label' => __( 'Right' ), + 'icon' => 'dashicons dashicons-arrow-right-alt', + ), + ), + array( + 'left bottom' => array( + 'label' => __( 'Bottom Left' ), + 'icon' => 'dashicons dashicons-arrow-left-alt', + ), + 'center bottom' => array( + 'label' => __( 'Bottom' ), + 'icon' => 'dashicons dashicons-arrow-down-alt', + ), + 'right bottom' => array( + 'label' => __( 'Bottom Right' ), + 'icon' => 'dashicons dashicons-arrow-right-alt', + ), + ), + ); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ false ); + + $uploaded_file = $_FILES['import']; + $wp_filetype = wp_check_filetype_and_ext( $uploaded_file['tmp_name'], $uploaded_file['name'] ); + if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) { + wp_die( __( 'The uploaded file is not a valid image. Please try again.' ) ); + } + + $file = wp_handle_upload( $uploaded_file, $overrides ); + + if ( isset( $file['error'] ) ) { + wp_die( $file['error'] ); + } + + $url = $file['url']; + $type = $file['type']; + $file = $file['file']; + $filename = wp_basename( $file ); + + // Construct the attachment array. + $attachment = array( + 'post_title' => $filename, + 'post_content' => $url, + 'post_mime_type' => $type, + 'guid' => $url, + 'context' => 'custom-background', + ); + + // Save the data. + $id = wp_insert_attachment( $attachment, $file ); + + // Add the metadata. + wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) ); + update_post_meta( $id, '_wp_attachment_is_custom_background', get_option( 'stylesheet' ) ); + + set_theme_mod( 'background_image', sanitize_url( $url ) ); + + $thumbnail = wp_get_attachment_image_src( $id, 'thumbnail' ); + set_theme_mod( 'background_image_thumb', sanitize_url( $thumbnail[0] ) ); + + /** This filter is documented in wp-admin/includes/class-custom-image-header.php */ + $file = apply_filters( 'wp_create_file_in_uploads', $file, $id ); // For replication. + + $this->updated = true; + } + + /** + * Handles Ajax request for adding custom background context to an attachment. + * + * Triggers when the user adds a new background image from the + * Media Manager. + * + * @since 4.1.0 + */ + public function ajax_background_add() { + check_ajax_referer( 'background-add', 'nonce' ); + + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_send_json_error(); + } + + $attachment_id = absint( $_POST['attachment_id'] ); + if ( $attachment_id < 1 ) { + wp_send_json_error(); + } + + update_post_meta( $attachment_id, '_wp_attachment_is_custom_background', get_stylesheet() ); + + wp_send_json_success(); + } + + /** + * @since 3.4.0 + * @deprecated 3.5.0 + * + * @param array $form_fields + * @return array $form_fields + */ + public function attachment_fields_to_edit( $form_fields ) { + return $form_fields; + } + + /** + * @since 3.4.0 + * @deprecated 3.5.0 + * + * @param array $tabs + * @return array $tabs + */ + public function filter_upload_tabs( $tabs ) { + return $tabs; + } + + /** + * @since 3.4.0 + * @deprecated 3.5.0 + */ + public function wp_set_background_image() { + check_ajax_referer( 'custom-background' ); + + if ( ! current_user_can( 'edit_theme_options' ) || ! isset( $_POST['attachment_id'] ) ) { + exit; + } + + $attachment_id = absint( $_POST['attachment_id'] ); + + $sizes = array_keys( + /** This filter is documented in wp-admin/includes/media.php */ + apply_filters( + 'image_size_names_choose', + array( + 'thumbnail' => __( 'Thumbnail' ), + 'medium' => __( 'Medium' ), + 'large' => __( 'Large' ), + 'full' => __( 'Full Size' ), + ) + ) + ); + + $size = 'thumbnail'; + if ( in_array( $_POST['size'], $sizes, true ) ) { + $size = esc_attr( $_POST['size'] ); + } + + update_post_meta( $attachment_id, '_wp_attachment_is_custom_background', get_option( 'stylesheet' ) ); + + $url = wp_get_attachment_image_src( $attachment_id, $size ); + $thumbnail = wp_get_attachment_image_src( $attachment_id, 'thumbnail' ); + set_theme_mod( 'background_image', sanitize_url( $url[0] ) ); + set_theme_mod( 'background_image_thumb', sanitize_url( $thumbnail[0] ) ); + exit; + } +} diff --git a/wp-admin/includes/class-custom-image-header.php b/wp-admin/includes/class-custom-image-header.php new file mode 100644 index 0000000..ee3bcb1 --- /dev/null +++ b/wp-admin/includes/class-custom-image-header.php @@ -0,0 +1,1617 @@ +admin_header_callback = $admin_header_callback; + $this->admin_image_div_callback = $admin_image_div_callback; + + add_action( 'admin_menu', array( $this, 'init' ) ); + + add_action( 'customize_save_after', array( $this, 'customize_set_last_used' ) ); + add_action( 'wp_ajax_custom-header-crop', array( $this, 'ajax_header_crop' ) ); + add_action( 'wp_ajax_custom-header-add', array( $this, 'ajax_header_add' ) ); + add_action( 'wp_ajax_custom-header-remove', array( $this, 'ajax_header_remove' ) ); + } + + /** + * Sets up the hooks for the Custom Header admin page. + * + * @since 2.1.0 + */ + public function init() { + $page = add_theme_page( + _x( 'Header', 'custom image header' ), + _x( 'Header', 'custom image header' ), + 'edit_theme_options', + 'custom-header', + array( $this, 'admin_page' ) + ); + + if ( ! $page ) { + return; + } + + add_action( "admin_print_scripts-{$page}", array( $this, 'js_includes' ) ); + add_action( "admin_print_styles-{$page}", array( $this, 'css_includes' ) ); + add_action( "admin_head-{$page}", array( $this, 'help' ) ); + add_action( "admin_head-{$page}", array( $this, 'take_action' ), 50 ); + add_action( "admin_head-{$page}", array( $this, 'js' ), 50 ); + + if ( $this->admin_header_callback ) { + add_action( "admin_head-{$page}", $this->admin_header_callback, 51 ); + } + } + + /** + * Adds contextual help. + * + * @since 3.0.0 + */ + public function help() { + get_current_screen()->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' ), + 'content' => + '

' . __( 'This screen is used to customize the header section of your theme.' ) . '

' . + '

' . __( 'You can choose from the theme’s default header images, or use one of your own. You can also customize how your Site Title and Tagline are displayed.' ) . '

', + ) + ); + + get_current_screen()->add_help_tab( + array( + 'id' => 'set-header-image', + 'title' => __( 'Header Image' ), + 'content' => + '

' . __( 'You can set a custom image header for your site. Simply upload the image and crop it, and the new header will go live immediately. Alternatively, you can use an image that has already been uploaded to your Media Library by clicking the “Choose Image” button.' ) . '

' . + '

' . __( 'Some themes come with additional header images bundled. If you see multiple images displayed, select the one you would like and click the “Save Changes” button.' ) . '

' . + '

' . __( 'If your theme has more than one default header image, or you have uploaded more than one custom header image, you have the option of having WordPress display a randomly different image on each page of your site. Click the “Random” radio button next to the Uploaded Images or Default Images section to enable this feature.' ) . '

' . + '

' . __( 'If you do not want a header image to be displayed on your site at all, click the “Remove Header Image” button at the bottom of the Header Image section of this page. If you want to re-enable the header image later, you just have to select one of the other image options and click “Save Changes”.' ) . '

', + ) + ); + + get_current_screen()->add_help_tab( + array( + 'id' => 'set-header-text', + 'title' => __( 'Header Text' ), + 'content' => + '

' . sprintf( + /* translators: %s: URL to General Settings screen. */ + __( 'For most themes, the header text is your Site Title and Tagline, as defined in the General Settings section.' ), + admin_url( 'options-general.php' ) + ) . + '

' . + '

' . __( 'In the Header Text section of this page, you can choose whether to display this text or hide it. You can also choose a color for the text by clicking the Select Color button and either typing in a legitimate HTML hex value, e.g. “#ff0000” for red, or by choosing a color using the color picker.' ) . '

' . + '

' . __( 'Do not forget to click “Save Changes” when you are done!' ) . '

', + ) + ); + + get_current_screen()->set_help_sidebar( + '

' . __( 'For more information:' ) . '

' . + '

' . __( 'Documentation on Custom Header' ) . '

' . + '

' . __( 'Support forums' ) . '

' + ); + } + + /** + * Gets the current step. + * + * @since 2.6.0 + * + * @return int Current step. + */ + public function step() { + if ( ! isset( $_GET['step'] ) ) { + return 1; + } + + $step = (int) $_GET['step']; + if ( $step < 1 || 3 < $step || + ( 2 === $step && ! wp_verify_nonce( $_REQUEST['_wpnonce-custom-header-upload'], 'custom-header-upload' ) ) || + ( 3 === $step && ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'custom-header-crop-image' ) ) + ) { + return 1; + } + + return $step; + } + + /** + * Sets up the enqueue for the JavaScript files. + * + * @since 2.1.0 + */ + public function js_includes() { + $step = $this->step(); + + if ( ( 1 === $step || 3 === $step ) ) { + wp_enqueue_media(); + wp_enqueue_script( 'custom-header' ); + if ( current_theme_supports( 'custom-header', 'header-text' ) ) { + wp_enqueue_script( 'wp-color-picker' ); + } + } elseif ( 2 === $step ) { + wp_enqueue_script( 'imgareaselect' ); + } + } + + /** + * Sets up the enqueue for the CSS files. + * + * @since 2.7.0 + */ + public function css_includes() { + $step = $this->step(); + + if ( ( 1 === $step || 3 === $step ) && current_theme_supports( 'custom-header', 'header-text' ) ) { + wp_enqueue_style( 'wp-color-picker' ); + } elseif ( 2 === $step ) { + wp_enqueue_style( 'imgareaselect' ); + } + } + + /** + * Executes custom header modification. + * + * @since 2.6.0 + */ + public function take_action() { + if ( ! current_user_can( 'edit_theme_options' ) ) { + return; + } + + if ( empty( $_POST ) ) { + return; + } + + $this->updated = true; + + if ( isset( $_POST['resetheader'] ) ) { + check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' ); + + $this->reset_header_image(); + + return; + } + + if ( isset( $_POST['removeheader'] ) ) { + check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' ); + + $this->remove_header_image(); + + return; + } + + if ( isset( $_POST['text-color'] ) && ! isset( $_POST['display-header-text'] ) ) { + check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' ); + + set_theme_mod( 'header_textcolor', 'blank' ); + } elseif ( isset( $_POST['text-color'] ) ) { + check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' ); + + $_POST['text-color'] = str_replace( '#', '', $_POST['text-color'] ); + + $color = preg_replace( '/[^0-9a-fA-F]/', '', $_POST['text-color'] ); + + if ( strlen( $color ) === 6 || strlen( $color ) === 3 ) { + set_theme_mod( 'header_textcolor', $color ); + } elseif ( ! $color ) { + set_theme_mod( 'header_textcolor', 'blank' ); + } + } + + if ( isset( $_POST['default-header'] ) ) { + check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' ); + + $this->set_header_image( $_POST['default-header'] ); + + return; + } + } + + /** + * Processes the default headers. + * + * @since 3.0.0 + * + * @global array $_wp_default_headers + */ + public function process_default_headers() { + global $_wp_default_headers; + + if ( ! isset( $_wp_default_headers ) ) { + return; + } + + if ( ! empty( $this->default_headers ) ) { + return; + } + + $this->default_headers = $_wp_default_headers; + $template_directory_uri = get_template_directory_uri(); + $stylesheet_directory_uri = get_stylesheet_directory_uri(); + + foreach ( array_keys( $this->default_headers ) as $header ) { + $this->default_headers[ $header ]['url'] = sprintf( + $this->default_headers[ $header ]['url'], + $template_directory_uri, + $stylesheet_directory_uri + ); + + $this->default_headers[ $header ]['thumbnail_url'] = sprintf( + $this->default_headers[ $header ]['thumbnail_url'], + $template_directory_uri, + $stylesheet_directory_uri + ); + } + } + + /** + * Displays UI for selecting one of several default headers. + * + * Shows the random image option if this theme has multiple header images. + * Random image option is on by default if no header has been set. + * + * @since 3.0.0 + * + * @param string $type The header type. One of 'default' (for the Uploaded Images control) + * or 'uploaded' (for the Uploaded Images control). + */ + public function show_header_selector( $type = 'default' ) { + if ( 'default' === $type ) { + $headers = $this->default_headers; + } else { + $headers = get_uploaded_header_images(); + $type = 'uploaded'; + } + + if ( 1 < count( $headers ) ) { + echo '
'; + echo ''; + echo '
'; + } + + echo '
'; + + foreach ( $headers as $header_key => $header ) { + $header_thumbnail = $header['thumbnail_url']; + $header_url = $header['url']; + $header_alt_text = empty( $header['alt_text'] ) ? '' : $header['alt_text']; + + echo '
'; + echo ''; + echo '
'; + } + + echo '
'; + } + + /** + * Executes JavaScript depending on step. + * + * @since 2.1.0 + */ + public function js() { + $step = $this->step(); + + if ( ( 1 === $step || 3 === $step ) && current_theme_supports( 'custom-header', 'header-text' ) ) { + $this->js_1(); + } elseif ( 2 === $step ) { + $this->js_2(); + } + } + + /** + * Displays JavaScript based on Step 1 and 3. + * + * @since 2.6.0 + */ + public function js_1() { + $default_color = ''; + if ( current_theme_supports( 'custom-header', 'default-text-color' ) ) { + $default_color = get_theme_support( 'custom-header', 'default-text-color' ); + if ( $default_color && ! str_contains( $default_color, '#' ) ) { + $default_color = '#' . $default_color; + } + } + ?> + + + + process_default_headers(); + ?> + +
+

+ + Customizer.' ), + admin_url( 'customize.php?autofocus[control]=header_image' ) + ); + wp_admin_notice( + $message, + array( + 'type' => 'info', + 'additional_classes' => array( 'hide-if-no-customize' ), + ) + ); + } + + if ( ! empty( $this->updated ) ) { + $updated_message = sprintf( + /* translators: %s: Home URL. */ + __( 'Header updated. Visit your site to see how it looks.' ), + esc_url( home_url( '/' ) ) + ); + wp_admin_notice( + $updated_message, + array( + 'id' => 'message', + 'additional_classes' => array( 'updated' ), + ) + ); + } + ?> + +

+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + default_headers ) ) : + ?> + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + +
+
+ + ' . __( 'Something went wrong.' ) . '' . + '

' . __( 'The active theme does not support uploading a custom header image.' ) . '

', + 403 + ); + } + + if ( empty( $_POST ) && isset( $_GET['file'] ) ) { + $attachment_id = absint( $_GET['file'] ); + $file = get_attached_file( $attachment_id, true ); + $url = wp_get_attachment_image_src( $attachment_id, 'full' ); + $url = $url[0]; + } elseif ( isset( $_POST ) ) { + $data = $this->step_2_manage_upload(); + $attachment_id = $data['attachment_id']; + $file = $data['file']; + $url = $data['url']; + } + + if ( file_exists( $file ) ) { + list( $width, $height, $type, $attr ) = wp_getimagesize( $file ); + } else { + $data = wp_get_attachment_metadata( $attachment_id ); + $height = isset( $data['height'] ) ? (int) $data['height'] : 0; + $width = isset( $data['width'] ) ? (int) $data['width'] : 0; + unset( $data ); + } + + $max_width = 0; + + // For flex, limit size of image displayed to 1500px unless theme says otherwise. + if ( current_theme_supports( 'custom-header', 'flex-width' ) ) { + $max_width = 1500; + } + + if ( current_theme_supports( 'custom-header', 'max-width' ) ) { + $max_width = max( $max_width, get_theme_support( 'custom-header', 'max-width' ) ); + } + + $max_width = max( $max_width, get_theme_support( 'custom-header', 'width' ) ); + + // If flexible height isn't supported and the image is the exact right size. + if ( ! current_theme_supports( 'custom-header', 'flex-height' ) + && ! current_theme_supports( 'custom-header', 'flex-width' ) + && (int) get_theme_support( 'custom-header', 'width' ) === $width + && (int) get_theme_support( 'custom-header', 'height' ) === $height + ) { + // Add the metadata. + if ( file_exists( $file ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) ); + } + + $this->set_header_image( compact( 'url', 'attachment_id', 'width', 'height' ) ); + + /** + * Filters the attachment file path after the custom header or background image is set. + * + * Used for file replication. + * + * @since 2.1.0 + * + * @param string $file Path to the file. + * @param int $attachment_id Attachment ID. + */ + $file = apply_filters( 'wp_create_file_in_uploads', $file, $attachment_id ); // For replication. + + return $this->finished(); + } elseif ( $width > $max_width ) { + $oitar = $width / $max_width; + + $image = wp_crop_image( + $attachment_id, + 0, + 0, + $width, + $height, + $max_width, + $height / $oitar, + false, + str_replace( wp_basename( $file ), 'midsize-' . wp_basename( $file ), $file ) + ); + + if ( ! $image || is_wp_error( $image ) ) { + wp_die( __( 'Image could not be processed. Please go back and try again.' ), __( 'Image Processing Error' ) ); + } + + /** This filter is documented in wp-admin/includes/class-custom-image-header.php */ + $image = apply_filters( 'wp_create_file_in_uploads', $image, $attachment_id ); // For replication. + + $url = str_replace( wp_basename( $url ), wp_basename( $image ), $url ); + $width = $width / $oitar; + $height = $height / $oitar; + } else { + $oitar = 1; + } + ?> + +
+

+ +
+

+

+ +
+ +
+ + + + + + + + + + + + +

+ + +

+
+
+ false ); + + $uploaded_file = $_FILES['import']; + $wp_filetype = wp_check_filetype_and_ext( $uploaded_file['tmp_name'], $uploaded_file['name'] ); + + if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) { + wp_die( __( 'The uploaded file is not a valid image. Please try again.' ) ); + } + + $file = wp_handle_upload( $uploaded_file, $overrides ); + + if ( isset( $file['error'] ) ) { + wp_die( $file['error'], __( 'Image Upload Error' ) ); + } + + $url = $file['url']; + $type = $file['type']; + $file = $file['file']; + $filename = wp_basename( $file ); + + // Construct the attachment array. + $attachment = array( + 'post_title' => $filename, + 'post_content' => $url, + 'post_mime_type' => $type, + 'guid' => $url, + 'context' => 'custom-header', + ); + + // Save the data. + $attachment_id = wp_insert_attachment( $attachment, $file ); + + return compact( 'attachment_id', 'file', 'filename', 'url', 'type' ); + } + + /** + * Displays third step of custom header image page. + * + * @since 2.1.0 + * @since 4.4.0 Switched to using wp_get_attachment_url() instead of the guid + * for retrieving the header image URL. + */ + public function step_3() { + check_admin_referer( 'custom-header-crop-image' ); + + if ( ! current_theme_supports( 'custom-header', 'uploads' ) ) { + wp_die( + '

' . __( 'Something went wrong.' ) . '

' . + '

' . __( 'The active theme does not support uploading a custom header image.' ) . '

', + 403 + ); + } + + if ( ! empty( $_POST['skip-cropping'] ) + && ! current_theme_supports( 'custom-header', 'flex-height' ) + && ! current_theme_supports( 'custom-header', 'flex-width' ) + ) { + wp_die( + '

' . __( 'Something went wrong.' ) . '

' . + '

' . __( 'The active theme does not support a flexible sized header image.' ) . '

', + 403 + ); + } + + if ( $_POST['oitar'] > 1 ) { + $_POST['x1'] = $_POST['x1'] * $_POST['oitar']; + $_POST['y1'] = $_POST['y1'] * $_POST['oitar']; + $_POST['width'] = $_POST['width'] * $_POST['oitar']; + $_POST['height'] = $_POST['height'] * $_POST['oitar']; + } + + $attachment_id = absint( $_POST['attachment_id'] ); + $original = get_attached_file( $attachment_id ); + + $dimensions = $this->get_header_dimensions( + array( + 'height' => $_POST['height'], + 'width' => $_POST['width'], + ) + ); + $height = $dimensions['dst_height']; + $width = $dimensions['dst_width']; + + if ( empty( $_POST['skip-cropping'] ) ) { + $cropped = wp_crop_image( + $attachment_id, + (int) $_POST['x1'], + (int) $_POST['y1'], + (int) $_POST['width'], + (int) $_POST['height'], + $width, + $height + ); + } elseif ( ! empty( $_POST['create-new-attachment'] ) ) { + $cropped = _copy_image_file( $attachment_id ); + } else { + $cropped = get_attached_file( $attachment_id ); + } + + if ( ! $cropped || is_wp_error( $cropped ) ) { + wp_die( __( 'Image could not be processed. Please go back and try again.' ), __( 'Image Processing Error' ) ); + } + + /** This filter is documented in wp-admin/includes/class-custom-image-header.php */ + $cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication. + + $attachment = $this->create_attachment_object( $cropped, $attachment_id ); + + if ( ! empty( $_POST['create-new-attachment'] ) ) { + unset( $attachment['ID'] ); + } + + // Update the attachment. + $attachment_id = $this->insert_attachment( $attachment, $cropped ); + + $url = wp_get_attachment_url( $attachment_id ); + $this->set_header_image( compact( 'url', 'attachment_id', 'width', 'height' ) ); + + // Cleanup. + $medium = str_replace( wp_basename( $original ), 'midsize-' . wp_basename( $original ), $original ); + if ( file_exists( $medium ) ) { + wp_delete_file( $medium ); + } + + if ( empty( $_POST['create-new-attachment'] ) && empty( $_POST['skip-cropping'] ) ) { + wp_delete_file( $original ); + } + + return $this->finished(); + } + + /** + * Displays last step of custom header image page. + * + * @since 2.1.0 + */ + public function finished() { + $this->updated = true; + $this->step_1(); + } + + /** + * Displays the page based on the current step. + * + * @since 2.1.0 + */ + public function admin_page() { + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_die( __( 'Sorry, you are not allowed to customize headers.' ) ); + } + + $step = $this->step(); + + if ( 2 === $step ) { + $this->step_2(); + } elseif ( 3 === $step ) { + $this->step_3(); + } else { + $this->step_1(); + } + } + + /** + * Unused since 3.5.0. + * + * @since 3.4.0 + * + * @param array $form_fields + * @return array $form_fields + */ + public function attachment_fields_to_edit( $form_fields ) { + return $form_fields; + } + + /** + * Unused since 3.5.0. + * + * @since 3.4.0 + * + * @param array $tabs + * @return array $tabs + */ + public function filter_upload_tabs( $tabs ) { + return $tabs; + } + + /** + * Chooses a header image, selected from existing uploaded and default headers, + * or provides an array of uploaded header data (either new, or from media library). + * + * @since 3.4.0 + * + * @param mixed $choice Which header image to select. Allows for values of 'random-default-image', + * for randomly cycling among the default images; 'random-uploaded-image', + * for randomly cycling among the uploaded images; the key of a default image + * registered for that theme; and the key of an image uploaded for that theme + * (the attachment ID of the image). Or an array of arguments: attachment_id, + * url, width, height. All are required. + */ + final public function set_header_image( $choice ) { + if ( is_array( $choice ) || is_object( $choice ) ) { + $choice = (array) $choice; + + if ( ! isset( $choice['attachment_id'] ) || ! isset( $choice['url'] ) ) { + return; + } + + $choice['url'] = sanitize_url( $choice['url'] ); + + $header_image_data = (object) array( + 'attachment_id' => $choice['attachment_id'], + 'url' => $choice['url'], + 'thumbnail_url' => $choice['url'], + 'height' => $choice['height'], + 'width' => $choice['width'], + ); + + update_post_meta( $choice['attachment_id'], '_wp_attachment_is_custom_header', get_stylesheet() ); + + set_theme_mod( 'header_image', $choice['url'] ); + set_theme_mod( 'header_image_data', $header_image_data ); + + return; + } + + if ( in_array( $choice, array( 'remove-header', 'random-default-image', 'random-uploaded-image' ), true ) ) { + set_theme_mod( 'header_image', $choice ); + remove_theme_mod( 'header_image_data' ); + + return; + } + + $uploaded = get_uploaded_header_images(); + + if ( $uploaded && isset( $uploaded[ $choice ] ) ) { + $header_image_data = $uploaded[ $choice ]; + } else { + $this->process_default_headers(); + if ( isset( $this->default_headers[ $choice ] ) ) { + $header_image_data = $this->default_headers[ $choice ]; + } else { + return; + } + } + + set_theme_mod( 'header_image', sanitize_url( $header_image_data['url'] ) ); + set_theme_mod( 'header_image_data', $header_image_data ); + } + + /** + * Removes a header image. + * + * @since 3.4.0 + */ + final public function remove_header_image() { + $this->set_header_image( 'remove-header' ); + } + + /** + * Resets a header image to the default image for the theme. + * + * This method does not do anything if the theme does not have a default header image. + * + * @since 3.4.0 + */ + final public function reset_header_image() { + $this->process_default_headers(); + $default = get_theme_support( 'custom-header', 'default-image' ); + + if ( ! $default ) { + $this->remove_header_image(); + return; + } + + $default = sprintf( $default, get_template_directory_uri(), get_stylesheet_directory_uri() ); + + $default_data = array(); + foreach ( $this->default_headers as $header => $details ) { + if ( $details['url'] === $default ) { + $default_data = $details; + break; + } + } + + set_theme_mod( 'header_image', $default ); + set_theme_mod( 'header_image_data', (object) $default_data ); + } + + /** + * Calculates width and height based on what the currently selected theme supports. + * + * @since 3.9.0 + * + * @param array $dimensions + * @return array dst_height and dst_width of header image. + */ + final public function get_header_dimensions( $dimensions ) { + $max_width = 0; + $width = absint( $dimensions['width'] ); + $height = absint( $dimensions['height'] ); + $theme_height = get_theme_support( 'custom-header', 'height' ); + $theme_width = get_theme_support( 'custom-header', 'width' ); + $has_flex_width = current_theme_supports( 'custom-header', 'flex-width' ); + $has_flex_height = current_theme_supports( 'custom-header', 'flex-height' ); + $has_max_width = current_theme_supports( 'custom-header', 'max-width' ); + $dst = array( + 'dst_height' => null, + 'dst_width' => null, + ); + + // For flex, limit size of image displayed to 1500px unless theme says otherwise. + if ( $has_flex_width ) { + $max_width = 1500; + } + + if ( $has_max_width ) { + $max_width = max( $max_width, get_theme_support( 'custom-header', 'max-width' ) ); + } + $max_width = max( $max_width, $theme_width ); + + if ( $has_flex_height && ( ! $has_flex_width || $width > $max_width ) ) { + $dst['dst_height'] = absint( $height * ( $max_width / $width ) ); + } elseif ( $has_flex_height && $has_flex_width ) { + $dst['dst_height'] = $height; + } else { + $dst['dst_height'] = $theme_height; + } + + if ( $has_flex_width && ( ! $has_flex_height || $width > $max_width ) ) { + $dst['dst_width'] = absint( $width * ( $max_width / $width ) ); + } elseif ( $has_flex_width && $has_flex_height ) { + $dst['dst_width'] = $width; + } else { + $dst['dst_width'] = $theme_width; + } + + return $dst; + } + + /** + * Creates an attachment 'object'. + * + * @since 3.9.0 + * + * @param string $cropped Cropped image URL. + * @param int $parent_attachment_id Attachment ID of parent image. + * @return array An array with attachment object data. + */ + final public function create_attachment_object( $cropped, $parent_attachment_id ) { + $parent = get_post( $parent_attachment_id ); + $parent_url = wp_get_attachment_url( $parent->ID ); + $url = str_replace( wp_basename( $parent_url ), wp_basename( $cropped ), $parent_url ); + + $size = wp_getimagesize( $cropped ); + $image_type = ( $size ) ? $size['mime'] : 'image/jpeg'; + + $attachment = array( + 'ID' => $parent_attachment_id, + 'post_title' => wp_basename( $cropped ), + 'post_mime_type' => $image_type, + 'guid' => $url, + 'context' => 'custom-header', + 'post_parent' => $parent_attachment_id, + ); + + return $attachment; + } + + /** + * Inserts an attachment and its metadata. + * + * @since 3.9.0 + * + * @param array $attachment An array with attachment object data. + * @param string $cropped File path to cropped image. + * @return int Attachment ID. + */ + final public function insert_attachment( $attachment, $cropped ) { + $parent_id = isset( $attachment['post_parent'] ) ? $attachment['post_parent'] : null; + unset( $attachment['post_parent'] ); + + $attachment_id = wp_insert_attachment( $attachment, $cropped ); + $metadata = wp_generate_attachment_metadata( $attachment_id, $cropped ); + + // If this is a crop, save the original attachment ID as metadata. + if ( $parent_id ) { + $metadata['attachment_parent'] = $parent_id; + } + + /** + * Filters the header image attachment metadata. + * + * @since 3.9.0 + * + * @see wp_generate_attachment_metadata() + * + * @param array $metadata Attachment metadata. + */ + $metadata = apply_filters( 'wp_header_image_attachment_metadata', $metadata ); + + wp_update_attachment_metadata( $attachment_id, $metadata ); + + return $attachment_id; + } + + /** + * Gets attachment uploaded by Media Manager, crops it, then saves it as a + * new object. Returns JSON-encoded object details. + * + * @since 3.9.0 + */ + public function ajax_header_crop() { + check_ajax_referer( 'image_editor-' . $_POST['id'], 'nonce' ); + + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_send_json_error(); + } + + if ( ! current_theme_supports( 'custom-header', 'uploads' ) ) { + wp_send_json_error(); + } + + $crop_details = $_POST['cropDetails']; + + $dimensions = $this->get_header_dimensions( + array( + 'height' => $crop_details['height'], + 'width' => $crop_details['width'], + ) + ); + + $attachment_id = absint( $_POST['id'] ); + + $cropped = wp_crop_image( + $attachment_id, + (int) $crop_details['x1'], + (int) $crop_details['y1'], + (int) $crop_details['width'], + (int) $crop_details['height'], + (int) $dimensions['dst_width'], + (int) $dimensions['dst_height'] + ); + + if ( ! $cropped || is_wp_error( $cropped ) ) { + wp_send_json_error( array( 'message' => __( 'Image could not be processed. Please go back and try again.' ) ) ); + } + + /** This filter is documented in wp-admin/includes/class-custom-image-header.php */ + $cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication. + + $attachment = $this->create_attachment_object( $cropped, $attachment_id ); + + $previous = $this->get_previous_crop( $attachment ); + + if ( $previous ) { + $attachment['ID'] = $previous; + } else { + unset( $attachment['ID'] ); + } + + $new_attachment_id = $this->insert_attachment( $attachment, $cropped ); + + $attachment['attachment_id'] = $new_attachment_id; + $attachment['url'] = wp_get_attachment_url( $new_attachment_id ); + + $attachment['width'] = $dimensions['dst_width']; + $attachment['height'] = $dimensions['dst_height']; + + wp_send_json_success( $attachment ); + } + + /** + * Given an attachment ID for a header image, updates its "last used" + * timestamp to now. + * + * Triggered when the user tries adds a new header image from the + * Media Manager, even if s/he doesn't save that change. + * + * @since 3.9.0 + */ + public function ajax_header_add() { + check_ajax_referer( 'header-add', 'nonce' ); + + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_send_json_error(); + } + + $attachment_id = absint( $_POST['attachment_id'] ); + if ( $attachment_id < 1 ) { + wp_send_json_error(); + } + + $key = '_wp_attachment_custom_header_last_used_' . get_stylesheet(); + update_post_meta( $attachment_id, $key, time() ); + update_post_meta( $attachment_id, '_wp_attachment_is_custom_header', get_stylesheet() ); + + wp_send_json_success(); + } + + /** + * Given an attachment ID for a header image, unsets it as a user-uploaded + * header image for the active theme. + * + * Triggered when the user clicks the overlay "X" button next to each image + * choice in the Customizer's Header tool. + * + * @since 3.9.0 + */ + public function ajax_header_remove() { + check_ajax_referer( 'header-remove', 'nonce' ); + + if ( ! current_user_can( 'edit_theme_options' ) ) { + wp_send_json_error(); + } + + $attachment_id = absint( $_POST['attachment_id'] ); + if ( $attachment_id < 1 ) { + wp_send_json_error(); + } + + $key = '_wp_attachment_custom_header_last_used_' . get_stylesheet(); + delete_post_meta( $attachment_id, $key ); + delete_post_meta( $attachment_id, '_wp_attachment_is_custom_header', get_stylesheet() ); + + wp_send_json_success(); + } + + /** + * Updates the last-used postmeta on a header image attachment after saving a new header image via the Customizer. + * + * @since 3.9.0 + * + * @param WP_Customize_Manager $wp_customize Customize manager. + */ + public function customize_set_last_used( $wp_customize ) { + + $header_image_data_setting = $wp_customize->get_setting( 'header_image_data' ); + + if ( ! $header_image_data_setting ) { + return; + } + + $data = $header_image_data_setting->post_value(); + + if ( ! isset( $data['attachment_id'] ) ) { + return; + } + + $attachment_id = $data['attachment_id']; + $key = '_wp_attachment_custom_header_last_used_' . get_stylesheet(); + update_post_meta( $attachment_id, $key, time() ); + } + + /** + * Gets the details of default header images if defined. + * + * @since 3.9.0 + * + * @return array Default header images. + */ + public function get_default_header_images() { + $this->process_default_headers(); + + // Get the default image if there is one. + $default = get_theme_support( 'custom-header', 'default-image' ); + + if ( ! $default ) { // If not, easy peasy. + return $this->default_headers; + } + + $default = sprintf( $default, get_template_directory_uri(), get_stylesheet_directory_uri() ); + + $already_has_default = false; + + foreach ( $this->default_headers as $k => $h ) { + if ( $h['url'] === $default ) { + $already_has_default = true; + break; + } + } + + if ( $already_has_default ) { + return $this->default_headers; + } + + // If the one true image isn't included in the default set, prepend it. + $header_images = array(); + $header_images['default'] = array( + 'url' => $default, + 'thumbnail_url' => $default, + 'description' => 'Default', + ); + + // The rest of the set comes after. + return array_merge( $header_images, $this->default_headers ); + } + + /** + * Gets the previously uploaded header images. + * + * @since 3.9.0 + * + * @return array Uploaded header images. + */ + public function get_uploaded_header_images() { + $header_images = get_uploaded_header_images(); + $timestamp_key = '_wp_attachment_custom_header_last_used_' . get_stylesheet(); + $alt_text_key = '_wp_attachment_image_alt'; + + foreach ( $header_images as &$header_image ) { + $header_meta = get_post_meta( $header_image['attachment_id'] ); + $header_image['timestamp'] = isset( $header_meta[ $timestamp_key ] ) ? $header_meta[ $timestamp_key ] : ''; + $header_image['alt_text'] = isset( $header_meta[ $alt_text_key ] ) ? $header_meta[ $alt_text_key ] : ''; + } + + return $header_images; + } + + /** + * Gets the ID of a previous crop from the same base image. + * + * @since 4.9.0 + * + * @param array $attachment An array with a cropped attachment object data. + * @return int|false An attachment ID if one exists. False if none. + */ + public function get_previous_crop( $attachment ) { + $header_images = $this->get_uploaded_header_images(); + + // Bail early if there are no header images. + if ( empty( $header_images ) ) { + return false; + } + + $previous = false; + + foreach ( $header_images as $image ) { + if ( $image['attachment_parent'] === $attachment['post_parent'] ) { + $previous = $image['attachment_id']; + break; + } + } + + return $previous; + } +} diff --git a/wp-admin/includes/class-file-upload-upgrader.php b/wp-admin/includes/class-file-upload-upgrader.php new file mode 100644 index 0000000..e625615 --- /dev/null +++ b/wp-admin/includes/class-file-upload-upgrader.php @@ -0,0 +1,158 @@ + false, + 'test_type' => false, + ); + $file = wp_handle_upload( $_FILES[ $form ], $overrides ); + + if ( isset( $file['error'] ) ) { + wp_die( $file['error'] ); + } + + if ( 'pluginzip' === $form || 'themezip' === $form ) { + $archive_is_valid = false; + + /** This filter is documented in wp-admin/includes/file.php */ + if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) { + $archive = new ZipArchive(); + $archive_is_valid = $archive->open( $file['file'], ZIPARCHIVE::CHECKCONS ); + + if ( true === $archive_is_valid ) { + $archive->close(); + } + } else { + require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; + + $archive = new PclZip( $file['file'] ); + $archive_is_valid = is_array( $archive->properties() ); + } + + if ( true !== $archive_is_valid ) { + wp_delete_file( $file['file'] ); + wp_die( __( 'Incompatible Archive.' ) ); + } + } + + $this->filename = $_FILES[ $form ]['name']; + $this->package = $file['file']; + + // Construct the attachment array. + $attachment = array( + 'post_title' => $this->filename, + 'post_content' => $file['url'], + 'post_mime_type' => $file['type'], + 'guid' => $file['url'], + 'context' => 'upgrader', + 'post_status' => 'private', + ); + + // Save the data. + $this->id = wp_insert_attachment( $attachment, $file['file'] ); + + // Schedule a cleanup for 2 hours from now in case of failed installation. + wp_schedule_single_event( time() + 2 * HOUR_IN_SECONDS, 'upgrader_scheduled_cleanup', array( $this->id ) ); + + } elseif ( is_numeric( $_GET[ $urlholder ] ) ) { + // Numeric Package = previously uploaded file, see above. + $this->id = (int) $_GET[ $urlholder ]; + $attachment = get_post( $this->id ); + if ( empty( $attachment ) ) { + wp_die( __( 'Please select a file' ) ); + } + + $this->filename = $attachment->post_title; + $this->package = get_attached_file( $attachment->ID ); + } else { + // Else, It's set to something, Back compat for plugins using the old (pre-3.3) File_Uploader handler. + $uploads = wp_upload_dir(); + if ( ! ( $uploads && false === $uploads['error'] ) ) { + wp_die( $uploads['error'] ); + } + + $this->filename = sanitize_file_name( $_GET[ $urlholder ] ); + $this->package = $uploads['basedir'] . '/' . $this->filename; + + if ( ! str_starts_with( realpath( $this->package ), realpath( $uploads['basedir'] ) ) ) { + wp_die( __( 'Please select a file' ) ); + } + } + } + + /** + * Deletes the attachment/uploaded file. + * + * @since 3.2.2 + * + * @return bool Whether the cleanup was successful. + */ + public function cleanup() { + if ( $this->id ) { + wp_delete_attachment( $this->id ); + + } elseif ( file_exists( $this->package ) ) { + return @unlink( $this->package ); + } + + return true; + } +} diff --git a/wp-admin/includes/class-ftp-pure.php b/wp-admin/includes/class-ftp-pure.php new file mode 100644 index 0000000..4c51876 --- /dev/null +++ b/wp-admin/includes/class-ftp-pure.php @@ -0,0 +1,186 @@ + +// +// + + function _settimeout($sock) { + if(!@stream_set_timeout($sock, $this->_timeout)) { + $this->PushError('_settimeout','socket set send timeout'); + $this->_quit(); + return FALSE; + } + return TRUE; + } + + function _connect($host, $port) { + $this->SendMSG("Creating socket"); + $sock = @fsockopen($host, $port, $errno, $errstr, $this->_timeout); + if (!$sock) { + $this->PushError('_connect','socket connect failed', $errstr." (".$errno.")"); + return FALSE; + } + $this->_connected=true; + return $sock; + } + + function _readmsg($fnction="_readmsg"){ + if(!$this->_connected) { + $this->PushError($fnction, 'Connect first'); + return FALSE; + } + $result=true; + $this->_message=""; + $this->_code=0; + $go=true; + do { + $tmp=@fgets($this->_ftp_control_sock, 512); + if($tmp===false) { + $go=$result=false; + $this->PushError($fnction,'Read failed'); + } else { + $this->_message.=$tmp; + if(preg_match("/^([0-9]{3})(-(.*[".CRLF."]{1,2})+\\1)? [^".CRLF."]+[".CRLF."]{1,2}$/", $this->_message, $regs)) $go=false; + } + } while($go); + if($this->LocalEcho) echo "GET < ".rtrim($this->_message, CRLF).CRLF; + $this->_code=(int)$regs[1]; + return $result; + } + + function _exec($cmd, $fnction="_exec") { + if(!$this->_ready) { + $this->PushError($fnction,'Connect first'); + return FALSE; + } + if($this->LocalEcho) echo "PUT > ",$cmd,CRLF; + $status=@fputs($this->_ftp_control_sock, $cmd.CRLF); + if($status===false) { + $this->PushError($fnction,'socket write failed'); + return FALSE; + } + $this->_lastaction=time(); + if(!$this->_readmsg($fnction)) return FALSE; + return TRUE; + } + + function _data_prepare($mode=FTP_ASCII) { + if(!$this->_settype($mode)) return FALSE; + if($this->_passive) { + if(!$this->_exec("PASV", "pasv")) { + $this->_data_close(); + return FALSE; + } + if(!$this->_checkCode()) { + $this->_data_close(); + return FALSE; + } + $ip_port = explode(",", preg_replace("/^.+ \\(?([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]+,[0-9]+)\\)?.*$/s", "\\1", $this->_message)); + $this->_datahost=$ip_port[0].".".$ip_port[1].".".$ip_port[2].".".$ip_port[3]; + $this->_dataport=(((int)$ip_port[4])<<8) + ((int)$ip_port[5]); + $this->SendMSG("Connecting to ".$this->_datahost.":".$this->_dataport); + $this->_ftp_data_sock=@fsockopen($this->_datahost, $this->_dataport, $errno, $errstr, $this->_timeout); + if(!$this->_ftp_data_sock) { + $this->PushError("_data_prepare","fsockopen fails", $errstr." (".$errno.")"); + $this->_data_close(); + return FALSE; + } + else $this->_ftp_data_sock; + } else { + $this->SendMSG("Only passive connections available!"); + return FALSE; + } + return TRUE; + } + + function _data_read($mode=FTP_ASCII, $fp=NULL) { + if(is_resource($fp)) $out=0; + else $out=""; + if(!$this->_passive) { + $this->SendMSG("Only passive connections available!"); + return FALSE; + } + while (!feof($this->_ftp_data_sock)) { + $block=fread($this->_ftp_data_sock, $this->_ftp_buff_size); + if($mode!=FTP_BINARY) $block=preg_replace("/\r\n|\r|\n/", $this->_eol_code[$this->OS_local], $block); + if(is_resource($fp)) $out+=fwrite($fp, $block, strlen($block)); + else $out.=$block; + } + return $out; + } + + function _data_write($mode=FTP_ASCII, $fp=NULL) { + if(is_resource($fp)) $out=0; + else $out=""; + if(!$this->_passive) { + $this->SendMSG("Only passive connections available!"); + return FALSE; + } + if(is_resource($fp)) { + while(!feof($fp)) { + $block=fread($fp, $this->_ftp_buff_size); + if(!$this->_data_write_block($mode, $block)) return false; + } + } elseif(!$this->_data_write_block($mode, $fp)) return false; + return TRUE; + } + + function _data_write_block($mode, $block) { + if($mode!=FTP_BINARY) $block=preg_replace("/\r\n|\r|\n/", $this->_eol_code[$this->OS_remote], $block); + do { + if(($t=@fwrite($this->_ftp_data_sock, $block))===FALSE) { + $this->PushError("_data_write","Can't write to socket"); + return FALSE; + } + $block=substr($block, $t); + } while(!empty($block)); + return true; + } + + function _data_close() { + @fclose($this->_ftp_data_sock); + $this->SendMSG("Disconnected data from remote host"); + return TRUE; + } + + function _quit($force=FALSE) { + if($this->_connected or $force) { + @fclose($this->_ftp_control_sock); + $this->_connected=false; + $this->SendMSG("Socket closed"); + } + } +} + +?> diff --git a/wp-admin/includes/class-ftp-sockets.php b/wp-admin/includes/class-ftp-sockets.php new file mode 100644 index 0000000..575b393 --- /dev/null +++ b/wp-admin/includes/class-ftp-sockets.php @@ -0,0 +1,246 @@ + +// +// + + function _settimeout($sock) { + if(!@socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>$this->_timeout, "usec"=>0))) { + $this->PushError('_connect','socket set receive timeout',socket_strerror(socket_last_error($sock))); + @socket_close($sock); + return FALSE; + } + if(!@socket_set_option($sock, SOL_SOCKET , SO_SNDTIMEO, array("sec"=>$this->_timeout, "usec"=>0))) { + $this->PushError('_connect','socket set send timeout',socket_strerror(socket_last_error($sock))); + @socket_close($sock); + return FALSE; + } + return true; + } + + function _connect($host, $port) { + $this->SendMSG("Creating socket"); + if(!($sock = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP))) { + $this->PushError('_connect','socket create failed',socket_strerror(socket_last_error($sock))); + return FALSE; + } + if(!$this->_settimeout($sock)) return FALSE; + $this->SendMSG("Connecting to \"".$host.":".$port."\""); + if (!($res = @socket_connect($sock, $host, $port))) { + $this->PushError('_connect','socket connect failed',socket_strerror(socket_last_error($sock))); + @socket_close($sock); + return FALSE; + } + $this->_connected=true; + return $sock; + } + + function _readmsg($fnction="_readmsg"){ + if(!$this->_connected) { + $this->PushError($fnction,'Connect first'); + return FALSE; + } + $result=true; + $this->_message=""; + $this->_code=0; + $go=true; + do { + $tmp=@socket_read($this->_ftp_control_sock, 4096, PHP_BINARY_READ); + if($tmp===false) { + $go=$result=false; + $this->PushError($fnction,'Read failed', socket_strerror(socket_last_error($this->_ftp_control_sock))); + } else { + $this->_message.=$tmp; + $go = !preg_match("/^([0-9]{3})(-.+\\1)? [^".CRLF."]+".CRLF."$/Us", $this->_message, $regs); + } + } while($go); + if($this->LocalEcho) echo "GET < ".rtrim($this->_message, CRLF).CRLF; + $this->_code=(int)$regs[1]; + return $result; + } + + function _exec($cmd, $fnction="_exec") { + if(!$this->_ready) { + $this->PushError($fnction,'Connect first'); + return FALSE; + } + if($this->LocalEcho) echo "PUT > ",$cmd,CRLF; + $status=@socket_write($this->_ftp_control_sock, $cmd.CRLF); + if($status===false) { + $this->PushError($fnction,'socket write failed', socket_strerror(socket_last_error($this->stream))); + return FALSE; + } + $this->_lastaction=time(); + if(!$this->_readmsg($fnction)) return FALSE; + return TRUE; + } + + function _data_prepare($mode=FTP_ASCII) { + if(!$this->_settype($mode)) return FALSE; + $this->SendMSG("Creating data socket"); + $this->_ftp_data_sock = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if ($this->_ftp_data_sock < 0) { + $this->PushError('_data_prepare','socket create failed',socket_strerror(socket_last_error($this->_ftp_data_sock))); + return FALSE; + } + if(!$this->_settimeout($this->_ftp_data_sock)) { + $this->_data_close(); + return FALSE; + } + if($this->_passive) { + if(!$this->_exec("PASV", "pasv")) { + $this->_data_close(); + return FALSE; + } + if(!$this->_checkCode()) { + $this->_data_close(); + return FALSE; + } + $ip_port = explode(",", preg_replace("/^.+ \\(?([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]+,[0-9]+)\\)?.*$/s", "\\1", $this->_message)); + $this->_datahost=$ip_port[0].".".$ip_port[1].".".$ip_port[2].".".$ip_port[3]; + $this->_dataport=(((int)$ip_port[4])<<8) + ((int)$ip_port[5]); + $this->SendMSG("Connecting to ".$this->_datahost.":".$this->_dataport); + if(!@socket_connect($this->_ftp_data_sock, $this->_datahost, $this->_dataport)) { + $this->PushError("_data_prepare","socket_connect", socket_strerror(socket_last_error($this->_ftp_data_sock))); + $this->_data_close(); + return FALSE; + } + else $this->_ftp_temp_sock=$this->_ftp_data_sock; + } else { + if(!@socket_getsockname($this->_ftp_control_sock, $addr, $port)) { + $this->PushError("_data_prepare","cannot get control socket information", socket_strerror(socket_last_error($this->_ftp_control_sock))); + $this->_data_close(); + return FALSE; + } + if(!@socket_bind($this->_ftp_data_sock,$addr)){ + $this->PushError("_data_prepare","cannot bind data socket", socket_strerror(socket_last_error($this->_ftp_data_sock))); + $this->_data_close(); + return FALSE; + } + if(!@socket_listen($this->_ftp_data_sock)) { + $this->PushError("_data_prepare","cannot listen data socket", socket_strerror(socket_last_error($this->_ftp_data_sock))); + $this->_data_close(); + return FALSE; + } + if(!@socket_getsockname($this->_ftp_data_sock, $this->_datahost, $this->_dataport)) { + $this->PushError("_data_prepare","cannot get data socket information", socket_strerror(socket_last_error($this->_ftp_data_sock))); + $this->_data_close(); + return FALSE; + } + if(!$this->_exec('PORT '.str_replace('.',',',$this->_datahost.'.'.($this->_dataport>>8).'.'.($this->_dataport&0x00FF)), "_port")) { + $this->_data_close(); + return FALSE; + } + if(!$this->_checkCode()) { + $this->_data_close(); + return FALSE; + } + } + return TRUE; + } + + function _data_read($mode=FTP_ASCII, $fp=NULL) { + $NewLine=$this->_eol_code[$this->OS_local]; + if(is_resource($fp)) $out=0; + else $out=""; + if(!$this->_passive) { + $this->SendMSG("Connecting to ".$this->_datahost.":".$this->_dataport); + $this->_ftp_temp_sock=socket_accept($this->_ftp_data_sock); + if($this->_ftp_temp_sock===FALSE) { + $this->PushError("_data_read","socket_accept", socket_strerror(socket_last_error($this->_ftp_temp_sock))); + $this->_data_close(); + return FALSE; + } + } + + while(($block=@socket_read($this->_ftp_temp_sock, $this->_ftp_buff_size, PHP_BINARY_READ))!==false) { + if($block==="") break; + if($mode!=FTP_BINARY) $block=preg_replace("/\r\n|\r|\n/", $this->_eol_code[$this->OS_local], $block); + if(is_resource($fp)) $out+=fwrite($fp, $block, strlen($block)); + else $out.=$block; + } + return $out; + } + + function _data_write($mode=FTP_ASCII, $fp=NULL) { + $NewLine=$this->_eol_code[$this->OS_local]; + if(is_resource($fp)) $out=0; + else $out=""; + if(!$this->_passive) { + $this->SendMSG("Connecting to ".$this->_datahost.":".$this->_dataport); + $this->_ftp_temp_sock=socket_accept($this->_ftp_data_sock); + if($this->_ftp_temp_sock===FALSE) { + $this->PushError("_data_write","socket_accept", socket_strerror(socket_last_error($this->_ftp_temp_sock))); + $this->_data_close(); + return false; + } + } + if(is_resource($fp)) { + while(!feof($fp)) { + $block=fread($fp, $this->_ftp_buff_size); + if(!$this->_data_write_block($mode, $block)) return false; + } + } elseif(!$this->_data_write_block($mode, $fp)) return false; + return true; + } + + function _data_write_block($mode, $block) { + if($mode!=FTP_BINARY) $block=preg_replace("/\r\n|\r|\n/", $this->_eol_code[$this->OS_remote], $block); + do { + if(($t=@socket_write($this->_ftp_temp_sock, $block))===FALSE) { + $this->PushError("_data_write","socket_write", socket_strerror(socket_last_error($this->_ftp_temp_sock))); + $this->_data_close(); + return FALSE; + } + $block=substr($block, $t); + } while(!empty($block)); + return true; + } + + function _data_close() { + @socket_close($this->_ftp_temp_sock); + @socket_close($this->_ftp_data_sock); + $this->SendMSG("Disconnected data from remote host"); + return TRUE; + } + + function _quit() { + if($this->_connected) { + @socket_close($this->_ftp_control_sock); + $this->_connected=false; + $this->SendMSG("Socket closed"); + } + } +} +?> diff --git a/wp-admin/includes/class-ftp.php b/wp-admin/includes/class-ftp.php new file mode 100644 index 0000000..7658a0b --- /dev/null +++ b/wp-admin/includes/class-ftp.php @@ -0,0 +1,913 @@ +LocalEcho=$le; + $this->Verbose=$verb; + $this->_lastaction=NULL; + $this->_error_array=array(); + $this->_eol_code=array(FTP_OS_Unix=>"\n", FTP_OS_Mac=>"\r", FTP_OS_Windows=>"\r\n"); + $this->AuthorizedTransferMode=array(FTP_AUTOASCII, FTP_ASCII, FTP_BINARY); + $this->OS_FullName=array(FTP_OS_Unix => 'UNIX', FTP_OS_Windows => 'WINDOWS', FTP_OS_Mac => 'MACOS'); + $this->AutoAsciiExt=array("ASP","BAT","C","CPP","CSS","CSV","JS","H","HTM","HTML","SHTML","INI","LOG","PHP3","PHTML","PL","PERL","SH","SQL","TXT"); + $this->_port_available=($port_mode==TRUE); + $this->SendMSG("Staring FTP client class".($this->_port_available?"":" without PORT mode support")); + $this->_connected=FALSE; + $this->_ready=FALSE; + $this->_can_restore=FALSE; + $this->_code=0; + $this->_message=""; + $this->_ftp_buff_size=4096; + $this->_curtype=NULL; + $this->SetUmask(0022); + $this->SetType(FTP_AUTOASCII); + $this->SetTimeout(30); + $this->Passive(!$this->_port_available); + $this->_login="anonymous"; + $this->_password="anon@ftp.com"; + $this->_features=array(); + $this->OS_local=FTP_OS_Unix; + $this->OS_remote=FTP_OS_Unix; + $this->features=array(); + if(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') $this->OS_local=FTP_OS_Windows; + elseif(strtoupper(substr(PHP_OS, 0, 3)) === 'MAC') $this->OS_local=FTP_OS_Mac; + } + + function ftp_base($port_mode=FALSE) { + $this->__construct($port_mode); + } + +// +// +// + + function parselisting($line) { + $is_windows = ($this->OS_remote == FTP_OS_Windows); + if ($is_windows && preg_match("/([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|) +(.+)/",$line,$lucifer)) { + $b = array(); + if ($lucifer[3]<70) { $lucifer[3]+=2000; } else { $lucifer[3]+=1900; } // 4digit year fix + $b['isdir'] = ($lucifer[7]==""); + if ( $b['isdir'] ) + $b['type'] = 'd'; + else + $b['type'] = 'f'; + $b['size'] = $lucifer[7]; + $b['month'] = $lucifer[1]; + $b['day'] = $lucifer[2]; + $b['year'] = $lucifer[3]; + $b['hour'] = $lucifer[4]; + $b['minute'] = $lucifer[5]; + $b['time'] = @mktime($lucifer[4]+(strcasecmp($lucifer[6],"PM")==0?12:0),$lucifer[5],0,$lucifer[1],$lucifer[2],$lucifer[3]); + $b['am/pm'] = $lucifer[6]; + $b['name'] = $lucifer[8]; + } else if (!$is_windows && $lucifer=preg_split("/[ ]/",$line,9,PREG_SPLIT_NO_EMPTY)) { + //echo $line."\n"; + $lcount=count($lucifer); + if ($lcount<8) return ''; + $b = array(); + $b['isdir'] = $lucifer[0][0] === "d"; + $b['islink'] = $lucifer[0][0] === "l"; + if ( $b['isdir'] ) + $b['type'] = 'd'; + elseif ( $b['islink'] ) + $b['type'] = 'l'; + else + $b['type'] = 'f'; + $b['perms'] = $lucifer[0]; + $b['number'] = $lucifer[1]; + $b['owner'] = $lucifer[2]; + $b['group'] = $lucifer[3]; + $b['size'] = $lucifer[4]; + if ($lcount==8) { + sscanf($lucifer[5],"%d-%d-%d",$b['year'],$b['month'],$b['day']); + sscanf($lucifer[6],"%d:%d",$b['hour'],$b['minute']); + $b['time'] = @mktime($b['hour'],$b['minute'],0,$b['month'],$b['day'],$b['year']); + $b['name'] = $lucifer[7]; + } else { + $b['month'] = $lucifer[5]; + $b['day'] = $lucifer[6]; + if (preg_match("/([0-9]{2}):([0-9]{2})/",$lucifer[7],$l2)) { + $b['year'] = gmdate("Y"); + $b['hour'] = $l2[1]; + $b['minute'] = $l2[2]; + } else { + $b['year'] = $lucifer[7]; + $b['hour'] = 0; + $b['minute'] = 0; + } + $b['time'] = strtotime(sprintf("%d %s %d %02d:%02d",$b['day'],$b['month'],$b['year'],$b['hour'],$b['minute'])); + $b['name'] = $lucifer[8]; + } + } + + return $b; + } + + function SendMSG($message = "", $crlf=true) { + if ($this->Verbose) { + echo $message.($crlf?CRLF:""); + flush(); + } + return TRUE; + } + + function SetType($mode=FTP_AUTOASCII) { + if(!in_array($mode, $this->AuthorizedTransferMode)) { + $this->SendMSG("Wrong type"); + return FALSE; + } + $this->_type=$mode; + $this->SendMSG("Transfer type: ".($this->_type==FTP_BINARY?"binary":($this->_type==FTP_ASCII?"ASCII":"auto ASCII") ) ); + return TRUE; + } + + function _settype($mode=FTP_ASCII) { + if($this->_ready) { + if($mode==FTP_BINARY) { + if($this->_curtype!=FTP_BINARY) { + if(!$this->_exec("TYPE I", "SetType")) return FALSE; + $this->_curtype=FTP_BINARY; + } + } elseif($this->_curtype!=FTP_ASCII) { + if(!$this->_exec("TYPE A", "SetType")) return FALSE; + $this->_curtype=FTP_ASCII; + } + } else return FALSE; + return TRUE; + } + + function Passive($pasv=NULL) { + if(is_null($pasv)) $this->_passive=!$this->_passive; + else $this->_passive=$pasv; + if(!$this->_port_available and !$this->_passive) { + $this->SendMSG("Only passive connections available!"); + $this->_passive=TRUE; + return FALSE; + } + $this->SendMSG("Passive mode ".($this->_passive?"on":"off")); + return TRUE; + } + + function SetServer($host, $port=21, $reconnect=true) { + if(!is_long($port)) { + $this->verbose=true; + $this->SendMSG("Incorrect port syntax"); + return FALSE; + } else { + $ip=@gethostbyname($host); + $dns=@gethostbyaddr($host); + if(!$ip) $ip=$host; + if(!$dns) $dns=$host; + // Validate the IPAddress PHP4 returns -1 for invalid, PHP5 false + // -1 === "255.255.255.255" which is the broadcast address which is also going to be invalid + $ipaslong = ip2long($ip); + if ( ($ipaslong == false) || ($ipaslong === -1) ) { + $this->SendMSG("Wrong host name/address \"".$host."\""); + return FALSE; + } + $this->_host=$ip; + $this->_fullhost=$dns; + $this->_port=$port; + $this->_dataport=$port-1; + } + $this->SendMSG("Host \"".$this->_fullhost."(".$this->_host."):".$this->_port."\""); + if($reconnect){ + if($this->_connected) { + $this->SendMSG("Reconnecting"); + if(!$this->quit(FTP_FORCE)) return FALSE; + if(!$this->connect()) return FALSE; + } + } + return TRUE; + } + + function SetUmask($umask=0022) { + $this->_umask=$umask; + umask($this->_umask); + $this->SendMSG("UMASK 0".decoct($this->_umask)); + return TRUE; + } + + function SetTimeout($timeout=30) { + $this->_timeout=$timeout; + $this->SendMSG("Timeout ".$this->_timeout); + if($this->_connected) + if(!$this->_settimeout($this->_ftp_control_sock)) return FALSE; + return TRUE; + } + + function connect($server=NULL) { + if(!empty($server)) { + if(!$this->SetServer($server)) return false; + } + if($this->_ready) return true; + $this->SendMsg('Local OS : '.$this->OS_FullName[$this->OS_local]); + if(!($this->_ftp_control_sock = $this->_connect($this->_host, $this->_port))) { + $this->SendMSG("Error : Cannot connect to remote host \"".$this->_fullhost." :".$this->_port."\""); + return FALSE; + } + $this->SendMSG("Connected to remote host \"".$this->_fullhost.":".$this->_port."\". Waiting for greeting."); + do { + if(!$this->_readmsg()) return FALSE; + if(!$this->_checkCode()) return FALSE; + $this->_lastaction=time(); + } while($this->_code<200); + $this->_ready=true; + $syst=$this->systype(); + if(!$syst) $this->SendMSG("Cannot detect remote OS"); + else { + if(preg_match("/win|dos|novell/i", $syst[0])) $this->OS_remote=FTP_OS_Windows; + elseif(preg_match("/os/i", $syst[0])) $this->OS_remote=FTP_OS_Mac; + elseif(preg_match("/(li|u)nix/i", $syst[0])) $this->OS_remote=FTP_OS_Unix; + else $this->OS_remote=FTP_OS_Mac; + $this->SendMSG("Remote OS: ".$this->OS_FullName[$this->OS_remote]); + } + if(!$this->features()) $this->SendMSG("Cannot get features list. All supported - disabled"); + else $this->SendMSG("Supported features: ".implode(", ", array_keys($this->_features))); + return TRUE; + } + + function quit($force=false) { + if($this->_ready) { + if(!$this->_exec("QUIT") and !$force) return FALSE; + if(!$this->_checkCode() and !$force) return FALSE; + $this->_ready=false; + $this->SendMSG("Session finished"); + } + $this->_quit(); + return TRUE; + } + + function login($user=NULL, $pass=NULL) { + if(!is_null($user)) $this->_login=$user; + else $this->_login="anonymous"; + if(!is_null($pass)) $this->_password=$pass; + else $this->_password="anon@anon.com"; + if(!$this->_exec("USER ".$this->_login, "login")) return FALSE; + if(!$this->_checkCode()) return FALSE; + if($this->_code!=230) { + if(!$this->_exec((($this->_code==331)?"PASS ":"ACCT ").$this->_password, "login")) return FALSE; + if(!$this->_checkCode()) return FALSE; + } + $this->SendMSG("Authentication succeeded"); + if(empty($this->_features)) { + if(!$this->features()) $this->SendMSG("Cannot get features list. All supported - disabled"); + else $this->SendMSG("Supported features: ".implode(", ", array_keys($this->_features))); + } + return TRUE; + } + + function pwd() { + if(!$this->_exec("PWD", "pwd")) return FALSE; + if(!$this->_checkCode()) return FALSE; + return preg_replace("/^[0-9]{3} \"(.+)\".*$/s", "\\1", $this->_message); + } + + function cdup() { + if(!$this->_exec("CDUP", "cdup")) return FALSE; + if(!$this->_checkCode()) return FALSE; + return true; + } + + function chdir($pathname) { + if(!$this->_exec("CWD ".$pathname, "chdir")) return FALSE; + if(!$this->_checkCode()) return FALSE; + return TRUE; + } + + function rmdir($pathname) { + if(!$this->_exec("RMD ".$pathname, "rmdir")) return FALSE; + if(!$this->_checkCode()) return FALSE; + return TRUE; + } + + function mkdir($pathname) { + if(!$this->_exec("MKD ".$pathname, "mkdir")) return FALSE; + if(!$this->_checkCode()) return FALSE; + return TRUE; + } + + function rename($from, $to) { + if(!$this->_exec("RNFR ".$from, "rename")) return FALSE; + if(!$this->_checkCode()) return FALSE; + if($this->_code==350) { + if(!$this->_exec("RNTO ".$to, "rename")) return FALSE; + if(!$this->_checkCode()) return FALSE; + } else return FALSE; + return TRUE; + } + + function filesize($pathname) { + if(!isset($this->_features["SIZE"])) { + $this->PushError("filesize", "not supported by server"); + return FALSE; + } + if(!$this->_exec("SIZE ".$pathname, "filesize")) return FALSE; + if(!$this->_checkCode()) return FALSE; + return preg_replace("/^[0-9]{3} ([0-9]+).*$/s", "\\1", $this->_message); + } + + function abort() { + if(!$this->_exec("ABOR", "abort")) return FALSE; + if(!$this->_checkCode()) { + if($this->_code!=426) return FALSE; + if(!$this->_readmsg("abort")) return FALSE; + if(!$this->_checkCode()) return FALSE; + } + return true; + } + + function mdtm($pathname) { + if(!isset($this->_features["MDTM"])) { + $this->PushError("mdtm", "not supported by server"); + return FALSE; + } + if(!$this->_exec("MDTM ".$pathname, "mdtm")) return FALSE; + if(!$this->_checkCode()) return FALSE; + $mdtm = preg_replace("/^[0-9]{3} ([0-9]+).*$/s", "\\1", $this->_message); + $date = sscanf($mdtm, "%4d%2d%2d%2d%2d%2d"); + $timestamp = mktime($date[3], $date[4], $date[5], $date[1], $date[2], $date[0]); + return $timestamp; + } + + function systype() { + if(!$this->_exec("SYST", "systype")) return FALSE; + if(!$this->_checkCode()) return FALSE; + $DATA = explode(" ", $this->_message); + return array($DATA[1], $DATA[3]); + } + + function delete($pathname) { + if(!$this->_exec("DELE ".$pathname, "delete")) return FALSE; + if(!$this->_checkCode()) return FALSE; + return TRUE; + } + + function site($command, $fnction="site") { + if(!$this->_exec("SITE ".$command, $fnction)) return FALSE; + if(!$this->_checkCode()) return FALSE; + return TRUE; + } + + function chmod($pathname, $mode) { + if(!$this->site( sprintf('CHMOD %o %s', $mode, $pathname), "chmod")) return FALSE; + return TRUE; + } + + function restore($from) { + if(!isset($this->_features["REST"])) { + $this->PushError("restore", "not supported by server"); + return FALSE; + } + if($this->_curtype!=FTP_BINARY) { + $this->PushError("restore", "cannot restore in ASCII mode"); + return FALSE; + } + if(!$this->_exec("REST ".$from, "resore")) return FALSE; + if(!$this->_checkCode()) return FALSE; + return TRUE; + } + + function features() { + if(!$this->_exec("FEAT", "features")) return FALSE; + if(!$this->_checkCode()) return FALSE; + $f=preg_split("/[".CRLF."]+/", preg_replace("/[0-9]{3}[ -].*[".CRLF."]+/", "", $this->_message), -1, PREG_SPLIT_NO_EMPTY); + $this->_features=array(); + foreach($f as $k=>$v) { + $v=explode(" ", trim($v)); + $this->_features[array_shift($v)]=$v; + } + return true; + } + + function rawlist($pathname="", $arg="") { + return $this->_list(($arg?" ".$arg:"").($pathname?" ".$pathname:""), "LIST", "rawlist"); + } + + function nlist($pathname="", $arg="") { + return $this->_list(($arg?" ".$arg:"").($pathname?" ".$pathname:""), "NLST", "nlist"); + } + + function is_exists($pathname) { + return $this->file_exists($pathname); + } + + function file_exists($pathname) { + $exists=true; + if(!$this->_exec("RNFR ".$pathname, "rename")) $exists=FALSE; + else { + if(!$this->_checkCode()) $exists=FALSE; + $this->abort(); + } + if($exists) $this->SendMSG("Remote file ".$pathname." exists"); + else $this->SendMSG("Remote file ".$pathname." does not exist"); + return $exists; + } + + function fget($fp, $remotefile, $rest=0) { + if($this->_can_restore and $rest!=0) fseek($fp, $rest); + $pi=pathinfo($remotefile); + if($this->_type==FTP_ASCII or ($this->_type==FTP_AUTOASCII and in_array(strtoupper($pi["extension"]), $this->AutoAsciiExt))) $mode=FTP_ASCII; + else $mode=FTP_BINARY; + if(!$this->_data_prepare($mode)) { + return FALSE; + } + if($this->_can_restore and $rest!=0) $this->restore($rest); + if(!$this->_exec("RETR ".$remotefile, "get")) { + $this->_data_close(); + return FALSE; + } + if(!$this->_checkCode()) { + $this->_data_close(); + return FALSE; + } + $out=$this->_data_read($mode, $fp); + $this->_data_close(); + if(!$this->_readmsg()) return FALSE; + if(!$this->_checkCode()) return FALSE; + return $out; + } + + function get($remotefile, $localfile=NULL, $rest=0) { + if(is_null($localfile)) $localfile=$remotefile; + if (@file_exists($localfile)) $this->SendMSG("Warning : local file will be overwritten"); + $fp = @fopen($localfile, "w"); + if (!$fp) { + $this->PushError("get","cannot open local file", "Cannot create \"".$localfile."\""); + return FALSE; + } + if($this->_can_restore and $rest!=0) fseek($fp, $rest); + $pi=pathinfo($remotefile); + if($this->_type==FTP_ASCII or ($this->_type==FTP_AUTOASCII and in_array(strtoupper($pi["extension"]), $this->AutoAsciiExt))) $mode=FTP_ASCII; + else $mode=FTP_BINARY; + if(!$this->_data_prepare($mode)) { + fclose($fp); + return FALSE; + } + if($this->_can_restore and $rest!=0) $this->restore($rest); + if(!$this->_exec("RETR ".$remotefile, "get")) { + $this->_data_close(); + fclose($fp); + return FALSE; + } + if(!$this->_checkCode()) { + $this->_data_close(); + fclose($fp); + return FALSE; + } + $out=$this->_data_read($mode, $fp); + fclose($fp); + $this->_data_close(); + if(!$this->_readmsg()) return FALSE; + if(!$this->_checkCode()) return FALSE; + return $out; + } + + function fput($remotefile, $fp, $rest=0) { + if($this->_can_restore and $rest!=0) fseek($fp, $rest); + $pi=pathinfo($remotefile); + if($this->_type==FTP_ASCII or ($this->_type==FTP_AUTOASCII and in_array(strtoupper($pi["extension"]), $this->AutoAsciiExt))) $mode=FTP_ASCII; + else $mode=FTP_BINARY; + if(!$this->_data_prepare($mode)) { + return FALSE; + } + if($this->_can_restore and $rest!=0) $this->restore($rest); + if(!$this->_exec("STOR ".$remotefile, "put")) { + $this->_data_close(); + return FALSE; + } + if(!$this->_checkCode()) { + $this->_data_close(); + return FALSE; + } + $ret=$this->_data_write($mode, $fp); + $this->_data_close(); + if(!$this->_readmsg()) return FALSE; + if(!$this->_checkCode()) return FALSE; + return $ret; + } + + function put($localfile, $remotefile=NULL, $rest=0) { + if(is_null($remotefile)) $remotefile=$localfile; + if (!file_exists($localfile)) { + $this->PushError("put","cannot open local file", "No such file or directory \"".$localfile."\""); + return FALSE; + } + $fp = @fopen($localfile, "r"); + + if (!$fp) { + $this->PushError("put","cannot open local file", "Cannot read file \"".$localfile."\""); + return FALSE; + } + if($this->_can_restore and $rest!=0) fseek($fp, $rest); + $pi=pathinfo($localfile); + if($this->_type==FTP_ASCII or ($this->_type==FTP_AUTOASCII and in_array(strtoupper($pi["extension"]), $this->AutoAsciiExt))) $mode=FTP_ASCII; + else $mode=FTP_BINARY; + if(!$this->_data_prepare($mode)) { + fclose($fp); + return FALSE; + } + if($this->_can_restore and $rest!=0) $this->restore($rest); + if(!$this->_exec("STOR ".$remotefile, "put")) { + $this->_data_close(); + fclose($fp); + return FALSE; + } + if(!$this->_checkCode()) { + $this->_data_close(); + fclose($fp); + return FALSE; + } + $ret=$this->_data_write($mode, $fp); + fclose($fp); + $this->_data_close(); + if(!$this->_readmsg()) return FALSE; + if(!$this->_checkCode()) return FALSE; + return $ret; + } + + function mput($local=".", $remote=NULL, $continious=false) { + $local=realpath($local); + if(!@file_exists($local)) { + $this->PushError("mput","cannot open local folder", "Cannot stat folder \"".$local."\""); + return FALSE; + } + if(!is_dir($local)) return $this->put($local, $remote); + if(empty($remote)) $remote="."; + elseif(!$this->file_exists($remote) and !$this->mkdir($remote)) return FALSE; + if($handle = opendir($local)) { + $list=array(); + while (false !== ($file = readdir($handle))) { + if ($file != "." && $file != "..") $list[]=$file; + } + closedir($handle); + } else { + $this->PushError("mput","cannot open local folder", "Cannot read folder \"".$local."\""); + return FALSE; + } + if(empty($list)) return TRUE; + $ret=true; + foreach($list as $el) { + if(is_dir($local."/".$el)) $t=$this->mput($local."/".$el, $remote."/".$el); + else $t=$this->put($local."/".$el, $remote."/".$el); + if(!$t) { + $ret=FALSE; + if(!$continious) break; + } + } + return $ret; + + } + + function mget($remote, $local=".", $continious=false) { + $list=$this->rawlist($remote, "-lA"); + if($list===false) { + $this->PushError("mget","cannot read remote folder list", "Cannot read remote folder \"".$remote."\" contents"); + return FALSE; + } + if(empty($list)) return true; + if(!@file_exists($local)) { + if(!@mkdir($local)) { + $this->PushError("mget","cannot create local folder", "Cannot create folder \"".$local."\""); + return FALSE; + } + } + foreach($list as $k=>$v) { + $list[$k]=$this->parselisting($v); + if( ! $list[$k] or $list[$k]["name"]=="." or $list[$k]["name"]=="..") unset($list[$k]); + } + $ret=true; + foreach($list as $el) { + if($el["type"]=="d") { + if(!$this->mget($remote."/".$el["name"], $local."/".$el["name"], $continious)) { + $this->PushError("mget", "cannot copy folder", "Cannot copy remote folder \"".$remote."/".$el["name"]."\" to local \"".$local."/".$el["name"]."\""); + $ret=false; + if(!$continious) break; + } + } else { + if(!$this->get($remote."/".$el["name"], $local."/".$el["name"])) { + $this->PushError("mget", "cannot copy file", "Cannot copy remote file \"".$remote."/".$el["name"]."\" to local \"".$local."/".$el["name"]."\""); + $ret=false; + if(!$continious) break; + } + } + @chmod($local."/".$el["name"], $el["perms"]); + $t=strtotime($el["date"]); + if($t!==-1 and $t!==false) @touch($local."/".$el["name"], $t); + } + return $ret; + } + + function mdel($remote, $continious=false) { + $list=$this->rawlist($remote, "-la"); + if($list===false) { + $this->PushError("mdel","cannot read remote folder list", "Cannot read remote folder \"".$remote."\" contents"); + return false; + } + + foreach($list as $k=>$v) { + $list[$k]=$this->parselisting($v); + if( ! $list[$k] or $list[$k]["name"]=="." or $list[$k]["name"]=="..") unset($list[$k]); + } + $ret=true; + + foreach($list as $el) { + if ( empty($el) ) + continue; + + if($el["type"]=="d") { + if(!$this->mdel($remote."/".$el["name"], $continious)) { + $ret=false; + if(!$continious) break; + } + } else { + if (!$this->delete($remote."/".$el["name"])) { + $this->PushError("mdel", "cannot delete file", "Cannot delete remote file \"".$remote."/".$el["name"]."\""); + $ret=false; + if(!$continious) break; + } + } + } + + if(!$this->rmdir($remote)) { + $this->PushError("mdel", "cannot delete folder", "Cannot delete remote folder \"".$remote."/".$el["name"]."\""); + $ret=false; + } + return $ret; + } + + function mmkdir($dir, $mode = 0777) { + if(empty($dir)) return FALSE; + if($this->is_exists($dir) or $dir == "/" ) return TRUE; + if(!$this->mmkdir(dirname($dir), $mode)) return false; + $r=$this->mkdir($dir, $mode); + $this->chmod($dir,$mode); + return $r; + } + + function glob($pattern, $handle=NULL) { + $path=$output=null; + if(PHP_OS=='WIN32') $slash='\\'; + else $slash='/'; + $lastpos=strrpos($pattern,$slash); + if(!($lastpos===false)) { + $path=substr($pattern,0,-$lastpos-1); + $pattern=substr($pattern,$lastpos); + } else $path=getcwd(); + if(is_array($handle) and !empty($handle)) { + foreach($handle as $dir) { + if($this->glob_pattern_match($pattern,$dir)) + $output[]=$dir; + } + } else { + $handle=@opendir($path); + if($handle===false) return false; + while($dir=readdir($handle)) { + if($this->glob_pattern_match($pattern,$dir)) + $output[]=$dir; + } + closedir($handle); + } + if(is_array($output)) return $output; + return false; + } + + function glob_pattern_match($pattern,$subject) { + $out=null; + $chunks=explode(';',$pattern); + foreach($chunks as $pattern) { + $escape=array('$','^','.','{','}','(',')','[',']','|'); + while(str_contains($pattern,'**')) + $pattern=str_replace('**','*',$pattern); + foreach($escape as $probe) + $pattern=str_replace($probe,"\\$probe",$pattern); + $pattern=str_replace('?*','*', + str_replace('*?','*', + str_replace('*',".*", + str_replace('?','.{1,1}',$pattern)))); + $out[]=$pattern; + } + if(count($out)==1) return($this->glob_regexp("^$out[0]$",$subject)); + else { + foreach($out as $tester) + // TODO: This should probably be glob_regexp(), but needs tests. + if($this->my_regexp("^$tester$",$subject)) return true; + } + return false; + } + + function glob_regexp($pattern,$subject) { + $sensitive=(PHP_OS!='WIN32'); + return ($sensitive? + preg_match( '/' . preg_quote( $pattern, '/' ) . '/', $subject ) : + preg_match( '/' . preg_quote( $pattern, '/' ) . '/i', $subject ) + ); + } + + function dirlist($remote) { + $list=$this->rawlist($remote, "-la"); + if($list===false) { + $this->PushError("dirlist","cannot read remote folder list", "Cannot read remote folder \"".$remote."\" contents"); + return false; + } + + $dirlist = array(); + foreach($list as $k=>$v) { + $entry=$this->parselisting($v); + if ( empty($entry) ) + continue; + + if($entry["name"]=="." or $entry["name"]=="..") + continue; + + $dirlist[$entry['name']] = $entry; + } + + return $dirlist; + } +// +// +// + function _checkCode() { + return ($this->_code<400 and $this->_code>0); + } + + function _list($arg="", $cmd="LIST", $fnction="_list") { + if(!$this->_data_prepare()) return false; + if(!$this->_exec($cmd.$arg, $fnction)) { + $this->_data_close(); + return FALSE; + } + if(!$this->_checkCode()) { + $this->_data_close(); + return FALSE; + } + $out=""; + if($this->_code<200) { + $out=$this->_data_read(); + $this->_data_close(); + if(!$this->_readmsg()) return FALSE; + if(!$this->_checkCode()) return FALSE; + if($out === FALSE ) return FALSE; + $out=preg_split("/[".CRLF."]+/", $out, -1, PREG_SPLIT_NO_EMPTY); +// $this->SendMSG(implode($this->_eol_code[$this->OS_local], $out)); + } + return $out; + } + +// +// +// +// Gnre une erreur pour traitement externe la classe + function PushError($fctname,$msg,$desc=false){ + $error=array(); + $error['time']=time(); + $error['fctname']=$fctname; + $error['msg']=$msg; + $error['desc']=$desc; + if($desc) $tmp=' ('.$desc.')'; else $tmp=''; + $this->SendMSG($fctname.': '.$msg.$tmp); + return(array_push($this->_error_array,$error)); + } + +// Rcupre une erreur externe + function PopError(){ + if(count($this->_error_array)) return(array_pop($this->_error_array)); + else return(false); + } +} + +$mod_sockets = extension_loaded( 'sockets' ); +if ( ! $mod_sockets && function_exists( 'dl' ) && is_callable( 'dl' ) ) { + $prefix = ( PHP_SHLIB_SUFFIX == 'dll' ) ? 'php_' : ''; + @dl( $prefix . 'sockets.' . PHP_SHLIB_SUFFIX ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.dlDeprecated + $mod_sockets = extension_loaded( 'sockets' ); +} + +require_once __DIR__ . "/class-ftp-" . ( $mod_sockets ? "sockets" : "pure" ) . ".php"; + +if ( $mod_sockets ) { + class ftp extends ftp_sockets {} +} else { + class ftp extends ftp_pure {} +} diff --git a/wp-admin/includes/class-language-pack-upgrader-skin.php b/wp-admin/includes/class-language-pack-upgrader-skin.php new file mode 100644 index 0000000..57b0a1c --- /dev/null +++ b/wp-admin/includes/class-language-pack-upgrader-skin.php @@ -0,0 +1,97 @@ + '', + 'nonce' => '', + 'title' => __( 'Update Translations' ), + 'skip_header_footer' => false, + ); + $args = wp_parse_args( $args, $defaults ); + if ( $args['skip_header_footer'] ) { + $this->done_header = true; + $this->done_footer = true; + $this->display_footer_actions = false; + } + parent::__construct( $args ); + } + + /** + */ + public function before() { + $name = $this->upgrader->get_name_for_update( $this->language_update ); + + echo '
'; + + /* translators: 1: Project name (plugin, theme, or WordPress), 2: Language. */ + printf( '

' . __( 'Updating translations for %1$s (%2$s)…' ) . '

', $name, $this->language_update->language ); + } + + /** + * @since 5.9.0 Renamed `$error` to `$errors` for PHP 8 named parameter support. + * + * @param string|WP_Error $errors Errors. + */ + public function error( $errors ) { + echo '
'; + parent::error( $errors ); + echo '
'; + } + + /** + */ + public function after() { + echo '
'; + } + + /** + */ + public function bulk_footer() { + $this->decrement_update_count( 'translation' ); + + $update_actions = array( + 'updates_page' => sprintf( + '%s', + self_admin_url( 'update-core.php' ), + __( 'Go to WordPress Updates page' ) + ), + ); + + /** + * Filters the list of action links available following a translations update. + * + * @since 3.7.0 + * + * @param string[] $update_actions Array of translations update links. + */ + $update_actions = apply_filters( 'update_translations_complete_actions', $update_actions ); + + if ( $update_actions && $this->display_footer_actions ) { + $this->feedback( implode( ' | ', $update_actions ) ); + } + } +} diff --git a/wp-admin/includes/class-language-pack-upgrader.php b/wp-admin/includes/class-language-pack-upgrader.php new file mode 100644 index 0000000..3c3d42a --- /dev/null +++ b/wp-admin/includes/class-language-pack-upgrader.php @@ -0,0 +1,474 @@ +is_vcs_checkout( WP_CONTENT_DIR ) ) { + return; + } + + foreach ( $language_updates as $key => $language_update ) { + $update = ! empty( $language_update->autoupdate ); + + /** + * Filters whether to asynchronously update translation for core, a plugin, or a theme. + * + * @since 4.0.0 + * + * @param bool $update Whether to update. + * @param object $language_update The update offer. + */ + $update = apply_filters( 'async_update_translation', $update, $language_update ); + + if ( ! $update ) { + unset( $language_updates[ $key ] ); + } + } + + if ( empty( $language_updates ) ) { + return; + } + + // Re-use the automatic upgrader skin if the parent upgrader is using it. + if ( $upgrader && $upgrader->skin instanceof Automatic_Upgrader_Skin ) { + $skin = $upgrader->skin; + } else { + $skin = new Language_Pack_Upgrader_Skin( + array( + 'skip_header_footer' => true, + ) + ); + } + + $lp_upgrader = new Language_Pack_Upgrader( $skin ); + $lp_upgrader->bulk_upgrade( $language_updates ); + } + + /** + * Initializes the upgrade strings. + * + * @since 3.7.0 + */ + public function upgrade_strings() { + $this->strings['starting_upgrade'] = __( 'Some of your translations need updating. Sit tight for a few more seconds while they are updated as well.' ); + $this->strings['up_to_date'] = __( 'Your translations are all up to date.' ); + $this->strings['no_package'] = __( 'Update package not available.' ); + /* translators: %s: Package URL. */ + $this->strings['downloading_package'] = sprintf( __( 'Downloading translation from %s…' ), '%s' ); + $this->strings['unpack_package'] = __( 'Unpacking the update…' ); + $this->strings['process_failed'] = __( 'Translation update failed.' ); + $this->strings['process_success'] = __( 'Translation updated successfully.' ); + $this->strings['remove_old'] = __( 'Removing the old version of the translation…' ); + $this->strings['remove_old_failed'] = __( 'Could not remove the old translation.' ); + } + + /** + * Upgrades a language pack. + * + * @since 3.7.0 + * + * @param string|false $update Optional. Whether an update offer is available. Default false. + * @param array $args Optional. Other optional arguments, see + * Language_Pack_Upgrader::bulk_upgrade(). Default empty array. + * @return array|bool|WP_Error The result of the upgrade, or a WP_Error object instead. + */ + public function upgrade( $update = false, $args = array() ) { + if ( $update ) { + $update = array( $update ); + } + + $results = $this->bulk_upgrade( $update, $args ); + + if ( ! is_array( $results ) ) { + return $results; + } + + return $results[0]; + } + + /** + * Upgrades several language packs at once. + * + * @since 3.7.0 + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @param object[] $language_updates Optional. Array of language packs to update. See {@see wp_get_translation_updates()}. + * Default empty array. + * @param array $args { + * Other arguments for upgrading multiple language packs. Default empty array. + * + * @type bool $clear_update_cache Whether to clear the update cache when done. + * Default true. + * } + * @return array|bool|WP_Error Will return an array of results, or true if there are no updates, + * false or WP_Error for initial errors. + */ + public function bulk_upgrade( $language_updates = array(), $args = array() ) { + global $wp_filesystem; + + $defaults = array( + 'clear_update_cache' => true, + ); + $parsed_args = wp_parse_args( $args, $defaults ); + + $this->init(); + $this->upgrade_strings(); + + if ( ! $language_updates ) { + $language_updates = wp_get_translation_updates(); + } + + if ( empty( $language_updates ) ) { + $this->skin->header(); + $this->skin->set_result( true ); + $this->skin->feedback( 'up_to_date' ); + $this->skin->bulk_footer(); + $this->skin->footer(); + return true; + } + + if ( 'upgrader_process_complete' === current_filter() ) { + $this->skin->feedback( 'starting_upgrade' ); + } + + // Remove any existing upgrade filters from the plugin/theme upgraders #WP29425 & #WP29230. + remove_all_filters( 'upgrader_pre_install' ); + remove_all_filters( 'upgrader_clear_destination' ); + remove_all_filters( 'upgrader_post_install' ); + remove_all_filters( 'upgrader_source_selection' ); + + add_filter( 'upgrader_source_selection', array( $this, 'check_package' ), 10, 2 ); + + $this->skin->header(); + + // Connect to the filesystem first. + $res = $this->fs_connect( array( WP_CONTENT_DIR, WP_LANG_DIR ) ); + if ( ! $res ) { + $this->skin->footer(); + return false; + } + + $results = array(); + + $this->update_count = count( $language_updates ); + $this->update_current = 0; + + /* + * The filesystem's mkdir() is not recursive. Make sure WP_LANG_DIR exists, + * as we then may need to create a /plugins or /themes directory inside of it. + */ + $remote_destination = $wp_filesystem->find_folder( WP_LANG_DIR ); + if ( ! $wp_filesystem->exists( $remote_destination ) ) { + if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) { + return new WP_Error( 'mkdir_failed_lang_dir', $this->strings['mkdir_failed'], $remote_destination ); + } + } + + $language_updates_results = array(); + + foreach ( $language_updates as $language_update ) { + + $this->skin->language_update = $language_update; + + $destination = WP_LANG_DIR; + if ( 'plugin' === $language_update->type ) { + $destination .= '/plugins'; + } elseif ( 'theme' === $language_update->type ) { + $destination .= '/themes'; + } + + ++$this->update_current; + + $options = array( + 'package' => $language_update->package, + 'destination' => $destination, + 'clear_destination' => true, + 'abort_if_destination_exists' => false, // We expect the destination to exist. + 'clear_working' => true, + 'is_multi' => true, + 'hook_extra' => array( + 'language_update_type' => $language_update->type, + 'language_update' => $language_update, + ), + ); + + $result = $this->run( $options ); + + $results[] = $this->result; + + // Prevent credentials auth screen from displaying multiple times. + if ( false === $result ) { + break; + } + + $language_updates_results[] = array( + 'language' => $language_update->language, + 'type' => $language_update->type, + 'slug' => isset( $language_update->slug ) ? $language_update->slug : 'default', + 'version' => $language_update->version, + ); + } + + // Remove upgrade hooks which are not required for translation updates. + remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 ); + remove_action( 'upgrader_process_complete', 'wp_version_check' ); + remove_action( 'upgrader_process_complete', 'wp_update_plugins' ); + remove_action( 'upgrader_process_complete', 'wp_update_themes' ); + + /** This action is documented in wp-admin/includes/class-wp-upgrader.php */ + do_action( + 'upgrader_process_complete', + $this, + array( + 'action' => 'update', + 'type' => 'translation', + 'bulk' => true, + 'translations' => $language_updates_results, + ) + ); + + // Re-add upgrade hooks. + add_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 ); + add_action( 'upgrader_process_complete', 'wp_version_check', 10, 0 ); + add_action( 'upgrader_process_complete', 'wp_update_plugins', 10, 0 ); + add_action( 'upgrader_process_complete', 'wp_update_themes', 10, 0 ); + + $this->skin->bulk_footer(); + + $this->skin->footer(); + + // Clean up our hooks, in case something else does an upgrade on this connection. + remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); + + if ( $parsed_args['clear_update_cache'] ) { + wp_clean_update_cache(); + } + + return $results; + } + + /** + * Checks that the package source contains .mo and .po files. + * + * Hooked to the {@see 'upgrader_source_selection'} filter by + * Language_Pack_Upgrader::bulk_upgrade(). + * + * @since 3.7.0 + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @param string|WP_Error $source The path to the downloaded package source. + * @param string $remote_source Remote file source location. + * @return string|WP_Error The source as passed, or a WP_Error object on failure. + */ + public function check_package( $source, $remote_source ) { + global $wp_filesystem; + + if ( is_wp_error( $source ) ) { + return $source; + } + + // Check that the folder contains a valid language. + $files = $wp_filesystem->dirlist( $remote_source ); + + // Check to see if a .po and .mo exist in the folder. + $po = false; + $mo = false; + foreach ( (array) $files as $file => $filedata ) { + if ( str_ends_with( $file, '.po' ) ) { + $po = true; + } elseif ( str_ends_with( $file, '.mo' ) ) { + $mo = true; + } + } + + if ( ! $mo || ! $po ) { + return new WP_Error( + 'incompatible_archive_pomo', + $this->strings['incompatible_archive'], + sprintf( + /* translators: 1: .po, 2: .mo */ + __( 'The language pack is missing either the %1$s or %2$s files.' ), + '.po', + '.mo' + ) + ); + } + + return $source; + } + + /** + * Gets the name of an item being updated. + * + * @since 3.7.0 + * + * @param object $update The data for an update. + * @return string The name of the item being updated. + */ + public function get_name_for_update( $update ) { + switch ( $update->type ) { + case 'core': + return 'WordPress'; // Not translated. + + case 'theme': + $theme = wp_get_theme( $update->slug ); + if ( $theme->exists() ) { + return $theme->Get( 'Name' ); + } + break; + case 'plugin': + $plugin_data = get_plugins( '/' . $update->slug ); + $plugin_data = reset( $plugin_data ); + if ( $plugin_data ) { + return $plugin_data['Name']; + } + break; + } + return ''; + } + + /** + * Clears existing translations where this item is going to be installed into. + * + * @since 5.1.0 + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @param string $remote_destination The location on the remote filesystem to be cleared. + * @return bool|WP_Error True upon success, WP_Error on failure. + */ + public function clear_destination( $remote_destination ) { + global $wp_filesystem; + + $language_update = $this->skin->language_update; + $language_directory = WP_LANG_DIR . '/'; // Local path for use with glob(). + + if ( 'core' === $language_update->type ) { + $files = array( + $remote_destination . $language_update->language . '.po', + $remote_destination . $language_update->language . '.mo', + $remote_destination . 'admin-' . $language_update->language . '.po', + $remote_destination . 'admin-' . $language_update->language . '.mo', + $remote_destination . 'admin-network-' . $language_update->language . '.po', + $remote_destination . 'admin-network-' . $language_update->language . '.mo', + $remote_destination . 'continents-cities-' . $language_update->language . '.po', + $remote_destination . 'continents-cities-' . $language_update->language . '.mo', + ); + + $json_translation_files = glob( $language_directory . $language_update->language . '-*.json' ); + if ( $json_translation_files ) { + foreach ( $json_translation_files as $json_translation_file ) { + $files[] = str_replace( $language_directory, $remote_destination, $json_translation_file ); + } + } + } else { + $files = array( + $remote_destination . $language_update->slug . '-' . $language_update->language . '.po', + $remote_destination . $language_update->slug . '-' . $language_update->language . '.mo', + ); + + $language_directory = $language_directory . $language_update->type . 's/'; + $json_translation_files = glob( $language_directory . $language_update->slug . '-' . $language_update->language . '-*.json' ); + if ( $json_translation_files ) { + foreach ( $json_translation_files as $json_translation_file ) { + $files[] = str_replace( $language_directory, $remote_destination, $json_translation_file ); + } + } + } + + $files = array_filter( $files, array( $wp_filesystem, 'exists' ) ); + + // No files to delete. + if ( ! $files ) { + return true; + } + + // Check all files are writable before attempting to clear the destination. + $unwritable_files = array(); + + // Check writability. + foreach ( $files as $file ) { + if ( ! $wp_filesystem->is_writable( $file ) ) { + // Attempt to alter permissions to allow writes and try again. + $wp_filesystem->chmod( $file, FS_CHMOD_FILE ); + if ( ! $wp_filesystem->is_writable( $file ) ) { + $unwritable_files[] = $file; + } + } + } + + if ( ! empty( $unwritable_files ) ) { + return new WP_Error( 'files_not_writable', $this->strings['files_not_writable'], implode( ', ', $unwritable_files ) ); + } + + foreach ( $files as $file ) { + if ( ! $wp_filesystem->delete( $file ) ) { + return new WP_Error( 'remove_old_failed', $this->strings['remove_old_failed'] ); + } + } + + return true; + } +} diff --git a/wp-admin/includes/class-pclzip.php b/wp-admin/includes/class-pclzip.php new file mode 100644 index 0000000..3fdade5 --- /dev/null +++ b/wp-admin/includes/class-pclzip.php @@ -0,0 +1,5732 @@ +zipname = $p_zipname; + $this->zip_fd = 0; + $this->magic_quotes_status = -1; + + // ----- Return + return; + } + + public function PclZip($p_zipname) { + self::__construct($p_zipname); + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : + // create($p_filelist, $p_add_dir="", $p_remove_dir="") + // create($p_filelist, $p_option, $p_option_value, ...) + // Description : + // This method supports two different synopsis. The first one is historical. + // This method creates a Zip Archive. The Zip file is created in the + // filesystem. The files and directories indicated in $p_filelist + // are added in the archive. See the parameters description for the + // supported format of $p_filelist. + // When a directory is in the list, the directory and its content is added + // in the archive. + // In this synopsis, the function takes an optional variable list of + // options. See below the supported options. + // Parameters : + // $p_filelist : An array containing file or directory names, or + // a string containing one filename or one directory name, or + // a string containing a list of filenames and/or directory + // names separated by spaces. + // $p_add_dir : A path to add before the real path of the archived file, + // in order to have it memorized in the archive. + // $p_remove_dir : A path to remove from the real path of the file to archive, + // in order to have a shorter path memorized in the archive. + // When $p_add_dir and $p_remove_dir are set, $p_remove_dir + // is removed first, before $p_add_dir is added. + // Options : + // PCLZIP_OPT_ADD_PATH : + // PCLZIP_OPT_REMOVE_PATH : + // PCLZIP_OPT_REMOVE_ALL_PATH : + // PCLZIP_OPT_COMMENT : + // PCLZIP_CB_PRE_ADD : + // PCLZIP_CB_POST_ADD : + // Return Values : + // 0 on failure, + // The list of the added files, with a status of the add action. + // (see PclZip::listContent() for list entry format) + // -------------------------------------------------------------------------------- + function create($p_filelist) + { + $v_result=1; + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Set default values + $v_options = array(); + $v_options[PCLZIP_OPT_NO_COMPRESSION] = FALSE; + + // ----- Look for variable options arguments + $v_size = func_num_args(); + + // ----- Look for arguments + if ($v_size > 1) { + // ----- Get the arguments + $v_arg_list = func_get_args(); + + // ----- Remove from the options list the first argument + array_shift($v_arg_list); + $v_size--; + + // ----- Look for first arg + if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) { + + // ----- Parse the options + $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, + array (PCLZIP_OPT_REMOVE_PATH => 'optional', + PCLZIP_OPT_REMOVE_ALL_PATH => 'optional', + PCLZIP_OPT_ADD_PATH => 'optional', + PCLZIP_CB_PRE_ADD => 'optional', + PCLZIP_CB_POST_ADD => 'optional', + PCLZIP_OPT_NO_COMPRESSION => 'optional', + PCLZIP_OPT_COMMENT => 'optional', + PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional', + PCLZIP_OPT_TEMP_FILE_ON => 'optional', + PCLZIP_OPT_TEMP_FILE_OFF => 'optional' + //, PCLZIP_OPT_CRYPT => 'optional' + )); + if ($v_result != 1) { + return 0; + } + } + + // ----- Look for 2 args + // Here we need to support the first historic synopsis of the + // method. + else { + + // ----- Get the first argument + $v_options[PCLZIP_OPT_ADD_PATH] = $v_arg_list[0]; + + // ----- Look for the optional second argument + if ($v_size == 2) { + $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1]; + } + else if ($v_size > 2) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, + "Invalid number / type of arguments"); + return 0; + } + } + } + + // ----- Look for default option values + $this->privOptionDefaultThreshold($v_options); + + // ----- Init + $v_string_list = array(); + $v_att_list = array(); + $v_filedescr_list = array(); + $p_result_list = array(); + + // ----- Look if the $p_filelist is really an array + if (is_array($p_filelist)) { + + // ----- Look if the first element is also an array + // This will mean that this is a file description entry + if (isset($p_filelist[0]) && is_array($p_filelist[0])) { + $v_att_list = $p_filelist; + } + + // ----- The list is a list of string names + else { + $v_string_list = $p_filelist; + } + } + + // ----- Look if the $p_filelist is a string + else if (is_string($p_filelist)) { + // ----- Create a list from the string + $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist); + } + + // ----- Invalid variable type for $p_filelist + else { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_filelist"); + return 0; + } + + // ----- Reformat the string list + if (sizeof($v_string_list) != 0) { + foreach ($v_string_list as $v_string) { + if ($v_string != '') { + $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string; + } + else { + } + } + } + + // ----- For each file in the list check the attributes + $v_supported_attributes + = array ( PCLZIP_ATT_FILE_NAME => 'mandatory' + ,PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional' + ,PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional' + ,PCLZIP_ATT_FILE_MTIME => 'optional' + ,PCLZIP_ATT_FILE_CONTENT => 'optional' + ,PCLZIP_ATT_FILE_COMMENT => 'optional' + ); + foreach ($v_att_list as $v_entry) { + $v_result = $this->privFileDescrParseAtt($v_entry, + $v_filedescr_list[], + $v_options, + $v_supported_attributes); + if ($v_result != 1) { + return 0; + } + } + + // ----- Expand the filelist (expand directories) + $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options); + if ($v_result != 1) { + return 0; + } + + // ----- Call the create fct + $v_result = $this->privCreate($v_filedescr_list, $p_result_list, $v_options); + if ($v_result != 1) { + return 0; + } + + // ----- Return + return $p_result_list; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : + // add($p_filelist, $p_add_dir="", $p_remove_dir="") + // add($p_filelist, $p_option, $p_option_value, ...) + // Description : + // This method supports two synopsis. The first one is historical. + // This methods add the list of files in an existing archive. + // If a file with the same name already exists, it is added at the end of the + // archive, the first one is still present. + // If the archive does not exist, it is created. + // Parameters : + // $p_filelist : An array containing file or directory names, or + // a string containing one filename or one directory name, or + // a string containing a list of filenames and/or directory + // names separated by spaces. + // $p_add_dir : A path to add before the real path of the archived file, + // in order to have it memorized in the archive. + // $p_remove_dir : A path to remove from the real path of the file to archive, + // in order to have a shorter path memorized in the archive. + // When $p_add_dir and $p_remove_dir are set, $p_remove_dir + // is removed first, before $p_add_dir is added. + // Options : + // PCLZIP_OPT_ADD_PATH : + // PCLZIP_OPT_REMOVE_PATH : + // PCLZIP_OPT_REMOVE_ALL_PATH : + // PCLZIP_OPT_COMMENT : + // PCLZIP_OPT_ADD_COMMENT : + // PCLZIP_OPT_PREPEND_COMMENT : + // PCLZIP_CB_PRE_ADD : + // PCLZIP_CB_POST_ADD : + // Return Values : + // 0 on failure, + // The list of the added files, with a status of the add action. + // (see PclZip::listContent() for list entry format) + // -------------------------------------------------------------------------------- + function add($p_filelist) + { + $v_result=1; + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Set default values + $v_options = array(); + $v_options[PCLZIP_OPT_NO_COMPRESSION] = FALSE; + + // ----- Look for variable options arguments + $v_size = func_num_args(); + + // ----- Look for arguments + if ($v_size > 1) { + // ----- Get the arguments + $v_arg_list = func_get_args(); + + // ----- Remove form the options list the first argument + array_shift($v_arg_list); + $v_size--; + + // ----- Look for first arg + if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) { + + // ----- Parse the options + $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, + array (PCLZIP_OPT_REMOVE_PATH => 'optional', + PCLZIP_OPT_REMOVE_ALL_PATH => 'optional', + PCLZIP_OPT_ADD_PATH => 'optional', + PCLZIP_CB_PRE_ADD => 'optional', + PCLZIP_CB_POST_ADD => 'optional', + PCLZIP_OPT_NO_COMPRESSION => 'optional', + PCLZIP_OPT_COMMENT => 'optional', + PCLZIP_OPT_ADD_COMMENT => 'optional', + PCLZIP_OPT_PREPEND_COMMENT => 'optional', + PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional', + PCLZIP_OPT_TEMP_FILE_ON => 'optional', + PCLZIP_OPT_TEMP_FILE_OFF => 'optional' + //, PCLZIP_OPT_CRYPT => 'optional' + )); + if ($v_result != 1) { + return 0; + } + } + + // ----- Look for 2 args + // Here we need to support the first historic synopsis of the + // method. + else { + + // ----- Get the first argument + $v_options[PCLZIP_OPT_ADD_PATH] = $v_add_path = $v_arg_list[0]; + + // ----- Look for the optional second argument + if ($v_size == 2) { + $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1]; + } + else if ($v_size > 2) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments"); + + // ----- Return + return 0; + } + } + } + + // ----- Look for default option values + $this->privOptionDefaultThreshold($v_options); + + // ----- Init + $v_string_list = array(); + $v_att_list = array(); + $v_filedescr_list = array(); + $p_result_list = array(); + + // ----- Look if the $p_filelist is really an array + if (is_array($p_filelist)) { + + // ----- Look if the first element is also an array + // This will mean that this is a file description entry + if (isset($p_filelist[0]) && is_array($p_filelist[0])) { + $v_att_list = $p_filelist; + } + + // ----- The list is a list of string names + else { + $v_string_list = $p_filelist; + } + } + + // ----- Look if the $p_filelist is a string + else if (is_string($p_filelist)) { + // ----- Create a list from the string + $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist); + } + + // ----- Invalid variable type for $p_filelist + else { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type '".gettype($p_filelist)."' for p_filelist"); + return 0; + } + + // ----- Reformat the string list + if (sizeof($v_string_list) != 0) { + foreach ($v_string_list as $v_string) { + $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string; + } + } + + // ----- For each file in the list check the attributes + $v_supported_attributes + = array ( PCLZIP_ATT_FILE_NAME => 'mandatory' + ,PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional' + ,PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional' + ,PCLZIP_ATT_FILE_MTIME => 'optional' + ,PCLZIP_ATT_FILE_CONTENT => 'optional' + ,PCLZIP_ATT_FILE_COMMENT => 'optional' + ); + foreach ($v_att_list as $v_entry) { + $v_result = $this->privFileDescrParseAtt($v_entry, + $v_filedescr_list[], + $v_options, + $v_supported_attributes); + if ($v_result != 1) { + return 0; + } + } + + // ----- Expand the filelist (expand directories) + $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options); + if ($v_result != 1) { + return 0; + } + + // ----- Call the create fct + $v_result = $this->privAdd($v_filedescr_list, $p_result_list, $v_options); + if ($v_result != 1) { + return 0; + } + + // ----- Return + return $p_result_list; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : listContent() + // Description : + // This public method, gives the list of the files and directories, with their + // properties. + // The properties of each entries in the list are (used also in other functions) : + // filename : Name of the file. For a create or add action it is the filename + // given by the user. For an extract function it is the filename + // of the extracted file. + // stored_filename : Name of the file / directory stored in the archive. + // size : Size of the stored file. + // compressed_size : Size of the file's data compressed in the archive + // (without the headers overhead) + // mtime : Last known modification date of the file (UNIX timestamp) + // comment : Comment associated with the file + // folder : true | false + // index : index of the file in the archive + // status : status of the action (depending of the action) : + // Values are : + // ok : OK ! + // filtered : the file / dir is not extracted (filtered by user) + // already_a_directory : the file can not be extracted because a + // directory with the same name already exists + // write_protected : the file can not be extracted because a file + // with the same name already exists and is + // write protected + // newer_exist : the file was not extracted because a newer file exists + // path_creation_fail : the file is not extracted because the folder + // does not exist and can not be created + // write_error : the file was not extracted because there was an + // error while writing the file + // read_error : the file was not extracted because there was an error + // while reading the file + // invalid_header : the file was not extracted because of an archive + // format error (bad file header) + // Note that each time a method can continue operating when there + // is an action error on a file, the error is only logged in the file status. + // Return Values : + // 0 on an unrecoverable failure, + // The list of the files in the archive. + // -------------------------------------------------------------------------------- + function listContent() + { + $v_result=1; + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Check archive + if (!$this->privCheckFormat()) { + return(0); + } + + // ----- Call the extracting fct + $p_list = array(); + if (($v_result = $this->privList($p_list)) != 1) + { + unset($p_list); + return(0); + } + + // ----- Return + return $p_list; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : + // extract($p_path="./", $p_remove_path="") + // extract([$p_option, $p_option_value, ...]) + // Description : + // This method supports two synopsis. The first one is historical. + // This method extract all the files / directories from the archive to the + // folder indicated in $p_path. + // If you want to ignore the 'root' part of path of the memorized files + // you can indicate this in the optional $p_remove_path parameter. + // By default, if a newer file with the same name already exists, the + // file is not extracted. + // + // If both PCLZIP_OPT_PATH and PCLZIP_OPT_ADD_PATH options + // are used, the path indicated in PCLZIP_OPT_ADD_PATH is append + // at the end of the path value of PCLZIP_OPT_PATH. + // Parameters : + // $p_path : Path where the files and directories are to be extracted + // $p_remove_path : First part ('root' part) of the memorized path + // (if any similar) to remove while extracting. + // Options : + // PCLZIP_OPT_PATH : + // PCLZIP_OPT_ADD_PATH : + // PCLZIP_OPT_REMOVE_PATH : + // PCLZIP_OPT_REMOVE_ALL_PATH : + // PCLZIP_CB_PRE_EXTRACT : + // PCLZIP_CB_POST_EXTRACT : + // Return Values : + // 0 or a negative value on failure, + // The list of the extracted files, with a status of the action. + // (see PclZip::listContent() for list entry format) + // -------------------------------------------------------------------------------- + function extract() + { + $v_result=1; + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Check archive + if (!$this->privCheckFormat()) { + return(0); + } + + // ----- Set default values + $v_options = array(); +// $v_path = "./"; + $v_path = ''; + $v_remove_path = ""; + $v_remove_all_path = false; + + // ----- Look for variable options arguments + $v_size = func_num_args(); + + // ----- Default values for option + $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = FALSE; + + // ----- Look for arguments + if ($v_size > 0) { + // ----- Get the arguments + $v_arg_list = func_get_args(); + + // ----- Look for first arg + if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) { + + // ----- Parse the options + $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, + array (PCLZIP_OPT_PATH => 'optional', + PCLZIP_OPT_REMOVE_PATH => 'optional', + PCLZIP_OPT_REMOVE_ALL_PATH => 'optional', + PCLZIP_OPT_ADD_PATH => 'optional', + PCLZIP_CB_PRE_EXTRACT => 'optional', + PCLZIP_CB_POST_EXTRACT => 'optional', + PCLZIP_OPT_SET_CHMOD => 'optional', + PCLZIP_OPT_BY_NAME => 'optional', + PCLZIP_OPT_BY_EREG => 'optional', + PCLZIP_OPT_BY_PREG => 'optional', + PCLZIP_OPT_BY_INDEX => 'optional', + PCLZIP_OPT_EXTRACT_AS_STRING => 'optional', + PCLZIP_OPT_EXTRACT_IN_OUTPUT => 'optional', + PCLZIP_OPT_REPLACE_NEWER => 'optional' + ,PCLZIP_OPT_STOP_ON_ERROR => 'optional' + ,PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional', + PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional', + PCLZIP_OPT_TEMP_FILE_ON => 'optional', + PCLZIP_OPT_TEMP_FILE_OFF => 'optional' + )); + if ($v_result != 1) { + return 0; + } + + // ----- Set the arguments + if (isset($v_options[PCLZIP_OPT_PATH])) { + $v_path = $v_options[PCLZIP_OPT_PATH]; + } + if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) { + $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH]; + } + if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) { + $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH]; + } + if (isset($v_options[PCLZIP_OPT_ADD_PATH])) { + // ----- Check for '/' in last path char + if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) { + $v_path .= '/'; + } + $v_path .= $v_options[PCLZIP_OPT_ADD_PATH]; + } + } + + // ----- Look for 2 args + // Here we need to support the first historic synopsis of the + // method. + else { + + // ----- Get the first argument + $v_path = $v_arg_list[0]; + + // ----- Look for the optional second argument + if ($v_size == 2) { + $v_remove_path = $v_arg_list[1]; + } + else if ($v_size > 2) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments"); + + // ----- Return + return 0; + } + } + } + + // ----- Look for default option values + $this->privOptionDefaultThreshold($v_options); + + // ----- Trace + + // ----- Call the extracting fct + $p_list = array(); + $v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path, + $v_remove_all_path, $v_options); + if ($v_result < 1) { + unset($p_list); + return(0); + } + + // ----- Return + return $p_list; + } + // -------------------------------------------------------------------------------- + + + // -------------------------------------------------------------------------------- + // Function : + // extractByIndex($p_index, $p_path="./", $p_remove_path="") + // extractByIndex($p_index, [$p_option, $p_option_value, ...]) + // Description : + // This method supports two synopsis. The first one is historical. + // This method is doing a partial extract of the archive. + // The extracted files or folders are identified by their index in the + // archive (from 0 to n). + // Note that if the index identify a folder, only the folder entry is + // extracted, not all the files included in the archive. + // Parameters : + // $p_index : A single index (integer) or a string of indexes of files to + // extract. The form of the string is "0,4-6,8-12" with only numbers + // and '-' for range or ',' to separate ranges. No spaces or ';' + // are allowed. + // $p_path : Path where the files and directories are to be extracted + // $p_remove_path : First part ('root' part) of the memorized path + // (if any similar) to remove while extracting. + // Options : + // PCLZIP_OPT_PATH : + // PCLZIP_OPT_ADD_PATH : + // PCLZIP_OPT_REMOVE_PATH : + // PCLZIP_OPT_REMOVE_ALL_PATH : + // PCLZIP_OPT_EXTRACT_AS_STRING : The files are extracted as strings and + // not as files. + // The resulting content is in a new field 'content' in the file + // structure. + // This option must be used alone (any other options are ignored). + // PCLZIP_CB_PRE_EXTRACT : + // PCLZIP_CB_POST_EXTRACT : + // Return Values : + // 0 on failure, + // The list of the extracted files, with a status of the action. + // (see PclZip::listContent() for list entry format) + // -------------------------------------------------------------------------------- + //function extractByIndex($p_index, options...) + function extractByIndex($p_index) + { + $v_result=1; + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Check archive + if (!$this->privCheckFormat()) { + return(0); + } + + // ----- Set default values + $v_options = array(); +// $v_path = "./"; + $v_path = ''; + $v_remove_path = ""; + $v_remove_all_path = false; + + // ----- Look for variable options arguments + $v_size = func_num_args(); + + // ----- Default values for option + $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = FALSE; + + // ----- Look for arguments + if ($v_size > 1) { + // ----- Get the arguments + $v_arg_list = func_get_args(); + + // ----- Remove form the options list the first argument + array_shift($v_arg_list); + $v_size--; + + // ----- Look for first arg + if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) { + + // ----- Parse the options + $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, + array (PCLZIP_OPT_PATH => 'optional', + PCLZIP_OPT_REMOVE_PATH => 'optional', + PCLZIP_OPT_REMOVE_ALL_PATH => 'optional', + PCLZIP_OPT_EXTRACT_AS_STRING => 'optional', + PCLZIP_OPT_ADD_PATH => 'optional', + PCLZIP_CB_PRE_EXTRACT => 'optional', + PCLZIP_CB_POST_EXTRACT => 'optional', + PCLZIP_OPT_SET_CHMOD => 'optional', + PCLZIP_OPT_REPLACE_NEWER => 'optional' + ,PCLZIP_OPT_STOP_ON_ERROR => 'optional' + ,PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional', + PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional', + PCLZIP_OPT_TEMP_FILE_ON => 'optional', + PCLZIP_OPT_TEMP_FILE_OFF => 'optional' + )); + if ($v_result != 1) { + return 0; + } + + // ----- Set the arguments + if (isset($v_options[PCLZIP_OPT_PATH])) { + $v_path = $v_options[PCLZIP_OPT_PATH]; + } + if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) { + $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH]; + } + if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) { + $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH]; + } + if (isset($v_options[PCLZIP_OPT_ADD_PATH])) { + // ----- Check for '/' in last path char + if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) { + $v_path .= '/'; + } + $v_path .= $v_options[PCLZIP_OPT_ADD_PATH]; + } + if (!isset($v_options[PCLZIP_OPT_EXTRACT_AS_STRING])) { + $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = FALSE; + } + else { + } + } + + // ----- Look for 2 args + // Here we need to support the first historic synopsis of the + // method. + else { + + // ----- Get the first argument + $v_path = $v_arg_list[0]; + + // ----- Look for the optional second argument + if ($v_size == 2) { + $v_remove_path = $v_arg_list[1]; + } + else if ($v_size > 2) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments"); + + // ----- Return + return 0; + } + } + } + + // ----- Trace + + // ----- Trick + // Here I want to reuse extractByRule(), so I need to parse the $p_index + // with privParseOptions() + $v_arg_trick = array (PCLZIP_OPT_BY_INDEX, $p_index); + $v_options_trick = array(); + $v_result = $this->privParseOptions($v_arg_trick, sizeof($v_arg_trick), $v_options_trick, + array (PCLZIP_OPT_BY_INDEX => 'optional' )); + if ($v_result != 1) { + return 0; + } + $v_options[PCLZIP_OPT_BY_INDEX] = $v_options_trick[PCLZIP_OPT_BY_INDEX]; + + // ----- Look for default option values + $this->privOptionDefaultThreshold($v_options); + + // ----- Call the extracting fct + if (($v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path, $v_remove_all_path, $v_options)) < 1) { + return(0); + } + + // ----- Return + return $p_list; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : + // delete([$p_option, $p_option_value, ...]) + // Description : + // This method removes files from the archive. + // If no parameters are given, then all the archive is emptied. + // Parameters : + // None or optional arguments. + // Options : + // PCLZIP_OPT_BY_INDEX : + // PCLZIP_OPT_BY_NAME : + // PCLZIP_OPT_BY_EREG : + // PCLZIP_OPT_BY_PREG : + // Return Values : + // 0 on failure, + // The list of the files which are still present in the archive. + // (see PclZip::listContent() for list entry format) + // -------------------------------------------------------------------------------- + function delete() + { + $v_result=1; + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Check archive + if (!$this->privCheckFormat()) { + return(0); + } + + // ----- Set default values + $v_options = array(); + + // ----- Look for variable options arguments + $v_size = func_num_args(); + + // ----- Look for arguments + if ($v_size > 0) { + // ----- Get the arguments + $v_arg_list = func_get_args(); + + // ----- Parse the options + $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, + array (PCLZIP_OPT_BY_NAME => 'optional', + PCLZIP_OPT_BY_EREG => 'optional', + PCLZIP_OPT_BY_PREG => 'optional', + PCLZIP_OPT_BY_INDEX => 'optional' )); + if ($v_result != 1) { + return 0; + } + } + + // ----- Magic quotes trick + $this->privDisableMagicQuotes(); + + // ----- Call the delete fct + $v_list = array(); + if (($v_result = $this->privDeleteByRule($v_list, $v_options)) != 1) { + $this->privSwapBackMagicQuotes(); + unset($v_list); + return(0); + } + + // ----- Magic quotes trick + $this->privSwapBackMagicQuotes(); + + // ----- Return + return $v_list; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : deleteByIndex() + // Description : + // ***** Deprecated ***** + // delete(PCLZIP_OPT_BY_INDEX, $p_index) should be preferred. + // -------------------------------------------------------------------------------- + function deleteByIndex($p_index) + { + + $p_list = $this->delete(PCLZIP_OPT_BY_INDEX, $p_index); + + // ----- Return + return $p_list; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : properties() + // Description : + // This method gives the properties of the archive. + // The properties are : + // nb : Number of files in the archive + // comment : Comment associated with the archive file + // status : not_exist, ok + // Parameters : + // None + // Return Values : + // 0 on failure, + // An array with the archive properties. + // -------------------------------------------------------------------------------- + function properties() + { + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Magic quotes trick + $this->privDisableMagicQuotes(); + + // ----- Check archive + if (!$this->privCheckFormat()) { + $this->privSwapBackMagicQuotes(); + return(0); + } + + // ----- Default properties + $v_prop = array(); + $v_prop['comment'] = ''; + $v_prop['nb'] = 0; + $v_prop['status'] = 'not_exist'; + + // ----- Look if file exists + if (@is_file($this->zipname)) + { + // ----- Open the zip file + if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0) + { + $this->privSwapBackMagicQuotes(); + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \''.$this->zipname.'\' in binary read mode'); + + // ----- Return + return 0; + } + + // ----- Read the central directory information + $v_central_dir = array(); + if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) + { + $this->privSwapBackMagicQuotes(); + return 0; + } + + // ----- Close the zip file + $this->privCloseFd(); + + // ----- Set the user attributes + $v_prop['comment'] = $v_central_dir['comment']; + $v_prop['nb'] = $v_central_dir['entries']; + $v_prop['status'] = 'ok'; + } + + // ----- Magic quotes trick + $this->privSwapBackMagicQuotes(); + + // ----- Return + return $v_prop; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : duplicate() + // Description : + // This method creates an archive by copying the content of an other one. If + // the archive already exist, it is replaced by the new one without any warning. + // Parameters : + // $p_archive : The filename of a valid archive, or + // a valid PclZip object. + // Return Values : + // 1 on success. + // 0 or a negative value on error (error code). + // -------------------------------------------------------------------------------- + function duplicate($p_archive) + { + $v_result = 1; + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Look if the $p_archive is an instantiated PclZip object + if ($p_archive instanceof pclzip) + { + + // ----- Duplicate the archive + $v_result = $this->privDuplicate($p_archive->zipname); + } + + // ----- Look if the $p_archive is a string (so a filename) + else if (is_string($p_archive)) + { + + // ----- Check that $p_archive is a valid zip file + // TBC : Should also check the archive format + if (!is_file($p_archive)) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "No file with filename '".$p_archive."'"); + $v_result = PCLZIP_ERR_MISSING_FILE; + } + else { + // ----- Duplicate the archive + $v_result = $this->privDuplicate($p_archive); + } + } + + // ----- Invalid variable + else + { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add"); + $v_result = PCLZIP_ERR_INVALID_PARAMETER; + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : merge() + // Description : + // This method merge the $p_archive_to_add archive at the end of the current + // one ($this). + // If the archive ($this) does not exist, the merge becomes a duplicate. + // If the $p_archive_to_add archive does not exist, the merge is a success. + // Parameters : + // $p_archive_to_add : It can be directly the filename of a valid zip archive, + // or a PclZip object archive. + // Return Values : + // 1 on success, + // 0 or negative values on error (see below). + // -------------------------------------------------------------------------------- + function merge($p_archive_to_add) + { + $v_result = 1; + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Check archive + if (!$this->privCheckFormat()) { + return(0); + } + + // ----- Look if the $p_archive_to_add is an instantiated PclZip object + if ($p_archive_to_add instanceof pclzip) + { + + // ----- Merge the archive + $v_result = $this->privMerge($p_archive_to_add); + } + + // ----- Look if the $p_archive_to_add is a string (so a filename) + else if (is_string($p_archive_to_add)) + { + + // ----- Create a temporary archive + $v_object_archive = new PclZip($p_archive_to_add); + + // ----- Merge the archive + $v_result = $this->privMerge($v_object_archive); + } + + // ----- Invalid variable + else + { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add"); + $v_result = PCLZIP_ERR_INVALID_PARAMETER; + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + + + // -------------------------------------------------------------------------------- + // Function : errorCode() + // Description : + // Parameters : + // -------------------------------------------------------------------------------- + function errorCode() + { + if (PCLZIP_ERROR_EXTERNAL == 1) { + return(PclErrorCode()); + } + else { + return($this->error_code); + } + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : errorName() + // Description : + // Parameters : + // -------------------------------------------------------------------------------- + function errorName($p_with_code=false) + { + $v_name = array ( PCLZIP_ERR_NO_ERROR => 'PCLZIP_ERR_NO_ERROR', + PCLZIP_ERR_WRITE_OPEN_FAIL => 'PCLZIP_ERR_WRITE_OPEN_FAIL', + PCLZIP_ERR_READ_OPEN_FAIL => 'PCLZIP_ERR_READ_OPEN_FAIL', + PCLZIP_ERR_INVALID_PARAMETER => 'PCLZIP_ERR_INVALID_PARAMETER', + PCLZIP_ERR_MISSING_FILE => 'PCLZIP_ERR_MISSING_FILE', + PCLZIP_ERR_FILENAME_TOO_LONG => 'PCLZIP_ERR_FILENAME_TOO_LONG', + PCLZIP_ERR_INVALID_ZIP => 'PCLZIP_ERR_INVALID_ZIP', + PCLZIP_ERR_BAD_EXTRACTED_FILE => 'PCLZIP_ERR_BAD_EXTRACTED_FILE', + PCLZIP_ERR_DIR_CREATE_FAIL => 'PCLZIP_ERR_DIR_CREATE_FAIL', + PCLZIP_ERR_BAD_EXTENSION => 'PCLZIP_ERR_BAD_EXTENSION', + PCLZIP_ERR_BAD_FORMAT => 'PCLZIP_ERR_BAD_FORMAT', + PCLZIP_ERR_DELETE_FILE_FAIL => 'PCLZIP_ERR_DELETE_FILE_FAIL', + PCLZIP_ERR_RENAME_FILE_FAIL => 'PCLZIP_ERR_RENAME_FILE_FAIL', + PCLZIP_ERR_BAD_CHECKSUM => 'PCLZIP_ERR_BAD_CHECKSUM', + PCLZIP_ERR_INVALID_ARCHIVE_ZIP => 'PCLZIP_ERR_INVALID_ARCHIVE_ZIP', + PCLZIP_ERR_MISSING_OPTION_VALUE => 'PCLZIP_ERR_MISSING_OPTION_VALUE', + PCLZIP_ERR_INVALID_OPTION_VALUE => 'PCLZIP_ERR_INVALID_OPTION_VALUE', + PCLZIP_ERR_UNSUPPORTED_COMPRESSION => 'PCLZIP_ERR_UNSUPPORTED_COMPRESSION', + PCLZIP_ERR_UNSUPPORTED_ENCRYPTION => 'PCLZIP_ERR_UNSUPPORTED_ENCRYPTION' + ,PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE => 'PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE' + ,PCLZIP_ERR_DIRECTORY_RESTRICTION => 'PCLZIP_ERR_DIRECTORY_RESTRICTION' + ); + + if (isset($v_name[$this->error_code])) { + $v_value = $v_name[$this->error_code]; + } + else { + $v_value = 'NoName'; + } + + if ($p_with_code) { + return($v_value.' ('.$this->error_code.')'); + } + else { + return($v_value); + } + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : errorInfo() + // Description : + // Parameters : + // -------------------------------------------------------------------------------- + function errorInfo($p_full=false) + { + if (PCLZIP_ERROR_EXTERNAL == 1) { + return(PclErrorString()); + } + else { + if ($p_full) { + return($this->errorName(true)." : ".$this->error_string); + } + else { + return($this->error_string." [code ".$this->error_code."]"); + } + } + } + // -------------------------------------------------------------------------------- + + +// -------------------------------------------------------------------------------- +// ***** UNDER THIS LINE ARE DEFINED PRIVATE INTERNAL FUNCTIONS ***** +// ***** ***** +// ***** THESES FUNCTIONS MUST NOT BE USED DIRECTLY ***** +// -------------------------------------------------------------------------------- + + + + // -------------------------------------------------------------------------------- + // Function : privCheckFormat() + // Description : + // This method check that the archive exists and is a valid zip archive. + // Several level of check exists. (futur) + // Parameters : + // $p_level : Level of check. Default 0. + // 0 : Check the first bytes (magic codes) (default value)) + // 1 : 0 + Check the central directory (futur) + // 2 : 1 + Check each file header (futur) + // Return Values : + // true on success, + // false on error, the error code is set. + // -------------------------------------------------------------------------------- + function privCheckFormat($p_level=0) + { + $v_result = true; + + // ----- Reset the file system cache + clearstatcache(); + + // ----- Reset the error handler + $this->privErrorReset(); + + // ----- Look if the file exits + if (!is_file($this->zipname)) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "Missing archive file '".$this->zipname."'"); + return(false); + } + + // ----- Check that the file is readable + if (!is_readable($this->zipname)) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to read archive '".$this->zipname."'"); + return(false); + } + + // ----- Check the magic code + // TBC + + // ----- Check the central header + // TBC + + // ----- Check each file header + // TBC + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privParseOptions() + // Description : + // This internal methods reads the variable list of arguments ($p_options_list, + // $p_size) and generate an array with the options and values ($v_result_list). + // $v_requested_options contains the options that can be present and those that + // must be present. + // $v_requested_options is an array, with the option value as key, and 'optional', + // or 'mandatory' as value. + // Parameters : + // See above. + // Return Values : + // 1 on success. + // 0 on failure. + // -------------------------------------------------------------------------------- + function privParseOptions(&$p_options_list, $p_size, &$v_result_list, $v_requested_options=false) + { + $v_result=1; + + // ----- Read the options + $i=0; + while ($i<$p_size) { + + // ----- Check if the option is supported + if (!isset($v_requested_options[$p_options_list[$i]])) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid optional parameter '".$p_options_list[$i]."' for this method"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Look for next option + switch ($p_options_list[$i]) { + // ----- Look for options that request a path value + case PCLZIP_OPT_PATH : + case PCLZIP_OPT_REMOVE_PATH : + case PCLZIP_OPT_ADD_PATH : + // ----- Check the number of parameters + if (($i+1) >= $p_size) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Get the value + $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i+1], FALSE); + $i++; + break; + + case PCLZIP_OPT_TEMP_FILE_THRESHOLD : + // ----- Check the number of parameters + if (($i+1) >= $p_size) { + PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + return PclZip::errorCode(); + } + + // ----- Check for incompatible options + if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '".PclZipUtilOptionText($p_options_list[$i])."' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'"); + return PclZip::errorCode(); + } + + // ----- Check the value + $v_value = $p_options_list[$i+1]; + if ((!is_integer($v_value)) || ($v_value<0)) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Integer expected for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + return PclZip::errorCode(); + } + + // ----- Get the value (and convert it in bytes) + $v_result_list[$p_options_list[$i]] = $v_value*1048576; + $i++; + break; + + case PCLZIP_OPT_TEMP_FILE_ON : + // ----- Check for incompatible options + if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '".PclZipUtilOptionText($p_options_list[$i])."' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'"); + return PclZip::errorCode(); + } + + $v_result_list[$p_options_list[$i]] = true; + break; + + case PCLZIP_OPT_TEMP_FILE_OFF : + // ----- Check for incompatible options + if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_ON])) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '".PclZipUtilOptionText($p_options_list[$i])."' can not be used with option 'PCLZIP_OPT_TEMP_FILE_ON'"); + return PclZip::errorCode(); + } + // ----- Check for incompatible options + if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '".PclZipUtilOptionText($p_options_list[$i])."' can not be used with option 'PCLZIP_OPT_TEMP_FILE_THRESHOLD'"); + return PclZip::errorCode(); + } + + $v_result_list[$p_options_list[$i]] = true; + break; + + case PCLZIP_OPT_EXTRACT_DIR_RESTRICTION : + // ----- Check the number of parameters + if (($i+1) >= $p_size) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Get the value + if ( is_string($p_options_list[$i+1]) + && ($p_options_list[$i+1] != '')) { + $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i+1], FALSE); + $i++; + } + else { + } + break; + + // ----- Look for options that request an array of string for value + case PCLZIP_OPT_BY_NAME : + // ----- Check the number of parameters + if (($i+1) >= $p_size) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Get the value + if (is_string($p_options_list[$i+1])) { + $v_result_list[$p_options_list[$i]][0] = $p_options_list[$i+1]; + } + else if (is_array($p_options_list[$i+1])) { + $v_result_list[$p_options_list[$i]] = $p_options_list[$i+1]; + } + else { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + $i++; + break; + + // ----- Look for options that request an EREG or PREG expression + case PCLZIP_OPT_BY_EREG : + // ereg() is deprecated starting with PHP 5.3. Move PCLZIP_OPT_BY_EREG + // to PCLZIP_OPT_BY_PREG + $p_options_list[$i] = PCLZIP_OPT_BY_PREG; + case PCLZIP_OPT_BY_PREG : + //case PCLZIP_OPT_CRYPT : + // ----- Check the number of parameters + if (($i+1) >= $p_size) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Get the value + if (is_string($p_options_list[$i+1])) { + $v_result_list[$p_options_list[$i]] = $p_options_list[$i+1]; + } + else { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + $i++; + break; + + // ----- Look for options that takes a string + case PCLZIP_OPT_COMMENT : + case PCLZIP_OPT_ADD_COMMENT : + case PCLZIP_OPT_PREPEND_COMMENT : + // ----- Check the number of parameters + if (($i+1) >= $p_size) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, + "Missing parameter value for option '" + .PclZipUtilOptionText($p_options_list[$i]) + ."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Get the value + if (is_string($p_options_list[$i+1])) { + $v_result_list[$p_options_list[$i]] = $p_options_list[$i+1]; + } + else { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, + "Wrong parameter value for option '" + .PclZipUtilOptionText($p_options_list[$i]) + ."'"); + + // ----- Return + return PclZip::errorCode(); + } + $i++; + break; + + // ----- Look for options that request an array of index + case PCLZIP_OPT_BY_INDEX : + // ----- Check the number of parameters + if (($i+1) >= $p_size) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Get the value + $v_work_list = array(); + if (is_string($p_options_list[$i+1])) { + + // ----- Remove spaces + $p_options_list[$i+1] = strtr($p_options_list[$i+1], ' ', ''); + + // ----- Parse items + $v_work_list = explode(",", $p_options_list[$i+1]); + } + else if (is_integer($p_options_list[$i+1])) { + $v_work_list[0] = $p_options_list[$i+1].'-'.$p_options_list[$i+1]; + } + else if (is_array($p_options_list[$i+1])) { + $v_work_list = $p_options_list[$i+1]; + } + else { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Value must be integer, string or array for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Reduce the index list + // each index item in the list must be a couple with a start and + // an end value : [0,3], [5-5], [8-10], ... + // ----- Check the format of each item + $v_sort_flag=false; + $v_sort_value=0; + for ($j=0; $j= $p_size) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Get the value + $v_result_list[$p_options_list[$i]] = $p_options_list[$i+1]; + $i++; + break; + + // ----- Look for options that request a call-back + case PCLZIP_CB_PRE_EXTRACT : + case PCLZIP_CB_POST_EXTRACT : + case PCLZIP_CB_PRE_ADD : + case PCLZIP_CB_POST_ADD : + /* for futur use + case PCLZIP_CB_PRE_DELETE : + case PCLZIP_CB_POST_DELETE : + case PCLZIP_CB_PRE_LIST : + case PCLZIP_CB_POST_LIST : + */ + // ----- Check the number of parameters + if (($i+1) >= $p_size) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Get the value + $v_function_name = $p_options_list[$i+1]; + + // ----- Check that the value is a valid existing function + if (!function_exists($v_function_name)) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Function '".$v_function_name."()' is not an existing function for option '".PclZipUtilOptionText($p_options_list[$i])."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Set the attribute + $v_result_list[$p_options_list[$i]] = $v_function_name; + $i++; + break; + + default : + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, + "Unknown parameter '" + .$p_options_list[$i]."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Next options + $i++; + } + + // ----- Look for mandatory options + if ($v_requested_options !== false) { + for ($key=reset($v_requested_options); $key=key($v_requested_options); $key=next($v_requested_options)) { + // ----- Look for mandatory option + if ($v_requested_options[$key] == 'mandatory') { + // ----- Look if present + if (!isset($v_result_list[$key])) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter ".PclZipUtilOptionText($key)."(".$key.")"); + + // ----- Return + return PclZip::errorCode(); + } + } + } + } + + // ----- Look for default values + if (!isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) { + + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privOptionDefaultThreshold() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privOptionDefaultThreshold(&$p_options) + { + $v_result=1; + + if (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) + || isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) { + return $v_result; + } + + // ----- Get 'memory_limit' configuration value + $v_memory_limit = ini_get('memory_limit'); + $v_memory_limit = trim($v_memory_limit); + $v_memory_limit_int = (int) $v_memory_limit; + $last = strtolower(substr($v_memory_limit, -1)); + + if($last == 'g') + //$v_memory_limit_int = $v_memory_limit_int*1024*1024*1024; + $v_memory_limit_int = $v_memory_limit_int*1073741824; + if($last == 'm') + //$v_memory_limit_int = $v_memory_limit_int*1024*1024; + $v_memory_limit_int = $v_memory_limit_int*1048576; + if($last == 'k') + $v_memory_limit_int = $v_memory_limit_int*1024; + + $p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] = floor($v_memory_limit_int*PCLZIP_TEMPORARY_FILE_RATIO); + + + // ----- Sanity check : No threshold if value lower than 1M + if ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] < 1048576) { + unset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]); + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privFileDescrParseAtt() + // Description : + // Parameters : + // Return Values : + // 1 on success. + // 0 on failure. + // -------------------------------------------------------------------------------- + function privFileDescrParseAtt(&$p_file_list, &$p_filedescr, $v_options, $v_requested_options=false) + { + $v_result=1; + + // ----- For each file in the list check the attributes + foreach ($p_file_list as $v_key => $v_value) { + + // ----- Check if the option is supported + if (!isset($v_requested_options[$v_key])) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file attribute '".$v_key."' for this file"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Look for attribute + switch ($v_key) { + case PCLZIP_ATT_FILE_NAME : + if (!is_string($v_value)) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". String expected for attribute '".PclZipUtilOptionText($v_key)."'"); + return PclZip::errorCode(); + } + + $p_filedescr['filename'] = PclZipUtilPathReduction($v_value); + + if ($p_filedescr['filename'] == '') { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty filename for attribute '".PclZipUtilOptionText($v_key)."'"); + return PclZip::errorCode(); + } + + break; + + case PCLZIP_ATT_FILE_NEW_SHORT_NAME : + if (!is_string($v_value)) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". String expected for attribute '".PclZipUtilOptionText($v_key)."'"); + return PclZip::errorCode(); + } + + $p_filedescr['new_short_name'] = PclZipUtilPathReduction($v_value); + + if ($p_filedescr['new_short_name'] == '') { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty short filename for attribute '".PclZipUtilOptionText($v_key)."'"); + return PclZip::errorCode(); + } + break; + + case PCLZIP_ATT_FILE_NEW_FULL_NAME : + if (!is_string($v_value)) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". String expected for attribute '".PclZipUtilOptionText($v_key)."'"); + return PclZip::errorCode(); + } + + $p_filedescr['new_full_name'] = PclZipUtilPathReduction($v_value); + + if ($p_filedescr['new_full_name'] == '') { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty full filename for attribute '".PclZipUtilOptionText($v_key)."'"); + return PclZip::errorCode(); + } + break; + + // ----- Look for options that takes a string + case PCLZIP_ATT_FILE_COMMENT : + if (!is_string($v_value)) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". String expected for attribute '".PclZipUtilOptionText($v_key)."'"); + return PclZip::errorCode(); + } + + $p_filedescr['comment'] = $v_value; + break; + + case PCLZIP_ATT_FILE_MTIME : + if (!is_integer($v_value)) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". Integer expected for attribute '".PclZipUtilOptionText($v_key)."'"); + return PclZip::errorCode(); + } + + $p_filedescr['mtime'] = $v_value; + break; + + case PCLZIP_ATT_FILE_CONTENT : + $p_filedescr['content'] = $v_value; + break; + + default : + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, + "Unknown parameter '".$v_key."'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Look for mandatory options + if ($v_requested_options !== false) { + for ($key=reset($v_requested_options); $key=key($v_requested_options); $key=next($v_requested_options)) { + // ----- Look for mandatory option + if ($v_requested_options[$key] == 'mandatory') { + // ----- Look if present + if (!isset($p_file_list[$key])) { + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter ".PclZipUtilOptionText($key)."(".$key.")"); + return PclZip::errorCode(); + } + } + } + } + + // end foreach + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privFileDescrExpand() + // Description : + // This method look for each item of the list to see if its a file, a folder + // or a string to be added as file. For any other type of files (link, other) + // just ignore the item. + // Then prepare the information that will be stored for that file. + // When its a folder, expand the folder with all the files that are in that + // folder (recursively). + // Parameters : + // Return Values : + // 1 on success. + // 0 on failure. + // -------------------------------------------------------------------------------- + function privFileDescrExpand(&$p_filedescr_list, &$p_options) + { + $v_result=1; + + // ----- Create a result list + $v_result_list = array(); + + // ----- Look each entry + for ($i=0; $iprivCalculateStoredFilename($v_descr, $p_options); + + // ----- Add the descriptor in result list + $v_result_list[sizeof($v_result_list)] = $v_descr; + + // ----- Look for folder + if ($v_descr['type'] == 'folder') { + // ----- List of items in folder + $v_dirlist_descr = array(); + $v_dirlist_nb = 0; + if ($v_folder_handler = @opendir($v_descr['filename'])) { + while (($v_item_handler = @readdir($v_folder_handler)) !== false) { + + // ----- Skip '.' and '..' + if (($v_item_handler == '.') || ($v_item_handler == '..')) { + continue; + } + + // ----- Compose the full filename + $v_dirlist_descr[$v_dirlist_nb]['filename'] = $v_descr['filename'].'/'.$v_item_handler; + + // ----- Look for different stored filename + // Because the name of the folder was changed, the name of the + // files/sub-folders also change + if (($v_descr['stored_filename'] != $v_descr['filename']) + && (!isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH]))) { + if ($v_descr['stored_filename'] != '') { + $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_descr['stored_filename'].'/'.$v_item_handler; + } + else { + $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_item_handler; + } + } + + $v_dirlist_nb++; + } + + @closedir($v_folder_handler); + } + else { + // TBC : unable to open folder in read mode + } + + // ----- Expand each element of the list + if ($v_dirlist_nb != 0) { + // ----- Expand + if (($v_result = $this->privFileDescrExpand($v_dirlist_descr, $p_options)) != 1) { + return $v_result; + } + + // ----- Concat the resulting list + $v_result_list = array_merge($v_result_list, $v_dirlist_descr); + } + else { + } + + // ----- Free local array + unset($v_dirlist_descr); + } + } + + // ----- Get the result list + $p_filedescr_list = $v_result_list; + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privCreate() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privCreate($p_filedescr_list, &$p_result_list, &$p_options) + { + $v_result=1; + $v_list_detail = array(); + + // ----- Magic quotes trick + $this->privDisableMagicQuotes(); + + // ----- Open the file in write mode + if (($v_result = $this->privOpenFd('wb')) != 1) + { + // ----- Return + return $v_result; + } + + // ----- Add the list of files + $v_result = $this->privAddList($p_filedescr_list, $p_result_list, $p_options); + + // ----- Close + $this->privCloseFd(); + + // ----- Magic quotes trick + $this->privSwapBackMagicQuotes(); + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privAdd() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privAdd($p_filedescr_list, &$p_result_list, &$p_options) + { + $v_result=1; + $v_list_detail = array(); + + // ----- Look if the archive exists or is empty + if ((!is_file($this->zipname)) || (filesize($this->zipname) == 0)) + { + + // ----- Do a create + $v_result = $this->privCreate($p_filedescr_list, $p_result_list, $p_options); + + // ----- Return + return $v_result; + } + // ----- Magic quotes trick + $this->privDisableMagicQuotes(); + + // ----- Open the zip file + if (($v_result=$this->privOpenFd('rb')) != 1) + { + // ----- Magic quotes trick + $this->privSwapBackMagicQuotes(); + + // ----- Return + return $v_result; + } + + // ----- Read the central directory information + $v_central_dir = array(); + if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) + { + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + return $v_result; + } + + // ----- Go to beginning of File + @rewind($this->zip_fd); + + // ----- Creates a temporary file + $v_zip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.tmp'; + + // ----- Open the temporary file in write mode + if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0) + { + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_zip_temp_name.'\' in binary write mode'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Copy the files from the archive to the temporary file + // TBC : Here I should better append the file and go back to erase the central dir + $v_size = $v_central_dir['offset']; + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = fread($this->zip_fd, $v_read_size); + @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Swap the file descriptor + // Here is a trick : I swap the temporary fd with the zip fd, in order to use + // the following methods on the temporary fil and not the real archive + $v_swap = $this->zip_fd; + $this->zip_fd = $v_zip_temp_fd; + $v_zip_temp_fd = $v_swap; + + // ----- Add the files + $v_header_list = array(); + if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1) + { + fclose($v_zip_temp_fd); + $this->privCloseFd(); + @unlink($v_zip_temp_name); + $this->privSwapBackMagicQuotes(); + + // ----- Return + return $v_result; + } + + // ----- Store the offset of the central dir + $v_offset = @ftell($this->zip_fd); + + // ----- Copy the block of file headers from the old archive + $v_size = $v_central_dir['size']; + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @fread($v_zip_temp_fd, $v_read_size); + @fwrite($this->zip_fd, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Create the Central Dir files header + for ($i=0, $v_count=0; $iprivWriteCentralFileHeader($v_header_list[$i])) != 1) { + fclose($v_zip_temp_fd); + $this->privCloseFd(); + @unlink($v_zip_temp_name); + $this->privSwapBackMagicQuotes(); + + // ----- Return + return $v_result; + } + $v_count++; + } + + // ----- Transform the header to a 'usable' info + $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]); + } + + // ----- Zip file comment + $v_comment = $v_central_dir['comment']; + if (isset($p_options[PCLZIP_OPT_COMMENT])) { + $v_comment = $p_options[PCLZIP_OPT_COMMENT]; + } + if (isset($p_options[PCLZIP_OPT_ADD_COMMENT])) { + $v_comment = $v_comment.$p_options[PCLZIP_OPT_ADD_COMMENT]; + } + if (isset($p_options[PCLZIP_OPT_PREPEND_COMMENT])) { + $v_comment = $p_options[PCLZIP_OPT_PREPEND_COMMENT].$v_comment; + } + + // ----- Calculate the size of the central header + $v_size = @ftell($this->zip_fd)-$v_offset; + + // ----- Create the central dir footer + if (($v_result = $this->privWriteCentralHeader($v_count+$v_central_dir['entries'], $v_size, $v_offset, $v_comment)) != 1) + { + // ----- Reset the file list + unset($v_header_list); + $this->privSwapBackMagicQuotes(); + + // ----- Return + return $v_result; + } + + // ----- Swap back the file descriptor + $v_swap = $this->zip_fd; + $this->zip_fd = $v_zip_temp_fd; + $v_zip_temp_fd = $v_swap; + + // ----- Close + $this->privCloseFd(); + + // ----- Close the temporary file + @fclose($v_zip_temp_fd); + + // ----- Magic quotes trick + $this->privSwapBackMagicQuotes(); + + // ----- Delete the zip file + // TBC : I should test the result ... + @unlink($this->zipname); + + // ----- Rename the temporary file + // TBC : I should test the result ... + //@rename($v_zip_temp_name, $this->zipname); + PclZipUtilRename($v_zip_temp_name, $this->zipname); + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privOpenFd() + // Description : + // Parameters : + // -------------------------------------------------------------------------------- + function privOpenFd($p_mode) + { + $v_result=1; + + // ----- Look if already open + if ($this->zip_fd != 0) + { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Zip file \''.$this->zipname.'\' already open'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Open the zip file + if (($this->zip_fd = @fopen($this->zipname, $p_mode)) == 0) + { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \''.$this->zipname.'\' in '.$p_mode.' mode'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privCloseFd() + // Description : + // Parameters : + // -------------------------------------------------------------------------------- + function privCloseFd() + { + $v_result=1; + + if ($this->zip_fd != 0) + @fclose($this->zip_fd); + $this->zip_fd = 0; + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privAddList() + // Description : + // $p_add_dir and $p_remove_dir will give the ability to memorize a path which is + // different from the real path of the file. This is useful if you want to have PclTar + // running in any directory, and memorize relative path from an other directory. + // Parameters : + // $p_list : An array containing the file or directory names to add in the tar + // $p_result_list : list of added files with their properties (specially the status field) + // $p_add_dir : Path to add in the filename path archived + // $p_remove_dir : Path to remove in the filename path archived + // Return Values : + // -------------------------------------------------------------------------------- +// function privAddList($p_list, &$p_result_list, $p_add_dir, $p_remove_dir, $p_remove_all_dir, &$p_options) + function privAddList($p_filedescr_list, &$p_result_list, &$p_options) + { + $v_result=1; + + // ----- Add the files + $v_header_list = array(); + if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1) + { + // ----- Return + return $v_result; + } + + // ----- Store the offset of the central dir + $v_offset = @ftell($this->zip_fd); + + // ----- Create the Central Dir files header + for ($i=0,$v_count=0; $iprivWriteCentralFileHeader($v_header_list[$i])) != 1) { + // ----- Return + return $v_result; + } + $v_count++; + } + + // ----- Transform the header to a 'usable' info + $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]); + } + + // ----- Zip file comment + $v_comment = ''; + if (isset($p_options[PCLZIP_OPT_COMMENT])) { + $v_comment = $p_options[PCLZIP_OPT_COMMENT]; + } + + // ----- Calculate the size of the central header + $v_size = @ftell($this->zip_fd)-$v_offset; + + // ----- Create the central dir footer + if (($v_result = $this->privWriteCentralHeader($v_count, $v_size, $v_offset, $v_comment)) != 1) + { + // ----- Reset the file list + unset($v_header_list); + + // ----- Return + return $v_result; + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privAddFileList() + // Description : + // Parameters : + // $p_filedescr_list : An array containing the file description + // or directory names to add in the zip + // $p_result_list : list of added files with their properties (specially the status field) + // Return Values : + // -------------------------------------------------------------------------------- + function privAddFileList($p_filedescr_list, &$p_result_list, &$p_options) + { + $v_result=1; + $v_header = array(); + + // ----- Recuperate the current number of elt in list + $v_nb = sizeof($p_result_list); + + // ----- Loop on the files + for ($j=0; ($jprivAddFile($p_filedescr_list[$j], $v_header, + $p_options); + if ($v_result != 1) { + return $v_result; + } + + // ----- Store the file infos + $p_result_list[$v_nb++] = $v_header; + } + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privAddFile() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privAddFile($p_filedescr, &$p_header, &$p_options) + { + $v_result=1; + + // ----- Working variable + $p_filename = $p_filedescr['filename']; + + // TBC : Already done in the fileAtt check ... ? + if ($p_filename == "") { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file list parameter (invalid or empty list)"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Look for a stored different filename + /* TBC : Removed + if (isset($p_filedescr['stored_filename'])) { + $v_stored_filename = $p_filedescr['stored_filename']; + } + else { + $v_stored_filename = $p_filedescr['stored_filename']; + } + */ + + // ----- Set the file properties + clearstatcache(); + $p_header['version'] = 20; + $p_header['version_extracted'] = 10; + $p_header['flag'] = 0; + $p_header['compression'] = 0; + $p_header['crc'] = 0; + $p_header['compressed_size'] = 0; + $p_header['filename_len'] = strlen($p_filename); + $p_header['extra_len'] = 0; + $p_header['disk'] = 0; + $p_header['internal'] = 0; + $p_header['offset'] = 0; + $p_header['filename'] = $p_filename; +// TBC : Removed $p_header['stored_filename'] = $v_stored_filename; + $p_header['stored_filename'] = $p_filedescr['stored_filename']; + $p_header['extra'] = ''; + $p_header['status'] = 'ok'; + $p_header['index'] = -1; + + // ----- Look for regular file + if ($p_filedescr['type']=='file') { + $p_header['external'] = 0x00000000; + $p_header['size'] = filesize($p_filename); + } + + // ----- Look for regular folder + else if ($p_filedescr['type']=='folder') { + $p_header['external'] = 0x00000010; + $p_header['mtime'] = filemtime($p_filename); + $p_header['size'] = filesize($p_filename); + } + + // ----- Look for virtual file + else if ($p_filedescr['type'] == 'virtual_file') { + $p_header['external'] = 0x00000000; + $p_header['size'] = strlen($p_filedescr['content']); + } + + + // ----- Look for filetime + if (isset($p_filedescr['mtime'])) { + $p_header['mtime'] = $p_filedescr['mtime']; + } + else if ($p_filedescr['type'] == 'virtual_file') { + $p_header['mtime'] = time(); + } + else { + $p_header['mtime'] = filemtime($p_filename); + } + + // ------ Look for file comment + if (isset($p_filedescr['comment'])) { + $p_header['comment_len'] = strlen($p_filedescr['comment']); + $p_header['comment'] = $p_filedescr['comment']; + } + else { + $p_header['comment_len'] = 0; + $p_header['comment'] = ''; + } + + // ----- Look for pre-add callback + if (isset($p_options[PCLZIP_CB_PRE_ADD])) { + + // ----- Generate a local information + $v_local_header = array(); + $this->privConvertHeader2FileInfo($p_header, $v_local_header); + + // ----- Call the callback + // Here I do not use call_user_func() because I need to send a reference to the + // header. + $v_result = $p_options[PCLZIP_CB_PRE_ADD](PCLZIP_CB_PRE_ADD, $v_local_header); + if ($v_result == 0) { + // ----- Change the file status + $p_header['status'] = "skipped"; + $v_result = 1; + } + + // ----- Update the information + // Only some fields can be modified + if ($p_header['stored_filename'] != $v_local_header['stored_filename']) { + $p_header['stored_filename'] = PclZipUtilPathReduction($v_local_header['stored_filename']); + } + } + + // ----- Look for empty stored filename + if ($p_header['stored_filename'] == "") { + $p_header['status'] = "filtered"; + } + + // ----- Check the path length + if (strlen($p_header['stored_filename']) > 0xFF) { + $p_header['status'] = 'filename_too_long'; + } + + // ----- Look if no error, or file not skipped + if ($p_header['status'] == 'ok') { + + // ----- Look for a file + if ($p_filedescr['type'] == 'file') { + // ----- Look for using temporary file to zip + if ( (!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) + && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON]) + || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) + && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_header['size'])) ) ) { + $v_result = $this->privAddFileUsingTempFile($p_filedescr, $p_header, $p_options); + if ($v_result < PCLZIP_ERR_NO_ERROR) { + return $v_result; + } + } + + // ----- Use "in memory" zip algo + else { + + // ----- Open the source file + if (($v_file = @fopen($p_filename, "rb")) == 0) { + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode"); + return PclZip::errorCode(); + } + + // ----- Read the file content + if ($p_header['size'] > 0) { + $v_content = @fread($v_file, $p_header['size']); + } + else { + $v_content = ''; + } + + // ----- Close the file + @fclose($v_file); + + // ----- Calculate the CRC + $p_header['crc'] = @crc32($v_content); + + // ----- Look for no compression + if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) { + // ----- Set header parameters + $p_header['compressed_size'] = $p_header['size']; + $p_header['compression'] = 0; + } + + // ----- Look for normal compression + else { + // ----- Compress the content + $v_content = @gzdeflate($v_content); + + // ----- Set header parameters + $p_header['compressed_size'] = strlen($v_content); + $p_header['compression'] = 8; + } + + // ----- Call the header generation + if (($v_result = $this->privWriteFileHeader($p_header)) != 1) { + @fclose($v_file); + return $v_result; + } + + // ----- Write the compressed (or not) content + @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']); + + } + + } + + // ----- Look for a virtual file (a file from string) + else if ($p_filedescr['type'] == 'virtual_file') { + + $v_content = $p_filedescr['content']; + + // ----- Calculate the CRC + $p_header['crc'] = @crc32($v_content); + + // ----- Look for no compression + if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) { + // ----- Set header parameters + $p_header['compressed_size'] = $p_header['size']; + $p_header['compression'] = 0; + } + + // ----- Look for normal compression + else { + // ----- Compress the content + $v_content = @gzdeflate($v_content); + + // ----- Set header parameters + $p_header['compressed_size'] = strlen($v_content); + $p_header['compression'] = 8; + } + + // ----- Call the header generation + if (($v_result = $this->privWriteFileHeader($p_header)) != 1) { + @fclose($v_file); + return $v_result; + } + + // ----- Write the compressed (or not) content + @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']); + } + + // ----- Look for a directory + else if ($p_filedescr['type'] == 'folder') { + // ----- Look for directory last '/' + if (@substr($p_header['stored_filename'], -1) != '/') { + $p_header['stored_filename'] .= '/'; + } + + // ----- Set the file properties + $p_header['size'] = 0; + //$p_header['external'] = 0x41FF0010; // Value for a folder : to be checked + $p_header['external'] = 0x00000010; // Value for a folder : to be checked + + // ----- Call the header generation + if (($v_result = $this->privWriteFileHeader($p_header)) != 1) + { + return $v_result; + } + } + } + + // ----- Look for post-add callback + if (isset($p_options[PCLZIP_CB_POST_ADD])) { + + // ----- Generate a local information + $v_local_header = array(); + $this->privConvertHeader2FileInfo($p_header, $v_local_header); + + // ----- Call the callback + // Here I do not use call_user_func() because I need to send a reference to the + // header. + $v_result = $p_options[PCLZIP_CB_POST_ADD](PCLZIP_CB_POST_ADD, $v_local_header); + if ($v_result == 0) { + // ----- Ignored + $v_result = 1; + } + + // ----- Update the information + // Nothing can be modified + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privAddFileUsingTempFile() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privAddFileUsingTempFile($p_filedescr, &$p_header, &$p_options) + { + $v_result=PCLZIP_ERR_NO_ERROR; + + // ----- Working variable + $p_filename = $p_filedescr['filename']; + + + // ----- Open the source file + if (($v_file = @fopen($p_filename, "rb")) == 0) { + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode"); + return PclZip::errorCode(); + } + + // ----- Creates a compressed temporary file + $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.gz'; + if (($v_file_compressed = @gzopen($v_gzip_temp_name, "wb")) == 0) { + fclose($v_file); + PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary write mode'); + return PclZip::errorCode(); + } + + // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks + $v_size = filesize($p_filename); + while ($v_size != 0) { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @fread($v_file, $v_read_size); + //$v_binary_data = pack('a'.$v_read_size, $v_buffer); + @gzputs($v_file_compressed, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Close the file + @fclose($v_file); + @gzclose($v_file_compressed); + + // ----- Check the minimum file size + if (filesize($v_gzip_temp_name) < 18) { + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'gzip temporary file \''.$v_gzip_temp_name.'\' has invalid filesize - should be minimum 18 bytes'); + return PclZip::errorCode(); + } + + // ----- Extract the compressed attributes + if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0) { + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary read mode'); + return PclZip::errorCode(); + } + + // ----- Read the gzip file header + $v_binary_data = @fread($v_file_compressed, 10); + $v_data_header = unpack('a1id1/a1id2/a1cm/a1flag/Vmtime/a1xfl/a1os', $v_binary_data); + + // ----- Check some parameters + $v_data_header['os'] = bin2hex($v_data_header['os']); + + // ----- Read the gzip file footer + @fseek($v_file_compressed, filesize($v_gzip_temp_name)-8); + $v_binary_data = @fread($v_file_compressed, 8); + $v_data_footer = unpack('Vcrc/Vcompressed_size', $v_binary_data); + + // ----- Set the attributes + $p_header['compression'] = ord($v_data_header['cm']); + //$p_header['mtime'] = $v_data_header['mtime']; + $p_header['crc'] = $v_data_footer['crc']; + $p_header['compressed_size'] = filesize($v_gzip_temp_name)-18; + + // ----- Close the file + @fclose($v_file_compressed); + + // ----- Call the header generation + if (($v_result = $this->privWriteFileHeader($p_header)) != 1) { + return $v_result; + } + + // ----- Add the compressed data + if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0) + { + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary read mode'); + return PclZip::errorCode(); + } + + // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks + fseek($v_file_compressed, 10); + $v_size = $p_header['compressed_size']; + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @fread($v_file_compressed, $v_read_size); + //$v_binary_data = pack('a'.$v_read_size, $v_buffer); + @fwrite($this->zip_fd, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Close the file + @fclose($v_file_compressed); + + // ----- Unlink the temporary file + @unlink($v_gzip_temp_name); + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privCalculateStoredFilename() + // Description : + // Based on file descriptor properties and global options, this method + // calculate the filename that will be stored in the archive. + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privCalculateStoredFilename(&$p_filedescr, &$p_options) + { + $v_result=1; + + // ----- Working variables + $p_filename = $p_filedescr['filename']; + if (isset($p_options[PCLZIP_OPT_ADD_PATH])) { + $p_add_dir = $p_options[PCLZIP_OPT_ADD_PATH]; + } + else { + $p_add_dir = ''; + } + if (isset($p_options[PCLZIP_OPT_REMOVE_PATH])) { + $p_remove_dir = $p_options[PCLZIP_OPT_REMOVE_PATH]; + } + else { + $p_remove_dir = ''; + } + if (isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH])) { + $p_remove_all_dir = $p_options[PCLZIP_OPT_REMOVE_ALL_PATH]; + } + else { + $p_remove_all_dir = 0; + } + + + // ----- Look for full name change + if (isset($p_filedescr['new_full_name'])) { + // ----- Remove drive letter if any + $v_stored_filename = PclZipUtilTranslateWinPath($p_filedescr['new_full_name']); + } + + // ----- Look for path and/or short name change + else { + + // ----- Look for short name change + // Its when we change just the filename but not the path + if (isset($p_filedescr['new_short_name'])) { + $v_path_info = pathinfo($p_filename); + $v_dir = ''; + if ($v_path_info['dirname'] != '') { + $v_dir = $v_path_info['dirname'].'/'; + } + $v_stored_filename = $v_dir.$p_filedescr['new_short_name']; + } + else { + // ----- Calculate the stored filename + $v_stored_filename = $p_filename; + } + + // ----- Look for all path to remove + if ($p_remove_all_dir) { + $v_stored_filename = basename($p_filename); + } + // ----- Look for partial path remove + else if ($p_remove_dir != "") { + if (substr($p_remove_dir, -1) != '/') + $p_remove_dir .= "/"; + + if ( (substr($p_filename, 0, 2) == "./") + || (substr($p_remove_dir, 0, 2) == "./")) { + + if ( (substr($p_filename, 0, 2) == "./") + && (substr($p_remove_dir, 0, 2) != "./")) { + $p_remove_dir = "./".$p_remove_dir; + } + if ( (substr($p_filename, 0, 2) != "./") + && (substr($p_remove_dir, 0, 2) == "./")) { + $p_remove_dir = substr($p_remove_dir, 2); + } + } + + $v_compare = PclZipUtilPathInclusion($p_remove_dir, + $v_stored_filename); + if ($v_compare > 0) { + if ($v_compare == 2) { + $v_stored_filename = ""; + } + else { + $v_stored_filename = substr($v_stored_filename, + strlen($p_remove_dir)); + } + } + } + + // ----- Remove drive letter if any + $v_stored_filename = PclZipUtilTranslateWinPath($v_stored_filename); + + // ----- Look for path to add + if ($p_add_dir != "") { + if (substr($p_add_dir, -1) == "/") + $v_stored_filename = $p_add_dir.$v_stored_filename; + else + $v_stored_filename = $p_add_dir."/".$v_stored_filename; + } + } + + // ----- Filename (reduce the path of stored name) + $v_stored_filename = PclZipUtilPathReduction($v_stored_filename); + $p_filedescr['stored_filename'] = $v_stored_filename; + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privWriteFileHeader() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privWriteFileHeader(&$p_header) + { + $v_result=1; + + // ----- Store the offset position of the file + $p_header['offset'] = ftell($this->zip_fd); + + // ----- Transform UNIX mtime to DOS format mdate/mtime + $v_date = getdate($p_header['mtime']); + $v_mtime = ($v_date['hours']<<11) + ($v_date['minutes']<<5) + $v_date['seconds']/2; + $v_mdate = (($v_date['year']-1980)<<9) + ($v_date['mon']<<5) + $v_date['mday']; + + // ----- Packed data + $v_binary_data = pack("VvvvvvVVVvv", 0x04034b50, + $p_header['version_extracted'], $p_header['flag'], + $p_header['compression'], $v_mtime, $v_mdate, + $p_header['crc'], $p_header['compressed_size'], + $p_header['size'], + strlen($p_header['stored_filename']), + $p_header['extra_len']); + + // ----- Write the first 148 bytes of the header in the archive + fputs($this->zip_fd, $v_binary_data, 30); + + // ----- Write the variable fields + if (strlen($p_header['stored_filename']) != 0) + { + fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename'])); + } + if ($p_header['extra_len'] != 0) + { + fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']); + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privWriteCentralFileHeader() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privWriteCentralFileHeader(&$p_header) + { + $v_result=1; + + // TBC + //for(reset($p_header); $key = key($p_header); next($p_header)) { + //} + + // ----- Transform UNIX mtime to DOS format mdate/mtime + $v_date = getdate($p_header['mtime']); + $v_mtime = ($v_date['hours']<<11) + ($v_date['minutes']<<5) + $v_date['seconds']/2; + $v_mdate = (($v_date['year']-1980)<<9) + ($v_date['mon']<<5) + $v_date['mday']; + + + // ----- Packed data + $v_binary_data = pack("VvvvvvvVVVvvvvvVV", 0x02014b50, + $p_header['version'], $p_header['version_extracted'], + $p_header['flag'], $p_header['compression'], + $v_mtime, $v_mdate, $p_header['crc'], + $p_header['compressed_size'], $p_header['size'], + strlen($p_header['stored_filename']), + $p_header['extra_len'], $p_header['comment_len'], + $p_header['disk'], $p_header['internal'], + $p_header['external'], $p_header['offset']); + + // ----- Write the 42 bytes of the header in the zip file + fputs($this->zip_fd, $v_binary_data, 46); + + // ----- Write the variable fields + if (strlen($p_header['stored_filename']) != 0) + { + fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename'])); + } + if ($p_header['extra_len'] != 0) + { + fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']); + } + if ($p_header['comment_len'] != 0) + { + fputs($this->zip_fd, $p_header['comment'], $p_header['comment_len']); + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privWriteCentralHeader() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privWriteCentralHeader($p_nb_entries, $p_size, $p_offset, $p_comment) + { + $v_result=1; + + // ----- Packed data + $v_binary_data = pack("VvvvvVVv", 0x06054b50, 0, 0, $p_nb_entries, + $p_nb_entries, $p_size, + $p_offset, strlen($p_comment)); + + // ----- Write the 22 bytes of the header in the zip file + fputs($this->zip_fd, $v_binary_data, 22); + + // ----- Write the variable fields + if (strlen($p_comment) != 0) + { + fputs($this->zip_fd, $p_comment, strlen($p_comment)); + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privList() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privList(&$p_list) + { + $v_result=1; + + // ----- Magic quotes trick + $this->privDisableMagicQuotes(); + + // ----- Open the zip file + if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0) + { + // ----- Magic quotes trick + $this->privSwapBackMagicQuotes(); + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \''.$this->zipname.'\' in binary read mode'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Read the central directory information + $v_central_dir = array(); + if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) + { + $this->privSwapBackMagicQuotes(); + return $v_result; + } + + // ----- Go to beginning of Central Dir + @rewind($this->zip_fd); + if (@fseek($this->zip_fd, $v_central_dir['offset'])) + { + $this->privSwapBackMagicQuotes(); + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Read each entry + for ($i=0; $i<$v_central_dir['entries']; $i++) + { + // ----- Read the file header + if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1) + { + $this->privSwapBackMagicQuotes(); + return $v_result; + } + $v_header['index'] = $i; + + // ----- Get the only interesting attributes + $this->privConvertHeader2FileInfo($v_header, $p_list[$i]); + unset($v_header); + } + + // ----- Close the zip file + $this->privCloseFd(); + + // ----- Magic quotes trick + $this->privSwapBackMagicQuotes(); + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privConvertHeader2FileInfo() + // Description : + // This function takes the file information from the central directory + // entries and extract the interesting parameters that will be given back. + // The resulting file infos are set in the array $p_info + // $p_info['filename'] : Filename with full path. Given by user (add), + // extracted in the filesystem (extract). + // $p_info['stored_filename'] : Stored filename in the archive. + // $p_info['size'] = Size of the file. + // $p_info['compressed_size'] = Compressed size of the file. + // $p_info['mtime'] = Last modification date of the file. + // $p_info['comment'] = Comment associated with the file. + // $p_info['folder'] = true/false : indicates if the entry is a folder or not. + // $p_info['status'] = status of the action on the file. + // $p_info['crc'] = CRC of the file content. + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privConvertHeader2FileInfo($p_header, &$p_info) + { + $v_result=1; + + // ----- Get the interesting attributes + $v_temp_path = PclZipUtilPathReduction($p_header['filename']); + $p_info['filename'] = $v_temp_path; + $v_temp_path = PclZipUtilPathReduction($p_header['stored_filename']); + $p_info['stored_filename'] = $v_temp_path; + $p_info['size'] = $p_header['size']; + $p_info['compressed_size'] = $p_header['compressed_size']; + $p_info['mtime'] = $p_header['mtime']; + $p_info['comment'] = $p_header['comment']; + $p_info['folder'] = (($p_header['external']&0x00000010)==0x00000010); + $p_info['index'] = $p_header['index']; + $p_info['status'] = $p_header['status']; + $p_info['crc'] = $p_header['crc']; + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privExtractByRule() + // Description : + // Extract a file or directory depending of rules (by index, by name, ...) + // Parameters : + // $p_file_list : An array where will be placed the properties of each + // extracted file + // $p_path : Path to add while writing the extracted files + // $p_remove_path : Path to remove (from the file memorized path) while writing the + // extracted files. If the path does not match the file path, + // the file is extracted with its memorized path. + // $p_remove_path does not apply to 'list' mode. + // $p_path and $p_remove_path are commulative. + // Return Values : + // 1 on success,0 or less on error (see error code list) + // -------------------------------------------------------------------------------- + function privExtractByRule(&$p_file_list, $p_path, $p_remove_path, $p_remove_all_path, &$p_options) + { + $v_result=1; + + // ----- Magic quotes trick + $this->privDisableMagicQuotes(); + + // ----- Check the path + if ( ($p_path == "") + || ( (substr($p_path, 0, 1) != "/") + && (substr($p_path, 0, 3) != "../") + && (substr($p_path,1,2)!=":/"))) + $p_path = "./".$p_path; + + // ----- Reduce the path last (and duplicated) '/' + if (($p_path != "./") && ($p_path != "/")) + { + // ----- Look for the path end '/' + while (substr($p_path, -1) == "/") + { + $p_path = substr($p_path, 0, strlen($p_path)-1); + } + } + + // ----- Look for path to remove format (should end by /) + if (($p_remove_path != "") && (substr($p_remove_path, -1) != '/')) + { + $p_remove_path .= '/'; + } + $p_remove_path_size = strlen($p_remove_path); + + // ----- Open the zip file + if (($v_result = $this->privOpenFd('rb')) != 1) + { + $this->privSwapBackMagicQuotes(); + return $v_result; + } + + // ----- Read the central directory information + $v_central_dir = array(); + if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) + { + // ----- Close the zip file + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + + return $v_result; + } + + // ----- Start at beginning of Central Dir + $v_pos_entry = $v_central_dir['offset']; + + // ----- Read each entry + $j_start = 0; + for ($i=0, $v_nb_extracted=0; $i<$v_central_dir['entries']; $i++) + { + + // ----- Read next Central dir entry + @rewind($this->zip_fd); + if (@fseek($this->zip_fd, $v_pos_entry)) + { + // ----- Close the zip file + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Read the file header + $v_header = array(); + if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1) + { + // ----- Close the zip file + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + + return $v_result; + } + + // ----- Store the index + $v_header['index'] = $i; + + // ----- Store the file position + $v_pos_entry = ftell($this->zip_fd); + + // ----- Look for the specific extract rules + $v_extract = false; + + // ----- Look for extract by name rule + if ( (isset($p_options[PCLZIP_OPT_BY_NAME])) + && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) { + + // ----- Look if the filename is in the list + for ($j=0; ($j strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) + && (substr($v_header['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) { + $v_extract = true; + } + } + // ----- Look for a filename + elseif ($v_header['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) { + $v_extract = true; + } + } + } + + // ----- Look for extract by ereg rule + // ereg() is deprecated with PHP 5.3 + /* + else if ( (isset($p_options[PCLZIP_OPT_BY_EREG])) + && ($p_options[PCLZIP_OPT_BY_EREG] != "")) { + + if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header['stored_filename'])) { + $v_extract = true; + } + } + */ + + // ----- Look for extract by preg rule + else if ( (isset($p_options[PCLZIP_OPT_BY_PREG])) + && ($p_options[PCLZIP_OPT_BY_PREG] != "")) { + + if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header['stored_filename'])) { + $v_extract = true; + } + } + + // ----- Look for extract by index rule + else if ( (isset($p_options[PCLZIP_OPT_BY_INDEX])) + && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) { + + // ----- Look if the index is in the list + for ($j=$j_start; ($j=$p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i<=$p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) { + $v_extract = true; + } + if ($i>=$p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) { + $j_start = $j+1; + } + + if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start']>$i) { + break; + } + } + } + + // ----- Look for no rule, which means extract all the archive + else { + $v_extract = true; + } + + // ----- Check compression method + if ( ($v_extract) + && ( ($v_header['compression'] != 8) + && ($v_header['compression'] != 0))) { + $v_header['status'] = 'unsupported_compression'; + + // ----- Look for PCLZIP_OPT_STOP_ON_ERROR + if ( (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) + && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) { + + $this->privSwapBackMagicQuotes(); + + PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_COMPRESSION, + "Filename '".$v_header['stored_filename']."' is " + ."compressed by an unsupported compression " + ."method (".$v_header['compression'].") "); + + return PclZip::errorCode(); + } + } + + // ----- Check encrypted files + if (($v_extract) && (($v_header['flag'] & 1) == 1)) { + $v_header['status'] = 'unsupported_encryption'; + + // ----- Look for PCLZIP_OPT_STOP_ON_ERROR + if ( (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) + && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) { + + $this->privSwapBackMagicQuotes(); + + PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION, + "Unsupported encryption for " + ." filename '".$v_header['stored_filename'] + ."'"); + + return PclZip::errorCode(); + } + } + + // ----- Look for real extraction + if (($v_extract) && ($v_header['status'] != 'ok')) { + $v_result = $this->privConvertHeader2FileInfo($v_header, + $p_file_list[$v_nb_extracted++]); + if ($v_result != 1) { + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + return $v_result; + } + + $v_extract = false; + } + + // ----- Look for real extraction + if ($v_extract) + { + + // ----- Go to the file position + @rewind($this->zip_fd); + if (@fseek($this->zip_fd, $v_header['offset'])) + { + // ----- Close the zip file + $this->privCloseFd(); + + $this->privSwapBackMagicQuotes(); + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Look for extraction as string + if ($p_options[PCLZIP_OPT_EXTRACT_AS_STRING]) { + + $v_string = ''; + + // ----- Extracting the file + $v_result1 = $this->privExtractFileAsString($v_header, $v_string, $p_options); + if ($v_result1 < 1) { + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + return $v_result1; + } + + // ----- Get the only interesting attributes + if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted])) != 1) + { + // ----- Close the zip file + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + + return $v_result; + } + + // ----- Set the file content + $p_file_list[$v_nb_extracted]['content'] = $v_string; + + // ----- Next extracted file + $v_nb_extracted++; + + // ----- Look for user callback abort + if ($v_result1 == 2) { + break; + } + } + // ----- Look for extraction in standard output + elseif ( (isset($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT])) + && ($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT])) { + // ----- Extracting the file in standard output + $v_result1 = $this->privExtractFileInOutput($v_header, $p_options); + if ($v_result1 < 1) { + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + return $v_result1; + } + + // ----- Get the only interesting attributes + if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1) { + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + return $v_result; + } + + // ----- Look for user callback abort + if ($v_result1 == 2) { + break; + } + } + // ----- Look for normal extraction + else { + // ----- Extracting the file + $v_result1 = $this->privExtractFile($v_header, + $p_path, $p_remove_path, + $p_remove_all_path, + $p_options); + if ($v_result1 < 1) { + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + return $v_result1; + } + + // ----- Get the only interesting attributes + if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1) + { + // ----- Close the zip file + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + + return $v_result; + } + + // ----- Look for user callback abort + if ($v_result1 == 2) { + break; + } + } + } + } + + // ----- Close the zip file + $this->privCloseFd(); + $this->privSwapBackMagicQuotes(); + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privExtractFile() + // Description : + // Parameters : + // Return Values : + // + // 1 : ... ? + // PCLZIP_ERR_USER_ABORTED(2) : User ask for extraction stop in callback + // -------------------------------------------------------------------------------- + function privExtractFile(&$p_entry, $p_path, $p_remove_path, $p_remove_all_path, &$p_options) + { + $v_result=1; + + // ----- Read the file header + if (($v_result = $this->privReadFileHeader($v_header)) != 1) + { + // ----- Return + return $v_result; + } + + + // ----- Check that the file header is coherent with $p_entry info + if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) { + // TBC + } + + // ----- Look for all path to remove + if ($p_remove_all_path == true) { + // ----- Look for folder entry that not need to be extracted + if (($p_entry['external']&0x00000010)==0x00000010) { + + $p_entry['status'] = "filtered"; + + return $v_result; + } + + // ----- Get the basename of the path + $p_entry['filename'] = basename($p_entry['filename']); + } + + // ----- Look for path to remove + else if ($p_remove_path != "") + { + if (PclZipUtilPathInclusion($p_remove_path, $p_entry['filename']) == 2) + { + + // ----- Change the file status + $p_entry['status'] = "filtered"; + + // ----- Return + return $v_result; + } + + $p_remove_path_size = strlen($p_remove_path); + if (substr($p_entry['filename'], 0, $p_remove_path_size) == $p_remove_path) + { + + // ----- Remove the path + $p_entry['filename'] = substr($p_entry['filename'], $p_remove_path_size); + + } + } + + // ----- Add the path + if ($p_path != '') { + $p_entry['filename'] = $p_path."/".$p_entry['filename']; + } + + // ----- Check a base_dir_restriction + if (isset($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION])) { + $v_inclusion + = PclZipUtilPathInclusion($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION], + $p_entry['filename']); + if ($v_inclusion == 0) { + + PclZip::privErrorLog(PCLZIP_ERR_DIRECTORY_RESTRICTION, + "Filename '".$p_entry['filename']."' is " + ."outside PCLZIP_OPT_EXTRACT_DIR_RESTRICTION"); + + return PclZip::errorCode(); + } + } + + // ----- Look for pre-extract callback + if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) { + + // ----- Generate a local information + $v_local_header = array(); + $this->privConvertHeader2FileInfo($p_entry, $v_local_header); + + // ----- Call the callback + // Here I do not use call_user_func() because I need to send a reference to the + // header. + $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header); + if ($v_result == 0) { + // ----- Change the file status + $p_entry['status'] = "skipped"; + $v_result = 1; + } + + // ----- Look for abort result + if ($v_result == 2) { + // ----- This status is internal and will be changed in 'skipped' + $p_entry['status'] = "aborted"; + $v_result = PCLZIP_ERR_USER_ABORTED; + } + + // ----- Update the information + // Only some fields can be modified + $p_entry['filename'] = $v_local_header['filename']; + } + + + // ----- Look if extraction should be done + if ($p_entry['status'] == 'ok') { + + // ----- Look for specific actions while the file exist + if (file_exists($p_entry['filename'])) + { + + // ----- Look if file is a directory + if (is_dir($p_entry['filename'])) + { + + // ----- Change the file status + $p_entry['status'] = "already_a_directory"; + + // ----- Look for PCLZIP_OPT_STOP_ON_ERROR + // For historical reason first PclZip implementation does not stop + // when this kind of error occurs. + if ( (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) + && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) { + + PclZip::privErrorLog(PCLZIP_ERR_ALREADY_A_DIRECTORY, + "Filename '".$p_entry['filename']."' is " + ."already used by an existing directory"); + + return PclZip::errorCode(); + } + } + // ----- Look if file is write protected + else if (!is_writeable($p_entry['filename'])) + { + + // ----- Change the file status + $p_entry['status'] = "write_protected"; + + // ----- Look for PCLZIP_OPT_STOP_ON_ERROR + // For historical reason first PclZip implementation does not stop + // when this kind of error occurs. + if ( (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) + && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) { + + PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, + "Filename '".$p_entry['filename']."' exists " + ."and is write protected"); + + return PclZip::errorCode(); + } + } + + // ----- Look if the extracted file is older + else if (filemtime($p_entry['filename']) > $p_entry['mtime']) + { + // ----- Change the file status + if ( (isset($p_options[PCLZIP_OPT_REPLACE_NEWER])) + && ($p_options[PCLZIP_OPT_REPLACE_NEWER]===true)) { + } + else { + $p_entry['status'] = "newer_exist"; + + // ----- Look for PCLZIP_OPT_STOP_ON_ERROR + // For historical reason first PclZip implementation does not stop + // when this kind of error occurs. + if ( (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) + && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) { + + PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, + "Newer version of '".$p_entry['filename']."' exists " + ."and option PCLZIP_OPT_REPLACE_NEWER is not selected"); + + return PclZip::errorCode(); + } + } + } + else { + } + } + + // ----- Check the directory availability and create it if necessary + else { + if ((($p_entry['external']&0x00000010)==0x00000010) || (substr($p_entry['filename'], -1) == '/')) + $v_dir_to_check = $p_entry['filename']; + else if (!strstr($p_entry['filename'], "/")) + $v_dir_to_check = ""; + else + $v_dir_to_check = dirname($p_entry['filename']); + + if (($v_result = $this->privDirCheck($v_dir_to_check, (($p_entry['external']&0x00000010)==0x00000010))) != 1) { + + // ----- Change the file status + $p_entry['status'] = "path_creation_fail"; + + // ----- Return + //return $v_result; + $v_result = 1; + } + } + } + + // ----- Look if extraction should be done + if ($p_entry['status'] == 'ok') { + + // ----- Do the extraction (if not a folder) + if (!(($p_entry['external']&0x00000010)==0x00000010)) + { + // ----- Look for not compressed file + if ($p_entry['compression'] == 0) { + + // ----- Opening destination file + if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) + { + + // ----- Change the file status + $p_entry['status'] = "write_error"; + + // ----- Return + return $v_result; + } + + + // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks + $v_size = $p_entry['compressed_size']; + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @fread($this->zip_fd, $v_read_size); + /* Try to speed up the code + $v_binary_data = pack('a'.$v_read_size, $v_buffer); + @fwrite($v_dest_file, $v_binary_data, $v_read_size); + */ + @fwrite($v_dest_file, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Closing the destination file + fclose($v_dest_file); + + // ----- Change the file mtime + touch($p_entry['filename'], $p_entry['mtime']); + + + } + else { + // ----- TBC + // Need to be finished + if (($p_entry['flag'] & 1) == 1) { + PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION, 'File \''.$p_entry['filename'].'\' is encrypted. Encrypted files are not supported.'); + return PclZip::errorCode(); + } + + + // ----- Look for using temporary file to unzip + if ( (!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) + && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON]) + || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) + && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_entry['size'])) ) ) { + $v_result = $this->privExtractFileUsingTempFile($p_entry, $p_options); + if ($v_result < PCLZIP_ERR_NO_ERROR) { + return $v_result; + } + } + + // ----- Look for extract in memory + else { + + + // ----- Read the compressed file in a buffer (one shot) + if ($p_entry['compressed_size'] > 0) { + $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']); + } + else { + $v_buffer = ''; + } + + // ----- Decompress the file + $v_file_content = @gzinflate($v_buffer); + unset($v_buffer); + if ($v_file_content === FALSE) { + + // ----- Change the file status + // TBC + $p_entry['status'] = "error"; + + return $v_result; + } + + // ----- Opening destination file + if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) { + + // ----- Change the file status + $p_entry['status'] = "write_error"; + + return $v_result; + } + + // ----- Write the uncompressed data + @fwrite($v_dest_file, $v_file_content, $p_entry['size']); + unset($v_file_content); + + // ----- Closing the destination file + @fclose($v_dest_file); + + } + + // ----- Change the file mtime + @touch($p_entry['filename'], $p_entry['mtime']); + } + + // ----- Look for chmod option + if (isset($p_options[PCLZIP_OPT_SET_CHMOD])) { + + // ----- Change the mode of the file + @chmod($p_entry['filename'], $p_options[PCLZIP_OPT_SET_CHMOD]); + } + + } + } + + // ----- Change abort status + if ($p_entry['status'] == "aborted") { + $p_entry['status'] = "skipped"; + } + + // ----- Look for post-extract callback + elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) { + + // ----- Generate a local information + $v_local_header = array(); + $this->privConvertHeader2FileInfo($p_entry, $v_local_header); + + // ----- Call the callback + // Here I do not use call_user_func() because I need to send a reference to the + // header. + $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header); + + // ----- Look for abort result + if ($v_result == 2) { + $v_result = PCLZIP_ERR_USER_ABORTED; + } + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privExtractFileUsingTempFile() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privExtractFileUsingTempFile(&$p_entry, &$p_options) + { + $v_result=1; + + // ----- Creates a temporary file + $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.gz'; + if (($v_dest_file = @fopen($v_gzip_temp_name, "wb")) == 0) { + fclose($v_file); + PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary write mode'); + return PclZip::errorCode(); + } + + + // ----- Write gz file format header + $v_binary_data = pack('va1a1Va1a1', 0x8b1f, Chr($p_entry['compression']), Chr(0x00), time(), Chr(0x00), Chr(3)); + @fwrite($v_dest_file, $v_binary_data, 10); + + // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks + $v_size = $p_entry['compressed_size']; + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @fread($this->zip_fd, $v_read_size); + //$v_binary_data = pack('a'.$v_read_size, $v_buffer); + @fwrite($v_dest_file, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Write gz file format footer + $v_binary_data = pack('VV', $p_entry['crc'], $p_entry['size']); + @fwrite($v_dest_file, $v_binary_data, 8); + + // ----- Close the temporary file + @fclose($v_dest_file); + + // ----- Opening destination file + if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) { + $p_entry['status'] = "write_error"; + return $v_result; + } + + // ----- Open the temporary gz file + if (($v_src_file = @gzopen($v_gzip_temp_name, 'rb')) == 0) { + @fclose($v_dest_file); + $p_entry['status'] = "read_error"; + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary read mode'); + return PclZip::errorCode(); + } + + + // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks + $v_size = $p_entry['size']; + while ($v_size != 0) { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @gzread($v_src_file, $v_read_size); + //$v_binary_data = pack('a'.$v_read_size, $v_buffer); + @fwrite($v_dest_file, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + @fclose($v_dest_file); + @gzclose($v_src_file); + + // ----- Delete the temporary file + @unlink($v_gzip_temp_name); + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privExtractFileInOutput() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privExtractFileInOutput(&$p_entry, &$p_options) + { + $v_result=1; + + // ----- Read the file header + if (($v_result = $this->privReadFileHeader($v_header)) != 1) { + return $v_result; + } + + + // ----- Check that the file header is coherent with $p_entry info + if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) { + // TBC + } + + // ----- Look for pre-extract callback + if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) { + + // ----- Generate a local information + $v_local_header = array(); + $this->privConvertHeader2FileInfo($p_entry, $v_local_header); + + // ----- Call the callback + // Here I do not use call_user_func() because I need to send a reference to the + // header. +// eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);'); + $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header); + if ($v_result == 0) { + // ----- Change the file status + $p_entry['status'] = "skipped"; + $v_result = 1; + } + + // ----- Look for abort result + if ($v_result == 2) { + // ----- This status is internal and will be changed in 'skipped' + $p_entry['status'] = "aborted"; + $v_result = PCLZIP_ERR_USER_ABORTED; + } + + // ----- Update the information + // Only some fields can be modified + $p_entry['filename'] = $v_local_header['filename']; + } + + // ----- Trace + + // ----- Look if extraction should be done + if ($p_entry['status'] == 'ok') { + + // ----- Do the extraction (if not a folder) + if (!(($p_entry['external']&0x00000010)==0x00000010)) { + // ----- Look for not compressed file + if ($p_entry['compressed_size'] == $p_entry['size']) { + + // ----- Read the file in a buffer (one shot) + if ($p_entry['compressed_size'] > 0) { + $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']); + } + else { + $v_buffer = ''; + } + + // ----- Send the file to the output + echo $v_buffer; + unset($v_buffer); + } + else { + + // ----- Read the compressed file in a buffer (one shot) + if ($p_entry['compressed_size'] > 0) { + $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']); + } + else { + $v_buffer = ''; + } + + // ----- Decompress the file + $v_file_content = gzinflate($v_buffer); + unset($v_buffer); + + // ----- Send the file to the output + echo $v_file_content; + unset($v_file_content); + } + } + } + + // ----- Change abort status + if ($p_entry['status'] == "aborted") { + $p_entry['status'] = "skipped"; + } + + // ----- Look for post-extract callback + elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) { + + // ----- Generate a local information + $v_local_header = array(); + $this->privConvertHeader2FileInfo($p_entry, $v_local_header); + + // ----- Call the callback + // Here I do not use call_user_func() because I need to send a reference to the + // header. + $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header); + + // ----- Look for abort result + if ($v_result == 2) { + $v_result = PCLZIP_ERR_USER_ABORTED; + } + } + + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privExtractFileAsString() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privExtractFileAsString(&$p_entry, &$p_string, &$p_options) + { + $v_result=1; + + // ----- Read the file header + $v_header = array(); + if (($v_result = $this->privReadFileHeader($v_header)) != 1) + { + // ----- Return + return $v_result; + } + + + // ----- Check that the file header is coherent with $p_entry info + if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) { + // TBC + } + + // ----- Look for pre-extract callback + if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) { + + // ----- Generate a local information + $v_local_header = array(); + $this->privConvertHeader2FileInfo($p_entry, $v_local_header); + + // ----- Call the callback + // Here I do not use call_user_func() because I need to send a reference to the + // header. + $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header); + if ($v_result == 0) { + // ----- Change the file status + $p_entry['status'] = "skipped"; + $v_result = 1; + } + + // ----- Look for abort result + if ($v_result == 2) { + // ----- This status is internal and will be changed in 'skipped' + $p_entry['status'] = "aborted"; + $v_result = PCLZIP_ERR_USER_ABORTED; + } + + // ----- Update the information + // Only some fields can be modified + $p_entry['filename'] = $v_local_header['filename']; + } + + + // ----- Look if extraction should be done + if ($p_entry['status'] == 'ok') { + + // ----- Do the extraction (if not a folder) + if (!(($p_entry['external']&0x00000010)==0x00000010)) { + // ----- Look for not compressed file + // if ($p_entry['compressed_size'] == $p_entry['size']) + if ($p_entry['compression'] == 0) { + + // ----- Reading the file + if ($p_entry['compressed_size'] > 0) { + $p_string = @fread($this->zip_fd, $p_entry['compressed_size']); + } + else { + $p_string = ''; + } + } + else { + + // ----- Reading the file + if ($p_entry['compressed_size'] > 0) { + $v_data = @fread($this->zip_fd, $p_entry['compressed_size']); + } + else { + $v_data = ''; + } + + // ----- Decompress the file + if (($p_string = @gzinflate($v_data)) === FALSE) { + // TBC + } + } + + // ----- Trace + } + else { + // TBC : error : can not extract a folder in a string + } + + } + + // ----- Change abort status + if ($p_entry['status'] == "aborted") { + $p_entry['status'] = "skipped"; + } + + // ----- Look for post-extract callback + elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) { + + // ----- Generate a local information + $v_local_header = array(); + $this->privConvertHeader2FileInfo($p_entry, $v_local_header); + + // ----- Swap the content to header + $v_local_header['content'] = $p_string; + $p_string = ''; + + // ----- Call the callback + // Here I do not use call_user_func() because I need to send a reference to the + // header. + $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header); + + // ----- Swap back the content to header + $p_string = $v_local_header['content']; + unset($v_local_header['content']); + + // ----- Look for abort result + if ($v_result == 2) { + $v_result = PCLZIP_ERR_USER_ABORTED; + } + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privReadFileHeader() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privReadFileHeader(&$p_header) + { + $v_result=1; + + // ----- Read the 4 bytes signature + $v_binary_data = @fread($this->zip_fd, 4); + $v_data = unpack('Vid', $v_binary_data); + + // ----- Check signature + if ($v_data['id'] != 0x04034b50) + { + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Read the first 42 bytes of the header + $v_binary_data = fread($this->zip_fd, 26); + + // ----- Look for invalid block size + if (strlen($v_binary_data) != 26) + { + $p_header['filename'] = ""; + $p_header['status'] = "invalid_header"; + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : ".strlen($v_binary_data)); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Extract the values + $v_data = unpack('vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', $v_binary_data); + + // ----- Get filename + $p_header['filename'] = fread($this->zip_fd, $v_data['filename_len']); + + // ----- Get extra_fields + if ($v_data['extra_len'] != 0) { + $p_header['extra'] = fread($this->zip_fd, $v_data['extra_len']); + } + else { + $p_header['extra'] = ''; + } + + // ----- Extract properties + $p_header['version_extracted'] = $v_data['version']; + $p_header['compression'] = $v_data['compression']; + $p_header['size'] = $v_data['size']; + $p_header['compressed_size'] = $v_data['compressed_size']; + $p_header['crc'] = $v_data['crc']; + $p_header['flag'] = $v_data['flag']; + $p_header['filename_len'] = $v_data['filename_len']; + + // ----- Recuperate date in UNIX format + $p_header['mdate'] = $v_data['mdate']; + $p_header['mtime'] = $v_data['mtime']; + if ($p_header['mdate'] && $p_header['mtime']) + { + // ----- Extract time + $v_hour = ($p_header['mtime'] & 0xF800) >> 11; + $v_minute = ($p_header['mtime'] & 0x07E0) >> 5; + $v_seconde = ($p_header['mtime'] & 0x001F)*2; + + // ----- Extract date + $v_year = (($p_header['mdate'] & 0xFE00) >> 9) + 1980; + $v_month = ($p_header['mdate'] & 0x01E0) >> 5; + $v_day = $p_header['mdate'] & 0x001F; + + // ----- Get UNIX date format + $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year); + + } + else + { + $p_header['mtime'] = time(); + } + + // TBC + //for(reset($v_data); $key = key($v_data); next($v_data)) { + //} + + // ----- Set the stored filename + $p_header['stored_filename'] = $p_header['filename']; + + // ----- Set the status field + $p_header['status'] = "ok"; + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privReadCentralFileHeader() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privReadCentralFileHeader(&$p_header) + { + $v_result=1; + + // ----- Read the 4 bytes signature + $v_binary_data = @fread($this->zip_fd, 4); + $v_data = unpack('Vid', $v_binary_data); + + // ----- Check signature + if ($v_data['id'] != 0x02014b50) + { + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Read the first 42 bytes of the header + $v_binary_data = fread($this->zip_fd, 42); + + // ----- Look for invalid block size + if (strlen($v_binary_data) != 42) + { + $p_header['filename'] = ""; + $p_header['status'] = "invalid_header"; + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : ".strlen($v_binary_data)); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Extract the values + $p_header = unpack('vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', $v_binary_data); + + // ----- Get filename + if ($p_header['filename_len'] != 0) + $p_header['filename'] = fread($this->zip_fd, $p_header['filename_len']); + else + $p_header['filename'] = ''; + + // ----- Get extra + if ($p_header['extra_len'] != 0) + $p_header['extra'] = fread($this->zip_fd, $p_header['extra_len']); + else + $p_header['extra'] = ''; + + // ----- Get comment + if ($p_header['comment_len'] != 0) + $p_header['comment'] = fread($this->zip_fd, $p_header['comment_len']); + else + $p_header['comment'] = ''; + + // ----- Extract properties + + // ----- Recuperate date in UNIX format + //if ($p_header['mdate'] && $p_header['mtime']) + // TBC : bug : this was ignoring time with 0/0/0 + if (1) + { + // ----- Extract time + $v_hour = ($p_header['mtime'] & 0xF800) >> 11; + $v_minute = ($p_header['mtime'] & 0x07E0) >> 5; + $v_seconde = ($p_header['mtime'] & 0x001F)*2; + + // ----- Extract date + $v_year = (($p_header['mdate'] & 0xFE00) >> 9) + 1980; + $v_month = ($p_header['mdate'] & 0x01E0) >> 5; + $v_day = $p_header['mdate'] & 0x001F; + + // ----- Get UNIX date format + $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year); + + } + else + { + $p_header['mtime'] = time(); + } + + // ----- Set the stored filename + $p_header['stored_filename'] = $p_header['filename']; + + // ----- Set default status to ok + $p_header['status'] = 'ok'; + + // ----- Look if it is a directory + if (substr($p_header['filename'], -1) == '/') { + //$p_header['external'] = 0x41FF0010; + $p_header['external'] = 0x00000010; + } + + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privCheckFileHeaders() + // Description : + // Parameters : + // Return Values : + // 1 on success, + // 0 on error; + // -------------------------------------------------------------------------------- + function privCheckFileHeaders(&$p_local_header, &$p_central_header) + { + $v_result=1; + + // ----- Check the static values + // TBC + if ($p_local_header['filename'] != $p_central_header['filename']) { + } + if ($p_local_header['version_extracted'] != $p_central_header['version_extracted']) { + } + if ($p_local_header['flag'] != $p_central_header['flag']) { + } + if ($p_local_header['compression'] != $p_central_header['compression']) { + } + if ($p_local_header['mtime'] != $p_central_header['mtime']) { + } + if ($p_local_header['filename_len'] != $p_central_header['filename_len']) { + } + + // ----- Look for flag bit 3 + if (($p_local_header['flag'] & 8) == 8) { + $p_local_header['size'] = $p_central_header['size']; + $p_local_header['compressed_size'] = $p_central_header['compressed_size']; + $p_local_header['crc'] = $p_central_header['crc']; + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privReadEndCentralDir() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privReadEndCentralDir(&$p_central_dir) + { + $v_result=1; + + // ----- Go to the end of the zip file + $v_size = filesize($this->zipname); + @fseek($this->zip_fd, $v_size); + if (@ftell($this->zip_fd) != $v_size) + { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to go to the end of the archive \''.$this->zipname.'\''); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- First try : look if this is an archive with no commentaries (most of the time) + // in this case the end of central dir is at 22 bytes of the file end + $v_found = 0; + if ($v_size > 26) { + @fseek($this->zip_fd, $v_size-22); + if (($v_pos = @ftell($this->zip_fd)) != ($v_size-22)) + { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \''.$this->zipname.'\''); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Read for bytes + $v_binary_data = @fread($this->zip_fd, 4); + $v_data = @unpack('Vid', $v_binary_data); + + // ----- Check signature + if ($v_data['id'] == 0x06054b50) { + $v_found = 1; + } + + $v_pos = ftell($this->zip_fd); + } + + // ----- Go back to the maximum possible size of the Central Dir End Record + if (!$v_found) { + $v_maximum_size = 65557; // 0xFFFF + 22; + if ($v_maximum_size > $v_size) + $v_maximum_size = $v_size; + @fseek($this->zip_fd, $v_size-$v_maximum_size); + if (@ftell($this->zip_fd) != ($v_size-$v_maximum_size)) + { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \''.$this->zipname.'\''); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Read byte per byte in order to find the signature + $v_pos = ftell($this->zip_fd); + $v_bytes = 0x00000000; + while ($v_pos < $v_size) + { + // ----- Read a byte + $v_byte = @fread($this->zip_fd, 1); + + // ----- Add the byte + //$v_bytes = ($v_bytes << 8) | Ord($v_byte); + // Note we mask the old value down such that once shifted we can never end up with more than a 32bit number + // Otherwise on systems where we have 64bit integers the check below for the magic number will fail. + $v_bytes = ( ($v_bytes & 0xFFFFFF) << 8) | Ord($v_byte); + + // ----- Compare the bytes + if ($v_bytes == 0x504b0506) + { + $v_pos++; + break; + } + + $v_pos++; + } + + // ----- Look if not found end of central dir + if ($v_pos == $v_size) + { + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Unable to find End of Central Dir Record signature"); + + // ----- Return + return PclZip::errorCode(); + } + } + + // ----- Read the first 18 bytes of the header + $v_binary_data = fread($this->zip_fd, 18); + + // ----- Look for invalid block size + if (strlen($v_binary_data) != 18) + { + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid End of Central Dir Record size : ".strlen($v_binary_data)); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Extract the values + $v_data = unpack('vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size', $v_binary_data); + + // ----- Check the global size + if (($v_pos + $v_data['comment_size'] + 18) != $v_size) { + + // ----- Removed in release 2.2 see readme file + // The check of the file size is a little too strict. + // Some bugs where found when a zip is encrypted/decrypted with 'crypt'. + // While decrypted, zip has training 0 bytes + if (0) { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, + 'The central dir is not at the end of the archive.' + .' Some trailing bytes exists after the archive.'); + + // ----- Return + return PclZip::errorCode(); + } + } + + // ----- Get comment + if ($v_data['comment_size'] != 0) { + $p_central_dir['comment'] = fread($this->zip_fd, $v_data['comment_size']); + } + else + $p_central_dir['comment'] = ''; + + $p_central_dir['entries'] = $v_data['entries']; + $p_central_dir['disk_entries'] = $v_data['disk_entries']; + $p_central_dir['offset'] = $v_data['offset']; + $p_central_dir['size'] = $v_data['size']; + $p_central_dir['disk'] = $v_data['disk']; + $p_central_dir['disk_start'] = $v_data['disk_start']; + + // TBC + //for(reset($p_central_dir); $key = key($p_central_dir); next($p_central_dir)) { + //} + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privDeleteByRule() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privDeleteByRule(&$p_result_list, &$p_options) + { + $v_result=1; + $v_list_detail = array(); + + // ----- Open the zip file + if (($v_result=$this->privOpenFd('rb')) != 1) + { + // ----- Return + return $v_result; + } + + // ----- Read the central directory information + $v_central_dir = array(); + if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) + { + $this->privCloseFd(); + return $v_result; + } + + // ----- Go to beginning of File + @rewind($this->zip_fd); + + // ----- Scan all the files + // ----- Start at beginning of Central Dir + $v_pos_entry = $v_central_dir['offset']; + @rewind($this->zip_fd); + if (@fseek($this->zip_fd, $v_pos_entry)) + { + // ----- Close the zip file + $this->privCloseFd(); + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Read each entry + $v_header_list = array(); + $j_start = 0; + for ($i=0, $v_nb_extracted=0; $i<$v_central_dir['entries']; $i++) + { + + // ----- Read the file header + $v_header_list[$v_nb_extracted] = array(); + if (($v_result = $this->privReadCentralFileHeader($v_header_list[$v_nb_extracted])) != 1) + { + // ----- Close the zip file + $this->privCloseFd(); + + return $v_result; + } + + + // ----- Store the index + $v_header_list[$v_nb_extracted]['index'] = $i; + + // ----- Look for the specific extract rules + $v_found = false; + + // ----- Look for extract by name rule + if ( (isset($p_options[PCLZIP_OPT_BY_NAME])) + && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) { + + // ----- Look if the filename is in the list + for ($j=0; ($j strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) + && (substr($v_header_list[$v_nb_extracted]['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) { + $v_found = true; + } + elseif ( (($v_header_list[$v_nb_extracted]['external']&0x00000010)==0x00000010) /* Indicates a folder */ + && ($v_header_list[$v_nb_extracted]['stored_filename'].'/' == $p_options[PCLZIP_OPT_BY_NAME][$j])) { + $v_found = true; + } + } + // ----- Look for a filename + elseif ($v_header_list[$v_nb_extracted]['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) { + $v_found = true; + } + } + } + + // ----- Look for extract by ereg rule + // ereg() is deprecated with PHP 5.3 + /* + else if ( (isset($p_options[PCLZIP_OPT_BY_EREG])) + && ($p_options[PCLZIP_OPT_BY_EREG] != "")) { + + if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header_list[$v_nb_extracted]['stored_filename'])) { + $v_found = true; + } + } + */ + + // ----- Look for extract by preg rule + else if ( (isset($p_options[PCLZIP_OPT_BY_PREG])) + && ($p_options[PCLZIP_OPT_BY_PREG] != "")) { + + if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header_list[$v_nb_extracted]['stored_filename'])) { + $v_found = true; + } + } + + // ----- Look for extract by index rule + else if ( (isset($p_options[PCLZIP_OPT_BY_INDEX])) + && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) { + + // ----- Look if the index is in the list + for ($j=$j_start; ($j=$p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i<=$p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) { + $v_found = true; + } + if ($i>=$p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) { + $j_start = $j+1; + } + + if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start']>$i) { + break; + } + } + } + else { + $v_found = true; + } + + // ----- Look for deletion + if ($v_found) + { + unset($v_header_list[$v_nb_extracted]); + } + else + { + $v_nb_extracted++; + } + } + + // ----- Look if something need to be deleted + if ($v_nb_extracted > 0) { + + // ----- Creates a temporary file + $v_zip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.tmp'; + + // ----- Creates a temporary zip archive + $v_temp_zip = new PclZip($v_zip_temp_name); + + // ----- Open the temporary zip file in write mode + if (($v_result = $v_temp_zip->privOpenFd('wb')) != 1) { + $this->privCloseFd(); + + // ----- Return + return $v_result; + } + + // ----- Look which file need to be kept + for ($i=0; $izip_fd); + if (@fseek($this->zip_fd, $v_header_list[$i]['offset'])) { + // ----- Close the zip file + $this->privCloseFd(); + $v_temp_zip->privCloseFd(); + @unlink($v_zip_temp_name); + + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Read the file header + $v_local_header = array(); + if (($v_result = $this->privReadFileHeader($v_local_header)) != 1) { + // ----- Close the zip file + $this->privCloseFd(); + $v_temp_zip->privCloseFd(); + @unlink($v_zip_temp_name); + + // ----- Return + return $v_result; + } + + // ----- Check that local file header is same as central file header + if ($this->privCheckFileHeaders($v_local_header, + $v_header_list[$i]) != 1) { + // TBC + } + unset($v_local_header); + + // ----- Write the file header + if (($v_result = $v_temp_zip->privWriteFileHeader($v_header_list[$i])) != 1) { + // ----- Close the zip file + $this->privCloseFd(); + $v_temp_zip->privCloseFd(); + @unlink($v_zip_temp_name); + + // ----- Return + return $v_result; + } + + // ----- Read/write the data block + if (($v_result = PclZipUtilCopyBlock($this->zip_fd, $v_temp_zip->zip_fd, $v_header_list[$i]['compressed_size'])) != 1) { + // ----- Close the zip file + $this->privCloseFd(); + $v_temp_zip->privCloseFd(); + @unlink($v_zip_temp_name); + + // ----- Return + return $v_result; + } + } + + // ----- Store the offset of the central dir + $v_offset = @ftell($v_temp_zip->zip_fd); + + // ----- Re-Create the Central Dir files header + for ($i=0; $iprivWriteCentralFileHeader($v_header_list[$i])) != 1) { + $v_temp_zip->privCloseFd(); + $this->privCloseFd(); + @unlink($v_zip_temp_name); + + // ----- Return + return $v_result; + } + + // ----- Transform the header to a 'usable' info + $v_temp_zip->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]); + } + + + // ----- Zip file comment + $v_comment = ''; + if (isset($p_options[PCLZIP_OPT_COMMENT])) { + $v_comment = $p_options[PCLZIP_OPT_COMMENT]; + } + + // ----- Calculate the size of the central header + $v_size = @ftell($v_temp_zip->zip_fd)-$v_offset; + + // ----- Create the central dir footer + if (($v_result = $v_temp_zip->privWriteCentralHeader(sizeof($v_header_list), $v_size, $v_offset, $v_comment)) != 1) { + // ----- Reset the file list + unset($v_header_list); + $v_temp_zip->privCloseFd(); + $this->privCloseFd(); + @unlink($v_zip_temp_name); + + // ----- Return + return $v_result; + } + + // ----- Close + $v_temp_zip->privCloseFd(); + $this->privCloseFd(); + + // ----- Delete the zip file + // TBC : I should test the result ... + @unlink($this->zipname); + + // ----- Rename the temporary file + // TBC : I should test the result ... + //@rename($v_zip_temp_name, $this->zipname); + PclZipUtilRename($v_zip_temp_name, $this->zipname); + + // ----- Destroy the temporary archive + unset($v_temp_zip); + } + + // ----- Remove every files : reset the file + else if ($v_central_dir['entries'] != 0) { + $this->privCloseFd(); + + if (($v_result = $this->privOpenFd('wb')) != 1) { + return $v_result; + } + + if (($v_result = $this->privWriteCentralHeader(0, 0, 0, '')) != 1) { + return $v_result; + } + + $this->privCloseFd(); + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privDirCheck() + // Description : + // Check if a directory exists, if not it creates it and all the parents directory + // which may be useful. + // Parameters : + // $p_dir : Directory path to check. + // Return Values : + // 1 : OK + // -1 : Unable to create directory + // -------------------------------------------------------------------------------- + function privDirCheck($p_dir, $p_is_dir=false) + { + $v_result = 1; + + + // ----- Remove the final '/' + if (($p_is_dir) && (substr($p_dir, -1)=='/')) + { + $p_dir = substr($p_dir, 0, strlen($p_dir)-1); + } + + // ----- Check the directory availability + if ((is_dir($p_dir)) || ($p_dir == "")) + { + return 1; + } + + // ----- Extract parent directory + $p_parent_dir = dirname($p_dir); + + // ----- Just a check + if ($p_parent_dir != $p_dir) + { + // ----- Look for parent directory + if ($p_parent_dir != "") + { + if (($v_result = $this->privDirCheck($p_parent_dir)) != 1) + { + return $v_result; + } + } + } + + // ----- Create the directory + if (!@mkdir($p_dir, 0777)) + { + // ----- Error log + PclZip::privErrorLog(PCLZIP_ERR_DIR_CREATE_FAIL, "Unable to create directory '$p_dir'"); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privMerge() + // Description : + // If $p_archive_to_add does not exist, the function exit with a success result. + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privMerge(&$p_archive_to_add) + { + $v_result=1; + + // ----- Look if the archive_to_add exists + if (!is_file($p_archive_to_add->zipname)) + { + + // ----- Nothing to merge, so merge is a success + $v_result = 1; + + // ----- Return + return $v_result; + } + + // ----- Look if the archive exists + if (!is_file($this->zipname)) + { + + // ----- Do a duplicate + $v_result = $this->privDuplicate($p_archive_to_add->zipname); + + // ----- Return + return $v_result; + } + + // ----- Open the zip file + if (($v_result=$this->privOpenFd('rb')) != 1) + { + // ----- Return + return $v_result; + } + + // ----- Read the central directory information + $v_central_dir = array(); + if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) + { + $this->privCloseFd(); + return $v_result; + } + + // ----- Go to beginning of File + @rewind($this->zip_fd); + + // ----- Open the archive_to_add file + if (($v_result=$p_archive_to_add->privOpenFd('rb')) != 1) + { + $this->privCloseFd(); + + // ----- Return + return $v_result; + } + + // ----- Read the central directory information + $v_central_dir_to_add = array(); + if (($v_result = $p_archive_to_add->privReadEndCentralDir($v_central_dir_to_add)) != 1) + { + $this->privCloseFd(); + $p_archive_to_add->privCloseFd(); + + return $v_result; + } + + // ----- Go to beginning of File + @rewind($p_archive_to_add->zip_fd); + + // ----- Creates a temporary file + $v_zip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.tmp'; + + // ----- Open the temporary file in write mode + if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0) + { + $this->privCloseFd(); + $p_archive_to_add->privCloseFd(); + + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_zip_temp_name.'\' in binary write mode'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Copy the files from the archive to the temporary file + // TBC : Here I should better append the file and go back to erase the central dir + $v_size = $v_central_dir['offset']; + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = fread($this->zip_fd, $v_read_size); + @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Copy the files from the archive_to_add into the temporary file + $v_size = $v_central_dir_to_add['offset']; + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = fread($p_archive_to_add->zip_fd, $v_read_size); + @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Store the offset of the central dir + $v_offset = @ftell($v_zip_temp_fd); + + // ----- Copy the block of file headers from the old archive + $v_size = $v_central_dir['size']; + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @fread($this->zip_fd, $v_read_size); + @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Copy the block of file headers from the archive_to_add + $v_size = $v_central_dir_to_add['size']; + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @fread($p_archive_to_add->zip_fd, $v_read_size); + @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Merge the file comments + $v_comment = $v_central_dir['comment'].' '.$v_central_dir_to_add['comment']; + + // ----- Calculate the size of the (new) central header + $v_size = @ftell($v_zip_temp_fd)-$v_offset; + + // ----- Swap the file descriptor + // Here is a trick : I swap the temporary fd with the zip fd, in order to use + // the following methods on the temporary fil and not the real archive fd + $v_swap = $this->zip_fd; + $this->zip_fd = $v_zip_temp_fd; + $v_zip_temp_fd = $v_swap; + + // ----- Create the central dir footer + if (($v_result = $this->privWriteCentralHeader($v_central_dir['entries']+$v_central_dir_to_add['entries'], $v_size, $v_offset, $v_comment)) != 1) + { + $this->privCloseFd(); + $p_archive_to_add->privCloseFd(); + @fclose($v_zip_temp_fd); + $this->zip_fd = null; + + // ----- Reset the file list + unset($v_header_list); + + // ----- Return + return $v_result; + } + + // ----- Swap back the file descriptor + $v_swap = $this->zip_fd; + $this->zip_fd = $v_zip_temp_fd; + $v_zip_temp_fd = $v_swap; + + // ----- Close + $this->privCloseFd(); + $p_archive_to_add->privCloseFd(); + + // ----- Close the temporary file + @fclose($v_zip_temp_fd); + + // ----- Delete the zip file + // TBC : I should test the result ... + @unlink($this->zipname); + + // ----- Rename the temporary file + // TBC : I should test the result ... + //@rename($v_zip_temp_name, $this->zipname); + PclZipUtilRename($v_zip_temp_name, $this->zipname); + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privDuplicate() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privDuplicate($p_archive_filename) + { + $v_result=1; + + // ----- Look if the $p_archive_filename exists + if (!is_file($p_archive_filename)) + { + + // ----- Nothing to duplicate, so duplicate is a success. + $v_result = 1; + + // ----- Return + return $v_result; + } + + // ----- Open the zip file + if (($v_result=$this->privOpenFd('wb')) != 1) + { + // ----- Return + return $v_result; + } + + // ----- Open the temporary file in write mode + if (($v_zip_temp_fd = @fopen($p_archive_filename, 'rb')) == 0) + { + $this->privCloseFd(); + + PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive file \''.$p_archive_filename.'\' in binary write mode'); + + // ----- Return + return PclZip::errorCode(); + } + + // ----- Copy the files from the archive to the temporary file + // TBC : Here I should better append the file and go back to erase the central dir + $v_size = filesize($p_archive_filename); + while ($v_size != 0) + { + $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = fread($v_zip_temp_fd, $v_read_size); + @fwrite($this->zip_fd, $v_buffer, $v_read_size); + $v_size -= $v_read_size; + } + + // ----- Close + $this->privCloseFd(); + + // ----- Close the temporary file + @fclose($v_zip_temp_fd); + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privErrorLog() + // Description : + // Parameters : + // -------------------------------------------------------------------------------- + function privErrorLog($p_error_code=0, $p_error_string='') + { + if (PCLZIP_ERROR_EXTERNAL == 1) { + PclError($p_error_code, $p_error_string); + } + else { + $this->error_code = $p_error_code; + $this->error_string = $p_error_string; + } + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privErrorReset() + // Description : + // Parameters : + // -------------------------------------------------------------------------------- + function privErrorReset() + { + if (PCLZIP_ERROR_EXTERNAL == 1) { + PclErrorReset(); + } + else { + $this->error_code = 0; + $this->error_string = ''; + } + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privDisableMagicQuotes() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privDisableMagicQuotes() + { + $v_result=1; + + // EDIT for WordPress 5.3.0 + // magic_quote functions are deprecated in PHP 7.4, now assuming it's always off. + /* + + // ----- Look if function exists + if ( (!function_exists("get_magic_quotes_runtime")) + || (!function_exists("set_magic_quotes_runtime"))) { + return $v_result; + } + + // ----- Look if already done + if ($this->magic_quotes_status != -1) { + return $v_result; + } + + // ----- Get and memorize the magic_quote value + $this->magic_quotes_status = @get_magic_quotes_runtime(); + + // ----- Disable magic_quotes + if ($this->magic_quotes_status == 1) { + @set_magic_quotes_runtime(0); + } + */ + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : privSwapBackMagicQuotes() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function privSwapBackMagicQuotes() + { + $v_result=1; + + // EDIT for WordPress 5.3.0 + // magic_quote functions are deprecated in PHP 7.4, now assuming it's always off. + /* + + // ----- Look if function exists + if ( (!function_exists("get_magic_quotes_runtime")) + || (!function_exists("set_magic_quotes_runtime"))) { + return $v_result; + } + + // ----- Look if something to do + if ($this->magic_quotes_status != -1) { + return $v_result; + } + + // ----- Swap back magic_quotes + if ($this->magic_quotes_status == 1) { + @set_magic_quotes_runtime($this->magic_quotes_status); + } + + */ + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + } + // End of class + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : PclZipUtilPathReduction() + // Description : + // Parameters : + // Return Values : + // -------------------------------------------------------------------------------- + function PclZipUtilPathReduction($p_dir) + { + $v_result = ""; + + // ----- Look for not empty path + if ($p_dir != "") { + // ----- Explode path by directory names + $v_list = explode("/", $p_dir); + + // ----- Study directories from last to first + $v_skip = 0; + for ($i=sizeof($v_list)-1; $i>=0; $i--) { + // ----- Look for current path + if ($v_list[$i] == ".") { + // ----- Ignore this directory + // Should be the first $i=0, but no check is done + } + else if ($v_list[$i] == "..") { + $v_skip++; + } + else if ($v_list[$i] == "") { + // ----- First '/' i.e. root slash + if ($i == 0) { + $v_result = "/".$v_result; + if ($v_skip > 0) { + // ----- It is an invalid path, so the path is not modified + // TBC + $v_result = $p_dir; + $v_skip = 0; + } + } + // ----- Last '/' i.e. indicates a directory + else if ($i == (sizeof($v_list)-1)) { + $v_result = $v_list[$i]; + } + // ----- Double '/' inside the path + else { + // ----- Ignore only the double '//' in path, + // but not the first and last '/' + } + } + else { + // ----- Look for item to skip + if ($v_skip > 0) { + $v_skip--; + } + else { + $v_result = $v_list[$i].($i!=(sizeof($v_list)-1)?"/".$v_result:""); + } + } + } + + // ----- Look for skip + if ($v_skip > 0) { + while ($v_skip > 0) { + $v_result = '../'.$v_result; + $v_skip--; + } + } + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : PclZipUtilPathInclusion() + // Description : + // This function indicates if the path $p_path is under the $p_dir tree. Or, + // said in an other way, if the file or sub-dir $p_path is inside the dir + // $p_dir. + // The function indicates also if the path is exactly the same as the dir. + // This function supports path with duplicated '/' like '//', but does not + // support '.' or '..' statements. + // Parameters : + // Return Values : + // 0 if $p_path is not inside directory $p_dir + // 1 if $p_path is inside directory $p_dir + // 2 if $p_path is exactly the same as $p_dir + // -------------------------------------------------------------------------------- + function PclZipUtilPathInclusion($p_dir, $p_path) + { + $v_result = 1; + + // ----- Look for path beginning by ./ + if ( ($p_dir == '.') + || ((strlen($p_dir) >=2) && (substr($p_dir, 0, 2) == './'))) { + $p_dir = PclZipUtilTranslateWinPath(getcwd(), FALSE).'/'.substr($p_dir, 1); + } + if ( ($p_path == '.') + || ((strlen($p_path) >=2) && (substr($p_path, 0, 2) == './'))) { + $p_path = PclZipUtilTranslateWinPath(getcwd(), FALSE).'/'.substr($p_path, 1); + } + + // ----- Explode dir and path by directory separator + $v_list_dir = explode("/", $p_dir); + $v_list_dir_size = sizeof($v_list_dir); + $v_list_path = explode("/", $p_path); + $v_list_path_size = sizeof($v_list_path); + + // ----- Study directories paths + $i = 0; + $j = 0; + while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result)) { + + // ----- Look for empty dir (path reduction) + if ($v_list_dir[$i] == '') { + $i++; + continue; + } + if ($v_list_path[$j] == '') { + $j++; + continue; + } + + // ----- Compare the items + if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ( $v_list_path[$j] != '')) { + $v_result = 0; + } + + // ----- Next items + $i++; + $j++; + } + + // ----- Look if everything seems to be the same + if ($v_result) { + // ----- Skip all the empty items + while (($j < $v_list_path_size) && ($v_list_path[$j] == '')) $j++; + while (($i < $v_list_dir_size) && ($v_list_dir[$i] == '')) $i++; + + if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size)) { + // ----- There are exactly the same + $v_result = 2; + } + else if ($i < $v_list_dir_size) { + // ----- The path is shorter than the dir + $v_result = 0; + } + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : PclZipUtilCopyBlock() + // Description : + // Parameters : + // $p_mode : read/write compression mode + // 0 : src & dest normal + // 1 : src gzip, dest normal + // 2 : src normal, dest gzip + // 3 : src & dest gzip + // Return Values : + // -------------------------------------------------------------------------------- + function PclZipUtilCopyBlock($p_src, $p_dest, $p_size, $p_mode=0) + { + $v_result = 1; + + if ($p_mode==0) + { + while ($p_size != 0) + { + $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @fread($p_src, $v_read_size); + @fwrite($p_dest, $v_buffer, $v_read_size); + $p_size -= $v_read_size; + } + } + else if ($p_mode==1) + { + while ($p_size != 0) + { + $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @gzread($p_src, $v_read_size); + @fwrite($p_dest, $v_buffer, $v_read_size); + $p_size -= $v_read_size; + } + } + else if ($p_mode==2) + { + while ($p_size != 0) + { + $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @fread($p_src, $v_read_size); + @gzwrite($p_dest, $v_buffer, $v_read_size); + $p_size -= $v_read_size; + } + } + else if ($p_mode==3) + { + while ($p_size != 0) + { + $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE); + $v_buffer = @gzread($p_src, $v_read_size); + @gzwrite($p_dest, $v_buffer, $v_read_size); + $p_size -= $v_read_size; + } + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : PclZipUtilRename() + // Description : + // This function tries to do a simple rename() function. If it fails, it + // tries to copy the $p_src file in a new $p_dest file and then unlink the + // first one. + // Parameters : + // $p_src : Old filename + // $p_dest : New filename + // Return Values : + // 1 on success, 0 on failure. + // -------------------------------------------------------------------------------- + function PclZipUtilRename($p_src, $p_dest) + { + $v_result = 1; + + // ----- Try to rename the files + if (!@rename($p_src, $p_dest)) { + + // ----- Try to copy & unlink the src + if (!@copy($p_src, $p_dest)) { + $v_result = 0; + } + else if (!@unlink($p_src)) { + $v_result = 0; + } + } + + // ----- Return + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : PclZipUtilOptionText() + // Description : + // Translate option value in text. Mainly for debug purpose. + // Parameters : + // $p_option : the option value. + // Return Values : + // The option text value. + // -------------------------------------------------------------------------------- + function PclZipUtilOptionText($p_option) + { + + $v_list = get_defined_constants(); + for (reset($v_list); $v_key = key($v_list); next($v_list)) { + $v_prefix = substr($v_key, 0, 10); + if (( ($v_prefix == 'PCLZIP_OPT') + || ($v_prefix == 'PCLZIP_CB_') + || ($v_prefix == 'PCLZIP_ATT')) + && ($v_list[$v_key] == $p_option)) { + return $v_key; + } + } + + $v_result = 'Unknown'; + + return $v_result; + } + // -------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------- + // Function : PclZipUtilTranslateWinPath() + // Description : + // Translate windows path by replacing '\' by '/' and optionally removing + // drive letter. + // Parameters : + // $p_path : path to translate. + // $p_remove_disk_letter : true | false + // Return Values : + // The path translated. + // -------------------------------------------------------------------------------- + function PclZipUtilTranslateWinPath($p_path, $p_remove_disk_letter=true) + { + if (stristr(php_uname(), 'windows')) { + // ----- Look for potential disk letter + if (($p_remove_disk_letter) && (($v_position = strpos($p_path, ':')) != false)) { + $p_path = substr($p_path, $v_position+1); + } + // ----- Change potential windows directory separator + if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0,1) == '\\')) { + $p_path = strtr($p_path, '\\', '/'); + } + } + return $p_path; + } + // -------------------------------------------------------------------------------- + + +?> diff --git a/wp-admin/includes/class-plugin-installer-skin.php b/wp-admin/includes/class-plugin-installer-skin.php new file mode 100644 index 0000000..ed165ed --- /dev/null +++ b/wp-admin/includes/class-plugin-installer-skin.php @@ -0,0 +1,349 @@ + 'web', + 'url' => '', + 'plugin' => '', + 'nonce' => '', + 'title' => '', + 'overwrite' => '', + ); + $args = wp_parse_args( $args, $defaults ); + + $this->type = $args['type']; + $this->url = $args['url']; + $this->api = isset( $args['api'] ) ? $args['api'] : array(); + $this->overwrite = $args['overwrite']; + + parent::__construct( $args ); + } + + /** + * Performs an action before installing a plugin. + * + * @since 2.8.0 + */ + public function before() { + if ( ! empty( $this->api ) ) { + $this->upgrader->strings['process_success'] = sprintf( + $this->upgrader->strings['process_success_specific'], + $this->api->name, + $this->api->version + ); + } + } + + /** + * Hides the `process_failed` error when updating a plugin by uploading a zip file. + * + * @since 5.5.0 + * + * @param WP_Error $wp_error WP_Error object. + * @return bool True if the error should be hidden, false otherwise. + */ + public function hide_process_failed( $wp_error ) { + if ( + 'upload' === $this->type && + '' === $this->overwrite && + $wp_error->get_error_code() === 'folder_exists' + ) { + return true; + } + + return false; + } + + /** + * Performs an action following a plugin install. + * + * @since 2.8.0 + */ + public function after() { + // Check if the plugin can be overwritten and output the HTML. + if ( $this->do_overwrite() ) { + return; + } + + $plugin_file = $this->upgrader->plugin_info(); + + $install_actions = array(); + + $from = isset( $_GET['from'] ) ? wp_unslash( $_GET['from'] ) : 'plugins'; + + if ( 'import' === $from ) { + $install_actions['activate_plugin'] = sprintf( + '%s', + wp_nonce_url( 'plugins.php?action=activate&from=import&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ), + __( 'Activate Plugin & Run Importer' ) + ); + } elseif ( 'press-this' === $from ) { + $install_actions['activate_plugin'] = sprintf( + '%s', + wp_nonce_url( 'plugins.php?action=activate&from=press-this&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ), + __( 'Activate Plugin & Go to Press This' ) + ); + } else { + $install_actions['activate_plugin'] = sprintf( + '%s', + wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ), + __( 'Activate Plugin' ) + ); + } + + if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) { + $install_actions['network_activate'] = sprintf( + '%s', + wp_nonce_url( 'plugins.php?action=activate&networkwide=1&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ), + __( 'Network Activate' ) + ); + unset( $install_actions['activate_plugin'] ); + } + + if ( 'import' === $from ) { + $install_actions['importers_page'] = sprintf( + '%s', + admin_url( 'import.php' ), + __( 'Go to Importers' ) + ); + } elseif ( 'web' === $this->type ) { + $install_actions['plugins_page'] = sprintf( + '%s', + self_admin_url( 'plugin-install.php' ), + __( 'Go to Plugin Installer' ) + ); + } elseif ( 'upload' === $this->type && 'plugins' === $from ) { + $install_actions['plugins_page'] = sprintf( + '%s', + self_admin_url( 'plugin-install.php' ), + __( 'Go to Plugin Installer' ) + ); + } else { + $install_actions['plugins_page'] = sprintf( + '%s', + self_admin_url( 'plugins.php' ), + __( 'Go to Plugins page' ) + ); + } + + if ( ! $this->result || is_wp_error( $this->result ) ) { + unset( $install_actions['activate_plugin'], $install_actions['network_activate'] ); + } elseif ( ! current_user_can( 'activate_plugin', $plugin_file ) || is_plugin_active( $plugin_file ) ) { + unset( $install_actions['activate_plugin'] ); + } + + /** + * Filters the list of action links available following a single plugin installation. + * + * @since 2.7.0 + * + * @param string[] $install_actions Array of plugin action links. + * @param object $api Object containing WordPress.org API plugin data. Empty + * for non-API installs, such as when a plugin is installed + * via upload. + * @param string $plugin_file Path to the plugin file relative to the plugins directory. + */ + $install_actions = apply_filters( 'install_plugin_complete_actions', $install_actions, $this->api, $plugin_file ); + + if ( ! empty( $install_actions ) ) { + $this->feedback( implode( ' ', (array) $install_actions ) ); + } + } + + /** + * Checks if the plugin can be overwritten and outputs the HTML for overwriting a plugin on upload. + * + * @since 5.5.0 + * + * @return bool Whether the plugin can be overwritten and HTML was outputted. + */ + private function do_overwrite() { + if ( 'upload' !== $this->type || ! is_wp_error( $this->result ) || 'folder_exists' !== $this->result->get_error_code() ) { + return false; + } + + $folder = $this->result->get_error_data( 'folder_exists' ); + $folder = ltrim( substr( $folder, strlen( WP_PLUGIN_DIR ) ), '/' ); + + $current_plugin_data = false; + $all_plugins = get_plugins(); + + foreach ( $all_plugins as $plugin => $plugin_data ) { + if ( strrpos( $plugin, $folder ) !== 0 ) { + continue; + } + + $current_plugin_data = $plugin_data; + } + + $new_plugin_data = $this->upgrader->new_plugin_data; + + if ( ! $current_plugin_data || ! $new_plugin_data ) { + return false; + } + + echo '

' . esc_html__( 'This plugin is already installed.' ) . '

'; + + $this->is_downgrading = version_compare( $current_plugin_data['Version'], $new_plugin_data['Version'], '>' ); + + $rows = array( + 'Name' => __( 'Plugin name' ), + 'Version' => __( 'Version' ), + 'Author' => __( 'Author' ), + 'RequiresWP' => __( 'Required WordPress version' ), + 'RequiresPHP' => __( 'Required PHP version' ), + ); + + $table = ''; + $table .= ''; + $table .= ''; + + $is_same_plugin = true; // Let's consider only these rows. + + foreach ( $rows as $field => $label ) { + $old_value = ! empty( $current_plugin_data[ $field ] ) ? (string) $current_plugin_data[ $field ] : '-'; + $new_value = ! empty( $new_plugin_data[ $field ] ) ? (string) $new_plugin_data[ $field ] : '-'; + + $is_same_plugin = $is_same_plugin && ( $old_value === $new_value ); + + $diff_field = ( 'Version' !== $field && $new_value !== $old_value ); + $diff_version = ( 'Version' === $field && $this->is_downgrading ); + + $table .= ''; + $table .= ( $diff_field || $diff_version ) ? ''; + } + + $table .= '
' . esc_html_x( 'Current', 'plugin' ) . '' . esc_html_x( 'Uploaded', 'plugin' ) . '
' . $label . '' . wp_strip_all_tags( $old_value ) . '' : ''; + $table .= wp_strip_all_tags( $new_value ) . '
'; + + /** + * Filters the compare table output for overwriting a plugin package on upload. + * + * @since 5.5.0 + * + * @param string $table The output table with Name, Version, Author, RequiresWP, and RequiresPHP info. + * @param array $current_plugin_data Array with current plugin data. + * @param array $new_plugin_data Array with uploaded plugin data. + */ + echo apply_filters( 'install_plugin_overwrite_comparison', $table, $current_plugin_data, $new_plugin_data ); + + $install_actions = array(); + $can_update = true; + + $blocked_message = '

' . esc_html__( 'The plugin cannot be updated due to the following:' ) . '

'; + $blocked_message .= '
    '; + + $requires_php = isset( $new_plugin_data['RequiresPHP'] ) ? $new_plugin_data['RequiresPHP'] : null; + $requires_wp = isset( $new_plugin_data['RequiresWP'] ) ? $new_plugin_data['RequiresWP'] : null; + + if ( ! is_php_version_compatible( $requires_php ) ) { + $error = sprintf( + /* translators: 1: Current PHP version, 2: Version required by the uploaded plugin. */ + __( 'The PHP version on your server is %1$s, however the uploaded plugin requires %2$s.' ), + PHP_VERSION, + $requires_php + ); + + $blocked_message .= '
  • ' . esc_html( $error ) . '
  • '; + $can_update = false; + } + + if ( ! is_wp_version_compatible( $requires_wp ) ) { + $error = sprintf( + /* translators: 1: Current WordPress version, 2: Version required by the uploaded plugin. */ + __( 'Your WordPress version is %1$s, however the uploaded plugin requires %2$s.' ), + get_bloginfo( 'version' ), + $requires_wp + ); + + $blocked_message .= '
  • ' . esc_html( $error ) . '
  • '; + $can_update = false; + } + + $blocked_message .= '
'; + + if ( $can_update ) { + if ( $this->is_downgrading ) { + $warning = sprintf( + /* translators: %s: Documentation URL. */ + __( 'You are uploading an older version of a current plugin. You can continue to install the older version, but be sure to back up your database and files first.' ), + __( 'https://wordpress.org/documentation/article/wordpress-backups/' ) + ); + } else { + $warning = sprintf( + /* translators: %s: Documentation URL. */ + __( 'You are updating a plugin. Be sure to back up your database and files first.' ), + __( 'https://wordpress.org/documentation/article/wordpress-backups/' ) + ); + } + + echo '

' . $warning . '

'; + + $overwrite = $this->is_downgrading ? 'downgrade-plugin' : 'update-plugin'; + + $install_actions['overwrite_plugin'] = sprintf( + '%s', + wp_nonce_url( add_query_arg( 'overwrite', $overwrite, $this->url ), 'plugin-upload' ), + _x( 'Replace current with uploaded', 'plugin' ) + ); + } else { + echo $blocked_message; + } + + $cancel_url = add_query_arg( 'action', 'upload-plugin-cancel-overwrite', $this->url ); + + $install_actions['plugins_page'] = sprintf( + '%s', + wp_nonce_url( $cancel_url, 'plugin-upload-cancel-overwrite' ), + __( 'Cancel and go back' ) + ); + + /** + * Filters the list of action links available following a single plugin installation failure + * when overwriting is allowed. + * + * @since 5.5.0 + * + * @param string[] $install_actions Array of plugin action links. + * @param object $api Object containing WordPress.org API plugin data. + * @param array $new_plugin_data Array with uploaded plugin data. + */ + $install_actions = apply_filters( 'install_plugin_overwrite_actions', $install_actions, $this->api, $new_plugin_data ); + + if ( ! empty( $install_actions ) ) { + printf( + '', + __( 'The uploaded file has expired. Please go back and upload it again.' ) + ); + echo '

' . implode( ' ', (array) $install_actions ) . '

'; + } + + return true; + } +} diff --git a/wp-admin/includes/class-plugin-upgrader-skin.php b/wp-admin/includes/class-plugin-upgrader-skin.php new file mode 100644 index 0000000..0e3e778 --- /dev/null +++ b/wp-admin/includes/class-plugin-upgrader-skin.php @@ -0,0 +1,123 @@ + '', + 'plugin' => '', + 'nonce' => '', + 'title' => __( 'Update Plugin' ), + ); + $args = wp_parse_args( $args, $defaults ); + + $this->plugin = $args['plugin']; + + $this->plugin_active = is_plugin_active( $this->plugin ); + $this->plugin_network_active = is_plugin_active_for_network( $this->plugin ); + + parent::__construct( $args ); + } + + /** + * Performs an action following a single plugin update. + * + * @since 2.8.0 + */ + public function after() { + $this->plugin = $this->upgrader->plugin_info(); + if ( ! empty( $this->plugin ) && ! is_wp_error( $this->result ) && $this->plugin_active ) { + // Currently used only when JS is off for a single plugin update? + printf( + '', + esc_attr__( 'Update progress' ), + wp_nonce_url( 'update.php?action=activate-plugin&networkwide=' . $this->plugin_network_active . '&plugin=' . urlencode( $this->plugin ), 'activate-plugin_' . $this->plugin ) + ); + } + + $this->decrement_update_count( 'plugin' ); + + $update_actions = array( + 'activate_plugin' => sprintf( + '%s', + wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $this->plugin ), 'activate-plugin_' . $this->plugin ), + __( 'Activate Plugin' ) + ), + 'plugins_page' => sprintf( + '%s', + self_admin_url( 'plugins.php' ), + __( 'Go to Plugins page' ) + ), + ); + + if ( $this->plugin_active || ! $this->result || is_wp_error( $this->result ) || ! current_user_can( 'activate_plugin', $this->plugin ) ) { + unset( $update_actions['activate_plugin'] ); + } + + /** + * Filters the list of action links available following a single plugin update. + * + * @since 2.7.0 + * + * @param string[] $update_actions Array of plugin action links. + * @param string $plugin Path to the plugin file relative to the plugins directory. + */ + $update_actions = apply_filters( 'update_plugin_complete_actions', $update_actions, $this->plugin ); + + if ( ! empty( $update_actions ) ) { + $this->feedback( implode( ' | ', (array) $update_actions ) ); + } + } +} diff --git a/wp-admin/includes/class-plugin-upgrader.php b/wp-admin/includes/class-plugin-upgrader.php new file mode 100644 index 0000000..02743f6 --- /dev/null +++ b/wp-admin/includes/class-plugin-upgrader.php @@ -0,0 +1,712 @@ +strings['up_to_date'] = __( 'The plugin is at the latest version.' ); + $this->strings['no_package'] = __( 'Update package not available.' ); + /* translators: %s: Package URL. */ + $this->strings['downloading_package'] = sprintf( __( 'Downloading update from %s…' ), '%s' ); + $this->strings['unpack_package'] = __( 'Unpacking the update…' ); + $this->strings['remove_old'] = __( 'Removing the old version of the plugin…' ); + $this->strings['remove_old_failed'] = __( 'Could not remove the old plugin.' ); + $this->strings['process_failed'] = __( 'Plugin update failed.' ); + $this->strings['process_success'] = __( 'Plugin updated successfully.' ); + $this->strings['process_bulk_success'] = __( 'Plugins updated successfully.' ); + } + + /** + * Initializes the installation strings. + * + * @since 2.8.0 + */ + public function install_strings() { + $this->strings['no_package'] = __( 'Installation package not available.' ); + /* translators: %s: Package URL. */ + $this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s…' ), '%s' ); + $this->strings['unpack_package'] = __( 'Unpacking the package…' ); + $this->strings['installing_package'] = __( 'Installing the plugin…' ); + $this->strings['remove_old'] = __( 'Removing the current plugin…' ); + $this->strings['remove_old_failed'] = __( 'Could not remove the current plugin.' ); + $this->strings['no_files'] = __( 'The plugin contains no files.' ); + $this->strings['process_failed'] = __( 'Plugin installation failed.' ); + $this->strings['process_success'] = __( 'Plugin installed successfully.' ); + /* translators: 1: Plugin name, 2: Plugin version. */ + $this->strings['process_success_specific'] = __( 'Successfully installed the plugin %1$s %2$s.' ); + + if ( ! empty( $this->skin->overwrite ) ) { + if ( 'update-plugin' === $this->skin->overwrite ) { + $this->strings['installing_package'] = __( 'Updating the plugin…' ); + $this->strings['process_failed'] = __( 'Plugin update failed.' ); + $this->strings['process_success'] = __( 'Plugin updated successfully.' ); + } + + if ( 'downgrade-plugin' === $this->skin->overwrite ) { + $this->strings['installing_package'] = __( 'Downgrading the plugin…' ); + $this->strings['process_failed'] = __( 'Plugin downgrade failed.' ); + $this->strings['process_success'] = __( 'Plugin downgraded successfully.' ); + } + } + } + + /** + * Install a plugin package. + * + * @since 2.8.0 + * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional. + * + * @param string $package The full local path or URI of the package. + * @param array $args { + * Optional. Other arguments for installing a plugin package. Default empty array. + * + * @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. + * Default true. + * } + * @return bool|WP_Error True if the installation was successful, false or a WP_Error otherwise. + */ + public function install( $package, $args = array() ) { + $defaults = array( + 'clear_update_cache' => true, + 'overwrite_package' => false, // Do not overwrite files. + ); + $parsed_args = wp_parse_args( $args, $defaults ); + + $this->init(); + $this->install_strings(); + + add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); + + if ( $parsed_args['clear_update_cache'] ) { + // Clear cache so wp_update_plugins() knows about the new plugin. + add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 ); + } + + $this->run( + array( + 'package' => $package, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => $parsed_args['overwrite_package'], + 'clear_working' => true, + 'hook_extra' => array( + 'type' => 'plugin', + 'action' => 'install', + ), + ) + ); + + remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 ); + remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); + + if ( ! $this->result || is_wp_error( $this->result ) ) { + return $this->result; + } + + // Force refresh of plugin update information. + wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); + + if ( $parsed_args['overwrite_package'] ) { + /** + * Fires when the upgrader has successfully overwritten a currently installed + * plugin or theme with an uploaded zip package. + * + * @since 5.5.0 + * + * @param string $package The package file. + * @param array $data The new plugin or theme data. + * @param string $package_type The package type ('plugin' or 'theme'). + */ + do_action( 'upgrader_overwrote_package', $package, $this->new_plugin_data, 'plugin' ); + } + + return true; + } + + /** + * Upgrades a plugin. + * + * @since 2.8.0 + * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional. + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @param array $args { + * Optional. Other arguments for upgrading a plugin package. Default empty array. + * + * @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. + * Default true. + * } + * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise. + */ + public function upgrade( $plugin, $args = array() ) { + $defaults = array( + 'clear_update_cache' => true, + ); + $parsed_args = wp_parse_args( $args, $defaults ); + + $this->init(); + $this->upgrade_strings(); + + $current = get_site_transient( 'update_plugins' ); + if ( ! isset( $current->response[ $plugin ] ) ) { + $this->skin->before(); + $this->skin->set_result( false ); + $this->skin->error( 'up_to_date' ); + $this->skin->after(); + return false; + } + + // Get the URL to the zip file. + $r = $current->response[ $plugin ]; + + add_filter( 'upgrader_pre_install', array( $this, 'deactivate_plugin_before_upgrade' ), 10, 2 ); + add_filter( 'upgrader_pre_install', array( $this, 'active_before' ), 10, 2 ); + add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ), 10, 4 ); + add_filter( 'upgrader_post_install', array( $this, 'active_after' ), 10, 2 ); + /* + * There's a Trac ticket to move up the directory for zips which are made a bit differently, useful for non-.org plugins. + * 'source_selection' => array( $this, 'source_selection' ), + */ + if ( $parsed_args['clear_update_cache'] ) { + // Clear cache so wp_update_plugins() knows about the new plugin. + add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 ); + } + + $this->run( + array( + 'package' => $r->package, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => true, + 'clear_working' => true, + 'hook_extra' => array( + 'plugin' => $plugin, + 'type' => 'plugin', + 'action' => 'update', + 'temp_backup' => array( + 'slug' => dirname( $plugin ), + 'src' => WP_PLUGIN_DIR, + 'dir' => 'plugins', + ), + ), + ) + ); + + // Cleanup our hooks, in case something else does an upgrade on this connection. + remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 ); + remove_filter( 'upgrader_pre_install', array( $this, 'deactivate_plugin_before_upgrade' ) ); + remove_filter( 'upgrader_pre_install', array( $this, 'active_before' ) ); + remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ) ); + remove_filter( 'upgrader_post_install', array( $this, 'active_after' ) ); + + if ( ! $this->result || is_wp_error( $this->result ) ) { + return $this->result; + } + + // Force refresh of plugin update information. + wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); + + /* + * Ensure any future auto-update failures trigger a failure email by removing + * the last failure notification from the list when plugins update successfully. + */ + $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); + + if ( isset( $past_failure_emails[ $plugin ] ) ) { + unset( $past_failure_emails[ $plugin ] ); + update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); + } + + return true; + } + + /** + * Upgrades several plugins at once. + * + * @since 2.8.0 + * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional. + * + * @global string $wp_version The WordPress version string. + * + * @param string[] $plugins Array of paths to plugin files relative to the plugins directory. + * @param array $args { + * Optional. Other arguments for upgrading several plugins at once. + * + * @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. Default true. + * } + * @return array|false An array of results indexed by plugin file, or false if unable to connect to the filesystem. + */ + public function bulk_upgrade( $plugins, $args = array() ) { + global $wp_version; + + $defaults = array( + 'clear_update_cache' => true, + ); + $parsed_args = wp_parse_args( $args, $defaults ); + + $this->init(); + $this->bulk = true; + $this->upgrade_strings(); + + $current = get_site_transient( 'update_plugins' ); + + add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ), 10, 4 ); + + $this->skin->header(); + + // Connect to the filesystem first. + $res = $this->fs_connect( array( WP_CONTENT_DIR, WP_PLUGIN_DIR ) ); + if ( ! $res ) { + $this->skin->footer(); + return false; + } + + $this->skin->bulk_header(); + + /* + * Only start maintenance mode if: + * - running Multisite and there are one or more plugins specified, OR + * - a plugin with an update available is currently active. + * @todo For multisite, maintenance mode should only kick in for individual sites if at all possible. + */ + $maintenance = ( is_multisite() && ! empty( $plugins ) ); + foreach ( $plugins as $plugin ) { + $maintenance = $maintenance || ( is_plugin_active( $plugin ) && isset( $current->response[ $plugin ] ) ); + } + if ( $maintenance ) { + $this->maintenance_mode( true ); + } + + $results = array(); + + $this->update_count = count( $plugins ); + $this->update_current = 0; + foreach ( $plugins as $plugin ) { + ++$this->update_current; + $this->skin->plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin, false, true ); + + if ( ! isset( $current->response[ $plugin ] ) ) { + $this->skin->set_result( 'up_to_date' ); + $this->skin->before(); + $this->skin->feedback( 'up_to_date' ); + $this->skin->after(); + $results[ $plugin ] = true; + continue; + } + + // Get the URL to the zip file. + $r = $current->response[ $plugin ]; + + $this->skin->plugin_active = is_plugin_active( $plugin ); + + if ( isset( $r->requires ) && ! is_wp_version_compatible( $r->requires ) ) { + $result = new WP_Error( + 'incompatible_wp_required_version', + sprintf( + /* translators: 1: Current WordPress version, 2: WordPress version required by the new plugin version. */ + __( 'Your WordPress version is %1$s, however the new plugin version requires %2$s.' ), + $wp_version, + $r->requires + ) + ); + + $this->skin->before( $result ); + $this->skin->error( $result ); + $this->skin->after(); + } elseif ( isset( $r->requires_php ) && ! is_php_version_compatible( $r->requires_php ) ) { + $result = new WP_Error( + 'incompatible_php_required_version', + sprintf( + /* translators: 1: Current PHP version, 2: PHP version required by the new plugin version. */ + __( 'The PHP version on your server is %1$s, however the new plugin version requires %2$s.' ), + PHP_VERSION, + $r->requires_php + ) + ); + + $this->skin->before( $result ); + $this->skin->error( $result ); + $this->skin->after(); + } else { + add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); + $result = $this->run( + array( + 'package' => $r->package, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => true, + 'clear_working' => true, + 'is_multi' => true, + 'hook_extra' => array( + 'plugin' => $plugin, + 'temp_backup' => array( + 'slug' => dirname( $plugin ), + 'src' => WP_PLUGIN_DIR, + 'dir' => 'plugins', + ), + ), + ) + ); + remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); + } + + $results[ $plugin ] = $result; + + // Prevent credentials auth screen from displaying multiple times. + if ( false === $result ) { + break; + } + } // End foreach $plugins. + + $this->maintenance_mode( false ); + + // Force refresh of plugin update information. + wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); + + /** This action is documented in wp-admin/includes/class-wp-upgrader.php */ + do_action( + 'upgrader_process_complete', + $this, + array( + 'action' => 'update', + 'type' => 'plugin', + 'bulk' => true, + 'plugins' => $plugins, + ) + ); + + $this->skin->bulk_footer(); + + $this->skin->footer(); + + // Cleanup our hooks, in case something else does an upgrade on this connection. + remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ) ); + + /* + * Ensure any future auto-update failures trigger a failure email by removing + * the last failure notification from the list when plugins update successfully. + */ + $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); + + foreach ( $results as $plugin => $result ) { + // Maintain last failure notification when plugins failed to update manually. + if ( ! $result || is_wp_error( $result ) || ! isset( $past_failure_emails[ $plugin ] ) ) { + continue; + } + + unset( $past_failure_emails[ $plugin ] ); + } + + update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); + + return $results; + } + + /** + * Checks that the source package contains a valid plugin. + * + * Hooked to the {@see 'upgrader_source_selection'} filter by Plugin_Upgrader::install(). + * + * @since 3.3.0 + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * @global string $wp_version The WordPress version string. + * + * @param string $source The path to the downloaded package source. + * @return string|WP_Error The source as passed, or a WP_Error object on failure. + */ + public function check_package( $source ) { + global $wp_filesystem, $wp_version; + + $this->new_plugin_data = array(); + + if ( is_wp_error( $source ) ) { + return $source; + } + + $working_directory = str_replace( $wp_filesystem->wp_content_dir(), trailingslashit( WP_CONTENT_DIR ), $source ); + if ( ! is_dir( $working_directory ) ) { // Sanity check, if the above fails, let's not prevent installation. + return $source; + } + + // Check that the folder contains at least 1 valid plugin. + $files = glob( $working_directory . '*.php' ); + if ( $files ) { + foreach ( $files as $file ) { + $info = get_plugin_data( $file, false, false ); + if ( ! empty( $info['Name'] ) ) { + $this->new_plugin_data = $info; + break; + } + } + } + + if ( empty( $this->new_plugin_data ) ) { + return new WP_Error( 'incompatible_archive_no_plugins', $this->strings['incompatible_archive'], __( 'No valid plugins were found.' ) ); + } + + $requires_php = isset( $info['RequiresPHP'] ) ? $info['RequiresPHP'] : null; + $requires_wp = isset( $info['RequiresWP'] ) ? $info['RequiresWP'] : null; + + if ( ! is_php_version_compatible( $requires_php ) ) { + $error = sprintf( + /* translators: 1: Current PHP version, 2: Version required by the uploaded plugin. */ + __( 'The PHP version on your server is %1$s, however the uploaded plugin requires %2$s.' ), + PHP_VERSION, + $requires_php + ); + + return new WP_Error( 'incompatible_php_required_version', $this->strings['incompatible_archive'], $error ); + } + + if ( ! is_wp_version_compatible( $requires_wp ) ) { + $error = sprintf( + /* translators: 1: Current WordPress version, 2: Version required by the uploaded plugin. */ + __( 'Your WordPress version is %1$s, however the uploaded plugin requires %2$s.' ), + $wp_version, + $requires_wp + ); + + return new WP_Error( 'incompatible_wp_required_version', $this->strings['incompatible_archive'], $error ); + } + + return $source; + } + + /** + * Retrieves the path to the file that contains the plugin info. + * + * This isn't used internally in the class, but is called by the skins. + * + * @since 2.8.0 + * + * @return string|false The full path to the main plugin file, or false. + */ + public function plugin_info() { + if ( ! is_array( $this->result ) ) { + return false; + } + if ( empty( $this->result['destination_name'] ) ) { + return false; + } + + // Ensure to pass with leading slash. + $plugin = get_plugins( '/' . $this->result['destination_name'] ); + if ( empty( $plugin ) ) { + return false; + } + + // Assume the requested plugin is the first in the list. + $pluginfiles = array_keys( $plugin ); + + return $this->result['destination_name'] . '/' . $pluginfiles[0]; + } + + /** + * Deactivates a plugin before it is upgraded. + * + * Hooked to the {@see 'upgrader_pre_install'} filter by Plugin_Upgrader::upgrade(). + * + * @since 2.8.0 + * @since 4.1.0 Added a return value. + * + * @param bool|WP_Error $response The installation response before the installation has started. + * @param array $plugin Plugin package arguments. + * @return bool|WP_Error The original `$response` parameter or WP_Error. + */ + public function deactivate_plugin_before_upgrade( $response, $plugin ) { + + if ( is_wp_error( $response ) ) { // Bypass. + return $response; + } + + // When in cron (background updates) don't deactivate the plugin, as we require a browser to reactivate it. + if ( wp_doing_cron() ) { + return $response; + } + + $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; + if ( empty( $plugin ) ) { + return new WP_Error( 'bad_request', $this->strings['bad_request'] ); + } + + if ( is_plugin_active( $plugin ) ) { + // Deactivate the plugin silently, Prevent deactivation hooks from running. + deactivate_plugins( $plugin, true ); + } + + return $response; + } + + /** + * Turns on maintenance mode before attempting to background update an active plugin. + * + * Hooked to the {@see 'upgrader_pre_install'} filter by Plugin_Upgrader::upgrade(). + * + * @since 5.4.0 + * + * @param bool|WP_Error $response The installation response before the installation has started. + * @param array $plugin Plugin package arguments. + * @return bool|WP_Error The original `$response` parameter or WP_Error. + */ + public function active_before( $response, $plugin ) { + if ( is_wp_error( $response ) ) { + return $response; + } + + // Only enable maintenance mode when in cron (background update). + if ( ! wp_doing_cron() ) { + return $response; + } + + $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; + + // Only run if plugin is active. + if ( ! is_plugin_active( $plugin ) ) { + return $response; + } + + // Change to maintenance mode. Bulk edit handles this separately. + if ( ! $this->bulk ) { + $this->maintenance_mode( true ); + } + + return $response; + } + + /** + * Turns off maintenance mode after upgrading an active plugin. + * + * Hooked to the {@see 'upgrader_post_install'} filter by Plugin_Upgrader::upgrade(). + * + * @since 5.4.0 + * + * @param bool|WP_Error $response The installation response after the installation has finished. + * @param array $plugin Plugin package arguments. + * @return bool|WP_Error The original `$response` parameter or WP_Error. + */ + public function active_after( $response, $plugin ) { + if ( is_wp_error( $response ) ) { + return $response; + } + + // Only disable maintenance mode when in cron (background update). + if ( ! wp_doing_cron() ) { + return $response; + } + + $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; + + // Only run if plugin is active. + if ( ! is_plugin_active( $plugin ) ) { + return $response; + } + + // Time to remove maintenance mode. Bulk edit handles this separately. + if ( ! $this->bulk ) { + $this->maintenance_mode( false ); + } + + return $response; + } + + /** + * Deletes the old plugin during an upgrade. + * + * Hooked to the {@see 'upgrader_clear_destination'} filter by + * Plugin_Upgrader::upgrade() and Plugin_Upgrader::bulk_upgrade(). + * + * @since 2.8.0 + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @param bool|WP_Error $removed Whether the destination was cleared. + * True on success, WP_Error on failure. + * @param string $local_destination The local package destination. + * @param string $remote_destination The remote package destination. + * @param array $plugin Extra arguments passed to hooked filters. + * @return bool|WP_Error + */ + public function delete_old_plugin( $removed, $local_destination, $remote_destination, $plugin ) { + global $wp_filesystem; + + if ( is_wp_error( $removed ) ) { + return $removed; // Pass errors through. + } + + $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; + if ( empty( $plugin ) ) { + return new WP_Error( 'bad_request', $this->strings['bad_request'] ); + } + + $plugins_dir = $wp_filesystem->wp_plugins_dir(); + $this_plugin_dir = trailingslashit( dirname( $plugins_dir . $plugin ) ); + + if ( ! $wp_filesystem->exists( $this_plugin_dir ) ) { // If it's already vanished. + return $removed; + } + + /* + * If plugin is in its own directory, recursively delete the directory. + * Base check on if plugin includes directory separator AND that it's not the root plugin folder. + */ + if ( strpos( $plugin, '/' ) && $this_plugin_dir !== $plugins_dir ) { + $deleted = $wp_filesystem->delete( $this_plugin_dir, true ); + } else { + $deleted = $wp_filesystem->delete( $plugins_dir . $plugin ); + } + + if ( ! $deleted ) { + return new WP_Error( 'remove_old_failed', $this->strings['remove_old_failed'] ); + } + + return true; + } +} diff --git a/wp-admin/includes/class-theme-installer-skin.php b/wp-admin/includes/class-theme-installer-skin.php new file mode 100644 index 0000000..99fe322 --- /dev/null +++ b/wp-admin/includes/class-theme-installer-skin.php @@ -0,0 +1,384 @@ + 'web', + 'url' => '', + 'theme' => '', + 'nonce' => '', + 'title' => '', + 'overwrite' => '', + ); + $args = wp_parse_args( $args, $defaults ); + + $this->type = $args['type']; + $this->url = $args['url']; + $this->api = isset( $args['api'] ) ? $args['api'] : array(); + $this->overwrite = $args['overwrite']; + + parent::__construct( $args ); + } + + /** + * Performs an action before installing a theme. + * + * @since 2.8.0 + */ + public function before() { + if ( ! empty( $this->api ) ) { + $this->upgrader->strings['process_success'] = sprintf( + $this->upgrader->strings['process_success_specific'], + $this->api->name, + $this->api->version + ); + } + } + + /** + * Hides the `process_failed` error when updating a theme by uploading a zip file. + * + * @since 5.5.0 + * + * @param WP_Error $wp_error WP_Error object. + * @return bool True if the error should be hidden, false otherwise. + */ + public function hide_process_failed( $wp_error ) { + if ( + 'upload' === $this->type && + '' === $this->overwrite && + $wp_error->get_error_code() === 'folder_exists' + ) { + return true; + } + + return false; + } + + /** + * Performs an action following a single theme install. + * + * @since 2.8.0 + */ + public function after() { + if ( $this->do_overwrite() ) { + return; + } + + if ( empty( $this->upgrader->result['destination_name'] ) ) { + return; + } + + $theme_info = $this->upgrader->theme_info(); + if ( empty( $theme_info ) ) { + return; + } + + $name = $theme_info->display( 'Name' ); + $stylesheet = $this->upgrader->result['destination_name']; + $template = $theme_info->get_template(); + + $activate_link = add_query_arg( + array( + 'action' => 'activate', + 'template' => urlencode( $template ), + 'stylesheet' => urlencode( $stylesheet ), + ), + admin_url( 'themes.php' ) + ); + $activate_link = wp_nonce_url( $activate_link, 'switch-theme_' . $stylesheet ); + + $install_actions = array(); + + if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) && ! $theme_info->is_block_theme() ) { + $customize_url = add_query_arg( + array( + 'theme' => urlencode( $stylesheet ), + 'return' => urlencode( admin_url( 'web' === $this->type ? 'theme-install.php' : 'themes.php' ) ), + ), + admin_url( 'customize.php' ) + ); + + $install_actions['preview'] = sprintf( + '' . + '%s', + esc_url( $customize_url ), + __( 'Live Preview' ), + /* translators: Hidden accessibility text. %s: Theme name. */ + sprintf( __( 'Live Preview “%s”' ), $name ) + ); + } + + $install_actions['activate'] = sprintf( + '' . + '%s', + esc_url( $activate_link ), + __( 'Activate' ), + /* translators: Hidden accessibility text. %s: Theme name. */ + sprintf( _x( 'Activate “%s”', 'theme' ), $name ) + ); + + if ( is_network_admin() && current_user_can( 'manage_network_themes' ) ) { + $install_actions['network_enable'] = sprintf( + '%s', + esc_url( wp_nonce_url( 'themes.php?action=enable&theme=' . urlencode( $stylesheet ), 'enable-theme_' . $stylesheet ) ), + __( 'Network Enable' ) + ); + } + + if ( 'web' === $this->type ) { + $install_actions['themes_page'] = sprintf( + '%s', + self_admin_url( 'theme-install.php' ), + __( 'Go to Theme Installer' ) + ); + } elseif ( current_user_can( 'switch_themes' ) || current_user_can( 'edit_theme_options' ) ) { + $install_actions['themes_page'] = sprintf( + '%s', + self_admin_url( 'themes.php' ), + __( 'Go to Themes page' ) + ); + } + + if ( ! $this->result || is_wp_error( $this->result ) || is_network_admin() || ! current_user_can( 'switch_themes' ) ) { + unset( $install_actions['activate'], $install_actions['preview'] ); + } elseif ( get_option( 'template' ) === $stylesheet ) { + unset( $install_actions['activate'] ); + } + + /** + * Filters the list of action links available following a single theme installation. + * + * @since 2.8.0 + * + * @param string[] $install_actions Array of theme action links. + * @param object $api Object containing WordPress.org API theme data. + * @param string $stylesheet Theme directory name. + * @param WP_Theme $theme_info Theme object. + */ + $install_actions = apply_filters( 'install_theme_complete_actions', $install_actions, $this->api, $stylesheet, $theme_info ); + if ( ! empty( $install_actions ) ) { + $this->feedback( implode( ' | ', (array) $install_actions ) ); + } + } + + /** + * Checks if the theme can be overwritten and outputs the HTML for overwriting a theme on upload. + * + * @since 5.5.0 + * + * @return bool Whether the theme can be overwritten and HTML was outputted. + */ + private function do_overwrite() { + if ( 'upload' !== $this->type || ! is_wp_error( $this->result ) || 'folder_exists' !== $this->result->get_error_code() ) { + return false; + } + + $folder = $this->result->get_error_data( 'folder_exists' ); + $folder = rtrim( $folder, '/' ); + + $current_theme_data = false; + $all_themes = wp_get_themes( array( 'errors' => null ) ); + + foreach ( $all_themes as $theme ) { + $stylesheet_dir = wp_normalize_path( $theme->get_stylesheet_directory() ); + + if ( rtrim( $stylesheet_dir, '/' ) !== $folder ) { + continue; + } + + $current_theme_data = $theme; + } + + $new_theme_data = $this->upgrader->new_theme_data; + + if ( ! $current_theme_data || ! $new_theme_data ) { + return false; + } + + echo '

' . esc_html__( 'This theme is already installed.' ) . '

'; + + // Check errors for active theme. + if ( is_wp_error( $current_theme_data->errors() ) ) { + $this->feedback( 'current_theme_has_errors', $current_theme_data->errors()->get_error_message() ); + } + + $this->is_downgrading = version_compare( $current_theme_data['Version'], $new_theme_data['Version'], '>' ); + + $is_invalid_parent = false; + if ( ! empty( $new_theme_data['Template'] ) ) { + $is_invalid_parent = ! in_array( $new_theme_data['Template'], array_keys( $all_themes ), true ); + } + + $rows = array( + 'Name' => __( 'Theme name' ), + 'Version' => __( 'Version' ), + 'Author' => __( 'Author' ), + 'RequiresWP' => __( 'Required WordPress version' ), + 'RequiresPHP' => __( 'Required PHP version' ), + 'Template' => __( 'Parent theme' ), + ); + + $table = ''; + $table .= ''; + + $is_same_theme = true; // Let's consider only these rows. + + foreach ( $rows as $field => $label ) { + $old_value = $current_theme_data->display( $field, false ); + $old_value = $old_value ? (string) $old_value : '-'; + + $new_value = ! empty( $new_theme_data[ $field ] ) ? (string) $new_theme_data[ $field ] : '-'; + + if ( $old_value === $new_value && '-' === $new_value && 'Template' === $field ) { + continue; + } + + $is_same_theme = $is_same_theme && ( $old_value === $new_value ); + + $diff_field = ( 'Version' !== $field && $new_value !== $old_value ); + $diff_version = ( 'Version' === $field && $this->is_downgrading ); + $invalid_parent = false; + + if ( 'Template' === $field && $is_invalid_parent ) { + $invalid_parent = true; + $new_value .= ' ' . __( '(not found)' ); + } + + $table .= ''; + $table .= ( $diff_field || $diff_version || $invalid_parent ) ? ''; + } + + $table .= '
' . esc_html_x( 'Active', 'theme' ) . '' . esc_html_x( 'Uploaded', 'theme' ) . '
' . $label . '' . wp_strip_all_tags( $old_value ) . '' : ''; + $table .= wp_strip_all_tags( $new_value ) . '
'; + + /** + * Filters the compare table output for overwriting a theme package on upload. + * + * @since 5.5.0 + * + * @param string $table The output table with Name, Version, Author, RequiresWP, and RequiresPHP info. + * @param WP_Theme $current_theme_data Active theme data. + * @param array $new_theme_data Array with uploaded theme data. + */ + echo apply_filters( 'install_theme_overwrite_comparison', $table, $current_theme_data, $new_theme_data ); + + $install_actions = array(); + $can_update = true; + + $blocked_message = '

' . esc_html__( 'The theme cannot be updated due to the following:' ) . '

'; + $blocked_message .= '
    '; + + $requires_php = isset( $new_theme_data['RequiresPHP'] ) ? $new_theme_data['RequiresPHP'] : null; + $requires_wp = isset( $new_theme_data['RequiresWP'] ) ? $new_theme_data['RequiresWP'] : null; + + if ( ! is_php_version_compatible( $requires_php ) ) { + $error = sprintf( + /* translators: 1: Current PHP version, 2: Version required by the uploaded theme. */ + __( 'The PHP version on your server is %1$s, however the uploaded theme requires %2$s.' ), + PHP_VERSION, + $requires_php + ); + + $blocked_message .= '
  • ' . esc_html( $error ) . '
  • '; + $can_update = false; + } + + if ( ! is_wp_version_compatible( $requires_wp ) ) { + $error = sprintf( + /* translators: 1: Current WordPress version, 2: Version required by the uploaded theme. */ + __( 'Your WordPress version is %1$s, however the uploaded theme requires %2$s.' ), + get_bloginfo( 'version' ), + $requires_wp + ); + + $blocked_message .= '
  • ' . esc_html( $error ) . '
  • '; + $can_update = false; + } + + $blocked_message .= '
'; + + if ( $can_update ) { + if ( $this->is_downgrading ) { + $warning = sprintf( + /* translators: %s: Documentation URL. */ + __( 'You are uploading an older version of the active theme. You can continue to install the older version, but be sure to back up your database and files first.' ), + __( 'https://wordpress.org/documentation/article/wordpress-backups/' ) + ); + } else { + $warning = sprintf( + /* translators: %s: Documentation URL. */ + __( 'You are updating a theme. Be sure to back up your database and files first.' ), + __( 'https://wordpress.org/documentation/article/wordpress-backups/' ) + ); + } + + echo '

' . $warning . '

'; + + $overwrite = $this->is_downgrading ? 'downgrade-theme' : 'update-theme'; + + $install_actions['overwrite_theme'] = sprintf( + '%s', + wp_nonce_url( add_query_arg( 'overwrite', $overwrite, $this->url ), 'theme-upload' ), + _x( 'Replace active with uploaded', 'theme' ) + ); + } else { + echo $blocked_message; + } + + $cancel_url = add_query_arg( 'action', 'upload-theme-cancel-overwrite', $this->url ); + + $install_actions['themes_page'] = sprintf( + '%s', + wp_nonce_url( $cancel_url, 'theme-upload-cancel-overwrite' ), + __( 'Cancel and go back' ) + ); + + /** + * Filters the list of action links available following a single theme installation failure + * when overwriting is allowed. + * + * @since 5.5.0 + * + * @param string[] $install_actions Array of theme action links. + * @param object $api Object containing WordPress.org API theme data. + * @param array $new_theme_data Array with uploaded theme data. + */ + $install_actions = apply_filters( 'install_theme_overwrite_actions', $install_actions, $this->api, $new_theme_data ); + + if ( ! empty( $install_actions ) ) { + printf( + '', + __( 'The uploaded file has expired. Please go back and upload it again.' ) + ); + echo '

' . implode( ' ', (array) $install_actions ) . '

'; + } + + return true; + } +} diff --git a/wp-admin/includes/class-theme-upgrader-skin.php b/wp-admin/includes/class-theme-upgrader-skin.php new file mode 100644 index 0000000..97d76a8 --- /dev/null +++ b/wp-admin/includes/class-theme-upgrader-skin.php @@ -0,0 +1,144 @@ + '', + 'theme' => '', + 'nonce' => '', + 'title' => __( 'Update Theme' ), + ); + $args = wp_parse_args( $args, $defaults ); + + $this->theme = $args['theme']; + + parent::__construct( $args ); + } + + /** + * Performs an action following a single theme update. + * + * @since 2.8.0 + */ + public function after() { + $this->decrement_update_count( 'theme' ); + + $update_actions = array(); + $theme_info = $this->upgrader->theme_info(); + if ( $theme_info ) { + $name = $theme_info->display( 'Name' ); + $stylesheet = $this->upgrader->result['destination_name']; + $template = $theme_info->get_template(); + + $activate_link = add_query_arg( + array( + 'action' => 'activate', + 'template' => urlencode( $template ), + 'stylesheet' => urlencode( $stylesheet ), + ), + admin_url( 'themes.php' ) + ); + $activate_link = wp_nonce_url( $activate_link, 'switch-theme_' . $stylesheet ); + + $customize_url = add_query_arg( + array( + 'theme' => urlencode( $stylesheet ), + 'return' => urlencode( admin_url( 'themes.php' ) ), + ), + admin_url( 'customize.php' ) + ); + + if ( get_stylesheet() === $stylesheet ) { + if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) { + $update_actions['preview'] = sprintf( + '' . + '%s', + esc_url( $customize_url ), + __( 'Customize' ), + /* translators: Hidden accessibility text. %s: Theme name. */ + sprintf( __( 'Customize “%s”' ), $name ) + ); + } + } elseif ( current_user_can( 'switch_themes' ) ) { + if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) { + $update_actions['preview'] = sprintf( + '' . + '%s', + esc_url( $customize_url ), + __( 'Live Preview' ), + /* translators: Hidden accessibility text. %s: Theme name. */ + sprintf( __( 'Live Preview “%s”' ), $name ) + ); + } + + $update_actions['activate'] = sprintf( + '' . + '%s', + esc_url( $activate_link ), + __( 'Activate' ), + /* translators: Hidden accessibility text. %s: Theme name. */ + sprintf( _x( 'Activate “%s”', 'theme' ), $name ) + ); + } + + if ( ! $this->result || is_wp_error( $this->result ) || is_network_admin() ) { + unset( $update_actions['preview'], $update_actions['activate'] ); + } + } + + $update_actions['themes_page'] = sprintf( + '%s', + self_admin_url( 'themes.php' ), + __( 'Go to Themes page' ) + ); + + /** + * Filters the list of action links available following a single theme update. + * + * @since 2.8.0 + * + * @param string[] $update_actions Array of theme action links. + * @param string $theme Theme directory name. + */ + $update_actions = apply_filters( 'update_theme_complete_actions', $update_actions, $this->theme ); + + if ( ! empty( $update_actions ) ) { + $this->feedback( implode( ' | ', (array) $update_actions ) ); + } + } +} diff --git a/wp-admin/includes/class-theme-upgrader.php b/wp-admin/includes/class-theme-upgrader.php new file mode 100644 index 0000000..12bd477 --- /dev/null +++ b/wp-admin/includes/class-theme-upgrader.php @@ -0,0 +1,771 @@ +strings['up_to_date'] = __( 'The theme is at the latest version.' ); + $this->strings['no_package'] = __( 'Update package not available.' ); + /* translators: %s: Package URL. */ + $this->strings['downloading_package'] = sprintf( __( 'Downloading update from %s…' ), '%s' ); + $this->strings['unpack_package'] = __( 'Unpacking the update…' ); + $this->strings['remove_old'] = __( 'Removing the old version of the theme…' ); + $this->strings['remove_old_failed'] = __( 'Could not remove the old theme.' ); + $this->strings['process_failed'] = __( 'Theme update failed.' ); + $this->strings['process_success'] = __( 'Theme updated successfully.' ); + } + + /** + * Initializes the installation strings. + * + * @since 2.8.0 + */ + public function install_strings() { + $this->strings['no_package'] = __( 'Installation package not available.' ); + /* translators: %s: Package URL. */ + $this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s…' ), '%s' ); + $this->strings['unpack_package'] = __( 'Unpacking the package…' ); + $this->strings['installing_package'] = __( 'Installing the theme…' ); + $this->strings['remove_old'] = __( 'Removing the old version of the theme…' ); + $this->strings['remove_old_failed'] = __( 'Could not remove the old theme.' ); + $this->strings['no_files'] = __( 'The theme contains no files.' ); + $this->strings['process_failed'] = __( 'Theme installation failed.' ); + $this->strings['process_success'] = __( 'Theme installed successfully.' ); + /* translators: 1: Theme name, 2: Theme version. */ + $this->strings['process_success_specific'] = __( 'Successfully installed the theme %1$s %2$s.' ); + $this->strings['parent_theme_search'] = __( 'This theme requires a parent theme. Checking if it is installed…' ); + /* translators: 1: Theme name, 2: Theme version. */ + $this->strings['parent_theme_prepare_install'] = __( 'Preparing to install %1$s %2$s…' ); + /* translators: 1: Theme name, 2: Theme version. */ + $this->strings['parent_theme_currently_installed'] = __( 'The parent theme, %1$s %2$s, is currently installed.' ); + /* translators: 1: Theme name, 2: Theme version. */ + $this->strings['parent_theme_install_success'] = __( 'Successfully installed the parent theme, %1$s %2$s.' ); + /* translators: %s: Theme name. */ + $this->strings['parent_theme_not_found'] = sprintf( __( 'The parent theme could not be found. You will need to install the parent theme, %s, before you can use this child theme.' ), '%s' ); + /* translators: %s: Theme error. */ + $this->strings['current_theme_has_errors'] = __( 'The active theme has the following error: "%s".' ); + + if ( ! empty( $this->skin->overwrite ) ) { + if ( 'update-theme' === $this->skin->overwrite ) { + $this->strings['installing_package'] = __( 'Updating the theme…' ); + $this->strings['process_failed'] = __( 'Theme update failed.' ); + $this->strings['process_success'] = __( 'Theme updated successfully.' ); + } + + if ( 'downgrade-theme' === $this->skin->overwrite ) { + $this->strings['installing_package'] = __( 'Downgrading the theme…' ); + $this->strings['process_failed'] = __( 'Theme downgrade failed.' ); + $this->strings['process_success'] = __( 'Theme downgraded successfully.' ); + } + } + } + + /** + * Checks if a child theme is being installed and its parent also needs to be installed. + * + * Hooked to the {@see 'upgrader_post_install'} filter by Theme_Upgrader::install(). + * + * @since 3.4.0 + * + * @param bool $install_result + * @param array $hook_extra + * @param array $child_result + * @return bool + */ + public function check_parent_theme_filter( $install_result, $hook_extra, $child_result ) { + // Check to see if we need to install a parent theme. + $theme_info = $this->theme_info(); + + if ( ! $theme_info->parent() ) { + return $install_result; + } + + $this->skin->feedback( 'parent_theme_search' ); + + if ( ! $theme_info->parent()->errors() ) { + $this->skin->feedback( 'parent_theme_currently_installed', $theme_info->parent()->display( 'Name' ), $theme_info->parent()->display( 'Version' ) ); + // We already have the theme, fall through. + return $install_result; + } + + // We don't have the parent theme, let's install it. + $api = themes_api( + 'theme_information', + array( + 'slug' => $theme_info->get( 'Template' ), + 'fields' => array( + 'sections' => false, + 'tags' => false, + ), + ) + ); // Save on a bit of bandwidth. + + if ( ! $api || is_wp_error( $api ) ) { + $this->skin->feedback( 'parent_theme_not_found', $theme_info->get( 'Template' ) ); + // Don't show activate or preview actions after installation. + add_filter( 'install_theme_complete_actions', array( $this, 'hide_activate_preview_actions' ) ); + return $install_result; + } + + // Backup required data we're going to override: + $child_api = $this->skin->api; + $child_success_message = $this->strings['process_success']; + + // Override them. + $this->skin->api = $api; + + $this->strings['process_success_specific'] = $this->strings['parent_theme_install_success']; + + $this->skin->feedback( 'parent_theme_prepare_install', $api->name, $api->version ); + + add_filter( 'install_theme_complete_actions', '__return_false', 999 ); // Don't show any actions after installing the theme. + + // Install the parent theme. + $parent_result = $this->run( + array( + 'package' => $api->download_link, + 'destination' => get_theme_root(), + 'clear_destination' => false, // Do not overwrite files. + 'clear_working' => true, + ) + ); + + if ( is_wp_error( $parent_result ) ) { + add_filter( 'install_theme_complete_actions', array( $this, 'hide_activate_preview_actions' ) ); + } + + // Start cleaning up after the parent's installation. + remove_filter( 'install_theme_complete_actions', '__return_false', 999 ); + + // Reset child's result and data. + $this->result = $child_result; + $this->skin->api = $child_api; + $this->strings['process_success'] = $child_success_message; + + return $install_result; + } + + /** + * Don't display the activate and preview actions to the user. + * + * Hooked to the {@see 'install_theme_complete_actions'} filter by + * Theme_Upgrader::check_parent_theme_filter() when installing + * a child theme and installing the parent theme fails. + * + * @since 3.4.0 + * + * @param array $actions Preview actions. + * @return array + */ + public function hide_activate_preview_actions( $actions ) { + unset( $actions['activate'], $actions['preview'] ); + return $actions; + } + + /** + * Install a theme package. + * + * @since 2.8.0 + * @since 3.7.0 The `$args` parameter was added, making clearing the update cache optional. + * + * @param string $package The full local path or URI of the package. + * @param array $args { + * Optional. Other arguments for installing a theme package. Default empty array. + * + * @type bool $clear_update_cache Whether to clear the updates cache if successful. + * Default true. + * } + * + * @return bool|WP_Error True if the installation was successful, false or a WP_Error object otherwise. + */ + public function install( $package, $args = array() ) { + $defaults = array( + 'clear_update_cache' => true, + 'overwrite_package' => false, // Do not overwrite files. + ); + $parsed_args = wp_parse_args( $args, $defaults ); + + $this->init(); + $this->install_strings(); + + add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); + add_filter( 'upgrader_post_install', array( $this, 'check_parent_theme_filter' ), 10, 3 ); + + if ( $parsed_args['clear_update_cache'] ) { + // Clear cache so wp_update_themes() knows about the new theme. + add_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9, 0 ); + } + + $this->run( + array( + 'package' => $package, + 'destination' => get_theme_root(), + 'clear_destination' => $parsed_args['overwrite_package'], + 'clear_working' => true, + 'hook_extra' => array( + 'type' => 'theme', + 'action' => 'install', + ), + ) + ); + + remove_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9 ); + remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); + remove_filter( 'upgrader_post_install', array( $this, 'check_parent_theme_filter' ) ); + + if ( ! $this->result || is_wp_error( $this->result ) ) { + return $this->result; + } + + // Refresh the Theme Update information. + wp_clean_themes_cache( $parsed_args['clear_update_cache'] ); + + if ( $parsed_args['overwrite_package'] ) { + /** This action is documented in wp-admin/includes/class-plugin-upgrader.php */ + do_action( 'upgrader_overwrote_package', $package, $this->new_theme_data, 'theme' ); + } + + return true; + } + + /** + * Upgrades a theme. + * + * @since 2.8.0 + * @since 3.7.0 The `$args` parameter was added, making clearing the update cache optional. + * + * @param string $theme The theme slug. + * @param array $args { + * Optional. Other arguments for upgrading a theme. Default empty array. + * + * @type bool $clear_update_cache Whether to clear the update cache if successful. + * Default true. + * } + * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise. + */ + public function upgrade( $theme, $args = array() ) { + $defaults = array( + 'clear_update_cache' => true, + ); + $parsed_args = wp_parse_args( $args, $defaults ); + + $this->init(); + $this->upgrade_strings(); + + // Is an update available? + $current = get_site_transient( 'update_themes' ); + if ( ! isset( $current->response[ $theme ] ) ) { + $this->skin->before(); + $this->skin->set_result( false ); + $this->skin->error( 'up_to_date' ); + $this->skin->after(); + return false; + } + + $r = $current->response[ $theme ]; + + add_filter( 'upgrader_pre_install', array( $this, 'current_before' ), 10, 2 ); + add_filter( 'upgrader_post_install', array( $this, 'current_after' ), 10, 2 ); + add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ), 10, 4 ); + if ( $parsed_args['clear_update_cache'] ) { + // Clear cache so wp_update_themes() knows about the new theme. + add_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9, 0 ); + } + + $this->run( + array( + 'package' => $r['package'], + 'destination' => get_theme_root( $theme ), + 'clear_destination' => true, + 'clear_working' => true, + 'hook_extra' => array( + 'theme' => $theme, + 'type' => 'theme', + 'action' => 'update', + 'temp_backup' => array( + 'slug' => $theme, + 'src' => get_theme_root( $theme ), + 'dir' => 'themes', + ), + ), + ) + ); + + remove_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9 ); + remove_filter( 'upgrader_pre_install', array( $this, 'current_before' ) ); + remove_filter( 'upgrader_post_install', array( $this, 'current_after' ) ); + remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ) ); + + if ( ! $this->result || is_wp_error( $this->result ) ) { + return $this->result; + } + + wp_clean_themes_cache( $parsed_args['clear_update_cache'] ); + + /* + * Ensure any future auto-update failures trigger a failure email by removing + * the last failure notification from the list when themes update successfully. + */ + $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); + + if ( isset( $past_failure_emails[ $theme ] ) ) { + unset( $past_failure_emails[ $theme ] ); + update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); + } + + return true; + } + + /** + * Upgrades several themes at once. + * + * @since 3.0.0 + * @since 3.7.0 The `$args` parameter was added, making clearing the update cache optional. + * + * @param string[] $themes Array of the theme slugs. + * @param array $args { + * Optional. Other arguments for upgrading several themes at once. Default empty array. + * + * @type bool $clear_update_cache Whether to clear the update cache if successful. + * Default true. + * } + * @return array[]|false An array of results, or false if unable to connect to the filesystem. + */ + public function bulk_upgrade( $themes, $args = array() ) { + $defaults = array( + 'clear_update_cache' => true, + ); + $parsed_args = wp_parse_args( $args, $defaults ); + + $this->init(); + $this->bulk = true; + $this->upgrade_strings(); + + $current = get_site_transient( 'update_themes' ); + + add_filter( 'upgrader_pre_install', array( $this, 'current_before' ), 10, 2 ); + add_filter( 'upgrader_post_install', array( $this, 'current_after' ), 10, 2 ); + add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ), 10, 4 ); + + $this->skin->header(); + + // Connect to the filesystem first. + $res = $this->fs_connect( array( WP_CONTENT_DIR ) ); + if ( ! $res ) { + $this->skin->footer(); + return false; + } + + $this->skin->bulk_header(); + + /* + * Only start maintenance mode if: + * - running Multisite and there are one or more themes specified, OR + * - a theme with an update available is currently in use. + * @todo For multisite, maintenance mode should only kick in for individual sites if at all possible. + */ + $maintenance = ( is_multisite() && ! empty( $themes ) ); + foreach ( $themes as $theme ) { + $maintenance = $maintenance || get_stylesheet() === $theme || get_template() === $theme; + } + if ( $maintenance ) { + $this->maintenance_mode( true ); + } + + $results = array(); + + $this->update_count = count( $themes ); + $this->update_current = 0; + foreach ( $themes as $theme ) { + ++$this->update_current; + + $this->skin->theme_info = $this->theme_info( $theme ); + + if ( ! isset( $current->response[ $theme ] ) ) { + $this->skin->set_result( true ); + $this->skin->before(); + $this->skin->feedback( 'up_to_date' ); + $this->skin->after(); + $results[ $theme ] = true; + continue; + } + + // Get the URL to the zip file. + $r = $current->response[ $theme ]; + + $result = $this->run( + array( + 'package' => $r['package'], + 'destination' => get_theme_root( $theme ), + 'clear_destination' => true, + 'clear_working' => true, + 'is_multi' => true, + 'hook_extra' => array( + 'theme' => $theme, + 'temp_backup' => array( + 'slug' => $theme, + 'src' => get_theme_root( $theme ), + 'dir' => 'themes', + ), + ), + ) + ); + + $results[ $theme ] = $result; + + // Prevent credentials auth screen from displaying multiple times. + if ( false === $result ) { + break; + } + } // End foreach $themes. + + $this->maintenance_mode( false ); + + // Refresh the Theme Update information. + wp_clean_themes_cache( $parsed_args['clear_update_cache'] ); + + /** This action is documented in wp-admin/includes/class-wp-upgrader.php */ + do_action( + 'upgrader_process_complete', + $this, + array( + 'action' => 'update', + 'type' => 'theme', + 'bulk' => true, + 'themes' => $themes, + ) + ); + + $this->skin->bulk_footer(); + + $this->skin->footer(); + + // Cleanup our hooks, in case something else does an upgrade on this connection. + remove_filter( 'upgrader_pre_install', array( $this, 'current_before' ) ); + remove_filter( 'upgrader_post_install', array( $this, 'current_after' ) ); + remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ) ); + + /* + * Ensure any future auto-update failures trigger a failure email by removing + * the last failure notification from the list when themes update successfully. + */ + $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); + + foreach ( $results as $theme => $result ) { + // Maintain last failure notification when themes failed to update manually. + if ( ! $result || is_wp_error( $result ) || ! isset( $past_failure_emails[ $theme ] ) ) { + continue; + } + + unset( $past_failure_emails[ $theme ] ); + } + + update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); + + return $results; + } + + /** + * Checks that the package source contains a valid theme. + * + * Hooked to the {@see 'upgrader_source_selection'} filter by Theme_Upgrader::install(). + * + * @since 3.3.0 + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * @global string $wp_version The WordPress version string. + * + * @param string $source The path to the downloaded package source. + * @return string|WP_Error The source as passed, or a WP_Error object on failure. + */ + public function check_package( $source ) { + global $wp_filesystem, $wp_version; + + $this->new_theme_data = array(); + + if ( is_wp_error( $source ) ) { + return $source; + } + + // Check that the folder contains a valid theme. + $working_directory = str_replace( $wp_filesystem->wp_content_dir(), trailingslashit( WP_CONTENT_DIR ), $source ); + if ( ! is_dir( $working_directory ) ) { // Sanity check, if the above fails, let's not prevent installation. + return $source; + } + + // A proper archive should have a style.css file in the single subdirectory. + if ( ! file_exists( $working_directory . 'style.css' ) ) { + return new WP_Error( + 'incompatible_archive_theme_no_style', + $this->strings['incompatible_archive'], + sprintf( + /* translators: %s: style.css */ + __( 'The theme is missing the %s stylesheet.' ), + 'style.css' + ) + ); + } + + // All these headers are needed on Theme_Installer_Skin::do_overwrite(). + $info = get_file_data( + $working_directory . 'style.css', + array( + 'Name' => 'Theme Name', + 'Version' => 'Version', + 'Author' => 'Author', + 'Template' => 'Template', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + ) + ); + + if ( empty( $info['Name'] ) ) { + return new WP_Error( + 'incompatible_archive_theme_no_name', + $this->strings['incompatible_archive'], + sprintf( + /* translators: %s: style.css */ + __( 'The %s stylesheet does not contain a valid theme header.' ), + 'style.css' + ) + ); + } + + /* + * Parent themes must contain an index file: + * - classic themes require /index.php + * - block themes require /templates/index.html or block-templates/index.html (deprecated 5.9.0). + */ + if ( + empty( $info['Template'] ) && + ! file_exists( $working_directory . 'index.php' ) && + ! file_exists( $working_directory . 'templates/index.html' ) && + ! file_exists( $working_directory . 'block-templates/index.html' ) + ) { + return new WP_Error( + 'incompatible_archive_theme_no_index', + $this->strings['incompatible_archive'], + sprintf( + /* translators: 1: templates/index.html, 2: index.php, 3: Documentation URL, 4: Template, 5: style.css */ + __( 'Template is missing. Standalone themes need to have a %1$s or %2$s template file. Child themes need to have a %4$s header in the %5$s stylesheet.' ), + 'templates/index.html', + 'index.php', + __( 'https://developer.wordpress.org/themes/advanced-topics/child-themes/' ), + 'Template', + 'style.css' + ) + ); + } + + $requires_php = isset( $info['RequiresPHP'] ) ? $info['RequiresPHP'] : null; + $requires_wp = isset( $info['RequiresWP'] ) ? $info['RequiresWP'] : null; + + if ( ! is_php_version_compatible( $requires_php ) ) { + $error = sprintf( + /* translators: 1: Current PHP version, 2: Version required by the uploaded theme. */ + __( 'The PHP version on your server is %1$s, however the uploaded theme requires %2$s.' ), + PHP_VERSION, + $requires_php + ); + + return new WP_Error( 'incompatible_php_required_version', $this->strings['incompatible_archive'], $error ); + } + if ( ! is_wp_version_compatible( $requires_wp ) ) { + $error = sprintf( + /* translators: 1: Current WordPress version, 2: Version required by the uploaded theme. */ + __( 'Your WordPress version is %1$s, however the uploaded theme requires %2$s.' ), + $wp_version, + $requires_wp + ); + + return new WP_Error( 'incompatible_wp_required_version', $this->strings['incompatible_archive'], $error ); + } + + $this->new_theme_data = $info; + + return $source; + } + + /** + * Turns on maintenance mode before attempting to upgrade the active theme. + * + * Hooked to the {@see 'upgrader_pre_install'} filter by Theme_Upgrader::upgrade() and + * Theme_Upgrader::bulk_upgrade(). + * + * @since 2.8.0 + * + * @param bool|WP_Error $response The installation response before the installation has started. + * @param array $theme Theme arguments. + * @return bool|WP_Error The original `$response` parameter or WP_Error. + */ + public function current_before( $response, $theme ) { + if ( is_wp_error( $response ) ) { + return $response; + } + + $theme = isset( $theme['theme'] ) ? $theme['theme'] : ''; + + // Only run if active theme. + if ( get_stylesheet() !== $theme ) { + return $response; + } + + // Change to maintenance mode. Bulk edit handles this separately. + if ( ! $this->bulk ) { + $this->maintenance_mode( true ); + } + + return $response; + } + + /** + * Turns off maintenance mode after upgrading the active theme. + * + * Hooked to the {@see 'upgrader_post_install'} filter by Theme_Upgrader::upgrade() + * and Theme_Upgrader::bulk_upgrade(). + * + * @since 2.8.0 + * + * @param bool|WP_Error $response The installation response after the installation has finished. + * @param array $theme Theme arguments. + * @return bool|WP_Error The original `$response` parameter or WP_Error. + */ + public function current_after( $response, $theme ) { + if ( is_wp_error( $response ) ) { + return $response; + } + + $theme = isset( $theme['theme'] ) ? $theme['theme'] : ''; + + // Only run if active theme. + if ( get_stylesheet() !== $theme ) { + return $response; + } + + // Ensure stylesheet name hasn't changed after the upgrade: + if ( get_stylesheet() === $theme && $theme !== $this->result['destination_name'] ) { + wp_clean_themes_cache(); + $stylesheet = $this->result['destination_name']; + switch_theme( $stylesheet ); + } + + // Time to remove maintenance mode. Bulk edit handles this separately. + if ( ! $this->bulk ) { + $this->maintenance_mode( false ); + } + return $response; + } + + /** + * Deletes the old theme during an upgrade. + * + * Hooked to the {@see 'upgrader_clear_destination'} filter by Theme_Upgrader::upgrade() + * and Theme_Upgrader::bulk_upgrade(). + * + * @since 2.8.0 + * + * @global WP_Filesystem_Base $wp_filesystem Subclass + * + * @param bool $removed + * @param string $local_destination + * @param string $remote_destination + * @param array $theme + * @return bool + */ + public function delete_old_theme( $removed, $local_destination, $remote_destination, $theme ) { + global $wp_filesystem; + + if ( is_wp_error( $removed ) ) { + return $removed; // Pass errors through. + } + + if ( ! isset( $theme['theme'] ) ) { + return $removed; + } + + $theme = $theme['theme']; + $themes_dir = trailingslashit( $wp_filesystem->wp_themes_dir( $theme ) ); + if ( $wp_filesystem->exists( $themes_dir . $theme ) ) { + if ( ! $wp_filesystem->delete( $themes_dir . $theme, true ) ) { + return false; + } + } + + return true; + } + + /** + * Gets the WP_Theme object for a theme. + * + * @since 2.8.0 + * @since 3.0.0 The `$theme` argument was added. + * + * @param string $theme The directory name of the theme. This is optional, and if not supplied, + * the directory name from the last result will be used. + * @return WP_Theme|false The theme's info object, or false `$theme` is not supplied + * and the last result isn't set. + */ + public function theme_info( $theme = null ) { + if ( empty( $theme ) ) { + if ( ! empty( $this->result['destination_name'] ) ) { + $theme = $this->result['destination_name']; + } else { + return false; + } + } + + $theme = wp_get_theme( $theme ); + $theme->cache_delete(); + + return $theme; + } +} diff --git a/wp-admin/includes/class-walker-category-checklist.php b/wp-admin/includes/class-walker-category-checklist.php new file mode 100644 index 0000000..1deb3f9 --- /dev/null +++ b/wp-admin/includes/class-walker-category-checklist.php @@ -0,0 +1,138 @@ + 'parent', + 'id' => 'term_id', + ); // TODO: Decouple this. + + /** + * Starts the list before the elements are added. + * + * @see Walker:start_lvl() + * + * @since 2.5.1 + * + * @param string $output Used to append additional content (passed by reference). + * @param int $depth Depth of category. Used for tab indentation. + * @param array $args An array of arguments. See {@see wp_terms_checklist()}. + */ + public function start_lvl( &$output, $depth = 0, $args = array() ) { + $indent = str_repeat( "\t", $depth ); + $output .= "$indent
    \n"; + } + + /** + * Ends the list of after the elements are added. + * + * @see Walker::end_lvl() + * + * @since 2.5.1 + * + * @param string $output Used to append additional content (passed by reference). + * @param int $depth Depth of category. Used for tab indentation. + * @param array $args An array of arguments. See {@see wp_terms_checklist()}. + */ + public function end_lvl( &$output, $depth = 0, $args = array() ) { + $indent = str_repeat( "\t", $depth ); + $output .= "$indent
\n"; + } + + /** + * Start the element output. + * + * @see Walker::start_el() + * + * @since 2.5.1 + * @since 5.9.0 Renamed `$category` to `$data_object` and `$id` to `$current_object_id` + * to match parent class for PHP 8 named parameter support. + * + * @param string $output Used to append additional content (passed by reference). + * @param WP_Term $data_object The current term object. + * @param int $depth Depth of the term in reference to parents. Default 0. + * @param array $args An array of arguments. See {@see wp_terms_checklist()}. + * @param int $current_object_id Optional. ID of the current term. Default 0. + */ + public function start_el( &$output, $data_object, $depth = 0, $args = array(), $current_object_id = 0 ) { + // Restores the more descriptive, specific name for use within this method. + $category = $data_object; + + if ( empty( $args['taxonomy'] ) ) { + $taxonomy = 'category'; + } else { + $taxonomy = $args['taxonomy']; + } + + if ( 'category' === $taxonomy ) { + $name = 'post_category'; + } else { + $name = 'tax_input[' . $taxonomy . ']'; + } + + $args['popular_cats'] = ! empty( $args['popular_cats'] ) ? array_map( 'intval', $args['popular_cats'] ) : array(); + + $class = in_array( $category->term_id, $args['popular_cats'], true ) ? ' class="popular-category"' : ''; + + $args['selected_cats'] = ! empty( $args['selected_cats'] ) ? array_map( 'intval', $args['selected_cats'] ) : array(); + + if ( ! empty( $args['list_only'] ) ) { + $aria_checked = 'false'; + $inner_class = 'category'; + + if ( in_array( $category->term_id, $args['selected_cats'], true ) ) { + $inner_class .= ' selected'; + $aria_checked = 'true'; + } + + $output .= "\n" . '' . + ''; + } else { + $is_selected = in_array( $category->term_id, $args['selected_cats'], true ); + $is_disabled = ! empty( $args['disabled'] ); + + $output .= "\n
  • " . + ''; + } + } + + /** + * Ends the element output, if needed. + * + * @see Walker::end_el() + * + * @since 2.5.1 + * @since 5.9.0 Renamed `$category` to `$data_object` to match parent class for PHP 8 named parameter support. + * + * @param string $output Used to append additional content (passed by reference). + * @param WP_Term $data_object The current term object. + * @param int $depth Depth of the term in reference to parents. Default 0. + * @param array $args An array of arguments. See {@see wp_terms_checklist()}. + */ + public function end_el( &$output, $data_object, $depth = 0, $args = array() ) { + $output .= "
  • \n"; + } +} diff --git a/wp-admin/includes/class-walker-nav-menu-checklist.php b/wp-admin/includes/class-walker-nav-menu-checklist.php new file mode 100644 index 0000000..6fc5c41 --- /dev/null +++ b/wp-admin/includes/class-walker-nav-menu-checklist.php @@ -0,0 +1,126 @@ +db_fields = $fields; + } + } + + /** + * Starts the list before the elements are added. + * + * @see Walker_Nav_Menu::start_lvl() + * + * @since 3.0.0 + * + * @param string $output Used to append additional content (passed by reference). + * @param int $depth Depth of page. Used for padding. + * @param stdClass $args Not used. + */ + public function start_lvl( &$output, $depth = 0, $args = null ) { + $indent = str_repeat( "\t", $depth ); + $output .= "\n$indent
      \n"; + } + + /** + * Ends the list of after the elements are added. + * + * @see Walker_Nav_Menu::end_lvl() + * + * @since 3.0.0 + * + * @param string $output Used to append additional content (passed by reference). + * @param int $depth Depth of page. Used for padding. + * @param stdClass $args Not used. + */ + public function end_lvl( &$output, $depth = 0, $args = null ) { + $indent = str_repeat( "\t", $depth ); + $output .= "\n$indent
    "; + } + + /** + * Start the element output. + * + * @see Walker_Nav_Menu::start_el() + * + * @since 3.0.0 + * @since 5.9.0 Renamed `$item` to `$data_object` and `$id` to `$current_object_id` + * to match parent class for PHP 8 named parameter support. + * + * @global int $_nav_menu_placeholder + * @global int|string $nav_menu_selected_id + * + * @param string $output Used to append additional content (passed by reference). + * @param WP_Post $data_object Menu item data object. + * @param int $depth Depth of menu item. Used for padding. + * @param stdClass $args Not used. + * @param int $current_object_id Optional. ID of the current menu item. Default 0. + */ + public function start_el( &$output, $data_object, $depth = 0, $args = null, $current_object_id = 0 ) { + global $_nav_menu_placeholder, $nav_menu_selected_id; + + // Restores the more descriptive, specific name for use within this method. + $menu_item = $data_object; + + $_nav_menu_placeholder = ( 0 > $_nav_menu_placeholder ) ? (int) $_nav_menu_placeholder - 1 : -1; + $possible_object_id = isset( $menu_item->post_type ) && 'nav_menu_item' === $menu_item->post_type ? $menu_item->object_id : $_nav_menu_placeholder; + $possible_db_id = ( ! empty( $menu_item->ID ) ) && ( 0 < $possible_object_id ) ? (int) $menu_item->ID : 0; + + $indent = ( $depth ) ? str_repeat( "\t", $depth ) : ''; + + $output .= $indent . '
  • '; + $output .= ''; + + // Menu item hidden fields. + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + } +} diff --git a/wp-admin/includes/class-walker-nav-menu-edit.php b/wp-admin/includes/class-walker-nav-menu-edit.php new file mode 100644 index 0000000..7cc7052 --- /dev/null +++ b/wp-admin/includes/class-walker-nav-menu-edit.php @@ -0,0 +1,323 @@ + $_wp_nav_menu_max_depth ? $depth : $_wp_nav_menu_max_depth; + + ob_start(); + $item_id = esc_attr( $menu_item->ID ); + $removed_args = array( + 'action', + 'customlink-tab', + 'edit-menu-item', + 'menu-item', + 'page-tab', + '_wpnonce', + ); + + $original_title = false; + + if ( 'taxonomy' === $menu_item->type ) { + $original_object = get_term( (int) $menu_item->object_id, $menu_item->object ); + if ( $original_object && ! is_wp_error( $original_object ) ) { + $original_title = $original_object->name; + } + } elseif ( 'post_type' === $menu_item->type ) { + $original_object = get_post( $menu_item->object_id ); + if ( $original_object ) { + $original_title = get_the_title( $original_object->ID ); + } + } elseif ( 'post_type_archive' === $menu_item->type ) { + $original_object = get_post_type_object( $menu_item->object ); + if ( $original_object ) { + $original_title = $original_object->labels->archives; + } + } + + $classes = array( + 'menu-item menu-item-depth-' . $depth, + 'menu-item-' . esc_attr( $menu_item->object ), + 'menu-item-edit-' . ( ( isset( $_GET['edit-menu-item'] ) && $item_id === $_GET['edit-menu-item'] ) ? 'active' : 'inactive' ), + ); + + $title = $menu_item->title; + + if ( ! empty( $menu_item->_invalid ) ) { + $classes[] = 'menu-item-invalid'; + /* translators: %s: Title of an invalid menu item. */ + $title = sprintf( __( '%s (Invalid)' ), $menu_item->title ); + } elseif ( isset( $menu_item->post_status ) && 'draft' === $menu_item->post_status ) { + $classes[] = 'pending'; + /* translators: %s: Title of a menu item in draft status. */ + $title = sprintf( __( '%s (Pending)' ), $menu_item->title ); + } + + $title = ( ! isset( $menu_item->label ) || '' === $menu_item->label ) ? $title : $menu_item->label; + + $submenu_text = ''; + if ( 0 === $depth ) { + $submenu_text = 'style="display: none;"'; + } + + ?> +
  • '; + + else : // End $is_nginx. Construct an .htaccess file instead: + + $ms_files_rewriting = ''; + if ( is_multisite() && get_site_option( 'ms_files_rewriting' ) ) { + $ms_files_rewriting = "\n# uploaded files\nRewriteRule ^"; + $ms_files_rewriting .= $subdir_match . "files/(.+) {$rewrite_base}" . WPINC . "/ms-files.php?file={$subdir_replacement_12} [L]" . "\n"; + } + + $htaccess_file = <<

    '; + printf( + /* translators: 1: File name (.htaccess or web.config), 2: File path. */ + __( 'Add the following to your %1$s file in %2$s, replacing other WordPress rules:' ), + '.htaccess', + '' . $home_path . '' + ); + echo '

    '; + if ( ! $subdomain_install && WP_CONTENT_DIR !== ABSPATH . 'wp-content' ) { + echo '

    ' . __( 'Warning:' ) . ' ' . __( 'Subdirectory networks may not be fully compatible with custom wp-content directories.' ) . '

    '; + } + ?> +

    + + + + + +

    + + + + + + + '; + echo '

    ' . __( 'The character encoding of your site (UTF-8 is recommended)' ) . '

    '; +} diff --git a/wp-admin/includes/plugin-install.php b/wp-admin/includes/plugin-install.php new file mode 100644 index 0000000..7662076 --- /dev/null +++ b/wp-admin/includes/plugin-install.php @@ -0,0 +1,926 @@ +per_page ) ) { + $args->per_page = 24; + } + } + + if ( ! isset( $args->locale ) ) { + $args->locale = get_user_locale(); + } + + if ( ! isset( $args->wp_version ) ) { + $args->wp_version = substr( $wp_version, 0, 3 ); // x.y + } + + /** + * Filters the WordPress.org Plugin Installation API arguments. + * + * Important: An object MUST be returned to this filter. + * + * @since 2.7.0 + * + * @param object $args Plugin API arguments. + * @param string $action The type of information being requested from the Plugin Installation API. + */ + $args = apply_filters( 'plugins_api_args', $args, $action ); + + /** + * Filters the response for the current WordPress.org Plugin Installation API request. + * + * Returning a non-false value will effectively short-circuit the WordPress.org API request. + * + * If `$action` is 'query_plugins' or 'plugin_information', an object MUST be passed. + * If `$action` is 'hot_tags' or 'hot_categories', an array should be passed. + * + * @since 2.7.0 + * + * @param false|object|array $result The result object or array. Default false. + * @param string $action The type of information being requested from the Plugin Installation API. + * @param object $args Plugin API arguments. + */ + $res = apply_filters( 'plugins_api', false, $action, $args ); + + if ( false === $res ) { + + $url = 'http://api.wordpress.org/plugins/info/1.2/'; + $url = add_query_arg( + array( + 'action' => $action, + 'request' => $args, + ), + $url + ); + + $http_url = $url; + $ssl = wp_http_supports( array( 'ssl' ) ); + if ( $ssl ) { + $url = set_url_scheme( $url, 'https' ); + } + + $http_args = array( + 'timeout' => 15, + 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ), + ); + $request = wp_remote_get( $url, $http_args ); + + if ( $ssl && is_wp_error( $request ) ) { + if ( ! wp_is_json_request() ) { + trigger_error( + sprintf( + /* translators: %s: Support forums URL. */ + __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.' ), + __( 'https://wordpress.org/support/forums/' ) + ) . ' ' . __( '(WordPress could not establish a secure connection to WordPress.org. Please contact your server administrator.)' ), + headers_sent() || WP_DEBUG ? E_USER_WARNING : E_USER_NOTICE + ); + } + + $request = wp_remote_get( $http_url, $http_args ); + } + + if ( is_wp_error( $request ) ) { + $res = new WP_Error( + 'plugins_api_failed', + sprintf( + /* translators: %s: Support forums URL. */ + __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.' ), + __( 'https://wordpress.org/support/forums/' ) + ), + $request->get_error_message() + ); + } else { + $res = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( is_array( $res ) ) { + // Object casting is required in order to match the info/1.0 format. + $res = (object) $res; + } elseif ( null === $res ) { + $res = new WP_Error( + 'plugins_api_failed', + sprintf( + /* translators: %s: Support forums URL. */ + __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.' ), + __( 'https://wordpress.org/support/forums/' ) + ), + wp_remote_retrieve_body( $request ) + ); + } + + if ( isset( $res->error ) ) { + $res = new WP_Error( 'plugins_api_failed', $res->error ); + } + } + } elseif ( ! is_wp_error( $res ) ) { + $res->external = true; + } + + /** + * Filters the Plugin Installation API response results. + * + * @since 2.7.0 + * + * @param object|WP_Error $res Response object or WP_Error. + * @param string $action The type of information being requested from the Plugin Installation API. + * @param object $args Plugin API arguments. + */ + return apply_filters( 'plugins_api_result', $res, $action, $args ); +} + +/** + * Retrieves popular WordPress plugin tags. + * + * @since 2.7.0 + * + * @param array $args + * @return array|WP_Error + */ +function install_popular_tags( $args = array() ) { + $key = md5( serialize( $args ) ); + $tags = get_site_transient( 'poptags_' . $key ); + if ( false !== $tags ) { + return $tags; + } + + $tags = plugins_api( 'hot_tags', $args ); + + if ( is_wp_error( $tags ) ) { + return $tags; + } + + set_site_transient( 'poptags_' . $key, $tags, 3 * HOUR_IN_SECONDS ); + + return $tags; +} + +/** + * Displays the Featured tab of Add Plugins screen. + * + * @since 2.7.0 + */ +function install_dashboard() { + display_plugins_table(); + ?> + + '; +} + +/** + * Displays a search form for searching plugins. + * + * @since 2.7.0 + * @since 4.6.0 The `$type_selector` parameter was deprecated. + * + * @param bool $deprecated Not used. + */ +function install_search_form( $deprecated = true ) { + $type = isset( $_REQUEST['type'] ) ? wp_unslash( $_REQUEST['type'] ) : 'term'; + $term = isset( $_REQUEST['s'] ) ? urldecode( wp_unslash( $_REQUEST['s'] ) ) : ''; + ?> +
    + + + + + + 'search-submit' ) ); ?> +
    + +
    +

    +
    + + + + +
    +
    + +

    +
    + +

    + + + + +

    +
    + ' . __( 'You are using a development version of WordPress. These feature plugins are also under development. Learn more.' ) . '

    ', + 'https://make.wordpress.org/core/handbook/about/release-cycle/features-as-plugins/' + ); + break; + case 'install_plugins_featured': + printf( + /* translators: %s: https://wordpress.org/plugins/ */ + '

    ' . __( 'Plugins extend and expand the functionality of WordPress. You may install plugins in the WordPress Plugin Directory right from here, or upload a plugin in .zip format by clicking the button at the top of this page.' ) . '

    ', + __( 'https://wordpress.org/plugins/' ) + ); + break; + case 'install_plugins_recommended': + echo '

    ' . __( 'These suggestions are based on the plugins you and other users have installed.' ) . '

    '; + break; + case 'install_plugins_favorites': + if ( empty( $_GET['user'] ) && ! get_user_option( 'wporg_favorites' ) ) { + return; + } + break; + } + ?> +
    + display(); ?> +
    + response ) ) { + foreach ( (array) $update_plugins->response as $file => $plugin ) { + if ( $plugin->slug === $api->slug ) { + $status = 'update_available'; + $update_file = $file; + $version = $plugin->new_version; + if ( current_user_can( 'update_plugins' ) ) { + $url = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' . $update_file ), 'upgrade-plugin_' . $update_file ); + } + break; + } + } + } + + if ( 'install' === $status ) { + if ( is_dir( WP_PLUGIN_DIR . '/' . $api->slug ) ) { + $installed_plugin = get_plugins( '/' . $api->slug ); + if ( empty( $installed_plugin ) ) { + if ( current_user_can( 'install_plugins' ) ) { + $url = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $api->slug ), 'install-plugin_' . $api->slug ); + } + } else { + $key = array_keys( $installed_plugin ); + /* + * Use the first plugin regardless of the name. + * Could have issues for multiple plugins in one directory if they share different version numbers. + */ + $key = reset( $key ); + + $update_file = $api->slug . '/' . $key; + if ( version_compare( $api->version, $installed_plugin[ $key ]['Version'], '=' ) ) { + $status = 'latest_installed'; + } elseif ( version_compare( $api->version, $installed_plugin[ $key ]['Version'], '<' ) ) { + $status = 'newer_installed'; + $version = $installed_plugin[ $key ]['Version']; + } else { + // If the above update check failed, then that probably means that the update checker has out-of-date information, force a refresh. + if ( ! $loop ) { + delete_site_transient( 'update_plugins' ); + wp_update_plugins(); + return install_plugin_install_status( $api, true ); + } + } + } + } else { + // "install" & no directory with that slug. + if ( current_user_can( 'install_plugins' ) ) { + $url = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $api->slug ), 'install-plugin_' . $api->slug ); + } + } + } + if ( isset( $_GET['from'] ) ) { + $url .= '&from=' . urlencode( wp_unslash( $_GET['from'] ) ); + } + + $file = $update_file; + return compact( 'status', 'url', 'version', 'file' ); +} + +/** + * Displays plugin information in dialog box form. + * + * @since 2.7.0 + * + * @global string $tab + */ +function install_plugin_information() { + global $tab; + + if ( empty( $_REQUEST['plugin'] ) ) { + return; + } + + $api = plugins_api( + 'plugin_information', + array( + 'slug' => wp_unslash( $_REQUEST['plugin'] ), + ) + ); + + if ( is_wp_error( $api ) ) { + wp_die( $api ); + } + + $plugins_allowedtags = array( + 'a' => array( + 'href' => array(), + 'title' => array(), + 'target' => array(), + ), + 'abbr' => array( 'title' => array() ), + 'acronym' => array( 'title' => array() ), + 'code' => array(), + 'pre' => array(), + 'em' => array(), + 'strong' => array(), + 'div' => array( 'class' => array() ), + 'span' => array( 'class' => array() ), + 'p' => array(), + 'br' => array(), + 'ul' => array(), + 'ol' => array(), + 'li' => array(), + 'h1' => array(), + 'h2' => array(), + 'h3' => array(), + 'h4' => array(), + 'h5' => array(), + 'h6' => array(), + 'img' => array( + 'src' => array(), + 'class' => array(), + 'alt' => array(), + ), + 'blockquote' => array( 'cite' => true ), + ); + + $plugins_section_titles = array( + 'description' => _x( 'Description', 'Plugin installer section title' ), + 'installation' => _x( 'Installation', 'Plugin installer section title' ), + 'faq' => _x( 'FAQ', 'Plugin installer section title' ), + 'screenshots' => _x( 'Screenshots', 'Plugin installer section title' ), + 'changelog' => _x( 'Changelog', 'Plugin installer section title' ), + 'reviews' => _x( 'Reviews', 'Plugin installer section title' ), + 'other_notes' => _x( 'Other Notes', 'Plugin installer section title' ), + ); + + // Sanitize HTML. + foreach ( (array) $api->sections as $section_name => $content ) { + $api->sections[ $section_name ] = wp_kses( $content, $plugins_allowedtags ); + } + + foreach ( array( 'version', 'author', 'requires', 'tested', 'homepage', 'downloaded', 'slug' ) as $key ) { + if ( isset( $api->$key ) ) { + $api->$key = wp_kses( $api->$key, $plugins_allowedtags ); + } + } + + $_tab = esc_attr( $tab ); + + // Default to the Description tab, Do not translate, API returns English. + $section = isset( $_REQUEST['section'] ) ? wp_unslash( $_REQUEST['section'] ) : 'description'; + if ( empty( $section ) || ! isset( $api->sections[ $section ] ) ) { + $section_titles = array_keys( (array) $api->sections ); + $section = reset( $section_titles ); + } + + iframe_header( __( 'Plugin Installation' ) ); + + $_with_banner = ''; + + if ( ! empty( $api->banners ) && ( ! empty( $api->banners['low'] ) || ! empty( $api->banners['high'] ) ) ) { + $_with_banner = 'with-banner'; + $low = empty( $api->banners['low'] ) ? $api->banners['high'] : $api->banners['low']; + $high = empty( $api->banners['high'] ) ? $api->banners['low'] : $api->banners['high']; + ?> + + '; + echo "

    {$api->name}

    "; + echo "
    \n"; + + foreach ( (array) $api->sections as $section_name => $content ) { + if ( 'reviews' === $section_name && ( empty( $api->ratings ) || 0 === array_sum( (array) $api->ratings ) ) ) { + continue; + } + + if ( isset( $plugins_section_titles[ $section_name ] ) ) { + $title = $plugins_section_titles[ $section_name ]; + } else { + $title = ucwords( str_replace( '_', ' ', $section_name ) ); + } + + $class = ( $section_name === $section ) ? ' class="current"' : ''; + $href = add_query_arg( + array( + 'tab' => $tab, + 'section' => $section_name, + ) + ); + $href = esc_url( $href ); + $san_section = esc_attr( $section_name ); + echo "\t$title\n"; + } + + echo "
    \n"; + + ?> +
    +
    +
      + version ) ) { ?> +
    • version; ?>
    • + author ) ) { ?> +
    • author, '_blank' ); ?>
    • + last_updated ) ) { ?> +
    • + last_updated ) ) ); + ?> +
    • + requires ) ) { ?> +
    • + + requires ); + ?> +
    • + tested ) ) { ?> +
    • tested; ?>
    • + requires_php ) ) { ?> +
    • + + requires_php ); + ?> +
    • + active_installs ) ) { ?> +
    • + active_installs >= 1000000 ) { + $active_installs_millions = floor( $api->active_installs / 1000000 ); + printf( + /* translators: %s: Number of millions. */ + _nx( '%s+ Million', '%s+ Million', $active_installs_millions, 'Active plugin installations' ), + number_format_i18n( $active_installs_millions ) + ); + } elseif ( $api->active_installs < 10 ) { + _ex( 'Less Than 10', 'Active plugin installations' ); + } else { + echo number_format_i18n( $api->active_installs ) . '+'; + } + ?> +
    • + slug ) && empty( $api->external ) ) { ?> +
    • + homepage ) ) { ?> +
    • + donate_link ) && empty( $api->contributors ) ) { ?> +
    • + +
    + rating ) ) { ?> +

    + $api->rating, + 'type' => 'percent', + 'number' => $api->num_ratings, + ) + ); + ?> + + ratings ) && array_sum( (array) $api->ratings ) > 0 ) { + ?> +

    +

    + ratings as $key => $ratecount ) { + // Avoid div-by-zero. + $_rating = $api->num_ratings ? ( $ratecount / $api->num_ratings ) : 0; + $aria_label = esc_attr( + sprintf( + /* translators: 1: Number of stars (used to determine singular/plural), 2: Number of reviews. */ + _n( + 'Reviews with %1$d star: %2$s. Opens in a new tab.', + 'Reviews with %1$d stars: %2$s. Opens in a new tab.', + $key + ), + $key, + number_format_i18n( $ratecount ) + ) + ); + ?> +
    + + %s', + "https://wordpress.org/support/plugin/{$api->slug}/reviews/?filter={$key}", + $aria_label, + /* translators: %s: Number of stars. */ + sprintf( _n( '%d star', '%d stars', $key ), $key ) + ); + ?> + + + + + +
    + contributors ) ) { + ?> +

    +
      + contributors as $contrib_username => $contrib_details ) { + $contrib_name = $contrib_details['display_name']; + if ( ! $contrib_name ) { + $contrib_name = $contrib_username; + } + $contrib_name = esc_html( $contrib_name ); + + $contrib_profile = esc_url( $contrib_details['profile'] ); + $contrib_avatar = esc_url( add_query_arg( 's', '36', $contrib_details['avatar'] ) ); + + echo "
    • {$contrib_name}
    • "; + } + ?> +
    + donate_link ) ) { ?> + + + +
    +
    + requires_php ) ? $api->requires_php : null; + $requires_wp = isset( $api->requires ) ? $api->requires : null; + + $compatible_php = is_php_version_compatible( $requires_php ); + $compatible_wp = is_wp_version_compatible( $requires_wp ); + $tested_wp = ( empty( $api->tested ) || version_compare( get_bloginfo( 'version' ), $api->tested, '<=' ) ); + + if ( ! $compatible_php ) { + $compatible_php_notice_message = '

    '; + $compatible_php_notice_message .= __( 'Error: This plugin requires a newer version of PHP.' ); + + if ( current_user_can( 'update_php' ) ) { + $compatible_php_notice_message .= sprintf( + /* translators: %s: URL to Update PHP page. */ + ' ' . __( 'Click here to learn more about updating PHP.' ), + esc_url( wp_get_update_php_url() ) + ) . wp_update_php_annotation( '

    ', '', false ); + } else { + $compatible_php_notice_message .= '

    '; + } + + wp_admin_notice( + $compatible_php_notice_message, + array( + 'type' => 'error', + 'additional_classes' => array( 'notice-alt' ), + 'paragraph_wrap' => false, + ) + ); + } + + if ( ! $tested_wp ) { + wp_admin_notice( + __( 'Warning: This plugin has not been tested with your current version of WordPress.' ), + array( + 'type' => 'warning', + 'additional_classes' => array( 'notice-alt' ), + ) + ); + } elseif ( ! $compatible_wp ) { + $compatible_wp_notice_message = __( 'Error: This plugin requires a newer version of WordPress.' ); + if ( current_user_can( 'update_core' ) ) { + $compatible_wp_notice_message .= sprintf( + /* translators: %s: URL to WordPress Updates screen. */ + ' ' . __( 'Click here to update WordPress.' ), + esc_url( self_admin_url( 'update-core.php' ) ) + ); + } + + wp_admin_notice( + $compatible_wp_notice_message, + array( + 'type' => 'error', + 'additional_classes' => array( 'notice-alt' ), + ) + ); + } + + foreach ( (array) $api->sections as $section_name => $content ) { + $content = links_add_base_url( $content, 'https://wordpress.org/plugins/' . $api->slug . '/' ); + $content = links_add_target( $content, '_blank' ); + + $san_section = esc_attr( $section_name ); + + $display = ( $section_name === $section ) ? 'block' : 'none'; + + echo "\t
    \n"; + echo $content; + echo "\t
    \n"; + } + echo "
    \n"; + echo "
    \n"; + echo "\n"; // #plugin-information-scrollable + echo "\n"; + + iframe_footer(); + exit; +} diff --git a/wp-admin/includes/plugin.php b/wp-admin/includes/plugin.php new file mode 100644 index 0000000..f55bbd8 --- /dev/null +++ b/wp-admin/includes/plugin.php @@ -0,0 +1,2597 @@ + 'Plugin Name', + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'Description' => 'Description', + 'Author' => 'Author', + 'AuthorURI' => 'Author URI', + 'TextDomain' => 'Text Domain', + 'DomainPath' => 'Domain Path', + 'Network' => 'Network', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + 'UpdateURI' => 'Update URI', + // Site Wide Only is deprecated in favor of Network. + '_sitewide' => 'Site Wide Only', + ); + + $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' ); + + // Site Wide Only is the old header for Network. + if ( ! $plugin_data['Network'] && $plugin_data['_sitewide'] ) { + /* translators: 1: Site Wide Only: true, 2: Network: true */ + _deprecated_argument( __FUNCTION__, '3.0.0', sprintf( __( 'The %1$s plugin header is deprecated. Use %2$s instead.' ), 'Site Wide Only: true', 'Network: true' ) ); + $plugin_data['Network'] = $plugin_data['_sitewide']; + } + $plugin_data['Network'] = ( 'true' === strtolower( $plugin_data['Network'] ) ); + unset( $plugin_data['_sitewide'] ); + + // If no text domain is defined fall back to the plugin slug. + if ( ! $plugin_data['TextDomain'] ) { + $plugin_slug = dirname( plugin_basename( $plugin_file ) ); + if ( '.' !== $plugin_slug && ! str_contains( $plugin_slug, '/' ) ) { + $plugin_data['TextDomain'] = $plugin_slug; + } + } + + if ( $markup || $translate ) { + $plugin_data = _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup, $translate ); + } else { + $plugin_data['Title'] = $plugin_data['Name']; + $plugin_data['AuthorName'] = $plugin_data['Author']; + } + + return $plugin_data; +} + +/** + * Sanitizes plugin data, optionally adds markup, optionally translates. + * + * @since 2.7.0 + * + * @see get_plugin_data() + * + * @access private + * + * @param string $plugin_file Path to the main plugin file. + * @param array $plugin_data An array of plugin data. See get_plugin_data(). + * @param bool $markup Optional. If the returned data should have HTML markup applied. + * Default true. + * @param bool $translate Optional. If the returned data should be translated. Default true. + * @return array Plugin data. Values will be empty if not supplied by the plugin. + * See get_plugin_data() for the list of possible values. + */ +function _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup = true, $translate = true ) { + + // Sanitize the plugin filename to a WP_PLUGIN_DIR relative path. + $plugin_file = plugin_basename( $plugin_file ); + + // Translate fields. + if ( $translate ) { + $textdomain = $plugin_data['TextDomain']; + if ( $textdomain ) { + if ( ! is_textdomain_loaded( $textdomain ) ) { + if ( $plugin_data['DomainPath'] ) { + load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) . $plugin_data['DomainPath'] ); + } else { + load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) ); + } + } + } elseif ( 'hello.php' === basename( $plugin_file ) ) { + $textdomain = 'default'; + } + if ( $textdomain ) { + foreach ( array( 'Name', 'PluginURI', 'Description', 'Author', 'AuthorURI', 'Version' ) as $field ) { + if ( ! empty( $plugin_data[ $field ] ) ) { + // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain + $plugin_data[ $field ] = translate( $plugin_data[ $field ], $textdomain ); + } + } + } + } + + // Sanitize fields. + $allowed_tags_in_links = array( + 'abbr' => array( 'title' => true ), + 'acronym' => array( 'title' => true ), + 'code' => true, + 'em' => true, + 'strong' => true, + ); + + $allowed_tags = $allowed_tags_in_links; + $allowed_tags['a'] = array( + 'href' => true, + 'title' => true, + ); + + /* + * Name is marked up inside tags. Don't allow these. + * Author is too, but some plugins have used here (omitting Author URI). + */ + $plugin_data['Name'] = wp_kses( $plugin_data['Name'], $allowed_tags_in_links ); + $plugin_data['Author'] = wp_kses( $plugin_data['Author'], $allowed_tags ); + + $plugin_data['Description'] = wp_kses( $plugin_data['Description'], $allowed_tags ); + $plugin_data['Version'] = wp_kses( $plugin_data['Version'], $allowed_tags ); + + $plugin_data['PluginURI'] = esc_url( $plugin_data['PluginURI'] ); + $plugin_data['AuthorURI'] = esc_url( $plugin_data['AuthorURI'] ); + + $plugin_data['Title'] = $plugin_data['Name']; + $plugin_data['AuthorName'] = $plugin_data['Author']; + + // Apply markup. + if ( $markup ) { + if ( $plugin_data['PluginURI'] && $plugin_data['Name'] ) { + $plugin_data['Title'] = '' . $plugin_data['Name'] . ''; + } + + if ( $plugin_data['AuthorURI'] && $plugin_data['Author'] ) { + $plugin_data['Author'] = '' . $plugin_data['Author'] . ''; + } + + $plugin_data['Description'] = wptexturize( $plugin_data['Description'] ); + + if ( $plugin_data['Author'] ) { + $plugin_data['Description'] .= sprintf( + /* translators: %s: Plugin author. */ + ' ' . __( 'By %s.' ) . '', + $plugin_data['Author'] + ); + } + } + + return $plugin_data; +} + +/** + * Gets a list of a plugin's files. + * + * @since 2.8.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return string[] Array of file names relative to the plugin root. + */ +function get_plugin_files( $plugin ) { + $plugin_file = WP_PLUGIN_DIR . '/' . $plugin; + $dir = dirname( $plugin_file ); + + $plugin_files = array( plugin_basename( $plugin_file ) ); + + if ( is_dir( $dir ) && WP_PLUGIN_DIR !== $dir ) { + + /** + * Filters the array of excluded directories and files while scanning the folder. + * + * @since 4.9.0 + * + * @param string[] $exclusions Array of excluded directories and files. + */ + $exclusions = (array) apply_filters( 'plugin_files_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) ); + + $list_files = list_files( $dir, 100, $exclusions ); + $list_files = array_map( 'plugin_basename', $list_files ); + + $plugin_files = array_merge( $plugin_files, $list_files ); + $plugin_files = array_values( array_unique( $plugin_files ) ); + } + + return $plugin_files; +} + +/** + * Checks the plugins directory and retrieve all plugin files with plugin data. + * + * WordPress only supports plugin files in the base plugins directory + * (wp-content/plugins) and in one directory above the plugins directory + * (wp-content/plugins/my-plugin). The file it looks for has the plugin data + * and must be found in those two locations. It is recommended to keep your + * plugin files in their own directories. + * + * The file with the plugin data is the file that will be included and therefore + * needs to have the main execution for the plugin. This does not mean + * everything must be contained in the file and it is recommended that the file + * be split for maintainability. Keep everything in one file for extreme + * optimization purposes. + * + * @since 1.5.0 + * + * @param string $plugin_folder Optional. Relative path to single plugin folder. + * @return array[] Array of arrays of plugin data, keyed by plugin file name. See get_plugin_data(). + */ +function get_plugins( $plugin_folder = '' ) { + + $cache_plugins = wp_cache_get( 'plugins', 'plugins' ); + if ( ! $cache_plugins ) { + $cache_plugins = array(); + } + + if ( isset( $cache_plugins[ $plugin_folder ] ) ) { + return $cache_plugins[ $plugin_folder ]; + } + + $wp_plugins = array(); + $plugin_root = WP_PLUGIN_DIR; + if ( ! empty( $plugin_folder ) ) { + $plugin_root .= $plugin_folder; + } + + // Files in wp-content/plugins directory. + $plugins_dir = @opendir( $plugin_root ); + $plugin_files = array(); + + if ( $plugins_dir ) { + while ( ( $file = readdir( $plugins_dir ) ) !== false ) { + if ( str_starts_with( $file, '.' ) ) { + continue; + } + + if ( is_dir( $plugin_root . '/' . $file ) ) { + $plugins_subdir = @opendir( $plugin_root . '/' . $file ); + + if ( $plugins_subdir ) { + while ( ( $subfile = readdir( $plugins_subdir ) ) !== false ) { + if ( str_starts_with( $subfile, '.' ) ) { + continue; + } + + if ( str_ends_with( $subfile, '.php' ) ) { + $plugin_files[] = "$file/$subfile"; + } + } + + closedir( $plugins_subdir ); + } + } else { + if ( str_ends_with( $file, '.php' ) ) { + $plugin_files[] = $file; + } + } + } + + closedir( $plugins_dir ); + } + + if ( empty( $plugin_files ) ) { + return $wp_plugins; + } + + foreach ( $plugin_files as $plugin_file ) { + if ( ! is_readable( "$plugin_root/$plugin_file" ) ) { + continue; + } + + // Do not apply markup/translate as it will be cached. + $plugin_data = get_plugin_data( "$plugin_root/$plugin_file", false, false ); + + if ( empty( $plugin_data['Name'] ) ) { + continue; + } + + $wp_plugins[ plugin_basename( $plugin_file ) ] = $plugin_data; + } + + uasort( $wp_plugins, '_sort_uname_callback' ); + + $cache_plugins[ $plugin_folder ] = $wp_plugins; + wp_cache_set( 'plugins', $cache_plugins, 'plugins' ); + + return $wp_plugins; +} + +/** + * Checks the mu-plugins directory and retrieve all mu-plugin files with any plugin data. + * + * WordPress only includes mu-plugin files in the base mu-plugins directory (wp-content/mu-plugins). + * + * @since 3.0.0 + * @return array[] Array of arrays of mu-plugin data, keyed by plugin file name. See get_plugin_data(). + */ +function get_mu_plugins() { + $wp_plugins = array(); + $plugin_files = array(); + + if ( ! is_dir( WPMU_PLUGIN_DIR ) ) { + return $wp_plugins; + } + + // Files in wp-content/mu-plugins directory. + $plugins_dir = @opendir( WPMU_PLUGIN_DIR ); + if ( $plugins_dir ) { + while ( ( $file = readdir( $plugins_dir ) ) !== false ) { + if ( str_ends_with( $file, '.php' ) ) { + $plugin_files[] = $file; + } + } + } else { + return $wp_plugins; + } + + closedir( $plugins_dir ); + + if ( empty( $plugin_files ) ) { + return $wp_plugins; + } + + foreach ( $plugin_files as $plugin_file ) { + if ( ! is_readable( WPMU_PLUGIN_DIR . "/$plugin_file" ) ) { + continue; + } + + // Do not apply markup/translate as it will be cached. + $plugin_data = get_plugin_data( WPMU_PLUGIN_DIR . "/$plugin_file", false, false ); + + if ( empty( $plugin_data['Name'] ) ) { + $plugin_data['Name'] = $plugin_file; + } + + $wp_plugins[ $plugin_file ] = $plugin_data; + } + + if ( isset( $wp_plugins['index.php'] ) && filesize( WPMU_PLUGIN_DIR . '/index.php' ) <= 30 ) { + // Silence is golden. + unset( $wp_plugins['index.php'] ); + } + + uasort( $wp_plugins, '_sort_uname_callback' ); + + return $wp_plugins; +} + +/** + * Declares a callback to sort array by a 'Name' key. + * + * @since 3.1.0 + * + * @access private + * + * @param array $a array with 'Name' key. + * @param array $b array with 'Name' key. + * @return int Return 0 or 1 based on two string comparison. + */ +function _sort_uname_callback( $a, $b ) { + return strnatcasecmp( $a['Name'], $b['Name'] ); +} + +/** + * Checks the wp-content directory and retrieve all drop-ins with any plugin data. + * + * @since 3.0.0 + * @return array[] Array of arrays of dropin plugin data, keyed by plugin file name. See get_plugin_data(). + */ +function get_dropins() { + $dropins = array(); + $plugin_files = array(); + + $_dropins = _get_dropins(); + + // Files in wp-content directory. + $plugins_dir = @opendir( WP_CONTENT_DIR ); + if ( $plugins_dir ) { + while ( ( $file = readdir( $plugins_dir ) ) !== false ) { + if ( isset( $_dropins[ $file ] ) ) { + $plugin_files[] = $file; + } + } + } else { + return $dropins; + } + + closedir( $plugins_dir ); + + if ( empty( $plugin_files ) ) { + return $dropins; + } + + foreach ( $plugin_files as $plugin_file ) { + if ( ! is_readable( WP_CONTENT_DIR . "/$plugin_file" ) ) { + continue; + } + + // Do not apply markup/translate as it will be cached. + $plugin_data = get_plugin_data( WP_CONTENT_DIR . "/$plugin_file", false, false ); + + if ( empty( $plugin_data['Name'] ) ) { + $plugin_data['Name'] = $plugin_file; + } + + $dropins[ $plugin_file ] = $plugin_data; + } + + uksort( $dropins, 'strnatcasecmp' ); + + return $dropins; +} + +/** + * Returns drop-ins that WordPress uses. + * + * Includes Multisite drop-ins only when is_multisite() + * + * @since 3.0.0 + * @return array[] Key is file name. The value is an array, with the first value the + * purpose of the drop-in and the second value the name of the constant that must be + * true for the drop-in to be used, or true if no constant is required. + */ +function _get_dropins() { + $dropins = array( + 'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE + 'db.php' => array( __( 'Custom database class.' ), true ), // Auto on load. + 'db-error.php' => array( __( 'Custom database error message.' ), true ), // Auto on error. + 'install.php' => array( __( 'Custom installation script.' ), true ), // Auto on installation. + 'maintenance.php' => array( __( 'Custom maintenance message.' ), true ), // Auto on maintenance. + 'object-cache.php' => array( __( 'External object cache.' ), true ), // Auto on load. + 'php-error.php' => array( __( 'Custom PHP error message.' ), true ), // Auto on error. + 'fatal-error-handler.php' => array( __( 'Custom PHP fatal error handler.' ), true ), // Auto on error. + ); + + if ( is_multisite() ) { + $dropins['sunrise.php'] = array( __( 'Executed before Multisite is loaded.' ), 'SUNRISE' ); // SUNRISE + $dropins['blog-deleted.php'] = array( __( 'Custom site deleted message.' ), true ); // Auto on deleted blog. + $dropins['blog-inactive.php'] = array( __( 'Custom site inactive message.' ), true ); // Auto on inactive blog. + $dropins['blog-suspended.php'] = array( __( 'Custom site suspended message.' ), true ); // Auto on archived or spammed blog. + } + + return $dropins; +} + +/** + * Determines whether a plugin is active. + * + * Only plugins installed in the plugins/ folder can be active. + * + * Plugins in the mu-plugins/ folder can't be "activated," so this function will + * return false for those plugins. + * + * For more information on this and similar theme functions, check out + * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ + * Conditional Tags} article in the Theme Developer Handbook. + * + * @since 2.5.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return bool True, if in the active plugins list. False, not in the list. + */ +function is_plugin_active( $plugin ) { + return in_array( $plugin, (array) get_option( 'active_plugins', array() ), true ) || is_plugin_active_for_network( $plugin ); +} + +/** + * Determines whether the plugin is inactive. + * + * Reverse of is_plugin_active(). Used as a callback. + * + * For more information on this and similar theme functions, check out + * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ + * Conditional Tags} article in the Theme Developer Handbook. + * + * @since 3.1.0 + * + * @see is_plugin_active() + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return bool True if inactive. False if active. + */ +function is_plugin_inactive( $plugin ) { + return ! is_plugin_active( $plugin ); +} + +/** + * Determines whether the plugin is active for the entire network. + * + * Only plugins installed in the plugins/ folder can be active. + * + * Plugins in the mu-plugins/ folder can't be "activated," so this function will + * return false for those plugins. + * + * For more information on this and similar theme functions, check out + * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ + * Conditional Tags} article in the Theme Developer Handbook. + * + * @since 3.0.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return bool True if active for the network, otherwise false. + */ +function is_plugin_active_for_network( $plugin ) { + if ( ! is_multisite() ) { + return false; + } + + $plugins = get_site_option( 'active_sitewide_plugins' ); + if ( isset( $plugins[ $plugin ] ) ) { + return true; + } + + return false; +} + +/** + * Checks for "Network: true" in the plugin header to see if this should + * be activated only as a network wide plugin. The plugin would also work + * when Multisite is not enabled. + * + * Checks for "Site Wide Only: true" for backward compatibility. + * + * @since 3.0.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return bool True if plugin is network only, false otherwise. + */ +function is_network_only_plugin( $plugin ) { + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + if ( $plugin_data ) { + return $plugin_data['Network']; + } + return false; +} + +/** + * Attempts activation of plugin in a "sandbox" and redirects on success. + * + * A plugin that is already activated will not attempt to be activated again. + * + * The way it works is by setting the redirection to the error before trying to + * include the plugin file. If the plugin fails, then the redirection will not + * be overwritten with the success message. Also, the options will not be + * updated and the activation hook will not be called on plugin error. + * + * It should be noted that in no way the below code will actually prevent errors + * within the file. The code should not be used elsewhere to replicate the + * "sandbox", which uses redirection to work. + * {@source 13 1} + * + * If any errors are found or text is outputted, then it will be captured to + * ensure that the success redirection will update the error redirection. + * + * @since 2.5.0 + * @since 5.2.0 Test for WordPress version and PHP version compatibility. + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @param string $redirect Optional. URL to redirect to. + * @param bool $network_wide Optional. Whether to enable the plugin for all sites in the network + * or just the current site. Multisite only. Default false. + * @param bool $silent Optional. Whether to prevent calling activation hooks. Default false. + * @return null|WP_Error Null on success, WP_Error on invalid file. + */ +function activate_plugin( $plugin, $redirect = '', $network_wide = false, $silent = false ) { + $plugin = plugin_basename( trim( $plugin ) ); + + if ( is_multisite() && ( $network_wide || is_network_only_plugin( $plugin ) ) ) { + $network_wide = true; + $current = get_site_option( 'active_sitewide_plugins', array() ); + $_GET['networkwide'] = 1; // Back compat for plugins looking for this value. + } else { + $current = get_option( 'active_plugins', array() ); + } + + $valid = validate_plugin( $plugin ); + if ( is_wp_error( $valid ) ) { + return $valid; + } + + $requirements = validate_plugin_requirements( $plugin ); + if ( is_wp_error( $requirements ) ) { + return $requirements; + } + + if ( $network_wide && ! isset( $current[ $plugin ] ) + || ! $network_wide && ! in_array( $plugin, $current, true ) + ) { + if ( ! empty( $redirect ) ) { + // We'll override this later if the plugin can be included without fatal error. + wp_redirect( add_query_arg( '_error_nonce', wp_create_nonce( 'plugin-activation-error_' . $plugin ), $redirect ) ); + } + + ob_start(); + + // Load the plugin to test whether it throws any errors. + plugin_sandbox_scrape( $plugin ); + + if ( ! $silent ) { + /** + * Fires before a plugin is activated. + * + * If a plugin is silently activated (such as during an update), + * this hook does not fire. + * + * @since 2.9.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @param bool $network_wide Whether to enable the plugin for all sites in the network + * or just the current site. Multisite only. Default false. + */ + do_action( 'activate_plugin', $plugin, $network_wide ); + + /** + * Fires as a specific plugin is being activated. + * + * This hook is the "activation" hook used internally by register_activation_hook(). + * The dynamic portion of the hook name, `$plugin`, refers to the plugin basename. + * + * If a plugin is silently activated (such as during an update), this hook does not fire. + * + * @since 2.0.0 + * + * @param bool $network_wide Whether to enable the plugin for all sites in the network + * or just the current site. Multisite only. Default false. + */ + do_action( "activate_{$plugin}", $network_wide ); + } + + if ( $network_wide ) { + $current = get_site_option( 'active_sitewide_plugins', array() ); + $current[ $plugin ] = time(); + update_site_option( 'active_sitewide_plugins', $current ); + } else { + $current = get_option( 'active_plugins', array() ); + $current[] = $plugin; + sort( $current ); + update_option( 'active_plugins', $current ); + } + + if ( ! $silent ) { + /** + * Fires after a plugin has been activated. + * + * If a plugin is silently activated (such as during an update), + * this hook does not fire. + * + * @since 2.9.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @param bool $network_wide Whether to enable the plugin for all sites in the network + * or just the current site. Multisite only. Default false. + */ + do_action( 'activated_plugin', $plugin, $network_wide ); + } + + if ( ob_get_length() > 0 ) { + $output = ob_get_clean(); + return new WP_Error( 'unexpected_output', __( 'The plugin generated unexpected output.' ), $output ); + } + + ob_end_clean(); + } + + return null; +} + +/** + * Deactivates a single plugin or multiple plugins. + * + * The deactivation hook is disabled by the plugin upgrader by using the $silent + * parameter. + * + * @since 2.5.0 + * + * @param string|string[] $plugins Single plugin or list of plugins to deactivate. + * @param bool $silent Prevent calling deactivation hooks. Default false. + * @param bool|null $network_wide Whether to deactivate the plugin for all sites in the network. + * A value of null will deactivate plugins for both the network + * and the current site. Multisite only. Default null. + */ +function deactivate_plugins( $plugins, $silent = false, $network_wide = null ) { + if ( is_multisite() ) { + $network_current = get_site_option( 'active_sitewide_plugins', array() ); + } + $current = get_option( 'active_plugins', array() ); + $do_blog = false; + $do_network = false; + + foreach ( (array) $plugins as $plugin ) { + $plugin = plugin_basename( trim( $plugin ) ); + if ( ! is_plugin_active( $plugin ) ) { + continue; + } + + $network_deactivating = ( false !== $network_wide ) && is_plugin_active_for_network( $plugin ); + + if ( ! $silent ) { + /** + * Fires before a plugin is deactivated. + * + * If a plugin is silently deactivated (such as during an update), + * this hook does not fire. + * + * @since 2.9.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @param bool $network_deactivating Whether the plugin is deactivated for all sites in the network + * or just the current site. Multisite only. Default false. + */ + do_action( 'deactivate_plugin', $plugin, $network_deactivating ); + } + + if ( false !== $network_wide ) { + if ( is_plugin_active_for_network( $plugin ) ) { + $do_network = true; + unset( $network_current[ $plugin ] ); + } elseif ( $network_wide ) { + continue; + } + } + + if ( true !== $network_wide ) { + $key = array_search( $plugin, $current, true ); + if ( false !== $key ) { + $do_blog = true; + unset( $current[ $key ] ); + } + } + + if ( $do_blog && wp_is_recovery_mode() ) { + list( $extension ) = explode( '/', $plugin ); + wp_paused_plugins()->delete( $extension ); + } + + if ( ! $silent ) { + /** + * Fires as a specific plugin is being deactivated. + * + * This hook is the "deactivation" hook used internally by register_deactivation_hook(). + * The dynamic portion of the hook name, `$plugin`, refers to the plugin basename. + * + * If a plugin is silently deactivated (such as during an update), this hook does not fire. + * + * @since 2.0.0 + * + * @param bool $network_deactivating Whether the plugin is deactivated for all sites in the network + * or just the current site. Multisite only. Default false. + */ + do_action( "deactivate_{$plugin}", $network_deactivating ); + + /** + * Fires after a plugin is deactivated. + * + * If a plugin is silently deactivated (such as during an update), + * this hook does not fire. + * + * @since 2.9.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @param bool $network_deactivating Whether the plugin is deactivated for all sites in the network + * or just the current site. Multisite only. Default false. + */ + do_action( 'deactivated_plugin', $plugin, $network_deactivating ); + } + } + + if ( $do_blog ) { + update_option( 'active_plugins', $current ); + } + if ( $do_network ) { + update_site_option( 'active_sitewide_plugins', $network_current ); + } +} + +/** + * Activates multiple plugins. + * + * When WP_Error is returned, it does not mean that one of the plugins had + * errors. It means that one or more of the plugin file paths were invalid. + * + * The execution will be halted as soon as one of the plugins has an error. + * + * @since 2.6.0 + * + * @param string|string[] $plugins Single plugin or list of plugins to activate. + * @param string $redirect Redirect to page after successful activation. + * @param bool $network_wide Whether to enable the plugin for all sites in the network. + * Default false. + * @param bool $silent Prevent calling activation hooks. Default false. + * @return true|WP_Error True when finished or WP_Error if there were errors during a plugin activation. + */ +function activate_plugins( $plugins, $redirect = '', $network_wide = false, $silent = false ) { + if ( ! is_array( $plugins ) ) { + $plugins = array( $plugins ); + } + + $errors = array(); + foreach ( $plugins as $plugin ) { + if ( ! empty( $redirect ) ) { + $redirect = add_query_arg( 'plugin', $plugin, $redirect ); + } + $result = activate_plugin( $plugin, $redirect, $network_wide, $silent ); + if ( is_wp_error( $result ) ) { + $errors[ $plugin ] = $result; + } + } + + if ( ! empty( $errors ) ) { + return new WP_Error( 'plugins_invalid', __( 'One of the plugins is invalid.' ), $errors ); + } + + return true; +} + +/** + * Removes directory and files of a plugin for a list of plugins. + * + * @since 2.6.0 + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @param string[] $plugins List of plugin paths to delete, relative to the plugins directory. + * @param string $deprecated Not used. + * @return bool|null|WP_Error True on success, false if `$plugins` is empty, `WP_Error` on failure. + * `null` if filesystem credentials are required to proceed. + */ +function delete_plugins( $plugins, $deprecated = '' ) { + global $wp_filesystem; + + if ( empty( $plugins ) ) { + return false; + } + + $checked = array(); + foreach ( $plugins as $plugin ) { + $checked[] = 'checked[]=' . $plugin; + } + + $url = wp_nonce_url( 'plugins.php?action=delete-selected&verify-delete=1&' . implode( '&', $checked ), 'bulk-plugins' ); + + ob_start(); + $credentials = request_filesystem_credentials( $url ); + $data = ob_get_clean(); + + if ( false === $credentials ) { + if ( ! empty( $data ) ) { + require_once ABSPATH . 'wp-admin/admin-header.php'; + echo $data; + require_once ABSPATH . 'wp-admin/admin-footer.php'; + exit; + } + return; + } + + if ( ! WP_Filesystem( $credentials ) ) { + ob_start(); + // Failed to connect. Error and request again. + request_filesystem_credentials( $url, '', true ); + $data = ob_get_clean(); + + if ( ! empty( $data ) ) { + require_once ABSPATH . 'wp-admin/admin-header.php'; + echo $data; + require_once ABSPATH . 'wp-admin/admin-footer.php'; + exit; + } + return; + } + + if ( ! is_object( $wp_filesystem ) ) { + return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) ); + } + + if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { + return new WP_Error( 'fs_error', __( 'Filesystem error.' ), $wp_filesystem->errors ); + } + + // Get the base plugin folder. + $plugins_dir = $wp_filesystem->wp_plugins_dir(); + if ( empty( $plugins_dir ) ) { + return new WP_Error( 'fs_no_plugins_dir', __( 'Unable to locate WordPress plugin directory.' ) ); + } + + $plugins_dir = trailingslashit( $plugins_dir ); + + $plugin_translations = wp_get_installed_translations( 'plugins' ); + + $errors = array(); + + foreach ( $plugins as $plugin_file ) { + // Run Uninstall hook. + if ( is_uninstallable_plugin( $plugin_file ) ) { + uninstall_plugin( $plugin_file ); + } + + /** + * Fires immediately before a plugin deletion attempt. + * + * @since 4.4.0 + * + * @param string $plugin_file Path to the plugin file relative to the plugins directory. + */ + do_action( 'delete_plugin', $plugin_file ); + + $this_plugin_dir = trailingslashit( dirname( $plugins_dir . $plugin_file ) ); + + /* + * If plugin is in its own directory, recursively delete the directory. + * Base check on if plugin includes directory separator AND that it's not the root plugin folder. + */ + if ( strpos( $plugin_file, '/' ) && $this_plugin_dir !== $plugins_dir ) { + $deleted = $wp_filesystem->delete( $this_plugin_dir, true ); + } else { + $deleted = $wp_filesystem->delete( $plugins_dir . $plugin_file ); + } + + /** + * Fires immediately after a plugin deletion attempt. + * + * @since 4.4.0 + * + * @param string $plugin_file Path to the plugin file relative to the plugins directory. + * @param bool $deleted Whether the plugin deletion was successful. + */ + do_action( 'deleted_plugin', $plugin_file, $deleted ); + + if ( ! $deleted ) { + $errors[] = $plugin_file; + continue; + } + + $plugin_slug = dirname( $plugin_file ); + + if ( 'hello.php' === $plugin_file ) { + $plugin_slug = 'hello-dolly'; + } + + // Remove language files, silently. + if ( '.' !== $plugin_slug && ! empty( $plugin_translations[ $plugin_slug ] ) ) { + $translations = $plugin_translations[ $plugin_slug ]; + + foreach ( $translations as $translation => $data ) { + $wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.po' ); + $wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.mo' ); + + $json_translation_files = glob( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '-*.json' ); + if ( $json_translation_files ) { + array_map( array( $wp_filesystem, 'delete' ), $json_translation_files ); + } + } + } + } + + // Remove deleted plugins from the plugin updates list. + $current = get_site_transient( 'update_plugins' ); + if ( $current ) { + // Don't remove the plugins that weren't deleted. + $deleted = array_diff( $plugins, $errors ); + + foreach ( $deleted as $plugin_file ) { + unset( $current->response[ $plugin_file ] ); + } + + set_site_transient( 'update_plugins', $current ); + } + + if ( ! empty( $errors ) ) { + if ( 1 === count( $errors ) ) { + /* translators: %s: Plugin filename. */ + $message = __( 'Could not fully remove the plugin %s.' ); + } else { + /* translators: %s: Comma-separated list of plugin filenames. */ + $message = __( 'Could not fully remove the plugins %s.' ); + } + + return new WP_Error( 'could_not_remove_plugin', sprintf( $message, implode( ', ', $errors ) ) ); + } + + return true; +} + +/** + * Validates active plugins. + * + * Validate all active plugins, deactivates invalid and + * returns an array of deactivated ones. + * + * @since 2.5.0 + * @return WP_Error[] Array of plugin errors keyed by plugin file name. + */ +function validate_active_plugins() { + $plugins = get_option( 'active_plugins', array() ); + // Validate vartype: array. + if ( ! is_array( $plugins ) ) { + update_option( 'active_plugins', array() ); + $plugins = array(); + } + + if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) { + $network_plugins = (array) get_site_option( 'active_sitewide_plugins', array() ); + $plugins = array_merge( $plugins, array_keys( $network_plugins ) ); + } + + if ( empty( $plugins ) ) { + return array(); + } + + $invalid = array(); + + // Invalid plugins get deactivated. + foreach ( $plugins as $plugin ) { + $result = validate_plugin( $plugin ); + if ( is_wp_error( $result ) ) { + $invalid[ $plugin ] = $result; + deactivate_plugins( $plugin, true ); + } + } + return $invalid; +} + +/** + * Validates the plugin path. + * + * Checks that the main plugin file exists and is a valid plugin. See validate_file(). + * + * @since 2.5.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return int|WP_Error 0 on success, WP_Error on failure. + */ +function validate_plugin( $plugin ) { + if ( validate_file( $plugin ) ) { + return new WP_Error( 'plugin_invalid', __( 'Invalid plugin path.' ) ); + } + if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) { + return new WP_Error( 'plugin_not_found', __( 'Plugin file does not exist.' ) ); + } + + $installed_plugins = get_plugins(); + if ( ! isset( $installed_plugins[ $plugin ] ) ) { + return new WP_Error( 'no_plugin_header', __( 'The plugin does not have a valid header.' ) ); + } + return 0; +} + +/** + * Validates the plugin requirements for WordPress version and PHP version. + * + * Uses the information from `Requires at least` and `Requires PHP` headers + * defined in the plugin's main PHP file. + * + * @since 5.2.0 + * @since 5.3.0 Added support for reading the headers from the plugin's + * main PHP file, with `readme.txt` as a fallback. + * @since 5.8.0 Removed support for using `readme.txt` as a fallback. + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return true|WP_Error True if requirements are met, WP_Error on failure. + */ +function validate_plugin_requirements( $plugin ) { + $plugin_headers = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + + $requirements = array( + 'requires' => ! empty( $plugin_headers['RequiresWP'] ) ? $plugin_headers['RequiresWP'] : '', + 'requires_php' => ! empty( $plugin_headers['RequiresPHP'] ) ? $plugin_headers['RequiresPHP'] : '', + ); + + $compatible_wp = is_wp_version_compatible( $requirements['requires'] ); + $compatible_php = is_php_version_compatible( $requirements['requires_php'] ); + + $php_update_message = '

    ' . sprintf( + /* translators: %s: URL to Update PHP page. */ + __( 'Learn more about updating PHP.' ), + esc_url( wp_get_update_php_url() ) + ); + + $annotation = wp_get_update_php_annotation(); + + if ( $annotation ) { + $php_update_message .= '

    ' . $annotation . ''; + } + + if ( ! $compatible_wp && ! $compatible_php ) { + return new WP_Error( + 'plugin_wp_php_incompatible', + '

    ' . sprintf( + /* translators: 1: Current WordPress version, 2: Current PHP version, 3: Plugin name, 4: Required WordPress version, 5: Required PHP version. */ + _x( 'Error: Current versions of WordPress (%1$s) and PHP (%2$s) do not meet minimum requirements for %3$s. The plugin requires WordPress %4$s and PHP %5$s.', 'plugin' ), + get_bloginfo( 'version' ), + PHP_VERSION, + $plugin_headers['Name'], + $requirements['requires'], + $requirements['requires_php'] + ) . $php_update_message . '

    ' + ); + } elseif ( ! $compatible_php ) { + return new WP_Error( + 'plugin_php_incompatible', + '

    ' . sprintf( + /* translators: 1: Current PHP version, 2: Plugin name, 3: Required PHP version. */ + _x( 'Error: Current PHP version (%1$s) does not meet minimum requirements for %2$s. The plugin requires PHP %3$s.', 'plugin' ), + PHP_VERSION, + $plugin_headers['Name'], + $requirements['requires_php'] + ) . $php_update_message . '

    ' + ); + } elseif ( ! $compatible_wp ) { + return new WP_Error( + 'plugin_wp_incompatible', + '

    ' . sprintf( + /* translators: 1: Current WordPress version, 2: Plugin name, 3: Required WordPress version. */ + _x( 'Error: Current WordPress version (%1$s) does not meet minimum requirements for %2$s. The plugin requires WordPress %3$s.', 'plugin' ), + get_bloginfo( 'version' ), + $plugin_headers['Name'], + $requirements['requires'] + ) . '

    ' + ); + } + + return true; +} + +/** + * Determines whether the plugin can be uninstalled. + * + * @since 2.7.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return bool Whether plugin can be uninstalled. + */ +function is_uninstallable_plugin( $plugin ) { + $file = plugin_basename( $plugin ); + + $uninstallable_plugins = (array) get_option( 'uninstall_plugins' ); + if ( isset( $uninstallable_plugins[ $file ] ) || file_exists( WP_PLUGIN_DIR . '/' . dirname( $file ) . '/uninstall.php' ) ) { + return true; + } + + return false; +} + +/** + * Uninstalls a single plugin. + * + * Calls the uninstall hook, if it is available. + * + * @since 2.7.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return true|void True if a plugin's uninstall.php file has been found and included. + * Void otherwise. + */ +function uninstall_plugin( $plugin ) { + $file = plugin_basename( $plugin ); + + $uninstallable_plugins = (array) get_option( 'uninstall_plugins' ); + + /** + * Fires in uninstall_plugin() immediately before the plugin is uninstalled. + * + * @since 4.5.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @param array $uninstallable_plugins Uninstallable plugins. + */ + do_action( 'pre_uninstall_plugin', $plugin, $uninstallable_plugins ); + + if ( file_exists( WP_PLUGIN_DIR . '/' . dirname( $file ) . '/uninstall.php' ) ) { + if ( isset( $uninstallable_plugins[ $file ] ) ) { + unset( $uninstallable_plugins[ $file ] ); + update_option( 'uninstall_plugins', $uninstallable_plugins ); + } + unset( $uninstallable_plugins ); + + define( 'WP_UNINSTALL_PLUGIN', $file ); + + wp_register_plugin_realpath( WP_PLUGIN_DIR . '/' . $file ); + include_once WP_PLUGIN_DIR . '/' . dirname( $file ) . '/uninstall.php'; + + return true; + } + + if ( isset( $uninstallable_plugins[ $file ] ) ) { + $callable = $uninstallable_plugins[ $file ]; + unset( $uninstallable_plugins[ $file ] ); + update_option( 'uninstall_plugins', $uninstallable_plugins ); + unset( $uninstallable_plugins ); + + wp_register_plugin_realpath( WP_PLUGIN_DIR . '/' . $file ); + include_once WP_PLUGIN_DIR . '/' . $file; + + add_action( "uninstall_{$file}", $callable ); + + /** + * Fires in uninstall_plugin() once the plugin has been uninstalled. + * + * The action concatenates the 'uninstall_' prefix with the basename of the + * plugin passed to uninstall_plugin() to create a dynamically-named action. + * + * @since 2.7.0 + */ + do_action( "uninstall_{$file}" ); + } +} + +// +// Menu. +// + +/** + * Adds a top-level menu page. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 1.5.0 + * + * @global array $menu + * @global array $admin_page_hooks + * @global array $_registered_pages + * @global array $_parent_pages + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu page and only + * include lowercase alphanumeric, dashes, and underscores characters to be compatible + * with sanitize_key(). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param string $icon_url Optional. The URL to the icon to be used for this menu. + * * Pass a base64-encoded SVG using a data URI, which will be colored to match + * the color scheme. This should begin with 'data:image/svg+xml;base64,'. + * * Pass the name of a Dashicons helper class to use a font icon, + * e.g. 'dashicons-chart-pie'. + * * Pass 'none' to leave div.wp-menu-image empty so an icon can be added via CSS. + * @param int|float $position Optional. The position in the menu order this item should appear. + * @return string The resulting page's hook_suffix. + */ +function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $icon_url = '', $position = null ) { + global $menu, $admin_page_hooks, $_registered_pages, $_parent_pages; + + $menu_slug = plugin_basename( $menu_slug ); + + $admin_page_hooks[ $menu_slug ] = sanitize_title( $menu_title ); + + $hookname = get_plugin_page_hookname( $menu_slug, '' ); + + if ( ! empty( $callback ) && ! empty( $hookname ) && current_user_can( $capability ) ) { + add_action( $hookname, $callback ); + } + + if ( empty( $icon_url ) ) { + $icon_url = 'dashicons-admin-generic'; + $icon_class = 'menu-icon-generic '; + } else { + $icon_url = set_url_scheme( $icon_url ); + $icon_class = ''; + } + + $new_menu = array( $menu_title, $capability, $menu_slug, $page_title, 'menu-top ' . $icon_class . $hookname, $hookname, $icon_url ); + + if ( null !== $position && ! is_numeric( $position ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: add_menu_page() */ + __( 'The seventh parameter passed to %s should be numeric representing menu position.' ), + 'add_menu_page()' + ), + '6.0.0' + ); + $position = null; + } + + if ( null === $position || ! is_numeric( $position ) ) { + $menu[] = $new_menu; + } elseif ( isset( $menu[ (string) $position ] ) ) { + $collision_avoider = base_convert( substr( md5( $menu_slug . $menu_title ), -4 ), 16, 10 ) * 0.00001; + $position = (string) ( $position + $collision_avoider ); + $menu[ $position ] = $new_menu; + } else { + /* + * Cast menu position to a string. + * + * This allows for floats to be passed as the position. PHP will normally cast a float to an + * integer value, this ensures the float retains its mantissa (positive fractional part). + * + * A string containing an integer value, eg "10", is treated as a numeric index. + */ + $position = (string) $position; + $menu[ $position ] = $new_menu; + } + + $_registered_pages[ $hookname ] = true; + + // No parent as top level. + $_parent_pages[ $menu_slug ] = false; + + return $hookname; +} + +/** + * Adds a submenu page. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 1.5.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @global array $submenu + * @global array $menu + * @global array $_wp_real_parent_file + * @global bool $_wp_submenu_nopriv + * @global array $_registered_pages + * @global array $_parent_pages + * + * @param string $parent_slug The slug name for the parent menu (or the file name of a standard + * WordPress admin page). + * @param string $page_title The text to be displayed in the title tags of the page when the menu + * is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu + * and only include lowercase alphanumeric, dashes, and underscores characters + * to be compatible with sanitize_key(). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int|float $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + global $submenu, $menu, $_wp_real_parent_file, $_wp_submenu_nopriv, + $_registered_pages, $_parent_pages; + + $menu_slug = plugin_basename( $menu_slug ); + $parent_slug = plugin_basename( $parent_slug ); + + if ( isset( $_wp_real_parent_file[ $parent_slug ] ) ) { + $parent_slug = $_wp_real_parent_file[ $parent_slug ]; + } + + if ( ! current_user_can( $capability ) ) { + $_wp_submenu_nopriv[ $parent_slug ][ $menu_slug ] = true; + return false; + } + + /* + * If the parent doesn't already have a submenu, add a link to the parent + * as the first item in the submenu. If the submenu file is the same as the + * parent file someone is trying to link back to the parent manually. In + * this case, don't automatically add a link back to avoid duplication. + */ + if ( ! isset( $submenu[ $parent_slug ] ) && $menu_slug !== $parent_slug ) { + foreach ( (array) $menu as $parent_menu ) { + if ( $parent_menu[2] === $parent_slug && current_user_can( $parent_menu[1] ) ) { + $submenu[ $parent_slug ][] = array_slice( $parent_menu, 0, 4 ); + } + } + } + + $new_sub_menu = array( $menu_title, $capability, $menu_slug, $page_title ); + + if ( null !== $position && ! is_numeric( $position ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: add_submenu_page() */ + __( 'The seventh parameter passed to %s should be numeric representing menu position.' ), + 'add_submenu_page()' + ), + '5.3.0' + ); + $position = null; + } + + if ( + null === $position || + ( ! isset( $submenu[ $parent_slug ] ) || $position >= count( $submenu[ $parent_slug ] ) ) + ) { + $submenu[ $parent_slug ][] = $new_sub_menu; + } else { + // Test for a negative position. + $position = max( $position, 0 ); + if ( 0 === $position ) { + // For negative or `0` positions, prepend the submenu. + array_unshift( $submenu[ $parent_slug ], $new_sub_menu ); + } else { + $position = absint( $position ); + // Grab all of the items before the insertion point. + $before_items = array_slice( $submenu[ $parent_slug ], 0, $position, true ); + // Grab all of the items after the insertion point. + $after_items = array_slice( $submenu[ $parent_slug ], $position, null, true ); + // Add the new item. + $before_items[] = $new_sub_menu; + // Merge the items. + $submenu[ $parent_slug ] = array_merge( $before_items, $after_items ); + } + } + + // Sort the parent array. + ksort( $submenu[ $parent_slug ] ); + + $hookname = get_plugin_page_hookname( $menu_slug, $parent_slug ); + if ( ! empty( $callback ) && ! empty( $hookname ) ) { + add_action( $hookname, $callback ); + } + + $_registered_pages[ $hookname ] = true; + + /* + * Backward-compatibility for plugins using add_management_page(). + * See wp-admin/admin.php for redirect from edit.php to tools.php. + */ + if ( 'tools.php' === $parent_slug ) { + $_registered_pages[ get_plugin_page_hookname( $menu_slug, 'edit.php' ) ] = true; + } + + // No parent as top level. + $_parent_pages[ $menu_slug ] = $parent_slug; + + return $hookname; +} + +/** + * Adds a submenu page to the Tools main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 1.5.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_management_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'tools.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Settings main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 1.5.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_options_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'options-general.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Appearance main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 2.0.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_theme_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'themes.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Plugins main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 3.0.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_plugins_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'plugins.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Users/Profile main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 2.1.3 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_users_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + if ( current_user_can( 'edit_users' ) ) { + $parent = 'users.php'; + } else { + $parent = 'profile.php'; + } + return add_submenu_page( $parent, $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Dashboard main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 2.7.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_dashboard_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'index.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Posts main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 2.7.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_posts_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'edit.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Media main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 2.7.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_media_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'upload.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Links main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 2.7.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_links_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'link-manager.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Pages main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 2.7.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_pages_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'edit.php?post_type=page', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Adds a submenu page to the Comments main menu. + * + * This function takes a capability which will be used to determine whether + * or not a page is included in the menu. + * + * The function which is hooked in to handle the output of the page must check + * that the user has the required capability as well. + * + * @since 2.7.0 + * @since 5.3.0 Added the `$position` parameter. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $capability The capability required for this menu to be displayed to the user. + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param callable $callback Optional. The function to be called to output the content for this page. + * @param int $position Optional. The position in the menu order this item should appear. + * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + */ +function add_comments_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { + return add_submenu_page( 'edit-comments.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); +} + +/** + * Removes a top-level admin menu. + * + * Example usage: + * + * - `remove_menu_page( 'tools.php' )` + * - `remove_menu_page( 'plugin_menu_slug' )` + * + * @since 3.1.0 + * + * @global array $menu + * + * @param string $menu_slug The slug of the menu. + * @return array|false The removed menu on success, false if not found. + */ +function remove_menu_page( $menu_slug ) { + global $menu; + + foreach ( $menu as $i => $item ) { + if ( $menu_slug === $item[2] ) { + unset( $menu[ $i ] ); + return $item; + } + } + + return false; +} + +/** + * Removes an admin submenu. + * + * Example usage: + * + * - `remove_submenu_page( 'themes.php', 'nav-menus.php' )` + * - `remove_submenu_page( 'tools.php', 'plugin_submenu_slug' )` + * - `remove_submenu_page( 'plugin_menu_slug', 'plugin_submenu_slug' )` + * + * @since 3.1.0 + * + * @global array $submenu + * + * @param string $menu_slug The slug for the parent menu. + * @param string $submenu_slug The slug of the submenu. + * @return array|false The removed submenu on success, false if not found. + */ +function remove_submenu_page( $menu_slug, $submenu_slug ) { + global $submenu; + + if ( ! isset( $submenu[ $menu_slug ] ) ) { + return false; + } + + foreach ( $submenu[ $menu_slug ] as $i => $item ) { + if ( $submenu_slug === $item[2] ) { + unset( $submenu[ $menu_slug ][ $i ] ); + return $item; + } + } + + return false; +} + +/** + * Gets the URL to access a particular menu page based on the slug it was registered with. + * + * If the slug hasn't been registered properly, no URL will be returned. + * + * @since 3.0.0 + * + * @global array $_parent_pages + * + * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu). + * @param bool $display Optional. Whether or not to display the URL. Default true. + * @return string The menu page URL. + */ +function menu_page_url( $menu_slug, $display = true ) { + global $_parent_pages; + + if ( isset( $_parent_pages[ $menu_slug ] ) ) { + $parent_slug = $_parent_pages[ $menu_slug ]; + + if ( $parent_slug && ! isset( $_parent_pages[ $parent_slug ] ) ) { + $url = admin_url( add_query_arg( 'page', $menu_slug, $parent_slug ) ); + } else { + $url = admin_url( 'admin.php?page=' . $menu_slug ); + } + } else { + $url = ''; + } + + $url = esc_url( $url ); + + if ( $display ) { + echo $url; + } + + return $url; +} + +// +// Pluggable Menu Support -- Private. +// +/** + * Gets the parent file of the current admin page. + * + * @since 1.5.0 + * + * @global string $parent_file + * @global array $menu + * @global array $submenu + * @global string $pagenow The filename of the current screen. + * @global string $typenow The post type of the current screen. + * @global string $plugin_page + * @global array $_wp_real_parent_file + * @global array $_wp_menu_nopriv + * @global array $_wp_submenu_nopriv + * + * @param string $parent_page Optional. The slug name for the parent menu (or the file name + * of a standard WordPress admin page). Default empty string. + * @return string The parent file of the current admin page. + */ +function get_admin_page_parent( $parent_page = '' ) { + global $parent_file, $menu, $submenu, $pagenow, $typenow, + $plugin_page, $_wp_real_parent_file, $_wp_menu_nopriv, $_wp_submenu_nopriv; + + if ( ! empty( $parent_page ) && 'admin.php' !== $parent_page ) { + if ( isset( $_wp_real_parent_file[ $parent_page ] ) ) { + $parent_page = $_wp_real_parent_file[ $parent_page ]; + } + + return $parent_page; + } + + if ( 'admin.php' === $pagenow && isset( $plugin_page ) ) { + foreach ( (array) $menu as $parent_menu ) { + if ( $parent_menu[2] === $plugin_page ) { + $parent_file = $plugin_page; + + if ( isset( $_wp_real_parent_file[ $parent_file ] ) ) { + $parent_file = $_wp_real_parent_file[ $parent_file ]; + } + + return $parent_file; + } + } + if ( isset( $_wp_menu_nopriv[ $plugin_page ] ) ) { + $parent_file = $plugin_page; + + if ( isset( $_wp_real_parent_file[ $parent_file ] ) ) { + $parent_file = $_wp_real_parent_file[ $parent_file ]; + } + + return $parent_file; + } + } + + if ( isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $pagenow ][ $plugin_page ] ) ) { + $parent_file = $pagenow; + + if ( isset( $_wp_real_parent_file[ $parent_file ] ) ) { + $parent_file = $_wp_real_parent_file[ $parent_file ]; + } + + return $parent_file; + } + + foreach ( array_keys( (array) $submenu ) as $parent_page ) { + foreach ( $submenu[ $parent_page ] as $submenu_array ) { + if ( isset( $_wp_real_parent_file[ $parent_page ] ) ) { + $parent_page = $_wp_real_parent_file[ $parent_page ]; + } + + if ( ! empty( $typenow ) && "$pagenow?post_type=$typenow" === $submenu_array[2] ) { + $parent_file = $parent_page; + return $parent_page; + } elseif ( empty( $typenow ) && $pagenow === $submenu_array[2] + && ( empty( $parent_file ) || ! str_contains( $parent_file, '?' ) ) + ) { + $parent_file = $parent_page; + return $parent_page; + } elseif ( isset( $plugin_page ) && $plugin_page === $submenu_array[2] ) { + $parent_file = $parent_page; + return $parent_page; + } + } + } + + if ( empty( $parent_file ) ) { + $parent_file = ''; + } + return ''; +} + +/** + * Gets the title of the current admin page. + * + * @since 1.5.0 + * + * @global string $title + * @global array $menu + * @global array $submenu + * @global string $pagenow The filename of the current screen. + * @global string $typenow The post type of the current screen. + * @global string $plugin_page + * + * @return string The title of the current admin page. + */ +function get_admin_page_title() { + global $title, $menu, $submenu, $pagenow, $typenow, $plugin_page; + + if ( ! empty( $title ) ) { + return $title; + } + + $hook = get_plugin_page_hook( $plugin_page, $pagenow ); + + $parent = get_admin_page_parent(); + $parent1 = $parent; + + if ( empty( $parent ) ) { + foreach ( (array) $menu as $menu_array ) { + if ( isset( $menu_array[3] ) ) { + if ( $menu_array[2] === $pagenow ) { + $title = $menu_array[3]; + return $menu_array[3]; + } elseif ( isset( $plugin_page ) && $plugin_page === $menu_array[2] && $hook === $menu_array[5] ) { + $title = $menu_array[3]; + return $menu_array[3]; + } + } else { + $title = $menu_array[0]; + return $title; + } + } + } else { + foreach ( array_keys( $submenu ) as $parent ) { + foreach ( $submenu[ $parent ] as $submenu_array ) { + if ( isset( $plugin_page ) + && $plugin_page === $submenu_array[2] + && ( $pagenow === $parent + || $plugin_page === $parent + || $plugin_page === $hook + || 'admin.php' === $pagenow && $parent1 !== $submenu_array[2] + || ! empty( $typenow ) && "$pagenow?post_type=$typenow" === $parent ) + ) { + $title = $submenu_array[3]; + return $submenu_array[3]; + } + + if ( $submenu_array[2] !== $pagenow || isset( $_GET['page'] ) ) { // Not the current page. + continue; + } + + if ( isset( $submenu_array[3] ) ) { + $title = $submenu_array[3]; + return $submenu_array[3]; + } else { + $title = $submenu_array[0]; + return $title; + } + } + } + if ( empty( $title ) ) { + foreach ( $menu as $menu_array ) { + if ( isset( $plugin_page ) + && $plugin_page === $menu_array[2] + && 'admin.php' === $pagenow + && $parent1 === $menu_array[2] + ) { + $title = $menu_array[3]; + return $menu_array[3]; + } + } + } + } + + return $title; +} + +/** + * Gets the hook attached to the administrative page of a plugin. + * + * @since 1.5.0 + * + * @param string $plugin_page The slug name of the plugin page. + * @param string $parent_page The slug name for the parent menu (or the file name of a standard + * WordPress admin page). + * @return string|null Hook attached to the plugin page, null otherwise. + */ +function get_plugin_page_hook( $plugin_page, $parent_page ) { + $hook = get_plugin_page_hookname( $plugin_page, $parent_page ); + if ( has_action( $hook ) ) { + return $hook; + } else { + return null; + } +} + +/** + * Gets the hook name for the administrative page of a plugin. + * + * @since 1.5.0 + * + * @global array $admin_page_hooks + * + * @param string $plugin_page The slug name of the plugin page. + * @param string $parent_page The slug name for the parent menu (or the file name of a standard + * WordPress admin page). + * @return string Hook name for the plugin page. + */ +function get_plugin_page_hookname( $plugin_page, $parent_page ) { + global $admin_page_hooks; + + $parent = get_admin_page_parent( $parent_page ); + + $page_type = 'admin'; + if ( empty( $parent_page ) || 'admin.php' === $parent_page || isset( $admin_page_hooks[ $plugin_page ] ) ) { + if ( isset( $admin_page_hooks[ $plugin_page ] ) ) { + $page_type = 'toplevel'; + } elseif ( isset( $admin_page_hooks[ $parent ] ) ) { + $page_type = $admin_page_hooks[ $parent ]; + } + } elseif ( isset( $admin_page_hooks[ $parent ] ) ) { + $page_type = $admin_page_hooks[ $parent ]; + } + + $plugin_name = preg_replace( '!\.php!', '', $plugin_page ); + + return $page_type . '_page_' . $plugin_name; +} + +/** + * Determines whether the current user can access the current admin page. + * + * @since 1.5.0 + * + * @global string $pagenow The filename of the current screen. + * @global array $menu + * @global array $submenu + * @global array $_wp_menu_nopriv + * @global array $_wp_submenu_nopriv + * @global string $plugin_page + * @global array $_registered_pages + * + * @return bool True if the current user can access the admin page, false otherwise. + */ +function user_can_access_admin_page() { + global $pagenow, $menu, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv, + $plugin_page, $_registered_pages; + + $parent = get_admin_page_parent(); + + if ( ! isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $parent ][ $pagenow ] ) ) { + return false; + } + + if ( isset( $plugin_page ) ) { + if ( isset( $_wp_submenu_nopriv[ $parent ][ $plugin_page ] ) ) { + return false; + } + + $hookname = get_plugin_page_hookname( $plugin_page, $parent ); + + if ( ! isset( $_registered_pages[ $hookname ] ) ) { + return false; + } + } + + if ( empty( $parent ) ) { + if ( isset( $_wp_menu_nopriv[ $pagenow ] ) ) { + return false; + } + if ( isset( $_wp_submenu_nopriv[ $pagenow ][ $pagenow ] ) ) { + return false; + } + if ( isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $pagenow ][ $plugin_page ] ) ) { + return false; + } + if ( isset( $plugin_page ) && isset( $_wp_menu_nopriv[ $plugin_page ] ) ) { + return false; + } + + foreach ( array_keys( $_wp_submenu_nopriv ) as $key ) { + if ( isset( $_wp_submenu_nopriv[ $key ][ $pagenow ] ) ) { + return false; + } + if ( isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $key ][ $plugin_page ] ) ) { + return false; + } + } + + return true; + } + + if ( isset( $plugin_page ) && $plugin_page === $parent && isset( $_wp_menu_nopriv[ $plugin_page ] ) ) { + return false; + } + + if ( isset( $submenu[ $parent ] ) ) { + foreach ( $submenu[ $parent ] as $submenu_array ) { + if ( isset( $plugin_page ) && $submenu_array[2] === $plugin_page ) { + return current_user_can( $submenu_array[1] ); + } elseif ( $submenu_array[2] === $pagenow ) { + return current_user_can( $submenu_array[1] ); + } + } + } + + foreach ( $menu as $menu_array ) { + if ( $menu_array[2] === $parent ) { + return current_user_can( $menu_array[1] ); + } + } + + return true; +} + +/* Allowed list functions */ + +/** + * Refreshes the value of the allowed options list available via the 'allowed_options' hook. + * + * See the {@see 'allowed_options'} filter. + * + * @since 2.7.0 + * @since 5.5.0 `$new_whitelist_options` was renamed to `$new_allowed_options`. + * Please consider writing more inclusive code. + * + * @global array $new_allowed_options + * + * @param array $options + * @return array + */ +function option_update_filter( $options ) { + global $new_allowed_options; + + if ( is_array( $new_allowed_options ) ) { + $options = add_allowed_options( $new_allowed_options, $options ); + } + + return $options; +} + +/** + * Adds an array of options to the list of allowed options. + * + * @since 5.5.0 + * + * @global array $allowed_options + * + * @param array $new_options + * @param string|array $options + * @return array + */ +function add_allowed_options( $new_options, $options = '' ) { + if ( '' === $options ) { + global $allowed_options; + } else { + $allowed_options = $options; + } + + foreach ( $new_options as $page => $keys ) { + foreach ( $keys as $key ) { + if ( ! isset( $allowed_options[ $page ] ) || ! is_array( $allowed_options[ $page ] ) ) { + $allowed_options[ $page ] = array(); + $allowed_options[ $page ][] = $key; + } else { + $pos = array_search( $key, $allowed_options[ $page ], true ); + if ( false === $pos ) { + $allowed_options[ $page ][] = $key; + } + } + } + } + + return $allowed_options; +} + +/** + * Removes a list of options from the allowed options list. + * + * @since 5.5.0 + * + * @global array $allowed_options + * + * @param array $del_options + * @param string|array $options + * @return array + */ +function remove_allowed_options( $del_options, $options = '' ) { + if ( '' === $options ) { + global $allowed_options; + } else { + $allowed_options = $options; + } + + foreach ( $del_options as $page => $keys ) { + foreach ( $keys as $key ) { + if ( isset( $allowed_options[ $page ] ) && is_array( $allowed_options[ $page ] ) ) { + $pos = array_search( $key, $allowed_options[ $page ], true ); + if ( false !== $pos ) { + unset( $allowed_options[ $page ][ $pos ] ); + } + } + } + } + + return $allowed_options; +} + +/** + * Outputs nonce, action, and option_page fields for a settings page. + * + * @since 2.7.0 + * + * @param string $option_group A settings group name. This should match the group name + * used in register_setting(). + */ +function settings_fields( $option_group ) { + echo ""; + echo ''; + wp_nonce_field( "$option_group-options" ); +} + +/** + * Clears the plugins cache used by get_plugins() and by default, the plugin updates cache. + * + * @since 3.7.0 + * + * @param bool $clear_update_cache Whether to clear the plugin updates cache. Default true. + */ +function wp_clean_plugins_cache( $clear_update_cache = true ) { + if ( $clear_update_cache ) { + delete_site_transient( 'update_plugins' ); + } + wp_cache_delete( 'plugins', 'plugins' ); +} + +/** + * Loads a given plugin attempt to generate errors. + * + * @since 3.0.0 + * @since 4.4.0 Function was moved into the `wp-admin/includes/plugin.php` file. + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + */ +function plugin_sandbox_scrape( $plugin ) { + if ( ! defined( 'WP_SANDBOX_SCRAPING' ) ) { + define( 'WP_SANDBOX_SCRAPING', true ); + } + + wp_register_plugin_realpath( WP_PLUGIN_DIR . '/' . $plugin ); + include_once WP_PLUGIN_DIR . '/' . $plugin; +} + +/** + * Declares a helper function for adding content to the Privacy Policy Guide. + * + * Plugins and themes should suggest text for inclusion in the site's privacy policy. + * The suggested text should contain information about any functionality that affects user privacy, + * and will be shown on the Privacy Policy Guide screen. + * + * A plugin or theme can use this function multiple times as long as it will help to better present + * the suggested policy content. For example modular plugins such as WooCommerse or Jetpack + * can add or remove suggested content depending on the modules/extensions that are enabled. + * For more information see the Plugin Handbook: + * https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/. + * + * The HTML contents of the `$policy_text` supports use of a specialized `.privacy-policy-tutorial` + * CSS class which can be used to provide supplemental information. Any content contained within + * HTML elements that have the `.privacy-policy-tutorial` CSS class applied will be omitted + * from the clipboard when the section content is copied. + * + * Intended for use with the `'admin_init'` action. + * + * @since 4.9.6 + * + * @param string $plugin_name The name of the plugin or theme that is suggesting content + * for the site's privacy policy. + * @param string $policy_text The suggested content for inclusion in the policy. + */ +function wp_add_privacy_policy_content( $plugin_name, $policy_text ) { + if ( ! is_admin() ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: admin_init */ + __( 'The suggested privacy policy content should be added only in wp-admin by using the %s (or later) action.' ), + 'admin_init' + ), + '4.9.7' + ); + return; + } elseif ( ! doing_action( 'admin_init' ) && ! did_action( 'admin_init' ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: admin_init */ + __( 'The suggested privacy policy content should be added by using the %s (or later) action. Please see the inline documentation.' ), + 'admin_init' + ), + '4.9.7' + ); + return; + } + + if ( ! class_exists( 'WP_Privacy_Policy_Content' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-policy-content.php'; + } + + WP_Privacy_Policy_Content::add( $plugin_name, $policy_text ); +} + +/** + * Determines whether a plugin is technically active but was paused while + * loading. + * + * For more information on this and similar theme functions, check out + * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ + * Conditional Tags} article in the Theme Developer Handbook. + * + * @since 5.2.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return bool True, if in the list of paused plugins. False, if not in the list. + */ +function is_plugin_paused( $plugin ) { + if ( ! isset( $GLOBALS['_paused_plugins'] ) ) { + return false; + } + + if ( ! is_plugin_active( $plugin ) ) { + return false; + } + + list( $plugin ) = explode( '/', $plugin ); + + return array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ); +} + +/** + * Gets the error that was recorded for a paused plugin. + * + * @since 5.2.0 + * + * @param string $plugin Path to the plugin file relative to the plugins directory. + * @return array|false Array of error information as returned by `error_get_last()`, + * or false if none was recorded. + */ +function wp_get_plugin_error( $plugin ) { + if ( ! isset( $GLOBALS['_paused_plugins'] ) ) { + return false; + } + + list( $plugin ) = explode( '/', $plugin ); + + if ( ! array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ) ) { + return false; + } + + return $GLOBALS['_paused_plugins'][ $plugin ]; +} + +/** + * Tries to resume a single plugin. + * + * If a redirect was provided, we first ensure the plugin does not throw fatal + * errors anymore. + * + * The way it works is by setting the redirection to the error before trying to + * include the plugin file. If the plugin fails, then the redirection will not + * be overwritten with the success message and the plugin will not be resumed. + * + * @since 5.2.0 + * + * @param string $plugin Single plugin to resume. + * @param string $redirect Optional. URL to redirect to. Default empty string. + * @return true|WP_Error True on success, false if `$plugin` was not paused, + * `WP_Error` on failure. + */ +function resume_plugin( $plugin, $redirect = '' ) { + /* + * We'll override this later if the plugin could be resumed without + * creating a fatal error. + */ + if ( ! empty( $redirect ) ) { + wp_redirect( + add_query_arg( + '_error_nonce', + wp_create_nonce( 'plugin-resume-error_' . $plugin ), + $redirect + ) + ); + + // Load the plugin to test whether it throws a fatal error. + ob_start(); + plugin_sandbox_scrape( $plugin ); + ob_clean(); + } + + list( $extension ) = explode( '/', $plugin ); + + $result = wp_paused_plugins()->delete( $extension ); + + if ( ! $result ) { + return new WP_Error( + 'could_not_resume_plugin', + __( 'Could not resume the plugin.' ) + ); + } + + return true; +} + +/** + * Renders an admin notice in case some plugins have been paused due to errors. + * + * @since 5.2.0 + * + * @global string $pagenow The filename of the current screen. + */ +function paused_plugins_notice() { + if ( 'plugins.php' === $GLOBALS['pagenow'] ) { + return; + } + + if ( ! current_user_can( 'resume_plugins' ) ) { + return; + } + + if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) { + return; + } + + $message = sprintf( + '%s
    %s

    %s', + __( 'One or more plugins failed to load properly.' ), + __( 'You can find more details and make changes on the Plugins screen.' ), + esc_url( admin_url( 'plugins.php?plugin_status=paused' ) ), + __( 'Go to the Plugins screen' ) + ); + wp_admin_notice( + $message, + array( 'type' => 'error' ) + ); +} + +/** + * Renders an admin notice when a plugin was deactivated during an update. + * + * Displays an admin notice in case a plugin has been deactivated during an + * upgrade due to incompatibility with the current version of WordPress. + * + * @since 5.8.0 + * @access private + * + * @global string $pagenow The filename of the current screen. + * @global string $wp_version The WordPress version string. + */ +function deactivated_plugins_notice() { + if ( 'plugins.php' === $GLOBALS['pagenow'] ) { + return; + } + + if ( ! current_user_can( 'activate_plugins' ) ) { + return; + } + + $blog_deactivated_plugins = get_option( 'wp_force_deactivated_plugins' ); + $site_deactivated_plugins = array(); + + if ( false === $blog_deactivated_plugins ) { + // Option not in database, add an empty array to avoid extra DB queries on subsequent loads. + update_option( 'wp_force_deactivated_plugins', array() ); + } + + if ( is_multisite() ) { + $site_deactivated_plugins = get_site_option( 'wp_force_deactivated_plugins' ); + if ( false === $site_deactivated_plugins ) { + // Option not in database, add an empty array to avoid extra DB queries on subsequent loads. + update_site_option( 'wp_force_deactivated_plugins', array() ); + } + } + + if ( empty( $blog_deactivated_plugins ) && empty( $site_deactivated_plugins ) ) { + // No deactivated plugins. + return; + } + + $deactivated_plugins = array_merge( $blog_deactivated_plugins, $site_deactivated_plugins ); + + foreach ( $deactivated_plugins as $plugin ) { + if ( ! empty( $plugin['version_compatible'] ) && ! empty( $plugin['version_deactivated'] ) ) { + $explanation = sprintf( + /* translators: 1: Name of deactivated plugin, 2: Plugin version deactivated, 3: Current WP version, 4: Compatible plugin version. */ + __( '%1$s %2$s was deactivated due to incompatibility with WordPress %3$s, please upgrade to %1$s %4$s or later.' ), + $plugin['plugin_name'], + $plugin['version_deactivated'], + $GLOBALS['wp_version'], + $plugin['version_compatible'] + ); + } else { + $explanation = sprintf( + /* translators: 1: Name of deactivated plugin, 2: Plugin version deactivated, 3: Current WP version. */ + __( '%1$s %2$s was deactivated due to incompatibility with WordPress %3$s.' ), + $plugin['plugin_name'], + ! empty( $plugin['version_deactivated'] ) ? $plugin['version_deactivated'] : '', + $GLOBALS['wp_version'], + $plugin['version_compatible'] + ); + } + + $message = sprintf( + '%s
    %s

    %s', + sprintf( + /* translators: %s: Name of deactivated plugin. */ + __( '%s plugin deactivated during WordPress upgrade.' ), + $plugin['plugin_name'] + ), + $explanation, + esc_url( admin_url( 'plugins.php?plugin_status=inactive' ) ), + __( 'Go to the Plugins screen' ) + ); + wp_admin_notice( $message, array( 'type' => 'warning' ) ); + } + + // Empty the options. + update_option( 'wp_force_deactivated_plugins', array() ); + if ( is_multisite() ) { + update_site_option( 'wp_force_deactivated_plugins', array() ); + } +} diff --git a/wp-admin/includes/post.php b/wp-admin/includes/post.php new file mode 100644 index 0000000..b9986d1 --- /dev/null +++ b/wp-admin/includes/post.php @@ -0,0 +1,2637 @@ +cap->create_posts ) ) { + if ( 'page' === $post_data['post_type'] ) { + return new WP_Error( 'edit_others_pages', __( 'Sorry, you are not allowed to create pages as this user.' ) ); + } else { + return new WP_Error( 'edit_others_posts', __( 'Sorry, you are not allowed to create posts as this user.' ) ); + } + } + + if ( isset( $post_data['content'] ) ) { + $post_data['post_content'] = $post_data['content']; + } + + if ( isset( $post_data['excerpt'] ) ) { + $post_data['post_excerpt'] = $post_data['excerpt']; + } + + if ( isset( $post_data['parent_id'] ) ) { + $post_data['post_parent'] = (int) $post_data['parent_id']; + } + + if ( isset( $post_data['trackback_url'] ) ) { + $post_data['to_ping'] = $post_data['trackback_url']; + } + + $post_data['user_ID'] = get_current_user_id(); + + if ( ! empty( $post_data['post_author_override'] ) ) { + $post_data['post_author'] = (int) $post_data['post_author_override']; + } else { + if ( ! empty( $post_data['post_author'] ) ) { + $post_data['post_author'] = (int) $post_data['post_author']; + } else { + $post_data['post_author'] = (int) $post_data['user_ID']; + } + } + + if ( isset( $post_data['user_ID'] ) && ( $post_data['post_author'] != $post_data['user_ID'] ) + && ! current_user_can( $ptype->cap->edit_others_posts ) ) { + + if ( $update ) { + if ( 'page' === $post_data['post_type'] ) { + return new WP_Error( 'edit_others_pages', __( 'Sorry, you are not allowed to edit pages as this user.' ) ); + } else { + return new WP_Error( 'edit_others_posts', __( 'Sorry, you are not allowed to edit posts as this user.' ) ); + } + } else { + if ( 'page' === $post_data['post_type'] ) { + return new WP_Error( 'edit_others_pages', __( 'Sorry, you are not allowed to create pages as this user.' ) ); + } else { + return new WP_Error( 'edit_others_posts', __( 'Sorry, you are not allowed to create posts as this user.' ) ); + } + } + } + + if ( ! empty( $post_data['post_status'] ) ) { + $post_data['post_status'] = sanitize_key( $post_data['post_status'] ); + + // No longer an auto-draft. + if ( 'auto-draft' === $post_data['post_status'] ) { + $post_data['post_status'] = 'draft'; + } + + if ( ! get_post_status_object( $post_data['post_status'] ) ) { + unset( $post_data['post_status'] ); + } + } + + // What to do based on which button they pressed. + if ( isset( $post_data['saveasdraft'] ) && '' !== $post_data['saveasdraft'] ) { + $post_data['post_status'] = 'draft'; + } + if ( isset( $post_data['saveasprivate'] ) && '' !== $post_data['saveasprivate'] ) { + $post_data['post_status'] = 'private'; + } + if ( isset( $post_data['publish'] ) && ( '' !== $post_data['publish'] ) + && ( ! isset( $post_data['post_status'] ) || 'private' !== $post_data['post_status'] ) + ) { + $post_data['post_status'] = 'publish'; + } + if ( isset( $post_data['advanced'] ) && '' !== $post_data['advanced'] ) { + $post_data['post_status'] = 'draft'; + } + if ( isset( $post_data['pending'] ) && '' !== $post_data['pending'] ) { + $post_data['post_status'] = 'pending'; + } + + if ( isset( $post_data['ID'] ) ) { + $post_id = $post_data['ID']; + } else { + $post_id = false; + } + $previous_status = $post_id ? get_post_field( 'post_status', $post_id ) : false; + + if ( isset( $post_data['post_status'] ) && 'private' === $post_data['post_status'] && ! current_user_can( $ptype->cap->publish_posts ) ) { + $post_data['post_status'] = $previous_status ? $previous_status : 'pending'; + } + + $published_statuses = array( 'publish', 'future' ); + + /* + * Posts 'submitted for approval' are submitted to $_POST the same as if they were being published. + * Change status from 'publish' to 'pending' if user lacks permissions to publish or to resave published posts. + */ + if ( isset( $post_data['post_status'] ) + && ( in_array( $post_data['post_status'], $published_statuses, true ) + && ! current_user_can( $ptype->cap->publish_posts ) ) + ) { + if ( ! in_array( $previous_status, $published_statuses, true ) || ! current_user_can( 'edit_post', $post_id ) ) { + $post_data['post_status'] = 'pending'; + } + } + + if ( ! isset( $post_data['post_status'] ) ) { + $post_data['post_status'] = 'auto-draft' === $previous_status ? 'draft' : $previous_status; + } + + if ( isset( $post_data['post_password'] ) && ! current_user_can( $ptype->cap->publish_posts ) ) { + unset( $post_data['post_password'] ); + } + + if ( ! isset( $post_data['comment_status'] ) ) { + $post_data['comment_status'] = 'closed'; + } + + if ( ! isset( $post_data['ping_status'] ) ) { + $post_data['ping_status'] = 'closed'; + } + + foreach ( array( 'aa', 'mm', 'jj', 'hh', 'mn' ) as $timeunit ) { + if ( ! empty( $post_data[ 'hidden_' . $timeunit ] ) && $post_data[ 'hidden_' . $timeunit ] != $post_data[ $timeunit ] ) { + $post_data['edit_date'] = '1'; + break; + } + } + + if ( ! empty( $post_data['edit_date'] ) ) { + $aa = $post_data['aa']; + $mm = $post_data['mm']; + $jj = $post_data['jj']; + $hh = $post_data['hh']; + $mn = $post_data['mn']; + $ss = $post_data['ss']; + $aa = ( $aa <= 0 ) ? gmdate( 'Y' ) : $aa; + $mm = ( $mm <= 0 ) ? gmdate( 'n' ) : $mm; + $jj = ( $jj > 31 ) ? 31 : $jj; + $jj = ( $jj <= 0 ) ? gmdate( 'j' ) : $jj; + $hh = ( $hh > 23 ) ? $hh - 24 : $hh; + $mn = ( $mn > 59 ) ? $mn - 60 : $mn; + $ss = ( $ss > 59 ) ? $ss - 60 : $ss; + + $post_data['post_date'] = sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $aa, $mm, $jj, $hh, $mn, $ss ); + + $valid_date = wp_checkdate( $mm, $jj, $aa, $post_data['post_date'] ); + if ( ! $valid_date ) { + return new WP_Error( 'invalid_date', __( 'Invalid date.' ) ); + } + + /* + * Only assign a post date if the user has explicitly set a new value. + * See #59125 and #19907. + */ + $previous_date = $post_id ? get_post_field( 'post_date', $post_id ) : false; + if ( $previous_date && $previous_date !== $post_data['post_date'] ) { + $post_data['edit_date'] = true; + $post_data['post_date_gmt'] = get_gmt_from_date( $post_data['post_date'] ); + } else { + $post_data['edit_date'] = false; + unset( $post_data['post_date'] ); + unset( $post_data['post_date_gmt'] ); + } + } + + if ( isset( $post_data['post_category'] ) ) { + $category_object = get_taxonomy( 'category' ); + if ( ! current_user_can( $category_object->cap->assign_terms ) ) { + unset( $post_data['post_category'] ); + } + } + + return $post_data; +} + +/** + * Returns only allowed post data fields. + * + * @since 5.0.1 + * + * @param array|WP_Error|null $post_data The array of post data to process, or an error object. + * Defaults to the `$_POST` superglobal. + * @return array|WP_Error Array of post data on success, WP_Error on failure. + */ +function _wp_get_allowed_postdata( $post_data = null ) { + if ( empty( $post_data ) ) { + $post_data = $_POST; + } + + // Pass through errors. + if ( is_wp_error( $post_data ) ) { + return $post_data; + } + + return array_diff_key( $post_data, array_flip( array( 'meta_input', 'file', 'guid' ) ) ); +} + +/** + * Updates an existing post with values provided in `$_POST`. + * + * If post data is passed as an argument, it is treated as an array of data + * keyed appropriately for turning into a post object. + * + * If post data is not passed, the `$_POST` global variable is used instead. + * + * @since 1.5.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param array|null $post_data Optional. The array of post data to process. + * Defaults to the `$_POST` superglobal. + * @return int Post ID. + */ +function edit_post( $post_data = null ) { + global $wpdb; + + if ( empty( $post_data ) ) { + $post_data = &$_POST; + } + + // Clear out any data in internal vars. + unset( $post_data['filter'] ); + + $post_id = (int) $post_data['post_ID']; + $post = get_post( $post_id ); + + $post_data['post_type'] = $post->post_type; + $post_data['post_mime_type'] = $post->post_mime_type; + + if ( ! empty( $post_data['post_status'] ) ) { + $post_data['post_status'] = sanitize_key( $post_data['post_status'] ); + + if ( 'inherit' === $post_data['post_status'] ) { + unset( $post_data['post_status'] ); + } + } + + $ptype = get_post_type_object( $post_data['post_type'] ); + if ( ! current_user_can( 'edit_post', $post_id ) ) { + if ( 'page' === $post_data['post_type'] ) { + wp_die( __( 'Sorry, you are not allowed to edit this page.' ) ); + } else { + wp_die( __( 'Sorry, you are not allowed to edit this post.' ) ); + } + } + + if ( post_type_supports( $ptype->name, 'revisions' ) ) { + $revisions = wp_get_post_revisions( + $post_id, + array( + 'order' => 'ASC', + 'posts_per_page' => 1, + ) + ); + $revision = current( $revisions ); + + // Check if the revisions have been upgraded. + if ( $revisions && _wp_get_post_revision_version( $revision ) < 1 ) { + _wp_upgrade_revisions_of_post( $post, wp_get_post_revisions( $post_id ) ); + } + } + + if ( isset( $post_data['visibility'] ) ) { + switch ( $post_data['visibility'] ) { + case 'public': + $post_data['post_password'] = ''; + break; + case 'password': + unset( $post_data['sticky'] ); + break; + case 'private': + $post_data['post_status'] = 'private'; + $post_data['post_password'] = ''; + unset( $post_data['sticky'] ); + break; + } + } + + $post_data = _wp_translate_postdata( true, $post_data ); + if ( is_wp_error( $post_data ) ) { + wp_die( $post_data->get_error_message() ); + } + $translated = _wp_get_allowed_postdata( $post_data ); + + // Post formats. + if ( isset( $post_data['post_format'] ) ) { + set_post_format( $post_id, $post_data['post_format'] ); + } + + $format_meta_urls = array( 'url', 'link_url', 'quote_source_url' ); + foreach ( $format_meta_urls as $format_meta_url ) { + $keyed = '_format_' . $format_meta_url; + if ( isset( $post_data[ $keyed ] ) ) { + update_post_meta( $post_id, $keyed, wp_slash( sanitize_url( wp_unslash( $post_data[ $keyed ] ) ) ) ); + } + } + + $format_keys = array( 'quote', 'quote_source_name', 'image', 'gallery', 'audio_embed', 'video_embed' ); + + foreach ( $format_keys as $key ) { + $keyed = '_format_' . $key; + if ( isset( $post_data[ $keyed ] ) ) { + if ( current_user_can( 'unfiltered_html' ) ) { + update_post_meta( $post_id, $keyed, $post_data[ $keyed ] ); + } else { + update_post_meta( $post_id, $keyed, wp_filter_post_kses( $post_data[ $keyed ] ) ); + } + } + } + + if ( 'attachment' === $post_data['post_type'] && preg_match( '#^(audio|video)/#', $post_data['post_mime_type'] ) ) { + $id3data = wp_get_attachment_metadata( $post_id ); + if ( ! is_array( $id3data ) ) { + $id3data = array(); + } + + foreach ( wp_get_attachment_id3_keys( $post, 'edit' ) as $key => $label ) { + if ( isset( $post_data[ 'id3_' . $key ] ) ) { + $id3data[ $key ] = sanitize_text_field( wp_unslash( $post_data[ 'id3_' . $key ] ) ); + } + } + wp_update_attachment_metadata( $post_id, $id3data ); + } + + // Meta stuff. + if ( isset( $post_data['meta'] ) && $post_data['meta'] ) { + foreach ( $post_data['meta'] as $key => $value ) { + $meta = get_post_meta_by_id( $key ); + if ( ! $meta ) { + continue; + } + + if ( $meta->post_id != $post_id ) { + continue; + } + + if ( is_protected_meta( $meta->meta_key, 'post' ) + || ! current_user_can( 'edit_post_meta', $post_id, $meta->meta_key ) + ) { + continue; + } + + if ( is_protected_meta( $value['key'], 'post' ) + || ! current_user_can( 'edit_post_meta', $post_id, $value['key'] ) + ) { + continue; + } + + update_meta( $key, $value['key'], $value['value'] ); + } + } + + if ( isset( $post_data['deletemeta'] ) && $post_data['deletemeta'] ) { + foreach ( $post_data['deletemeta'] as $key => $value ) { + $meta = get_post_meta_by_id( $key ); + if ( ! $meta ) { + continue; + } + + if ( $meta->post_id != $post_id ) { + continue; + } + + if ( is_protected_meta( $meta->meta_key, 'post' ) + || ! current_user_can( 'delete_post_meta', $post_id, $meta->meta_key ) + ) { + continue; + } + + delete_meta( $key ); + } + } + + // Attachment stuff. + if ( 'attachment' === $post_data['post_type'] ) { + if ( isset( $post_data['_wp_attachment_image_alt'] ) ) { + $image_alt = wp_unslash( $post_data['_wp_attachment_image_alt'] ); + + if ( get_post_meta( $post_id, '_wp_attachment_image_alt', true ) !== $image_alt ) { + $image_alt = wp_strip_all_tags( $image_alt, true ); + + // update_post_meta() expects slashed. + update_post_meta( $post_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) ); + } + } + + $attachment_data = isset( $post_data['attachments'][ $post_id ] ) ? $post_data['attachments'][ $post_id ] : array(); + + /** This filter is documented in wp-admin/includes/media.php */ + $translated = apply_filters( 'attachment_fields_to_save', $translated, $attachment_data ); + } + + // Convert taxonomy input to term IDs, to avoid ambiguity. + if ( isset( $post_data['tax_input'] ) ) { + foreach ( (array) $post_data['tax_input'] as $taxonomy => $terms ) { + $tax_object = get_taxonomy( $taxonomy ); + + if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) { + $translated['tax_input'][ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) ); + } + } + } + + add_meta( $post_id ); + + update_post_meta( $post_id, '_edit_last', get_current_user_id() ); + + $success = wp_update_post( $translated ); + + // If the save failed, see if we can sanity check the main fields and try again. + if ( ! $success && is_callable( array( $wpdb, 'strip_invalid_text_for_column' ) ) ) { + $fields = array( 'post_title', 'post_content', 'post_excerpt' ); + + foreach ( $fields as $field ) { + if ( isset( $translated[ $field ] ) ) { + $translated[ $field ] = $wpdb->strip_invalid_text_for_column( $wpdb->posts, $field, $translated[ $field ] ); + } + } + + wp_update_post( $translated ); + } + + // Now that we have an ID we can fix any attachment anchor hrefs. + _fix_attachment_links( $post_id ); + + wp_set_post_lock( $post_id ); + + if ( current_user_can( $ptype->cap->edit_others_posts ) && current_user_can( $ptype->cap->publish_posts ) ) { + if ( ! empty( $post_data['sticky'] ) ) { + stick_post( $post_id ); + } else { + unstick_post( $post_id ); + } + } + + return $post_id; +} + +/** + * Processes the post data for the bulk editing of posts. + * + * Updates all bulk edited posts/pages, adding (but not removing) tags and + * categories. Skips pages when they would be their own parent or child. + * + * @since 2.7.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param array|null $post_data Optional. The array of post data to process. + * Defaults to the `$_POST` superglobal. + * @return array + */ +function bulk_edit_posts( $post_data = null ) { + global $wpdb; + + if ( empty( $post_data ) ) { + $post_data = &$_POST; + } + + if ( isset( $post_data['post_type'] ) ) { + $ptype = get_post_type_object( $post_data['post_type'] ); + } else { + $ptype = get_post_type_object( 'post' ); + } + + if ( ! current_user_can( $ptype->cap->edit_posts ) ) { + if ( 'page' === $ptype->name ) { + wp_die( __( 'Sorry, you are not allowed to edit pages.' ) ); + } else { + wp_die( __( 'Sorry, you are not allowed to edit posts.' ) ); + } + } + + if ( -1 == $post_data['_status'] ) { + $post_data['post_status'] = null; + unset( $post_data['post_status'] ); + } else { + $post_data['post_status'] = $post_data['_status']; + } + unset( $post_data['_status'] ); + + if ( ! empty( $post_data['post_status'] ) ) { + $post_data['post_status'] = sanitize_key( $post_data['post_status'] ); + + if ( 'inherit' === $post_data['post_status'] ) { + unset( $post_data['post_status'] ); + } + } + + $post_ids = array_map( 'intval', (array) $post_data['post'] ); + + $reset = array( + 'post_author', + 'post_status', + 'post_password', + 'post_parent', + 'page_template', + 'comment_status', + 'ping_status', + 'keep_private', + 'tax_input', + 'post_category', + 'sticky', + 'post_format', + ); + + foreach ( $reset as $field ) { + if ( isset( $post_data[ $field ] ) && ( '' === $post_data[ $field ] || -1 == $post_data[ $field ] ) ) { + unset( $post_data[ $field ] ); + } + } + + if ( isset( $post_data['post_category'] ) ) { + if ( is_array( $post_data['post_category'] ) && ! empty( $post_data['post_category'] ) ) { + $new_cats = array_map( 'absint', $post_data['post_category'] ); + } else { + unset( $post_data['post_category'] ); + } + } + + $tax_input = array(); + if ( isset( $post_data['tax_input'] ) ) { + foreach ( $post_data['tax_input'] as $tax_name => $terms ) { + if ( empty( $terms ) ) { + continue; + } + + if ( is_taxonomy_hierarchical( $tax_name ) ) { + $tax_input[ $tax_name ] = array_map( 'absint', $terms ); + } else { + $comma = _x( ',', 'tag delimiter' ); + if ( ',' !== $comma ) { + $terms = str_replace( $comma, ',', $terms ); + } + $tax_input[ $tax_name ] = explode( ',', trim( $terms, " \n\t\r\0\x0B," ) ); + } + } + } + + if ( isset( $post_data['post_parent'] ) && (int) $post_data['post_parent'] ) { + $parent = (int) $post_data['post_parent']; + $pages = $wpdb->get_results( "SELECT ID, post_parent FROM $wpdb->posts WHERE post_type = 'page'" ); + $children = array(); + + for ( $i = 0; $i < 50 && $parent > 0; $i++ ) { + $children[] = $parent; + + foreach ( $pages as $page ) { + if ( (int) $page->ID === $parent ) { + $parent = (int) $page->post_parent; + break; + } + } + } + } + + $updated = array(); + $skipped = array(); + $locked = array(); + $shared_post_data = $post_data; + + foreach ( $post_ids as $post_id ) { + // Start with fresh post data with each iteration. + $post_data = $shared_post_data; + + $post_type_object = get_post_type_object( get_post_type( $post_id ) ); + + if ( ! isset( $post_type_object ) + || ( isset( $children ) && in_array( $post_id, $children, true ) ) + || ! current_user_can( 'edit_post', $post_id ) + ) { + $skipped[] = $post_id; + continue; + } + + if ( wp_check_post_lock( $post_id ) ) { + $locked[] = $post_id; + continue; + } + + $post = get_post( $post_id ); + $tax_names = get_object_taxonomies( $post ); + + foreach ( $tax_names as $tax_name ) { + $taxonomy_obj = get_taxonomy( $tax_name ); + + if ( ! $taxonomy_obj->show_in_quick_edit ) { + continue; + } + + if ( isset( $tax_input[ $tax_name ] ) && current_user_can( $taxonomy_obj->cap->assign_terms ) ) { + $new_terms = $tax_input[ $tax_name ]; + } else { + $new_terms = array(); + } + + if ( $taxonomy_obj->hierarchical ) { + $current_terms = (array) wp_get_object_terms( $post_id, $tax_name, array( 'fields' => 'ids' ) ); + } else { + $current_terms = (array) wp_get_object_terms( $post_id, $tax_name, array( 'fields' => 'names' ) ); + } + + $post_data['tax_input'][ $tax_name ] = array_merge( $current_terms, $new_terms ); + } + + if ( isset( $new_cats ) && in_array( 'category', $tax_names, true ) ) { + $cats = (array) wp_get_post_categories( $post_id ); + $post_data['post_category'] = array_unique( array_merge( $cats, $new_cats ) ); + unset( $post_data['tax_input']['category'] ); + } + + $post_data['post_ID'] = $post_id; + $post_data['post_type'] = $post->post_type; + $post_data['post_mime_type'] = $post->post_mime_type; + + foreach ( array( 'comment_status', 'ping_status', 'post_author' ) as $field ) { + if ( ! isset( $post_data[ $field ] ) ) { + $post_data[ $field ] = $post->$field; + } + } + + $post_data = _wp_translate_postdata( true, $post_data ); + if ( is_wp_error( $post_data ) ) { + $skipped[] = $post_id; + continue; + } + $post_data = _wp_get_allowed_postdata( $post_data ); + + if ( isset( $shared_post_data['post_format'] ) ) { + set_post_format( $post_id, $shared_post_data['post_format'] ); + } + + // Prevent wp_insert_post() from overwriting post format with the old data. + unset( $post_data['tax_input']['post_format'] ); + + // Reset post date of scheduled post to be published. + if ( + in_array( $post->post_status, array( 'future', 'draft' ), true ) && + 'publish' === $post_data['post_status'] + ) { + $post_data['post_date'] = current_time( 'mysql' ); + $post_data['post_date_gmt'] = ''; + } + + $post_id = wp_update_post( $post_data ); + update_post_meta( $post_id, '_edit_last', get_current_user_id() ); + $updated[] = $post_id; + + if ( isset( $post_data['sticky'] ) && current_user_can( $ptype->cap->edit_others_posts ) ) { + if ( 'sticky' === $post_data['sticky'] ) { + stick_post( $post_id ); + } else { + unstick_post( $post_id ); + } + } + } + + /** + * Fires after processing the post data for bulk edit. + * + * @since 6.3.0 + * + * @param int[] $updated An array of updated post IDs. + * @param array $shared_post_data Associative array containing the post data. + */ + do_action( 'bulk_edit_posts', $updated, $shared_post_data ); + + return array( + 'updated' => $updated, + 'skipped' => $skipped, + 'locked' => $locked, + ); +} + +/** + * Returns default post information to use when populating the "Write Post" form. + * + * @since 2.0.0 + * + * @param string $post_type Optional. A post type string. Default 'post'. + * @param bool $create_in_db Optional. Whether to insert the post into database. Default false. + * @return WP_Post Post object containing all the default post data as attributes + */ +function get_default_post_to_edit( $post_type = 'post', $create_in_db = false ) { + $post_title = ''; + if ( ! empty( $_REQUEST['post_title'] ) ) { + $post_title = esc_html( wp_unslash( $_REQUEST['post_title'] ) ); + } + + $post_content = ''; + if ( ! empty( $_REQUEST['content'] ) ) { + $post_content = esc_html( wp_unslash( $_REQUEST['content'] ) ); + } + + $post_excerpt = ''; + if ( ! empty( $_REQUEST['excerpt'] ) ) { + $post_excerpt = esc_html( wp_unslash( $_REQUEST['excerpt'] ) ); + } + + if ( $create_in_db ) { + $post_id = wp_insert_post( + array( + 'post_title' => __( 'Auto Draft' ), + 'post_type' => $post_type, + 'post_status' => 'auto-draft', + ), + false, + false + ); + $post = get_post( $post_id ); + if ( current_theme_supports( 'post-formats' ) && post_type_supports( $post->post_type, 'post-formats' ) && get_option( 'default_post_format' ) ) { + set_post_format( $post, get_option( 'default_post_format' ) ); + } + wp_after_insert_post( $post, false, null ); + + // Schedule auto-draft cleanup. + if ( ! wp_next_scheduled( 'wp_scheduled_auto_draft_delete' ) ) { + wp_schedule_event( time(), 'daily', 'wp_scheduled_auto_draft_delete' ); + } + } else { + $post = new stdClass(); + $post->ID = 0; + $post->post_author = ''; + $post->post_date = ''; + $post->post_date_gmt = ''; + $post->post_password = ''; + $post->post_name = ''; + $post->post_type = $post_type; + $post->post_status = 'draft'; + $post->to_ping = ''; + $post->pinged = ''; + $post->comment_status = get_default_comment_status( $post_type ); + $post->ping_status = get_default_comment_status( $post_type, 'pingback' ); + $post->post_pingback = get_option( 'default_pingback_flag' ); + $post->post_category = get_option( 'default_category' ); + $post->page_template = 'default'; + $post->post_parent = 0; + $post->menu_order = 0; + $post = new WP_Post( $post ); + } + + /** + * Filters the default post content initially used in the "Write Post" form. + * + * @since 1.5.0 + * + * @param string $post_content Default post content. + * @param WP_Post $post Post object. + */ + $post->post_content = (string) apply_filters( 'default_content', $post_content, $post ); + + /** + * Filters the default post title initially used in the "Write Post" form. + * + * @since 1.5.0 + * + * @param string $post_title Default post title. + * @param WP_Post $post Post object. + */ + $post->post_title = (string) apply_filters( 'default_title', $post_title, $post ); + + /** + * Filters the default post excerpt initially used in the "Write Post" form. + * + * @since 1.5.0 + * + * @param string $post_excerpt Default post excerpt. + * @param WP_Post $post Post object. + */ + $post->post_excerpt = (string) apply_filters( 'default_excerpt', $post_excerpt, $post ); + + return $post; +} + +/** + * Determines if a post exists based on title, content, date and type. + * + * @since 2.0.0 + * @since 5.2.0 Added the `$type` parameter. + * @since 5.8.0 Added the `$status` parameter. + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param string $title Post title. + * @param string $content Optional. Post content. + * @param string $date Optional. Post date. + * @param string $type Optional. Post type. + * @param string $status Optional. Post status. + * @return int Post ID if post exists, 0 otherwise. + */ +function post_exists( $title, $content = '', $date = '', $type = '', $status = '' ) { + global $wpdb; + + $post_title = wp_unslash( sanitize_post_field( 'post_title', $title, 0, 'db' ) ); + $post_content = wp_unslash( sanitize_post_field( 'post_content', $content, 0, 'db' ) ); + $post_date = wp_unslash( sanitize_post_field( 'post_date', $date, 0, 'db' ) ); + $post_type = wp_unslash( sanitize_post_field( 'post_type', $type, 0, 'db' ) ); + $post_status = wp_unslash( sanitize_post_field( 'post_status', $status, 0, 'db' ) ); + + $query = "SELECT ID FROM $wpdb->posts WHERE 1=1"; + $args = array(); + + if ( ! empty( $date ) ) { + $query .= ' AND post_date = %s'; + $args[] = $post_date; + } + + if ( ! empty( $title ) ) { + $query .= ' AND post_title = %s'; + $args[] = $post_title; + } + + if ( ! empty( $content ) ) { + $query .= ' AND post_content = %s'; + $args[] = $post_content; + } + + if ( ! empty( $type ) ) { + $query .= ' AND post_type = %s'; + $args[] = $post_type; + } + + if ( ! empty( $status ) ) { + $query .= ' AND post_status = %s'; + $args[] = $post_status; + } + + if ( ! empty( $args ) ) { + return (int) $wpdb->get_var( $wpdb->prepare( $query, $args ) ); + } + + return 0; +} + +/** + * Creates a new post from the "Write Post" form using `$_POST` information. + * + * @since 2.1.0 + * + * @global WP_User $current_user + * + * @return int|WP_Error Post ID on success, WP_Error on failure. + */ +function wp_write_post() { + if ( isset( $_POST['post_type'] ) ) { + $ptype = get_post_type_object( $_POST['post_type'] ); + } else { + $ptype = get_post_type_object( 'post' ); + } + + if ( ! current_user_can( $ptype->cap->edit_posts ) ) { + if ( 'page' === $ptype->name ) { + return new WP_Error( 'edit_pages', __( 'Sorry, you are not allowed to create pages on this site.' ) ); + } else { + return new WP_Error( 'edit_posts', __( 'Sorry, you are not allowed to create posts or drafts on this site.' ) ); + } + } + + $_POST['post_mime_type'] = ''; + + // Clear out any data in internal vars. + unset( $_POST['filter'] ); + + // Edit, don't write, if we have a post ID. + if ( isset( $_POST['post_ID'] ) ) { + return edit_post(); + } + + if ( isset( $_POST['visibility'] ) ) { + switch ( $_POST['visibility'] ) { + case 'public': + $_POST['post_password'] = ''; + break; + case 'password': + unset( $_POST['sticky'] ); + break; + case 'private': + $_POST['post_status'] = 'private'; + $_POST['post_password'] = ''; + unset( $_POST['sticky'] ); + break; + } + } + + $translated = _wp_translate_postdata( false ); + if ( is_wp_error( $translated ) ) { + return $translated; + } + $translated = _wp_get_allowed_postdata( $translated ); + + // Create the post. + $post_id = wp_insert_post( $translated ); + if ( is_wp_error( $post_id ) ) { + return $post_id; + } + + if ( empty( $post_id ) ) { + return 0; + } + + add_meta( $post_id ); + + add_post_meta( $post_id, '_edit_last', $GLOBALS['current_user']->ID ); + + // Now that we have an ID we can fix any attachment anchor hrefs. + _fix_attachment_links( $post_id ); + + wp_set_post_lock( $post_id ); + + return $post_id; +} + +/** + * Calls wp_write_post() and handles the errors. + * + * @since 2.0.0 + * + * @return int|void Post ID on success, void on failure. + */ +function write_post() { + $result = wp_write_post(); + if ( is_wp_error( $result ) ) { + wp_die( $result->get_error_message() ); + } else { + return $result; + } +} + +// +// Post Meta. +// + +/** + * Adds post meta data defined in the `$_POST` superglobal for a post with given ID. + * + * @since 1.2.0 + * + * @param int $post_id + * @return int|bool + */ +function add_meta( $post_id ) { + $post_id = (int) $post_id; + + $metakeyselect = isset( $_POST['metakeyselect'] ) ? wp_unslash( trim( $_POST['metakeyselect'] ) ) : ''; + $metakeyinput = isset( $_POST['metakeyinput'] ) ? wp_unslash( trim( $_POST['metakeyinput'] ) ) : ''; + $metavalue = isset( $_POST['metavalue'] ) ? $_POST['metavalue'] : ''; + if ( is_string( $metavalue ) ) { + $metavalue = trim( $metavalue ); + } + + if ( ( ( '#NONE#' !== $metakeyselect ) && ! empty( $metakeyselect ) ) || ! empty( $metakeyinput ) ) { + /* + * We have a key/value pair. If both the select and the input + * for the key have data, the input takes precedence. + */ + if ( '#NONE#' !== $metakeyselect ) { + $metakey = $metakeyselect; + } + + if ( $metakeyinput ) { + $metakey = $metakeyinput; // Default. + } + + if ( is_protected_meta( $metakey, 'post' ) || ! current_user_can( 'add_post_meta', $post_id, $metakey ) ) { + return false; + } + + $metakey = wp_slash( $metakey ); + + return add_post_meta( $post_id, $metakey, $metavalue ); + } + + return false; +} + +/** + * Deletes post meta data by meta ID. + * + * @since 1.2.0 + * + * @param int $mid + * @return bool + */ +function delete_meta( $mid ) { + return delete_metadata_by_mid( 'post', $mid ); +} + +/** + * Returns a list of previously defined keys. + * + * @since 1.2.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @return string[] Array of meta key names. + */ +function get_meta_keys() { + global $wpdb; + + $keys = $wpdb->get_col( + "SELECT meta_key + FROM $wpdb->postmeta + GROUP BY meta_key + ORDER BY meta_key" + ); + + return $keys; +} + +/** + * Returns post meta data by meta ID. + * + * @since 2.1.0 + * + * @param int $mid + * @return object|bool + */ +function get_post_meta_by_id( $mid ) { + return get_metadata_by_mid( 'post', $mid ); +} + +/** + * Returns meta data for the given post ID. + * + * @since 1.2.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param int $postid A post ID. + * @return array[] { + * Array of meta data arrays for the given post ID. + * + * @type array ...$0 { + * Associative array of meta data. + * + * @type string $meta_key Meta key. + * @type mixed $meta_value Meta value. + * @type string $meta_id Meta ID as a numeric string. + * @type string $post_id Post ID as a numeric string. + * } + * } + */ +function has_meta( $postid ) { + global $wpdb; + + return $wpdb->get_results( + $wpdb->prepare( + "SELECT meta_key, meta_value, meta_id, post_id + FROM $wpdb->postmeta WHERE post_id = %d + ORDER BY meta_key,meta_id", + $postid + ), + ARRAY_A + ); +} + +/** + * Updates post meta data by meta ID. + * + * @since 1.2.0 + * + * @param int $meta_id Meta ID. + * @param string $meta_key Meta key. Expect slashed. + * @param string $meta_value Meta value. Expect slashed. + * @return bool + */ +function update_meta( $meta_id, $meta_key, $meta_value ) { + $meta_key = wp_unslash( $meta_key ); + $meta_value = wp_unslash( $meta_value ); + + return update_metadata_by_mid( 'post', $meta_id, $meta_value, $meta_key ); +} + +// +// Private. +// + +/** + * Replaces hrefs of attachment anchors with up-to-date permalinks. + * + * @since 2.3.0 + * @access private + * + * @param int|object $post Post ID or post object. + * @return void|int|WP_Error Void if nothing fixed. 0 or WP_Error on update failure. The post ID on update success. + */ +function _fix_attachment_links( $post ) { + $post = get_post( $post, ARRAY_A ); + $content = $post['post_content']; + + // Don't run if no pretty permalinks or post is not published, scheduled, or privately published. + if ( ! get_option( 'permalink_structure' ) || ! in_array( $post['post_status'], array( 'publish', 'future', 'private' ), true ) ) { + return; + } + + // Short if there aren't any links or no '?attachment_id=' strings (strpos cannot be zero). + if ( ! strpos( $content, '?attachment_id=' ) || ! preg_match_all( '/]+)>[\s\S]+?<\/a>/', $content, $link_matches ) ) { + return; + } + + $site_url = get_bloginfo( 'url' ); + $site_url = substr( $site_url, (int) strpos( $site_url, '://' ) ); // Remove the http(s). + $replace = ''; + + foreach ( $link_matches[1] as $key => $value ) { + if ( ! strpos( $value, '?attachment_id=' ) || ! strpos( $value, 'wp-att-' ) + || ! preg_match( '/href=(["\'])[^"\']*\?attachment_id=(\d+)[^"\']*\\1/', $value, $url_match ) + || ! preg_match( '/rel=["\'][^"\']*wp-att-(\d+)/', $value, $rel_match ) ) { + continue; + } + + $quote = $url_match[1]; // The quote (single or double). + $url_id = (int) $url_match[2]; + $rel_id = (int) $rel_match[1]; + + if ( ! $url_id || ! $rel_id || $url_id != $rel_id || ! str_contains( $url_match[0], $site_url ) ) { + continue; + } + + $link = $link_matches[0][ $key ]; + $replace = str_replace( $url_match[0], 'href=' . $quote . get_attachment_link( $url_id ) . $quote, $link ); + + $content = str_replace( $link, $replace, $content ); + } + + if ( $replace ) { + $post['post_content'] = $content; + // Escape data pulled from DB. + $post = add_magic_quotes( $post ); + + return wp_update_post( $post ); + } +} + +/** + * Returns all the possible statuses for a post type. + * + * @since 2.5.0 + * + * @param string $type The post_type you want the statuses for. Default 'post'. + * @return string[] An array of all the statuses for the supplied post type. + */ +function get_available_post_statuses( $type = 'post' ) { + $stati = wp_count_posts( $type ); + + return array_keys( get_object_vars( $stati ) ); +} + +/** + * Runs the query to fetch the posts for listing on the edit posts page. + * + * @since 2.5.0 + * + * @param array|false $q Optional. Array of query variables to use to build the query. + * Defaults to the `$_GET` superglobal. + * @return array + */ +function wp_edit_posts_query( $q = false ) { + if ( false === $q ) { + $q = $_GET; + } + $q['m'] = isset( $q['m'] ) ? (int) $q['m'] : 0; + $q['cat'] = isset( $q['cat'] ) ? (int) $q['cat'] : 0; + $post_stati = get_post_stati(); + + if ( isset( $q['post_type'] ) && in_array( $q['post_type'], get_post_types(), true ) ) { + $post_type = $q['post_type']; + } else { + $post_type = 'post'; + } + + $avail_post_stati = get_available_post_statuses( $post_type ); + $post_status = ''; + $perm = ''; + + if ( isset( $q['post_status'] ) && in_array( $q['post_status'], $post_stati, true ) ) { + $post_status = $q['post_status']; + $perm = 'readable'; + } + + $orderby = ''; + + if ( isset( $q['orderby'] ) ) { + $orderby = $q['orderby']; + } elseif ( isset( $q['post_status'] ) && in_array( $q['post_status'], array( 'pending', 'draft' ), true ) ) { + $orderby = 'modified'; + } + + $order = ''; + + if ( isset( $q['order'] ) ) { + $order = $q['order']; + } elseif ( isset( $q['post_status'] ) && 'pending' === $q['post_status'] ) { + $order = 'ASC'; + } + + $per_page = "edit_{$post_type}_per_page"; + $posts_per_page = (int) get_user_option( $per_page ); + if ( empty( $posts_per_page ) || $posts_per_page < 1 ) { + $posts_per_page = 20; + } + + /** + * Filters the number of items per page to show for a specific 'per_page' type. + * + * The dynamic portion of the hook name, `$post_type`, refers to the post type. + * + * Possible hook names include: + * + * - `edit_post_per_page` + * - `edit_page_per_page` + * - `edit_attachment_per_page` + * + * @since 3.0.0 + * + * @param int $posts_per_page Number of posts to display per page for the given post + * type. Default 20. + */ + $posts_per_page = apply_filters( "edit_{$post_type}_per_page", $posts_per_page ); + + /** + * Filters the number of posts displayed per page when specifically listing "posts". + * + * @since 2.8.0 + * + * @param int $posts_per_page Number of posts to be displayed. Default 20. + * @param string $post_type The post type. + */ + $posts_per_page = apply_filters( 'edit_posts_per_page', $posts_per_page, $post_type ); + + $query = compact( 'post_type', 'post_status', 'perm', 'order', 'orderby', 'posts_per_page' ); + + // Hierarchical types require special args. + if ( is_post_type_hierarchical( $post_type ) && empty( $orderby ) ) { + $query['orderby'] = 'menu_order title'; + $query['order'] = 'asc'; + $query['posts_per_page'] = -1; + $query['posts_per_archive_page'] = -1; + $query['fields'] = 'id=>parent'; + } + + if ( ! empty( $q['show_sticky'] ) ) { + $query['post__in'] = (array) get_option( 'sticky_posts' ); + } + + wp( $query ); + + return $avail_post_stati; +} + +/** + * Returns the query variables for the current attachments request. + * + * @since 4.2.0 + * + * @param array|false $q Optional. Array of query variables to use to build the query. + * Defaults to the `$_GET` superglobal. + * @return array The parsed query vars. + */ +function wp_edit_attachments_query_vars( $q = false ) { + if ( false === $q ) { + $q = $_GET; + } + $q['m'] = isset( $q['m'] ) ? (int) $q['m'] : 0; + $q['cat'] = isset( $q['cat'] ) ? (int) $q['cat'] : 0; + $q['post_type'] = 'attachment'; + $post_type = get_post_type_object( 'attachment' ); + $states = 'inherit'; + if ( current_user_can( $post_type->cap->read_private_posts ) ) { + $states .= ',private'; + } + + $q['post_status'] = isset( $q['status'] ) && 'trash' === $q['status'] ? 'trash' : $states; + $q['post_status'] = isset( $q['attachment-filter'] ) && 'trash' === $q['attachment-filter'] ? 'trash' : $states; + + $media_per_page = (int) get_user_option( 'upload_per_page' ); + if ( empty( $media_per_page ) || $media_per_page < 1 ) { + $media_per_page = 20; + } + + /** + * Filters the number of items to list per page when listing media items. + * + * @since 2.9.0 + * + * @param int $media_per_page Number of media to list. Default 20. + */ + $q['posts_per_page'] = apply_filters( 'upload_per_page', $media_per_page ); + + $post_mime_types = get_post_mime_types(); + if ( isset( $q['post_mime_type'] ) && ! array_intersect( (array) $q['post_mime_type'], array_keys( $post_mime_types ) ) ) { + unset( $q['post_mime_type'] ); + } + + foreach ( array_keys( $post_mime_types ) as $type ) { + if ( isset( $q['attachment-filter'] ) && "post_mime_type:$type" === $q['attachment-filter'] ) { + $q['post_mime_type'] = $type; + break; + } + } + + if ( isset( $q['detached'] ) || ( isset( $q['attachment-filter'] ) && 'detached' === $q['attachment-filter'] ) ) { + $q['post_parent'] = 0; + } + + if ( isset( $q['mine'] ) || ( isset( $q['attachment-filter'] ) && 'mine' === $q['attachment-filter'] ) ) { + $q['author'] = get_current_user_id(); + } + + // Filter query clauses to include filenames. + if ( isset( $q['s'] ) ) { + add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); + } + + return $q; +} + +/** + * Executes a query for attachments. An array of WP_Query arguments + * can be passed in, which will override the arguments set by this function. + * + * @since 2.5.0 + * + * @param array|false $q Optional. Array of query variables to use to build the query. + * Defaults to the `$_GET` superglobal. + * @return array + */ +function wp_edit_attachments_query( $q = false ) { + wp( wp_edit_attachments_query_vars( $q ) ); + + $post_mime_types = get_post_mime_types(); + $avail_post_mime_types = get_available_post_mime_types( 'attachment' ); + + return array( $post_mime_types, $avail_post_mime_types ); +} + +/** + * Returns the list of classes to be used by a meta box. + * + * @since 2.5.0 + * + * @param string $box_id Meta box ID (used in the 'id' attribute for the meta box). + * @param string $screen_id The screen on which the meta box is shown. + * @return string Space-separated string of class names. + */ +function postbox_classes( $box_id, $screen_id ) { + if ( isset( $_GET['edit'] ) && $_GET['edit'] == $box_id ) { + $classes = array( '' ); + } elseif ( get_user_option( 'closedpostboxes_' . $screen_id ) ) { + $closed = get_user_option( 'closedpostboxes_' . $screen_id ); + if ( ! is_array( $closed ) ) { + $classes = array( '' ); + } else { + $classes = in_array( $box_id, $closed, true ) ? array( 'closed' ) : array( '' ); + } + } else { + $classes = array( '' ); + } + + /** + * Filters the postbox classes for a specific screen and box ID combo. + * + * The dynamic portions of the hook name, `$screen_id` and `$box_id`, refer to + * the screen ID and meta box ID, respectively. + * + * @since 3.2.0 + * + * @param string[] $classes An array of postbox classes. + */ + $classes = apply_filters( "postbox_classes_{$screen_id}_{$box_id}", $classes ); + + return implode( ' ', $classes ); +} + +/** + * Returns a sample permalink based on the post name. + * + * @since 2.5.0 + * + * @param int|WP_Post $post Post ID or post object. + * @param string|null $title Optional. Title to override the post's current title + * when generating the post name. Default null. + * @param string|null $name Optional. Name to override the post name. Default null. + * @return array { + * Array containing the sample permalink with placeholder for the post name, and the post name. + * + * @type string $0 The permalink with placeholder for the post name. + * @type string $1 The post name. + * } + */ +function get_sample_permalink( $post, $title = null, $name = null ) { + $post = get_post( $post ); + + if ( ! $post ) { + return array( '', '' ); + } + + $ptype = get_post_type_object( $post->post_type ); + + $original_status = $post->post_status; + $original_date = $post->post_date; + $original_name = $post->post_name; + $original_filter = $post->filter; + + // Hack: get_permalink() would return plain permalink for drafts, so we will fake that our post is published. + if ( in_array( $post->post_status, array( 'draft', 'pending', 'future' ), true ) ) { + $post->post_status = 'publish'; + $post->post_name = sanitize_title( $post->post_name ? $post->post_name : $post->post_title, $post->ID ); + } + + /* + * If the user wants to set a new name -- override the current one. + * Note: if empty name is supplied -- use the title instead, see #6072. + */ + if ( ! is_null( $name ) ) { + $post->post_name = sanitize_title( $name ? $name : $title, $post->ID ); + } + + $post->post_name = wp_unique_post_slug( $post->post_name, $post->ID, $post->post_status, $post->post_type, $post->post_parent ); + + $post->filter = 'sample'; + + $permalink = get_permalink( $post, true ); + + // Replace custom post_type token with generic pagename token for ease of use. + $permalink = str_replace( "%$post->post_type%", '%pagename%', $permalink ); + + // Handle page hierarchy. + if ( $ptype->hierarchical ) { + $uri = get_page_uri( $post ); + if ( $uri ) { + $uri = untrailingslashit( $uri ); + $uri = strrev( stristr( strrev( $uri ), '/' ) ); + $uri = untrailingslashit( $uri ); + } + + /** This filter is documented in wp-admin/edit-tag-form.php */ + $uri = apply_filters( 'editable_slug', $uri, $post ); + if ( ! empty( $uri ) ) { + $uri .= '/'; + } + $permalink = str_replace( '%pagename%', "{$uri}%pagename%", $permalink ); + } + + /** This filter is documented in wp-admin/edit-tag-form.php */ + $permalink = array( $permalink, apply_filters( 'editable_slug', $post->post_name, $post ) ); + $post->post_status = $original_status; + $post->post_date = $original_date; + $post->post_name = $original_name; + $post->filter = $original_filter; + + /** + * Filters the sample permalink. + * + * @since 4.4.0 + * + * @param array $permalink { + * Array containing the sample permalink with placeholder for the post name, and the post name. + * + * @type string $0 The permalink with placeholder for the post name. + * @type string $1 The post name. + * } + * @param int $post_id Post ID. + * @param string $title Post title. + * @param string $name Post name (slug). + * @param WP_Post $post Post object. + */ + return apply_filters( 'get_sample_permalink', $permalink, $post->ID, $title, $name, $post ); +} + +/** + * Returns the HTML of the sample permalink slug editor. + * + * @since 2.5.0 + * + * @param int|WP_Post $post Post ID or post object. + * @param string|null $new_title Optional. New title. Default null. + * @param string|null $new_slug Optional. New slug. Default null. + * @return string The HTML of the sample permalink slug editor. + */ +function get_sample_permalink_html( $post, $new_title = null, $new_slug = null ) { + $post = get_post( $post ); + + if ( ! $post ) { + return ''; + } + + list($permalink, $post_name) = get_sample_permalink( $post->ID, $new_title, $new_slug ); + + $view_link = false; + $preview_target = ''; + + if ( current_user_can( 'read_post', $post->ID ) ) { + if ( 'draft' === $post->post_status || empty( $post->post_name ) ) { + $view_link = get_preview_post_link( $post ); + $preview_target = " target='wp-preview-{$post->ID}'"; + } else { + if ( 'publish' === $post->post_status || 'attachment' === $post->post_type ) { + $view_link = get_permalink( $post ); + } else { + // Allow non-published (private, future) to be viewed at a pretty permalink, in case $post->post_name is set. + $view_link = str_replace( array( '%pagename%', '%postname%' ), $post->post_name, $permalink ); + } + } + } + + // Permalinks without a post/page name placeholder don't have anything to edit. + if ( ! str_contains( $permalink, '%postname%' ) && ! str_contains( $permalink, '%pagename%' ) ) { + $return = '' . __( 'Permalink:' ) . "\n"; + + if ( false !== $view_link ) { + $display_link = urldecode( $view_link ); + $return .= '' . esc_html( $display_link ) . "\n"; + } else { + $return .= '' . $permalink . "\n"; + } + + // Encourage a pretty permalink setting. + if ( ! get_option( 'permalink_structure' ) && current_user_can( 'manage_options' ) + && ! ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) == $post->ID ) + ) { + $return .= '' . __( 'Change Permalink Structure' ) . "\n"; + } + } else { + if ( mb_strlen( $post_name ) > 34 ) { + $post_name_abridged = mb_substr( $post_name, 0, 16 ) . '…' . mb_substr( $post_name, -16 ); + } else { + $post_name_abridged = $post_name; + } + + $post_name_html = '' . esc_html( $post_name_abridged ) . ''; + $display_link = str_replace( array( '%pagename%', '%postname%' ), $post_name_html, esc_html( urldecode( $permalink ) ) ); + + $return = '' . __( 'Permalink:' ) . "\n"; + $return .= '' . $display_link . "\n"; + $return .= '‎'; // Fix bi-directional text display defect in RTL languages. + $return .= '\n"; + $return .= '' . esc_html( $post_name ) . "\n"; + } + + /** + * Filters the sample permalink HTML markup. + * + * @since 2.9.0 + * @since 4.4.0 Added `$post` parameter. + * + * @param string $return Sample permalink HTML markup. + * @param int $post_id Post ID. + * @param string|null $new_title New sample permalink title. + * @param string|null $new_slug New sample permalink slug. + * @param WP_Post $post Post object. + */ + $return = apply_filters( 'get_sample_permalink_html', $return, $post->ID, $new_title, $new_slug, $post ); + + return $return; +} + +/** + * Returns HTML for the post thumbnail meta box. + * + * @since 2.9.0 + * + * @param int|null $thumbnail_id Optional. Thumbnail attachment ID. Default null. + * @param int|WP_Post|null $post Optional. The post ID or object associated + * with the thumbnail. Defaults to global $post. + * @return string The post thumbnail HTML. + */ +function _wp_post_thumbnail_html( $thumbnail_id = null, $post = null ) { + $_wp_additional_image_sizes = wp_get_additional_image_sizes(); + + $post = get_post( $post ); + $post_type_object = get_post_type_object( $post->post_type ); + $set_thumbnail_link = '

    %s

    '; + $upload_iframe_src = get_upload_iframe_src( 'image', $post->ID ); + + $content = sprintf( + $set_thumbnail_link, + esc_url( $upload_iframe_src ), + '', // Empty when there's no featured image set, `aria-describedby` attribute otherwise. + esc_html( $post_type_object->labels->set_featured_image ) + ); + + if ( $thumbnail_id && get_post( $thumbnail_id ) ) { + $size = isset( $_wp_additional_image_sizes['post-thumbnail'] ) ? 'post-thumbnail' : array( 266, 266 ); + + /** + * Filters the size used to display the post thumbnail image in the 'Featured image' meta box. + * + * Note: When a theme adds 'post-thumbnail' support, a special 'post-thumbnail' + * image size is registered, which differs from the 'thumbnail' image size + * managed via the Settings > Media screen. + * + * @since 4.4.0 + * + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + * @param int $thumbnail_id Post thumbnail attachment ID. + * @param WP_Post $post The post object associated with the thumbnail. + */ + $size = apply_filters( 'admin_post_thumbnail_size', $size, $thumbnail_id, $post ); + + $thumbnail_html = wp_get_attachment_image( $thumbnail_id, $size ); + + if ( ! empty( $thumbnail_html ) ) { + $content = sprintf( + $set_thumbnail_link, + esc_url( $upload_iframe_src ), + ' aria-describedby="set-post-thumbnail-desc"', + $thumbnail_html + ); + $content .= '

    ' . __( 'Click the image to edit or update' ) . '

    '; + $content .= '

    ' . esc_html( $post_type_object->labels->remove_featured_image ) . '

    '; + } + } + + $content .= ''; + + /** + * Filters the admin post thumbnail HTML markup to return. + * + * @since 2.9.0 + * @since 3.5.0 Added the `$post_id` parameter. + * @since 4.6.0 Added the `$thumbnail_id` parameter. + * + * @param string $content Admin post thumbnail HTML markup. + * @param int $post_id Post ID. + * @param int|null $thumbnail_id Thumbnail attachment ID, or null if there isn't one. + */ + return apply_filters( 'admin_post_thumbnail_html', $content, $post->ID, $thumbnail_id ); +} + +/** + * Determines whether the post is currently being edited by another user. + * + * @since 2.5.0 + * + * @param int|WP_Post $post ID or object of the post to check for editing. + * @return int|false ID of the user with lock. False if the post does not exist, post is not locked, + * the user with lock does not exist, or the post is locked by current user. + */ +function wp_check_post_lock( $post ) { + $post = get_post( $post ); + + if ( ! $post ) { + return false; + } + + $lock = get_post_meta( $post->ID, '_edit_lock', true ); + + if ( ! $lock ) { + return false; + } + + $lock = explode( ':', $lock ); + $time = $lock[0]; + $user = isset( $lock[1] ) ? $lock[1] : get_post_meta( $post->ID, '_edit_last', true ); + + if ( ! get_userdata( $user ) ) { + return false; + } + + /** This filter is documented in wp-admin/includes/ajax-actions.php */ + $time_window = apply_filters( 'wp_check_post_lock_window', 150 ); + + if ( $time && $time > time() - $time_window && get_current_user_id() != $user ) { + return $user; + } + + return false; +} + +/** + * Marks the post as currently being edited by the current user. + * + * @since 2.5.0 + * + * @param int|WP_Post $post ID or object of the post being edited. + * @return array|false { + * Array of the lock time and user ID. False if the post does not exist, or there + * is no current user. + * + * @type int $0 The current time as a Unix timestamp. + * @type int $1 The ID of the current user. + * } + */ +function wp_set_post_lock( $post ) { + $post = get_post( $post ); + + if ( ! $post ) { + return false; + } + + $user_id = get_current_user_id(); + + if ( 0 == $user_id ) { + return false; + } + + $now = time(); + $lock = "$now:$user_id"; + + update_post_meta( $post->ID, '_edit_lock', $lock ); + + return array( $now, $user_id ); +} + +/** + * Outputs the HTML for the notice to say that someone else is editing or has taken over editing of this post. + * + * @since 2.8.5 + */ +function _admin_notice_post_locked() { + $post = get_post(); + + if ( ! $post ) { + return; + } + + $user = null; + $user_id = wp_check_post_lock( $post->ID ); + + if ( $user_id ) { + $user = get_userdata( $user_id ); + } + + if ( $user ) { + /** + * Filters whether to show the post locked dialog. + * + * Returning false from the filter will prevent the dialog from being displayed. + * + * @since 3.6.0 + * + * @param bool $display Whether to display the dialog. Default true. + * @param WP_Post $post Post object. + * @param WP_User $user The user with the lock for the post. + */ + if ( ! apply_filters( 'show_post_locked_dialog', true, $post, $user ) ) { + return; + } + + $locked = true; + } else { + $locked = false; + } + + $sendback = wp_get_referer(); + if ( $locked && $sendback && ! str_contains( $sendback, 'post.php' ) && ! str_contains( $sendback, 'post-new.php' ) ) { + + $sendback_text = __( 'Go back' ); + } else { + $sendback = admin_url( 'edit.php' ); + + if ( 'post' !== $post->post_type ) { + $sendback = add_query_arg( 'post_type', $post->post_type, $sendback ); + } + + $sendback_text = get_post_type_object( $post->post_type )->labels->all_items; + } + + $hidden = $locked ? '' : ' hidden'; + + ?> +
    +
    +
    + post_type )->public ) { + if ( 'publish' === $post->post_status || $user->ID != $post->post_author ) { + // Latest content is in autosave. + $nonce = wp_create_nonce( 'post_preview_' . $post->ID ); + $query_args['preview_id'] = $post->ID; + $query_args['preview_nonce'] = $nonce; + } + } + + $preview_link = get_preview_post_link( $post->ID, $query_args ); + + /** + * Filters whether to allow the post lock to be overridden. + * + * Returning false from the filter will disable the ability + * to override the post lock. + * + * @since 3.6.0 + * + * @param bool $override Whether to allow the post lock to be overridden. Default true. + * @param WP_Post $post Post object. + * @param WP_User $user The user with the lock for the post. + */ + $override = apply_filters( 'override_post_lock', true, $post, $user ); + $tab_last = $override ? '' : ' wp-tab-last'; + + ?> +
    +
    ID, 64 ); ?>
    +

    + display_name ) ); + } else { + /* translators: %s: User's display name. */ + printf( __( '%s is currently editing this post.' ), esc_html( $user->display_name ) ); + } + ?> +

    + +

    + + + + + + +

    +
    + +
    +
    +

    +
    + + +

    + +

    +
    + +
    +
    + ID; + $new_autosave['post_author'] = $post_author; + + $post = get_post( $post_id ); + + // If the new autosave has the same content as the post, delete the autosave. + $autosave_is_different = false; + foreach ( array_intersect( array_keys( $new_autosave ), array_keys( _wp_post_revision_fields( $post ) ) ) as $field ) { + if ( normalize_whitespace( $new_autosave[ $field ] ) !== normalize_whitespace( $post->$field ) ) { + $autosave_is_different = true; + break; + } + } + + if ( ! $autosave_is_different ) { + wp_delete_post_revision( $old_autosave->ID ); + return 0; + } + + /** + * Fires before an autosave is stored. + * + * @since 4.1.0 + * @since 6.4.0 The `$is_update` parameter was added to indicate if the autosave is being updated or was newly created. + * + * @param array $new_autosave Post array - the autosave that is about to be saved. + * @param bool $is_update Whether this is an existing autosave. + */ + do_action( 'wp_creating_autosave', $new_autosave, true ); + return wp_update_post( $new_autosave ); + } + + // _wp_put_post_revision() expects unescaped. + $post_data = wp_unslash( $post_data ); + + // Otherwise create the new autosave as a special post revision. + $revision = _wp_put_post_revision( $post_data, true ); + + if ( ! is_wp_error( $revision ) && 0 !== $revision ) { + + /** This action is documented in wp-admin/includes/post.php */ + do_action( 'wp_creating_autosave', get_post( $revision, ARRAY_A ), false ); + } + + return $revision; +} + +/** + * Autosave the revisioned meta fields. + * + * Iterates through the revisioned meta fields and checks each to see if they are set, + * and have a changed value. If so, the meta value is saved and attached to the autosave. + * + * @since 6.4.0 + * + * @param array $new_autosave The new post data being autosaved. + */ +function wp_autosave_post_revisioned_meta_fields( $new_autosave ) { + /* + * The post data arrives as either $_POST['data']['wp_autosave'] or the $_POST + * itself. This sets $posted_data to the correct variable. + * + * Ignoring sanitization to avoid altering meta. Ignoring the nonce check because + * this is hooked on inner core hooks where a valid nonce was already checked. + */ + $posted_data = isset( $_POST['data']['wp_autosave'] ) ? $_POST['data']['wp_autosave'] : $_POST; + + $post_type = get_post_type( $new_autosave['post_parent'] ); + + /* + * Go thru the revisioned meta keys and save them as part of the autosave, if + * the meta key is part of the posted data, the meta value is not blank and + * the the meta value has changes from the last autosaved value. + */ + foreach ( wp_post_revision_meta_keys( $post_type ) as $meta_key ) { + + if ( + isset( $posted_data[ $meta_key ] ) && + get_post_meta( $new_autosave['ID'], $meta_key, true ) !== wp_unslash( $posted_data[ $meta_key ] ) + ) { + /* + * Use the underlying delete_metadata() and add_metadata() functions + * vs delete_post_meta() and add_post_meta() to make sure we're working + * with the actual revision meta. + */ + delete_metadata( 'post', $new_autosave['ID'], $meta_key ); + + /* + * One last check to ensure meta value not empty(). + */ + if ( ! empty( $posted_data[ $meta_key ] ) ) { + /* + * Add the revisions meta data to the autosave. + */ + add_metadata( 'post', $new_autosave['ID'], $meta_key, $posted_data[ $meta_key ] ); + } + } + } +} + +/** + * Saves a draft or manually autosaves for the purpose of showing a post preview. + * + * @since 2.7.0 + * + * @return string URL to redirect to show the preview. + */ +function post_preview() { + + $post_id = (int) $_POST['post_ID']; + $_POST['ID'] = $post_id; + + $post = get_post( $post_id ); + + if ( ! $post ) { + wp_die( __( 'Sorry, you are not allowed to edit this post.' ) ); + } + + if ( ! current_user_can( 'edit_post', $post->ID ) ) { + wp_die( __( 'Sorry, you are not allowed to edit this post.' ) ); + } + + $is_autosave = false; + + if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() == $post->post_author + && ( 'draft' === $post->post_status || 'auto-draft' === $post->post_status ) + ) { + $saved_post_id = edit_post(); + } else { + $is_autosave = true; + + if ( isset( $_POST['post_status'] ) && 'auto-draft' === $_POST['post_status'] ) { + $_POST['post_status'] = 'draft'; + } + + $saved_post_id = wp_create_post_autosave( $post->ID ); + } + + if ( is_wp_error( $saved_post_id ) ) { + wp_die( $saved_post_id->get_error_message() ); + } + + $query_args = array(); + + if ( $is_autosave && $saved_post_id ) { + $query_args['preview_id'] = $post->ID; + $query_args['preview_nonce'] = wp_create_nonce( 'post_preview_' . $post->ID ); + + if ( isset( $_POST['post_format'] ) ) { + $query_args['post_format'] = empty( $_POST['post_format'] ) ? 'standard' : sanitize_key( $_POST['post_format'] ); + } + + if ( isset( $_POST['_thumbnail_id'] ) ) { + $query_args['_thumbnail_id'] = ( (int) $_POST['_thumbnail_id'] <= 0 ) ? '-1' : (int) $_POST['_thumbnail_id']; + } + } + + return get_preview_post_link( $post, $query_args ); +} + +/** + * Saves a post submitted with XHR. + * + * Intended for use with heartbeat and autosave.js + * + * @since 3.9.0 + * + * @param array $post_data Associative array of the submitted post data. + * @return mixed The value 0 or WP_Error on failure. The saved post ID on success. + * The ID can be the draft post_id or the autosave revision post_id. + */ +function wp_autosave( $post_data ) { + // Back-compat. + if ( ! defined( 'DOING_AUTOSAVE' ) ) { + define( 'DOING_AUTOSAVE', true ); + } + + $post_id = (int) $post_data['post_id']; + $post_data['ID'] = $post_id; + $post_data['post_ID'] = $post_id; + + if ( false === wp_verify_nonce( $post_data['_wpnonce'], 'update-post_' . $post_id ) ) { + return new WP_Error( 'invalid_nonce', __( 'Error while saving.' ) ); + } + + $post = get_post( $post_id ); + + if ( ! current_user_can( 'edit_post', $post->ID ) ) { + return new WP_Error( 'edit_posts', __( 'Sorry, you are not allowed to edit this item.' ) ); + } + + if ( 'auto-draft' === $post->post_status ) { + $post_data['post_status'] = 'draft'; + } + + if ( 'page' !== $post_data['post_type'] && ! empty( $post_data['catslist'] ) ) { + $post_data['post_category'] = explode( ',', $post_data['catslist'] ); + } + + if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() == $post->post_author + && ( 'auto-draft' === $post->post_status || 'draft' === $post->post_status ) + ) { + // Drafts and auto-drafts are just overwritten by autosave for the same user if the post is not locked. + return edit_post( wp_slash( $post_data ) ); + } else { + /* + * Non-drafts or other users' drafts are not overwritten. + * The autosave is stored in a special post revision for each user. + */ + return wp_create_post_autosave( wp_slash( $post_data ) ); + } +} + +/** + * Redirects to previous page. + * + * @since 2.7.0 + * + * @param int $post_id Optional. Post ID. + */ +function redirect_post( $post_id = '' ) { + if ( isset( $_POST['save'] ) || isset( $_POST['publish'] ) ) { + $status = get_post_status( $post_id ); + + if ( isset( $_POST['publish'] ) ) { + switch ( $status ) { + case 'pending': + $message = 8; + break; + case 'future': + $message = 9; + break; + default: + $message = 6; + } + } else { + $message = 'draft' === $status ? 10 : 1; + } + + $location = add_query_arg( 'message', $message, get_edit_post_link( $post_id, 'url' ) ); + } elseif ( isset( $_POST['addmeta'] ) && $_POST['addmeta'] ) { + $location = add_query_arg( 'message', 2, wp_get_referer() ); + $location = explode( '#', $location ); + $location = $location[0] . '#postcustom'; + } elseif ( isset( $_POST['deletemeta'] ) && $_POST['deletemeta'] ) { + $location = add_query_arg( 'message', 3, wp_get_referer() ); + $location = explode( '#', $location ); + $location = $location[0] . '#postcustom'; + } else { + $location = add_query_arg( 'message', 4, get_edit_post_link( $post_id, 'url' ) ); + } + + /** + * Filters the post redirect destination URL. + * + * @since 2.9.0 + * + * @param string $location The destination URL. + * @param int $post_id The post ID. + */ + wp_redirect( apply_filters( 'redirect_post_location', $location, $post_id ) ); + exit; +} + +/** + * Sanitizes POST values from a checkbox taxonomy metabox. + * + * @since 5.1.0 + * + * @param string $taxonomy The taxonomy name. + * @param array $terms Raw term data from the 'tax_input' field. + * @return int[] Array of sanitized term IDs. + */ +function taxonomy_meta_box_sanitize_cb_checkboxes( $taxonomy, $terms ) { + return array_map( 'intval', $terms ); +} + +/** + * Sanitizes POST values from an input taxonomy metabox. + * + * @since 5.1.0 + * + * @param string $taxonomy The taxonomy name. + * @param array|string $terms Raw term data from the 'tax_input' field. + * @return array + */ +function taxonomy_meta_box_sanitize_cb_input( $taxonomy, $terms ) { + /* + * Assume that a 'tax_input' string is a comma-separated list of term names. + * Some languages may use a character other than a comma as a delimiter, so we standardize on + * commas before parsing the list. + */ + if ( ! is_array( $terms ) ) { + $comma = _x( ',', 'tag delimiter' ); + if ( ',' !== $comma ) { + $terms = str_replace( $comma, ',', $terms ); + } + $terms = explode( ',', trim( $terms, " \n\t\r\0\x0B," ) ); + } + + $clean_terms = array(); + foreach ( $terms as $term ) { + // Empty terms are invalid input. + if ( empty( $term ) ) { + continue; + } + + $_term = get_terms( + array( + 'taxonomy' => $taxonomy, + 'name' => $term, + 'fields' => 'ids', + 'hide_empty' => false, + ) + ); + + if ( ! empty( $_term ) ) { + $clean_terms[] = (int) $_term[0]; + } else { + // No existing term was found, so pass the string. A new term will be created. + $clean_terms[] = $term; + } + } + + return $clean_terms; +} + +/** + * Prepares server-registered blocks for the block editor. + * + * Returns an associative array of registered block data keyed by block name. Data includes properties + * of a block relevant for client registration. + * + * @since 5.0.0 + * @since 6.3.0 Added `selectors` field. + * @since 6.4.0 Added `block_hooks` field. + * + * @return array An associative array of registered block data. + */ +function get_block_editor_server_block_settings() { + $block_registry = WP_Block_Type_Registry::get_instance(); + $blocks = array(); + $fields_to_pick = array( + 'api_version' => 'apiVersion', + 'title' => 'title', + 'description' => 'description', + 'icon' => 'icon', + 'attributes' => 'attributes', + 'provides_context' => 'providesContext', + 'uses_context' => 'usesContext', + 'block_hooks' => 'blockHooks', + 'selectors' => 'selectors', + 'supports' => 'supports', + 'category' => 'category', + 'styles' => 'styles', + 'textdomain' => 'textdomain', + 'parent' => 'parent', + 'ancestor' => 'ancestor', + 'keywords' => 'keywords', + 'example' => 'example', + 'variations' => 'variations', + ); + + foreach ( $block_registry->get_all_registered() as $block_name => $block_type ) { + foreach ( $fields_to_pick as $field => $key ) { + if ( ! isset( $block_type->{ $field } ) ) { + continue; + } + + if ( ! isset( $blocks[ $block_name ] ) ) { + $blocks[ $block_name ] = array(); + } + + $blocks[ $block_name ][ $key ] = $block_type->{ $field }; + } + } + + return $blocks; +} + +/** + * Renders the meta boxes forms. + * + * @since 5.0.0 + * + * @global WP_Post $post Global post object. + * @global WP_Screen $current_screen WordPress current screen object. + * @global array $wp_meta_boxes + */ +function the_block_editor_meta_boxes() { + global $post, $current_screen, $wp_meta_boxes; + + // Handle meta box state. + $_original_meta_boxes = $wp_meta_boxes; + + /** + * Fires right before the meta boxes are rendered. + * + * This allows for the filtering of meta box data, that should already be + * present by this point. Do not use as a means of adding meta box data. + * + * @since 5.0.0 + * + * @param array $wp_meta_boxes Global meta box state. + */ + $wp_meta_boxes = apply_filters( 'filter_block_editor_meta_boxes', $wp_meta_boxes ); + $locations = array( 'side', 'normal', 'advanced' ); + $priorities = array( 'high', 'sorted', 'core', 'default', 'low' ); + + // Render meta boxes. + ?> +
    + +
    +
    + + +
    + +
    + +
    + + id ][ $location ] ) ) { + continue; + } + + foreach ( $priorities as $priority ) { + if ( ! isset( $wp_meta_boxes[ $current_screen->id ][ $location ][ $priority ] ) ) { + continue; + } + + $meta_boxes = (array) $wp_meta_boxes[ $current_screen->id ][ $location ][ $priority ]; + foreach ( $meta_boxes as $meta_box ) { + if ( false == $meta_box || ! $meta_box['title'] ) { + continue; + } + + // If a meta box is just here for back compat, don't show it in the block editor. + if ( isset( $meta_box['args']['__back_compat_meta_box'] ) && $meta_box['args']['__back_compat_meta_box'] ) { + continue; + } + + $meta_boxes_per_location[ $location ][] = array( + 'id' => $meta_box['id'], + 'title' => $meta_box['title'], + ); + } + } + } + + /* + * Sadly we probably cannot add this data directly into editor settings. + * + * Some meta boxes need `admin_head` to fire for meta box registry. + * `admin_head` fires after `admin_enqueue_scripts`, which is where we create + * our editor instance. + */ + $script = 'window._wpLoadBlockEditor.then( function() { + wp.data.dispatch( \'core/edit-post\' ).setAvailableMetaBoxesPerLocation( ' . wp_json_encode( $meta_boxes_per_location ) . ' ); + } );'; + + wp_add_inline_script( 'wp-edit-post', $script ); + + /* + * When `wp-edit-post` is output in the ``, the inline script needs to be manually printed. + * Otherwise, meta boxes will not display because inline scripts for `wp-edit-post` + * will not be printed again after this point. + */ + if ( wp_script_is( 'wp-edit-post', 'done' ) ) { + printf( "\n", trim( $script ) ); + } + + /* + * If the 'postcustom' meta box is enabled, then we need to perform + * some extra initialization on it. + */ + $enable_custom_fields = (bool) get_user_meta( get_current_user_id(), 'enable_custom_fields', true ); + + if ( $enable_custom_fields ) { + $script = "( function( $ ) { + if ( $('#postcustom').length ) { + $( '#the-list' ).wpList( { + addBefore: function( s ) { + s.data += '&post_id=$post->ID'; + return s; + }, + addAfter: function() { + $('table#list-table').show(); + } + }); + } + } )( jQuery );"; + wp_enqueue_script( 'wp-lists' ); + wp_add_inline_script( 'wp-lists', $script ); + } + + /* + * Refresh nonces used by the meta box loader. + * + * The logic is very similar to that provided by post.js for the classic editor. + */ + $script = "( function( $ ) { + var check, timeout; + + function schedule() { + check = false; + window.clearTimeout( timeout ); + timeout = window.setTimeout( function() { check = true; }, 300000 ); + } + + $( document ).on( 'heartbeat-send.wp-refresh-nonces', function( e, data ) { + var post_id, \$authCheck = $( '#wp-auth-check-wrap' ); + + if ( check || ( \$authCheck.length && ! \$authCheck.hasClass( 'hidden' ) ) ) { + if ( ( post_id = $( '#post_ID' ).val() ) && $( '#_wpnonce' ).val() ) { + data['wp-refresh-metabox-loader-nonces'] = { + post_id: post_id + }; + } + } + }).on( 'heartbeat-tick.wp-refresh-nonces', function( e, data ) { + var nonces = data['wp-refresh-metabox-loader-nonces']; + + if ( nonces ) { + if ( nonces.replace ) { + if ( nonces.replace.metabox_loader_nonce && window._wpMetaBoxUrl && wp.url ) { + window._wpMetaBoxUrl= wp.url.addQueryArgs( window._wpMetaBoxUrl, { 'meta-box-loader-nonce': nonces.replace.metabox_loader_nonce } ); + } + + if ( nonces.replace._wpnonce ) { + $( '#_wpnonce' ).val( nonces.replace._wpnonce ); + } + } + } + }).ready( function() { + schedule(); + }); + } )( jQuery );"; + wp_add_inline_script( 'heartbeat', $script ); + + // Reset meta box data. + $wp_meta_boxes = $_original_meta_boxes; +} + +/** + * Renders the hidden form required for the meta boxes form. + * + * @since 5.0.0 + * + * @param WP_Post $post Current post object. + */ +function the_block_editor_meta_box_post_form_hidden_fields( $post ) { + $form_extra = ''; + if ( 'auto-draft' === $post->post_status ) { + $form_extra .= ""; + } + $form_action = 'editpost'; + $nonce_action = 'update-post_' . $post->ID; + $form_extra .= ""; + $referer = wp_get_referer(); + $current_user = wp_get_current_user(); + $user_id = $current_user->ID; + wp_nonce_field( $nonce_action ); + + /* + * Some meta boxes hook into these actions to add hidden input fields in the classic post form. + * For backward compatibility, we can capture the output from these actions, + * and extract the hidden input fields. + */ + ob_start(); + /** This filter is documented in wp-admin/edit-form-advanced.php */ + do_action( 'edit_form_after_title', $post ); + /** This filter is documented in wp-admin/edit-form-advanced.php */ + do_action( 'edit_form_advanced', $post ); + $classic_output = ob_get_clean(); + + $classic_elements = wp_html_split( $classic_output ); + $hidden_inputs = ''; + foreach ( $classic_elements as $element ) { + if ( ! str_starts_with( $element, ' + + + + + + + + ` fields, which will be POSTed back to + * the server when meta boxes are saved. + * + * @since 5.0.0 + * + * @param WP_Post $post The post that is being edited. + */ + do_action( 'block_editor_meta_box_hidden_fields', $post ); +} + +/** + * Disables block editor for wp_navigation type posts so they can be managed via the UI. + * + * @since 5.9.0 + * @access private + * + * @param bool $value Whether the CPT supports block editor or not. + * @param string $post_type Post type. + * @return bool Whether the block editor should be disabled or not. + */ +function _disable_block_editor_for_navigation_post_type( $value, $post_type ) { + if ( 'wp_navigation' === $post_type ) { + return false; + } + + return $value; +} + +/** + * This callback disables the content editor for wp_navigation type posts. + * Content editor cannot handle wp_navigation type posts correctly. + * We cannot disable the "editor" feature in the wp_navigation's CPT definition + * because it disables the ability to save navigation blocks via REST API. + * + * @since 5.9.0 + * @access private + * + * @param WP_Post $post An instance of WP_Post class. + */ +function _disable_content_editor_for_navigation_post_type( $post ) { + $post_type = get_post_type( $post ); + if ( 'wp_navigation' !== $post_type ) { + return; + } + + remove_post_type_support( $post_type, 'editor' ); +} + +/** + * This callback enables content editor for wp_navigation type posts. + * We need to enable it back because we disable it to hide + * the content editor for wp_navigation type posts. + * + * @since 5.9.0 + * @access private + * + * @see _disable_content_editor_for_navigation_post_type + * + * @param WP_Post $post An instance of WP_Post class. + */ +function _enable_content_editor_for_navigation_post_type( $post ) { + $post_type = get_post_type( $post ); + if ( 'wp_navigation' !== $post_type ) { + return; + } + + add_post_type_support( $post_type, 'editor' ); +} diff --git a/wp-admin/includes/privacy-tools.php b/wp-admin/includes/privacy-tools.php new file mode 100644 index 0000000..c5d7c9a --- /dev/null +++ b/wp-admin/includes/privacy-tools.php @@ -0,0 +1,968 @@ +post_type ) { + return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) ); + } + + $result = wp_send_user_request( $request_id ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + return new WP_Error( 'privacy_request_error', __( 'Unable to initiate confirmation for personal data request.' ) ); + } + + return true; +} + +/** + * Marks a request as completed by the admin and logs the current timestamp. + * + * @since 4.9.6 + * @access private + * + * @param int $request_id Request ID. + * @return int|WP_Error Request ID on success, or a WP_Error on failure. + */ +function _wp_privacy_completed_request( $request_id ) { + // Get the request. + $request_id = absint( $request_id ); + $request = wp_get_user_request( $request_id ); + + if ( ! $request ) { + return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) ); + } + + update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); + + $result = wp_update_post( + array( + 'ID' => $request_id, + 'post_status' => 'request-completed', + ) + ); + + return $result; +} + +/** + * Handle list table actions. + * + * @since 4.9.6 + * @access private + */ +function _wp_personal_data_handle_actions() { + if ( isset( $_POST['privacy_action_email_retry'] ) ) { + check_admin_referer( 'bulk-privacy_requests' ); + + $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['privacy_action_email_retry'] ) ) ) ); + $result = _wp_privacy_resend_request( $request_id ); + + if ( is_wp_error( $result ) ) { + add_settings_error( + 'privacy_action_email_retry', + 'privacy_action_email_retry', + $result->get_error_message(), + 'error' + ); + } else { + add_settings_error( + 'privacy_action_email_retry', + 'privacy_action_email_retry', + __( 'Confirmation request sent again successfully.' ), + 'success' + ); + } + } elseif ( isset( $_POST['action'] ) ) { + $action = ! empty( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : ''; + + switch ( $action ) { + case 'add_export_personal_data_request': + case 'add_remove_personal_data_request': + check_admin_referer( 'personal-data-request' ); + + if ( ! isset( $_POST['type_of_action'], $_POST['username_or_email_for_privacy_request'] ) ) { + add_settings_error( + 'action_type', + 'action_type', + __( 'Invalid personal data action.' ), + 'error' + ); + } + $action_type = sanitize_text_field( wp_unslash( $_POST['type_of_action'] ) ); + $username_or_email_address = sanitize_text_field( wp_unslash( $_POST['username_or_email_for_privacy_request'] ) ); + $email_address = ''; + $status = 'pending'; + + if ( ! isset( $_POST['send_confirmation_email'] ) ) { + $status = 'confirmed'; + } + + if ( ! in_array( $action_type, _wp_privacy_action_request_types(), true ) ) { + add_settings_error( + 'action_type', + 'action_type', + __( 'Invalid personal data action.' ), + 'error' + ); + } + + if ( ! is_email( $username_or_email_address ) ) { + $user = get_user_by( 'login', $username_or_email_address ); + if ( ! $user instanceof WP_User ) { + add_settings_error( + 'username_or_email_for_privacy_request', + 'username_or_email_for_privacy_request', + __( 'Unable to add this request. A valid email address or username must be supplied.' ), + 'error' + ); + } else { + $email_address = $user->user_email; + } + } else { + $email_address = $username_or_email_address; + } + + if ( empty( $email_address ) ) { + break; + } + + $request_id = wp_create_user_request( $email_address, $action_type, array(), $status ); + $message = ''; + + if ( is_wp_error( $request_id ) ) { + $message = $request_id->get_error_message(); + } elseif ( ! $request_id ) { + $message = __( 'Unable to initiate confirmation request.' ); + } + + if ( $message ) { + add_settings_error( + 'username_or_email_for_privacy_request', + 'username_or_email_for_privacy_request', + $message, + 'error' + ); + break; + } + + if ( 'pending' === $status ) { + wp_send_user_request( $request_id ); + + $message = __( 'Confirmation request initiated successfully.' ); + } elseif ( 'confirmed' === $status ) { + $message = __( 'Request added successfully.' ); + } + + if ( $message ) { + add_settings_error( + 'username_or_email_for_privacy_request', + 'username_or_email_for_privacy_request', + $message, + 'success' + ); + break; + } + } + } +} + +/** + * Cleans up failed and expired requests before displaying the list table. + * + * @since 4.9.6 + * @access private + */ +function _wp_personal_data_cleanup_requests() { + /** This filter is documented in wp-includes/user.php */ + $expires = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS ); + + $requests_query = new WP_Query( + array( + 'post_type' => 'user_request', + 'posts_per_page' => -1, + 'post_status' => 'request-pending', + 'fields' => 'ids', + 'date_query' => array( + array( + 'column' => 'post_modified_gmt', + 'before' => $expires . ' seconds ago', + ), + ), + ) + ); + + $request_ids = $requests_query->posts; + + foreach ( $request_ids as $request_id ) { + wp_update_post( + array( + 'ID' => $request_id, + 'post_status' => 'request-failed', + 'post_password' => '', + ) + ); + } +} + +/** + * Generate a single group for the personal data export report. + * + * @since 4.9.6 + * @since 5.4.0 Added the `$group_id` and `$groups_count` parameters. + * + * @param array $group_data { + * The group data to render. + * + * @type string $group_label The user-facing heading for the group, e.g. 'Comments'. + * @type array $items { + * An array of group items. + * + * @type array $group_item_data { + * An array of name-value pairs for the item. + * + * @type string $name The user-facing name of an item name-value pair, e.g. 'IP Address'. + * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. + * } + * } + * } + * @param string $group_id The group identifier. + * @param int $groups_count The number of all groups + * @return string The HTML for this group and its items. + */ +function wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id = '', $groups_count = 1 ) { + $group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id ); + + $group_html = '

    '; + $group_html .= esc_html( $group_data['group_label'] ); + + $items_count = count( (array) $group_data['items'] ); + if ( $items_count > 1 ) { + $group_html .= sprintf( ' (%d)', $items_count ); + } + + $group_html .= '

    '; + + if ( ! empty( $group_data['group_description'] ) ) { + $group_html .= '

    ' . esc_html( $group_data['group_description'] ) . '

    '; + } + + $group_html .= '
    '; + + foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) { + $group_html .= ''; + $group_html .= ''; + + foreach ( (array) $group_item_data as $group_item_datum ) { + $value = $group_item_datum['value']; + // If it looks like a link, make it a link. + if ( ! str_contains( $value, ' ' ) && ( str_starts_with( $value, 'http://' ) || str_starts_with( $value, 'https://' ) ) ) { + $value = '' . esc_html( $value ) . ''; + } + + $group_html .= ''; + $group_html .= ''; + $group_html .= ''; + $group_html .= ''; + } + + $group_html .= ''; + $group_html .= '
    ' . esc_html( $group_item_datum['name'] ) . '' . wp_kses( $value, 'personal_data_export' ) . '
    '; + } + + if ( $groups_count > 1 ) { + $group_html .= '
    '; + $group_html .= ' ' . esc_html__( 'Go to top' ) . ''; + $group_html .= '
    '; + } + + $group_html .= '
    '; + + return $group_html; +} + +/** + * Generate the personal data export file. + * + * @since 4.9.6 + * + * @param int $request_id The export request ID. + */ +function wp_privacy_generate_personal_data_export_file( $request_id ) { + if ( ! class_exists( 'ZipArchive' ) ) { + wp_send_json_error( __( 'Unable to generate personal data export file. ZipArchive not available.' ) ); + } + + // Get the request. + $request = wp_get_user_request( $request_id ); + + if ( ! $request || 'export_personal_data' !== $request->action_name ) { + wp_send_json_error( __( 'Invalid request ID when generating personal data export file.' ) ); + } + + $email_address = $request->email; + + if ( ! is_email( $email_address ) ) { + wp_send_json_error( __( 'Invalid email address when generating personal data export file.' ) ); + } + + // Create the exports folder if needed. + $exports_dir = wp_privacy_exports_dir(); + $exports_url = wp_privacy_exports_url(); + + if ( ! wp_mkdir_p( $exports_dir ) ) { + wp_send_json_error( __( 'Unable to create personal data export folder.' ) ); + } + + // Protect export folder from browsing. + $index_pathname = $exports_dir . 'index.php'; + if ( ! file_exists( $index_pathname ) ) { + $file = fopen( $index_pathname, 'w' ); + if ( false === $file ) { + wp_send_json_error( __( 'Unable to protect personal data export folder from browsing.' ) ); + } + fwrite( $file, " _x( 'About', 'personal data group label' ), + /* translators: Description for the About section in a personal data export. */ + 'group_description' => _x( 'Overview of export report.', 'personal data group description' ), + 'items' => array( + 'about-1' => array( + array( + 'name' => _x( 'Report generated for', 'email address' ), + 'value' => $email_address, + ), + array( + 'name' => _x( 'For site', 'website name' ), + 'value' => get_bloginfo( 'name' ), + ), + array( + 'name' => _x( 'At URL', 'website URL' ), + 'value' => get_bloginfo( 'url' ), + ), + array( + 'name' => _x( 'On', 'date/time' ), + 'value' => current_time( 'mysql' ), + ), + ), + ), + ); + + // And now, all the Groups. + $groups = get_post_meta( $request_id, '_export_data_grouped', true ); + if ( is_array( $groups ) ) { + // Merge in the special "About" group. + $groups = array_merge( array( 'about' => $about_group ), $groups ); + $groups_count = count( $groups ); + } else { + if ( false !== $groups ) { + _doing_it_wrong( + __FUNCTION__, + /* translators: %s: Post meta key. */ + sprintf( __( 'The %s post meta must be an array.' ), '_export_data_grouped' ), + '5.8.0' + ); + } + + $groups = null; + $groups_count = 0; + } + + // Convert the groups to JSON format. + $groups_json = wp_json_encode( $groups ); + + if ( false === $groups_json ) { + $error_message = sprintf( + /* translators: %s: Error message. */ + __( 'Unable to encode the personal data for export. Error: %s' ), + json_last_error_msg() + ); + + wp_send_json_error( $error_message ); + } + + /* + * Handle the JSON export. + */ + $file = fopen( $json_report_pathname, 'w' ); + + if ( false === $file ) { + wp_send_json_error( __( 'Unable to open personal data export file (JSON report) for writing.' ) ); + } + + fwrite( $file, '{' ); + fwrite( $file, '"' . $title . '":' ); + fwrite( $file, $groups_json ); + fwrite( $file, '}' ); + fclose( $file ); + + /* + * Handle the HTML export. + */ + $file = fopen( $html_report_pathname, 'w' ); + + if ( false === $file ) { + wp_send_json_error( __( 'Unable to open personal data export (HTML report) for writing.' ) ); + } + + fwrite( $file, "\n" ); + fwrite( $file, "\n" ); + fwrite( $file, "\n" ); + fwrite( $file, "\n" ); + fwrite( $file, "' ); + fwrite( $file, '' ); + fwrite( $file, esc_html( $title ) ); + fwrite( $file, '' ); + fwrite( $file, "\n" ); + fwrite( $file, "\n" ); + fwrite( $file, '

    ' . esc_html__( 'Personal Data Export' ) . '

    ' ); + + // Create TOC. + if ( $groups_count > 1 ) { + fwrite( $file, '
    ' ); + fwrite( $file, '

    ' . esc_html__( 'Table of Contents' ) . '

    ' ); + fwrite( $file, '
      ' ); + foreach ( (array) $groups as $group_id => $group_data ) { + $group_label = esc_html( $group_data['group_label'] ); + $group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id ); + $group_items_count = count( (array) $group_data['items'] ); + if ( $group_items_count > 1 ) { + $group_label .= sprintf( ' (%d)', $group_items_count ); + } + fwrite( $file, '
    • ' ); + fwrite( $file, '' . $group_label . '' ); + fwrite( $file, '
    • ' ); + } + fwrite( $file, '
    ' ); + fwrite( $file, '
    ' ); + } + + // Now, iterate over every group in $groups and have the formatter render it in HTML. + foreach ( (array) $groups as $group_id => $group_data ) { + fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id, $groups_count ) ); + } + + fwrite( $file, "\n" ); + fwrite( $file, "\n" ); + fclose( $file ); + + /* + * Now, generate the ZIP. + * + * If an archive has already been generated, then remove it and reuse the filename, + * to avoid breaking any URLs that may have been previously sent via email. + */ + $error = false; + + // This meta value is used from version 5.5. + $archive_filename = get_post_meta( $request_id, '_export_file_name', true ); + + // This one stored an absolute path and is used for backward compatibility. + $archive_pathname = get_post_meta( $request_id, '_export_file_path', true ); + + // If a filename meta exists, use it. + if ( ! empty( $archive_filename ) ) { + $archive_pathname = $exports_dir . $archive_filename; + } elseif ( ! empty( $archive_pathname ) ) { + // If a full path meta exists, use it and create the new meta value. + $archive_filename = basename( $archive_pathname ); + + update_post_meta( $request_id, '_export_file_name', $archive_filename ); + + // Remove the back-compat meta values. + delete_post_meta( $request_id, '_export_file_url' ); + delete_post_meta( $request_id, '_export_file_path' ); + } else { + // If there's no filename or full path stored, create a new file. + $archive_filename = $file_basename . '.zip'; + $archive_pathname = $exports_dir . $archive_filename; + + update_post_meta( $request_id, '_export_file_name', $archive_filename ); + } + + $archive_url = $exports_url . $archive_filename; + + if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) { + wp_delete_file( $archive_pathname ); + } + + $zip = new ZipArchive(); + if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) { + if ( ! $zip->addFile( $json_report_pathname, 'export.json' ) ) { + $error = __( 'Unable to archive the personal data export file (JSON format).' ); + } + + if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) { + $error = __( 'Unable to archive the personal data export file (HTML format).' ); + } + + $zip->close(); + + if ( ! $error ) { + /** + * Fires right after all personal data has been written to the export file. + * + * @since 4.9.6 + * @since 5.4.0 Added the `$json_report_pathname` parameter. + * + * @param string $archive_pathname The full path to the export file on the filesystem. + * @param string $archive_url The URL of the archive file. + * @param string $html_report_pathname The full path to the HTML personal data report on the filesystem. + * @param int $request_id The export request ID. + * @param string $json_report_pathname The full path to the JSON personal data report on the filesystem. + */ + do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id, $json_report_pathname ); + } + } else { + $error = __( 'Unable to open personal data export file (archive) for writing.' ); + } + + // Remove the JSON file. + unlink( $json_report_pathname ); + + // Remove the HTML file. + unlink( $html_report_pathname ); + + if ( $error ) { + wp_send_json_error( $error ); + } +} + +/** + * Send an email to the user with a link to the personal data export file + * + * @since 4.9.6 + * + * @param int $request_id The request ID for this personal data export. + * @return true|WP_Error True on success or `WP_Error` on failure. + */ +function wp_privacy_send_personal_data_export_email( $request_id ) { + // Get the request. + $request = wp_get_user_request( $request_id ); + + if ( ! $request || 'export_personal_data' !== $request->action_name ) { + return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) ); + } + + // Localize message content for user; fallback to site default for visitors. + if ( ! empty( $request->user_id ) ) { + $switched_locale = switch_to_user_locale( $request->user_id ); + } else { + $switched_locale = switch_to_locale( get_locale() ); + } + + /** This filter is documented in wp-includes/functions.php */ + $expiration = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS ); + $expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration ); + + $exports_url = wp_privacy_exports_url(); + $export_file_name = get_post_meta( $request_id, '_export_file_name', true ); + $export_file_url = $exports_url . $export_file_name; + + $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); + $site_url = home_url(); + + /** + * Filters the recipient of the personal data export email notification. + * Should be used with great caution to avoid sending the data export link to wrong emails. + * + * @since 5.3.0 + * + * @param string $request_email The email address of the notification recipient. + * @param WP_User_Request $request The request that is initiating the notification. + */ + $request_email = apply_filters( 'wp_privacy_personal_data_email_to', $request->email, $request ); + + $email_data = array( + 'request' => $request, + 'expiration' => $expiration, + 'expiration_date' => $expiration_date, + 'message_recipient' => $request_email, + 'export_file_url' => $export_file_url, + 'sitename' => $site_name, + 'siteurl' => $site_url, + ); + + /* translators: Personal data export notification email subject. %s: Site title. */ + $subject = sprintf( __( '[%s] Personal Data Export' ), $site_name ); + + /** + * Filters the subject of the email sent when an export request is completed. + * + * @since 5.3.0 + * + * @param string $subject The email subject. + * @param string $sitename The name of the site. + * @param array $email_data { + * Data relating to the account action email. + * + * @type WP_User_Request $request User request object. + * @type int $expiration The time in seconds until the export file expires. + * @type string $expiration_date The localized date and time when the export file expires. + * @type string $message_recipient The address that the email will be sent to. Defaults + * to the value of `$request->email`, but can be changed + * by the `wp_privacy_personal_data_email_to` filter. + * @type string $export_file_url The export file URL. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. + * } + */ + $subject = apply_filters( 'wp_privacy_personal_data_email_subject', $subject, $site_name, $email_data ); + + /* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */ + $email_text = __( + 'Howdy, + +Your request for an export of personal data has been completed. You may +download your personal data by clicking on the link below. For privacy +and security, we will automatically delete the file on ###EXPIRATION###, +so please download it before then. + +###LINK### + +Regards, +All at ###SITENAME### +###SITEURL###' + ); + + /** + * Filters the text of the email sent with a personal data export file. + * + * The following strings have a special meaning and will get replaced dynamically: + * ###EXPIRATION### The date when the URL will be automatically deleted. + * ###LINK### URL of the personal data export file for the user. + * ###SITENAME### The name of the site. + * ###SITEURL### The URL to the site. + * + * @since 4.9.6 + * @since 5.3.0 Introduced the `$email_data` array. + * + * @param string $email_text Text in the email. + * @param int $request_id The request ID for this personal data export. + * @param array $email_data { + * Data relating to the account action email. + * + * @type WP_User_Request $request User request object. + * @type int $expiration The time in seconds until the export file expires. + * @type string $expiration_date The localized date and time when the export file expires. + * @type string $message_recipient The address that the email will be sent to. Defaults + * to the value of `$request->email`, but can be changed + * by the `wp_privacy_personal_data_email_to` filter. + * @type string $export_file_url The export file URL. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. + */ + $content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id, $email_data ); + + $content = str_replace( '###EXPIRATION###', $expiration_date, $content ); + $content = str_replace( '###LINK###', sanitize_url( $export_file_url ), $content ); + $content = str_replace( '###EMAIL###', $request_email, $content ); + $content = str_replace( '###SITENAME###', $site_name, $content ); + $content = str_replace( '###SITEURL###', sanitize_url( $site_url ), $content ); + + $headers = ''; + + /** + * Filters the headers of the email sent with a personal data export file. + * + * @since 5.4.0 + * + * @param string|array $headers The email headers. + * @param string $subject The email subject. + * @param string $content The email content. + * @param int $request_id The request ID. + * @param array $email_data { + * Data relating to the account action email. + * + * @type WP_User_Request $request User request object. + * @type int $expiration The time in seconds until the export file expires. + * @type string $expiration_date The localized date and time when the export file expires. + * @type string $message_recipient The address that the email will be sent to. Defaults + * to the value of `$request->email`, but can be changed + * by the `wp_privacy_personal_data_email_to` filter. + * @type string $export_file_url The export file URL. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. + * } + */ + $headers = apply_filters( 'wp_privacy_personal_data_email_headers', $headers, $subject, $content, $request_id, $email_data ); + + $mail_success = wp_mail( $request_email, $subject, $content, $headers ); + + if ( $switched_locale ) { + restore_previous_locale(); + } + + if ( ! $mail_success ) { + return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) ); + } + + return true; +} + +/** + * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file. + * + * @since 4.9.6 + * + * @see 'wp_privacy_personal_data_export_page' + * + * @param array $response The response from the personal data exporter for the given page. + * @param int $exporter_index The index of the personal data exporter. Begins at 1. + * @param string $email_address The email address of the user whose personal data this is. + * @param int $page The page of personal data for this exporter. Begins at 1. + * @param int $request_id The request ID for this personal data export. + * @param bool $send_as_email Whether the final results of the export should be emailed to the user. + * @param string $exporter_key The slug (key) of the exporter. + * @return array The filtered response. + */ +function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) { + /* Do some simple checks on the shape of the response from the exporter. + * If the exporter response is malformed, don't attempt to consume it - let it + * pass through to generate a warning to the user by default Ajax processing. + */ + if ( ! is_array( $response ) ) { + return $response; + } + + if ( ! array_key_exists( 'done', $response ) ) { + return $response; + } + + if ( ! array_key_exists( 'data', $response ) ) { + return $response; + } + + if ( ! is_array( $response['data'] ) ) { + return $response; + } + + // Get the request. + $request = wp_get_user_request( $request_id ); + + if ( ! $request || 'export_personal_data' !== $request->action_name ) { + wp_send_json_error( __( 'Invalid request ID when merging personal data to export.' ) ); + } + + $export_data = array(); + + // First exporter, first page? Reset the report data accumulation array. + if ( 1 === $exporter_index && 1 === $page ) { + update_post_meta( $request_id, '_export_data_raw', $export_data ); + } else { + $accumulated_data = get_post_meta( $request_id, '_export_data_raw', true ); + + if ( $accumulated_data ) { + $export_data = $accumulated_data; + } + } + + // Now, merge the data from the exporter response into the data we have accumulated already. + $export_data = array_merge( $export_data, $response['data'] ); + update_post_meta( $request_id, '_export_data_raw', $export_data ); + + // If we are not yet on the last page of the last exporter, return now. + /** This filter is documented in wp-admin/includes/ajax-actions.php */ + $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); + $is_last_exporter = count( $exporters ) === $exporter_index; + $exporter_done = $response['done']; + if ( ! $is_last_exporter || ! $exporter_done ) { + return $response; + } + + // Last exporter, last page - let's prepare the export file. + + // First we need to re-organize the raw data hierarchically in groups and items. + $groups = array(); + foreach ( (array) $export_data as $export_datum ) { + $group_id = $export_datum['group_id']; + $group_label = $export_datum['group_label']; + + $group_description = ''; + if ( ! empty( $export_datum['group_description'] ) ) { + $group_description = $export_datum['group_description']; + } + + if ( ! array_key_exists( $group_id, $groups ) ) { + $groups[ $group_id ] = array( + 'group_label' => $group_label, + 'group_description' => $group_description, + 'items' => array(), + ); + } + + $item_id = $export_datum['item_id']; + if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) { + $groups[ $group_id ]['items'][ $item_id ] = array(); + } + + $old_item_data = $groups[ $group_id ]['items'][ $item_id ]; + $merged_item_data = array_merge( $export_datum['data'], $old_item_data ); + $groups[ $group_id ]['items'][ $item_id ] = $merged_item_data; + } + + // Then save the grouped data into the request. + delete_post_meta( $request_id, '_export_data_raw' ); + update_post_meta( $request_id, '_export_data_grouped', $groups ); + + /** + * Generate the export file from the collected, grouped personal data. + * + * @since 4.9.6 + * + * @param int $request_id The export request ID. + */ + do_action( 'wp_privacy_personal_data_export_file', $request_id ); + + // Clear the grouped data now that it is no longer needed. + delete_post_meta( $request_id, '_export_data_grouped' ); + + // If the destination is email, send it now. + if ( $send_as_email ) { + $mail_success = wp_privacy_send_personal_data_export_email( $request_id ); + if ( is_wp_error( $mail_success ) ) { + wp_send_json_error( $mail_success->get_error_message() ); + } + + // Update the request to completed state when the export email is sent. + _wp_privacy_completed_request( $request_id ); + } else { + // Modify the response to include the URL of the export file so the browser can fetch it. + $exports_url = wp_privacy_exports_url(); + $export_file_name = get_post_meta( $request_id, '_export_file_name', true ); + $export_file_url = $exports_url . $export_file_name; + + if ( ! empty( $export_file_url ) ) { + $response['url'] = $export_file_url; + } + } + + return $response; +} + +/** + * Mark erasure requests as completed after processing is finished. + * + * This intercepts the Ajax responses to personal data eraser page requests, and + * monitors the status of a request. Once all of the processing has finished, the + * request is marked as completed. + * + * @since 4.9.6 + * + * @see 'wp_privacy_personal_data_erasure_page' + * + * @param array $response The response from the personal data eraser for + * the given page. + * @param int $eraser_index The index of the personal data eraser. Begins + * at 1. + * @param string $email_address The email address of the user whose personal + * data this is. + * @param int $page The page of personal data for this eraser. + * Begins at 1. + * @param int $request_id The request ID for this personal data erasure. + * @return array The filtered response. + */ +function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) { + /* + * If the eraser response is malformed, don't attempt to consume it; let it + * pass through, so that the default Ajax processing will generate a warning + * to the user. + */ + if ( ! is_array( $response ) ) { + return $response; + } + + if ( ! array_key_exists( 'done', $response ) ) { + return $response; + } + + if ( ! array_key_exists( 'items_removed', $response ) ) { + return $response; + } + + if ( ! array_key_exists( 'items_retained', $response ) ) { + return $response; + } + + if ( ! array_key_exists( 'messages', $response ) ) { + return $response; + } + + // Get the request. + $request = wp_get_user_request( $request_id ); + + if ( ! $request || 'remove_personal_data' !== $request->action_name ) { + wp_send_json_error( __( 'Invalid request ID when processing personal data to erase.' ) ); + } + + /** This filter is documented in wp-admin/includes/ajax-actions.php */ + $erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() ); + $is_last_eraser = count( $erasers ) === $eraser_index; + $eraser_done = $response['done']; + + if ( ! $is_last_eraser || ! $eraser_done ) { + return $response; + } + + _wp_privacy_completed_request( $request_id ); + + /** + * Fires immediately after a personal data erasure request has been marked completed. + * + * @since 4.9.6 + * + * @param int $request_id The privacy request post ID associated with this request. + */ + do_action( 'wp_privacy_personal_data_erased', $request_id ); + + return $response; +} diff --git a/wp-admin/includes/revision.php b/wp-admin/includes/revision.php new file mode 100644 index 0000000..8ed45fd --- /dev/null +++ b/wp-admin/includes/revision.php @@ -0,0 +1,473 @@ +post_parent !== $post->ID && $compare_from->ID !== $post->ID ) { + return false; + } + if ( $compare_to->post_parent !== $post->ID && $compare_to->ID !== $post->ID ) { + return false; + } + + if ( $compare_from && strtotime( $compare_from->post_date_gmt ) > strtotime( $compare_to->post_date_gmt ) ) { + $temp = $compare_from; + $compare_from = $compare_to; + $compare_to = $temp; + } + + // Add default title if title field is empty. + if ( $compare_from && empty( $compare_from->post_title ) ) { + $compare_from->post_title = __( '(no title)' ); + } + if ( empty( $compare_to->post_title ) ) { + $compare_to->post_title = __( '(no title)' ); + } + + $return = array(); + + foreach ( _wp_post_revision_fields( $post ) as $field => $name ) { + /** + * Contextually filter a post revision field. + * + * The dynamic portion of the hook name, `$field`, corresponds to a name of a + * field of the revision object. + * + * Possible hook names include: + * + * - `_wp_post_revision_field_post_title` + * - `_wp_post_revision_field_post_content` + * - `_wp_post_revision_field_post_excerpt` + * + * @since 3.6.0 + * + * @param string $revision_field The current revision field to compare to or from. + * @param string $field The current revision field. + * @param WP_Post $compare_from The revision post object to compare to or from. + * @param string $context The context of whether the current revision is the old + * or the new one. Values are 'to' or 'from'. + */ + $content_from = $compare_from ? apply_filters( "_wp_post_revision_field_{$field}", $compare_from->$field, $field, $compare_from, 'from' ) : ''; + + /** This filter is documented in wp-admin/includes/revision.php */ + $content_to = apply_filters( "_wp_post_revision_field_{$field}", $compare_to->$field, $field, $compare_to, 'to' ); + + $args = array( + 'show_split_view' => true, + 'title_left' => __( 'Removed' ), + 'title_right' => __( 'Added' ), + ); + + /** + * Filters revisions text diff options. + * + * Filters the options passed to wp_text_diff() when viewing a post revision. + * + * @since 4.1.0 + * + * @param array $args { + * Associative array of options to pass to wp_text_diff(). + * + * @type bool $show_split_view True for split view (two columns), false for + * un-split view (single column). Default true. + * } + * @param string $field The current revision field. + * @param WP_Post $compare_from The revision post to compare from. + * @param WP_Post $compare_to The revision post to compare to. + */ + $args = apply_filters( 'revision_text_diff_options', $args, $field, $compare_from, $compare_to ); + + $diff = wp_text_diff( $content_from, $content_to, $args ); + + if ( ! $diff && 'post_title' === $field ) { + /* + * It's a better user experience to still show the Title, even if it didn't change. + * No, you didn't see this. + */ + $diff = ''; + + // In split screen mode, show the title before/after side by side. + if ( true === $args['show_split_view'] ) { + $diff .= ''; + } else { + $diff .= ''; + + // In single column mode, only show the title once if unchanged. + if ( $compare_from->post_title !== $compare_to->post_title ) { + $diff .= ''; + } + } + + $diff .= ''; + $diff .= '
    ' . esc_html( $compare_from->post_title ) . '' . esc_html( $compare_to->post_title ) . '' . esc_html( $compare_from->post_title ) . '
    ' . esc_html( $compare_to->post_title ) . '
    '; + } + + if ( $diff ) { + $return[] = array( + 'id' => $field, + 'name' => $name, + 'diff' => $diff, + ); + } + } + + /** + * Filters the fields displayed in the post revision diff UI. + * + * @since 4.1.0 + * + * @param array[] $return Array of revision UI fields. Each item is an array of id, name, and diff. + * @param WP_Post $compare_from The revision post to compare from. + * @param WP_Post $compare_to The revision post to compare to. + */ + return apply_filters( 'wp_get_revision_ui_diff', $return, $compare_from, $compare_to ); +} + +/** + * Prepare revisions for JavaScript. + * + * @since 3.6.0 + * + * @param WP_Post|int $post The post object or post ID. + * @param int $selected_revision_id The selected revision ID. + * @param int $from Optional. The revision ID to compare from. + * @return array An associative array of revision data and related settings. + */ +function wp_prepare_revisions_for_js( $post, $selected_revision_id, $from = null ) { + $post = get_post( $post ); + $authors = array(); + $now_gmt = time(); + + $revisions = wp_get_post_revisions( + $post->ID, + array( + 'order' => 'ASC', + 'check_enabled' => false, + ) + ); + // If revisions are disabled, we only want autosaves and the current post. + if ( ! wp_revisions_enabled( $post ) ) { + foreach ( $revisions as $revision_id => $revision ) { + if ( ! wp_is_post_autosave( $revision ) ) { + unset( $revisions[ $revision_id ] ); + } + } + $revisions = array( $post->ID => $post ) + $revisions; + } + + $show_avatars = get_option( 'show_avatars' ); + + update_post_author_caches( $revisions ); + + $can_restore = current_user_can( 'edit_post', $post->ID ); + $current_id = false; + + foreach ( $revisions as $revision ) { + $modified = strtotime( $revision->post_modified ); + $modified_gmt = strtotime( $revision->post_modified_gmt . ' +0000' ); + if ( $can_restore ) { + $restore_link = str_replace( + '&', + '&', + wp_nonce_url( + add_query_arg( + array( + 'revision' => $revision->ID, + 'action' => 'restore', + ), + admin_url( 'revision.php' ) + ), + "restore-post_{$revision->ID}" + ) + ); + } + + if ( ! isset( $authors[ $revision->post_author ] ) ) { + $authors[ $revision->post_author ] = array( + 'id' => (int) $revision->post_author, + 'avatar' => $show_avatars ? get_avatar( $revision->post_author, 32 ) : '', + 'name' => get_the_author_meta( 'display_name', $revision->post_author ), + ); + } + + $autosave = (bool) wp_is_post_autosave( $revision ); + $current = ! $autosave && $revision->post_modified_gmt === $post->post_modified_gmt; + if ( $current && ! empty( $current_id ) ) { + // If multiple revisions have the same post_modified_gmt, highest ID is current. + if ( $current_id < $revision->ID ) { + $revisions[ $current_id ]['current'] = false; + $current_id = $revision->ID; + } else { + $current = false; + } + } elseif ( $current ) { + $current_id = $revision->ID; + } + + $revisions_data = array( + 'id' => $revision->ID, + 'title' => get_the_title( $post->ID ), + 'author' => $authors[ $revision->post_author ], + 'date' => date_i18n( __( 'M j, Y @ H:i' ), $modified ), + 'dateShort' => date_i18n( _x( 'j M @ H:i', 'revision date short format' ), $modified ), + /* translators: %s: Human-readable time difference. */ + 'timeAgo' => sprintf( __( '%s ago' ), human_time_diff( $modified_gmt, $now_gmt ) ), + 'autosave' => $autosave, + 'current' => $current, + 'restoreUrl' => $can_restore ? $restore_link : false, + ); + + /** + * Filters the array of revisions used on the revisions screen. + * + * @since 4.4.0 + * + * @param array $revisions_data { + * The bootstrapped data for the revisions screen. + * + * @type int $id Revision ID. + * @type string $title Title for the revision's parent WP_Post object. + * @type int $author Revision post author ID. + * @type string $date Date the revision was modified. + * @type string $dateShort Short-form version of the date the revision was modified. + * @type string $timeAgo GMT-aware amount of time ago the revision was modified. + * @type bool $autosave Whether the revision is an autosave. + * @type bool $current Whether the revision is both not an autosave and the post + * modified date matches the revision modified date (GMT-aware). + * @type bool|false $restoreUrl URL if the revision can be restored, false otherwise. + * } + * @param WP_Post $revision The revision's WP_Post object. + * @param WP_Post $post The revision's parent WP_Post object. + */ + $revisions[ $revision->ID ] = apply_filters( 'wp_prepare_revision_for_js', $revisions_data, $revision, $post ); + } + + /* + * If we only have one revision, the initial revision is missing. This happens + * when we have an autosave and the user has clicked 'View the Autosave'. + */ + if ( 1 === count( $revisions ) ) { + $revisions[ $post->ID ] = array( + 'id' => $post->ID, + 'title' => get_the_title( $post->ID ), + 'author' => $authors[ $revision->post_author ], + 'date' => date_i18n( __( 'M j, Y @ H:i' ), strtotime( $post->post_modified ) ), + 'dateShort' => date_i18n( _x( 'j M @ H:i', 'revision date short format' ), strtotime( $post->post_modified ) ), + /* translators: %s: Human-readable time difference. */ + 'timeAgo' => sprintf( __( '%s ago' ), human_time_diff( strtotime( $post->post_modified_gmt ), $now_gmt ) ), + 'autosave' => false, + 'current' => true, + 'restoreUrl' => false, + ); + $current_id = $post->ID; + } + + /* + * If a post has been saved since the latest revision (no revisioned fields + * were changed), we may not have a "current" revision. Mark the latest + * revision as "current". + */ + if ( empty( $current_id ) ) { + if ( $revisions[ $revision->ID ]['autosave'] ) { + $revision = end( $revisions ); + while ( $revision['autosave'] ) { + $revision = prev( $revisions ); + } + $current_id = $revision['id']; + } else { + $current_id = $revision->ID; + } + $revisions[ $current_id ]['current'] = true; + } + + // Now, grab the initial diff. + $compare_two_mode = is_numeric( $from ); + if ( ! $compare_two_mode ) { + $found = array_search( $selected_revision_id, array_keys( $revisions ), true ); + if ( $found ) { + $from = array_keys( array_slice( $revisions, $found - 1, 1, true ) ); + $from = reset( $from ); + } else { + $from = 0; + } + } + + $from = absint( $from ); + + $diffs = array( + array( + 'id' => $from . ':' . $selected_revision_id, + 'fields' => wp_get_revision_ui_diff( $post->ID, $from, $selected_revision_id ), + ), + ); + + return array( + 'postId' => $post->ID, + 'nonce' => wp_create_nonce( 'revisions-ajax-nonce' ), + 'revisionData' => array_values( $revisions ), + 'to' => $selected_revision_id, + 'from' => $from, + 'diffData' => $diffs, + 'baseUrl' => parse_url( admin_url( 'revision.php' ), PHP_URL_PATH ), + 'compareTwoMode' => absint( $compare_two_mode ), // Apparently booleans are not allowed. + 'revisionIds' => array_keys( $revisions ), + ); +} + +/** + * Print JavaScript templates required for the revisions experience. + * + * @since 4.1.0 + * + * @global WP_Post $post Global post object. + */ +function wp_print_revision_templates() { + global $post; + ?> + + + + + + + + + get_charset_collate(); + +/** + * Retrieve the SQL for creating database tables. + * + * @since 3.3.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param string $scope Optional. The tables for which to retrieve SQL. Can be all, global, ms_global, or blog tables. Defaults to all. + * @param int $blog_id Optional. The site ID for which to retrieve SQL. Default is the current site ID. + * @return string The SQL needed to create the requested tables. + */ +function wp_get_db_schema( $scope = 'all', $blog_id = null ) { + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + if ( $blog_id && (int) $blog_id !== $wpdb->blogid ) { + $old_blog_id = $wpdb->set_blog_id( $blog_id ); + } + + // Engage multisite if in the middle of turning it on from network.php. + $is_multisite = is_multisite() || ( defined( 'WP_INSTALLING_NETWORK' ) && WP_INSTALLING_NETWORK ); + + /* + * Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. + * As of 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which + * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. + */ + $max_index_length = 191; + + // Blog-specific tables. + $blog_tables = "CREATE TABLE $wpdb->termmeta ( + meta_id bigint(20) unsigned NOT NULL auto_increment, + term_id bigint(20) unsigned NOT NULL default '0', + meta_key varchar(255) default NULL, + meta_value longtext, + PRIMARY KEY (meta_id), + KEY term_id (term_id), + KEY meta_key (meta_key($max_index_length)) +) $charset_collate; +CREATE TABLE $wpdb->terms ( + term_id bigint(20) unsigned NOT NULL auto_increment, + name varchar(200) NOT NULL default '', + slug varchar(200) NOT NULL default '', + term_group bigint(10) NOT NULL default 0, + PRIMARY KEY (term_id), + KEY slug (slug($max_index_length)), + KEY name (name($max_index_length)) +) $charset_collate; +CREATE TABLE $wpdb->term_taxonomy ( + term_taxonomy_id bigint(20) unsigned NOT NULL auto_increment, + term_id bigint(20) unsigned NOT NULL default 0, + taxonomy varchar(32) NOT NULL default '', + description longtext NOT NULL, + parent bigint(20) unsigned NOT NULL default 0, + count bigint(20) NOT NULL default 0, + PRIMARY KEY (term_taxonomy_id), + UNIQUE KEY term_id_taxonomy (term_id,taxonomy), + KEY taxonomy (taxonomy) +) $charset_collate; +CREATE TABLE $wpdb->term_relationships ( + object_id bigint(20) unsigned NOT NULL default 0, + term_taxonomy_id bigint(20) unsigned NOT NULL default 0, + term_order int(11) NOT NULL default 0, + PRIMARY KEY (object_id,term_taxonomy_id), + KEY term_taxonomy_id (term_taxonomy_id) +) $charset_collate; +CREATE TABLE $wpdb->commentmeta ( + meta_id bigint(20) unsigned NOT NULL auto_increment, + comment_id bigint(20) unsigned NOT NULL default '0', + meta_key varchar(255) default NULL, + meta_value longtext, + PRIMARY KEY (meta_id), + KEY comment_id (comment_id), + KEY meta_key (meta_key($max_index_length)) +) $charset_collate; +CREATE TABLE $wpdb->comments ( + comment_ID bigint(20) unsigned NOT NULL auto_increment, + comment_post_ID bigint(20) unsigned NOT NULL default '0', + comment_author tinytext NOT NULL, + comment_author_email varchar(100) NOT NULL default '', + comment_author_url varchar(200) NOT NULL default '', + comment_author_IP varchar(100) NOT NULL default '', + comment_date datetime NOT NULL default '0000-00-00 00:00:00', + comment_date_gmt datetime NOT NULL default '0000-00-00 00:00:00', + comment_content text NOT NULL, + comment_karma int(11) NOT NULL default '0', + comment_approved varchar(20) NOT NULL default '1', + comment_agent varchar(255) NOT NULL default '', + comment_type varchar(20) NOT NULL default 'comment', + comment_parent bigint(20) unsigned NOT NULL default '0', + user_id bigint(20) unsigned NOT NULL default '0', + PRIMARY KEY (comment_ID), + KEY comment_post_ID (comment_post_ID), + KEY comment_approved_date_gmt (comment_approved,comment_date_gmt), + KEY comment_date_gmt (comment_date_gmt), + KEY comment_parent (comment_parent), + KEY comment_author_email (comment_author_email(10)) +) $charset_collate; +CREATE TABLE $wpdb->links ( + link_id bigint(20) unsigned NOT NULL auto_increment, + link_url varchar(255) NOT NULL default '', + link_name varchar(255) NOT NULL default '', + link_image varchar(255) NOT NULL default '', + link_target varchar(25) NOT NULL default '', + link_description varchar(255) NOT NULL default '', + link_visible varchar(20) NOT NULL default 'Y', + link_owner bigint(20) unsigned NOT NULL default '1', + link_rating int(11) NOT NULL default '0', + link_updated datetime NOT NULL default '0000-00-00 00:00:00', + link_rel varchar(255) NOT NULL default '', + link_notes mediumtext NOT NULL, + link_rss varchar(255) NOT NULL default '', + PRIMARY KEY (link_id), + KEY link_visible (link_visible) +) $charset_collate; +CREATE TABLE $wpdb->options ( + option_id bigint(20) unsigned NOT NULL auto_increment, + option_name varchar(191) NOT NULL default '', + option_value longtext NOT NULL, + autoload varchar(20) NOT NULL default 'yes', + PRIMARY KEY (option_id), + UNIQUE KEY option_name (option_name), + KEY autoload (autoload) +) $charset_collate; +CREATE TABLE $wpdb->postmeta ( + meta_id bigint(20) unsigned NOT NULL auto_increment, + post_id bigint(20) unsigned NOT NULL default '0', + meta_key varchar(255) default NULL, + meta_value longtext, + PRIMARY KEY (meta_id), + KEY post_id (post_id), + KEY meta_key (meta_key($max_index_length)) +) $charset_collate; +CREATE TABLE $wpdb->posts ( + ID bigint(20) unsigned NOT NULL auto_increment, + post_author bigint(20) unsigned NOT NULL default '0', + post_date datetime NOT NULL default '0000-00-00 00:00:00', + post_date_gmt datetime NOT NULL default '0000-00-00 00:00:00', + post_content longtext NOT NULL, + post_title text NOT NULL, + post_excerpt text NOT NULL, + post_status varchar(20) NOT NULL default 'publish', + comment_status varchar(20) NOT NULL default 'open', + ping_status varchar(20) NOT NULL default 'open', + post_password varchar(255) NOT NULL default '', + post_name varchar(200) NOT NULL default '', + to_ping text NOT NULL, + pinged text NOT NULL, + post_modified datetime NOT NULL default '0000-00-00 00:00:00', + post_modified_gmt datetime NOT NULL default '0000-00-00 00:00:00', + post_content_filtered longtext NOT NULL, + post_parent bigint(20) unsigned NOT NULL default '0', + guid varchar(255) NOT NULL default '', + menu_order int(11) NOT NULL default '0', + post_type varchar(20) NOT NULL default 'post', + post_mime_type varchar(100) NOT NULL default '', + comment_count bigint(20) NOT NULL default '0', + PRIMARY KEY (ID), + KEY post_name (post_name($max_index_length)), + KEY type_status_date (post_type,post_status,post_date,ID), + KEY post_parent (post_parent), + KEY post_author (post_author) +) $charset_collate;\n"; + + // Single site users table. The multisite flavor of the users table is handled below. + $users_single_table = "CREATE TABLE $wpdb->users ( + ID bigint(20) unsigned NOT NULL auto_increment, + user_login varchar(60) NOT NULL default '', + user_pass varchar(255) NOT NULL default '', + user_nicename varchar(50) NOT NULL default '', + user_email varchar(100) NOT NULL default '', + user_url varchar(100) NOT NULL default '', + user_registered datetime NOT NULL default '0000-00-00 00:00:00', + user_activation_key varchar(255) NOT NULL default '', + user_status int(11) NOT NULL default '0', + display_name varchar(250) NOT NULL default '', + PRIMARY KEY (ID), + KEY user_login_key (user_login), + KEY user_nicename (user_nicename), + KEY user_email (user_email) +) $charset_collate;\n"; + + // Multisite users table. + $users_multi_table = "CREATE TABLE $wpdb->users ( + ID bigint(20) unsigned NOT NULL auto_increment, + user_login varchar(60) NOT NULL default '', + user_pass varchar(255) NOT NULL default '', + user_nicename varchar(50) NOT NULL default '', + user_email varchar(100) NOT NULL default '', + user_url varchar(100) NOT NULL default '', + user_registered datetime NOT NULL default '0000-00-00 00:00:00', + user_activation_key varchar(255) NOT NULL default '', + user_status int(11) NOT NULL default '0', + display_name varchar(250) NOT NULL default '', + spam tinyint(2) NOT NULL default '0', + deleted tinyint(2) NOT NULL default '0', + PRIMARY KEY (ID), + KEY user_login_key (user_login), + KEY user_nicename (user_nicename), + KEY user_email (user_email) +) $charset_collate;\n"; + + // Usermeta. + $usermeta_table = "CREATE TABLE $wpdb->usermeta ( + umeta_id bigint(20) unsigned NOT NULL auto_increment, + user_id bigint(20) unsigned NOT NULL default '0', + meta_key varchar(255) default NULL, + meta_value longtext, + PRIMARY KEY (umeta_id), + KEY user_id (user_id), + KEY meta_key (meta_key($max_index_length)) +) $charset_collate;\n"; + + // Global tables. + if ( $is_multisite ) { + $global_tables = $users_multi_table . $usermeta_table; + } else { + $global_tables = $users_single_table . $usermeta_table; + } + + // Multisite global tables. + $ms_global_tables = "CREATE TABLE $wpdb->blogs ( + blog_id bigint(20) NOT NULL auto_increment, + site_id bigint(20) NOT NULL default '0', + domain varchar(200) NOT NULL default '', + path varchar(100) NOT NULL default '', + registered datetime NOT NULL default '0000-00-00 00:00:00', + last_updated datetime NOT NULL default '0000-00-00 00:00:00', + public tinyint(2) NOT NULL default '1', + archived tinyint(2) NOT NULL default '0', + mature tinyint(2) NOT NULL default '0', + spam tinyint(2) NOT NULL default '0', + deleted tinyint(2) NOT NULL default '0', + lang_id int(11) NOT NULL default '0', + PRIMARY KEY (blog_id), + KEY domain (domain(50),path(5)), + KEY lang_id (lang_id) +) $charset_collate; +CREATE TABLE $wpdb->blogmeta ( + meta_id bigint(20) unsigned NOT NULL auto_increment, + blog_id bigint(20) NOT NULL default '0', + meta_key varchar(255) default NULL, + meta_value longtext, + PRIMARY KEY (meta_id), + KEY meta_key (meta_key($max_index_length)), + KEY blog_id (blog_id) +) $charset_collate; +CREATE TABLE $wpdb->registration_log ( + ID bigint(20) NOT NULL auto_increment, + email varchar(255) NOT NULL default '', + IP varchar(30) NOT NULL default '', + blog_id bigint(20) NOT NULL default '0', + date_registered datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (ID), + KEY IP (IP) +) $charset_collate; +CREATE TABLE $wpdb->site ( + id bigint(20) NOT NULL auto_increment, + domain varchar(200) NOT NULL default '', + path varchar(100) NOT NULL default '', + PRIMARY KEY (id), + KEY domain (domain(140),path(51)) +) $charset_collate; +CREATE TABLE $wpdb->sitemeta ( + meta_id bigint(20) NOT NULL auto_increment, + site_id bigint(20) NOT NULL default '0', + meta_key varchar(255) default NULL, + meta_value longtext, + PRIMARY KEY (meta_id), + KEY meta_key (meta_key($max_index_length)), + KEY site_id (site_id) +) $charset_collate; +CREATE TABLE $wpdb->signups ( + signup_id bigint(20) NOT NULL auto_increment, + domain varchar(200) NOT NULL default '', + path varchar(100) NOT NULL default '', + title longtext NOT NULL, + user_login varchar(60) NOT NULL default '', + user_email varchar(100) NOT NULL default '', + registered datetime NOT NULL default '0000-00-00 00:00:00', + activated datetime NOT NULL default '0000-00-00 00:00:00', + active tinyint(1) NOT NULL default '0', + activation_key varchar(50) NOT NULL default '', + meta longtext, + PRIMARY KEY (signup_id), + KEY activation_key (activation_key), + KEY user_email (user_email), + KEY user_login_email (user_login,user_email), + KEY domain_path (domain(140),path(51)) +) $charset_collate;"; + + switch ( $scope ) { + case 'blog': + $queries = $blog_tables; + break; + case 'global': + $queries = $global_tables; + if ( $is_multisite ) { + $queries .= $ms_global_tables; + } + break; + case 'ms_global': + $queries = $ms_global_tables; + break; + case 'all': + default: + $queries = $global_tables . $blog_tables; + if ( $is_multisite ) { + $queries .= $ms_global_tables; + } + break; + } + + if ( isset( $old_blog_id ) ) { + $wpdb->set_blog_id( $old_blog_id ); + } + + return $queries; +} + +// Populate for back compat. +$wp_queries = wp_get_db_schema( 'all' ); + +/** + * Create WordPress options and set the default values. + * + * @since 1.5.0 + * @since 5.1.0 The $options parameter has been added. + * + * @global wpdb $wpdb WordPress database abstraction object. + * @global int $wp_db_version WordPress database version. + * @global int $wp_current_db_version The old (current) database version. + * + * @param array $options Optional. Custom option $key => $value pairs to use. Default empty array. + */ +function populate_options( array $options = array() ) { + global $wpdb, $wp_db_version, $wp_current_db_version; + + $guessurl = wp_guess_url(); + /** + * Fires before creating WordPress options and populating their default values. + * + * @since 2.6.0 + */ + do_action( 'populate_options' ); + + // If WP_DEFAULT_THEME doesn't exist, fall back to the latest core default theme. + $stylesheet = WP_DEFAULT_THEME; + $template = WP_DEFAULT_THEME; + $theme = wp_get_theme( WP_DEFAULT_THEME ); + if ( ! $theme->exists() ) { + $theme = WP_Theme::get_core_default_theme(); + } + + // If we can't find a core default theme, WP_DEFAULT_THEME is the best we can do. + if ( $theme ) { + $stylesheet = $theme->get_stylesheet(); + $template = $theme->get_template(); + } + + $timezone_string = ''; + $gmt_offset = 0; + /* + * translators: default GMT offset or timezone string. Must be either a valid offset (-12 to 14) + * or a valid timezone string (America/New_York). See https://www.php.net/manual/en/timezones.php + * for all timezone strings currently supported by PHP. + * + * Important: When a previous timezone string, like `Europe/Kiev`, has been superseded by an + * updated one, like `Europe/Kyiv`, as a rule of thumb, the **old** timezone name should be used + * in the "translation" to allow for the default timezone setting to be PHP cross-version compatible, + * as old timezone names will be recognized in new PHP versions, while new timezone names cannot + * be recognized in old PHP versions. + * + * To verify which timezone strings are available in the _oldest_ PHP version supported, you can + * use https://3v4l.org/6YQAt#v5.6.20 and replace the "BR" (Brazil) in the code line with the + * country code for which you want to look up the supported timezone names. + */ + $offset_or_tz = _x( '0', 'default GMT offset or timezone string' ); + if ( is_numeric( $offset_or_tz ) ) { + $gmt_offset = $offset_or_tz; + } elseif ( $offset_or_tz && in_array( $offset_or_tz, timezone_identifiers_list( DateTimeZone::ALL_WITH_BC ), true ) ) { + $timezone_string = $offset_or_tz; + } + + $defaults = array( + 'siteurl' => $guessurl, + 'home' => $guessurl, + 'blogname' => __( 'My Site' ), + 'blogdescription' => '', + 'users_can_register' => 0, + 'admin_email' => 'you@example.com', + /* translators: Default start of the week. 0 = Sunday, 1 = Monday. */ + 'start_of_week' => _x( '1', 'start of week' ), + 'use_balanceTags' => 0, + 'use_smilies' => 1, + 'require_name_email' => 1, + 'comments_notify' => 1, + 'posts_per_rss' => 10, + 'rss_use_excerpt' => 0, + 'mailserver_url' => 'mail.example.com', + 'mailserver_login' => 'login@example.com', + 'mailserver_pass' => 'password', + 'mailserver_port' => 110, + 'default_category' => 1, + 'default_comment_status' => 'open', + 'default_ping_status' => 'open', + 'default_pingback_flag' => 1, + 'posts_per_page' => 10, + /* translators: Default date format, see https://www.php.net/manual/datetime.format.php */ + 'date_format' => __( 'F j, Y' ), + /* translators: Default time format, see https://www.php.net/manual/datetime.format.php */ + 'time_format' => __( 'g:i a' ), + /* translators: Links last updated date format, see https://www.php.net/manual/datetime.format.php */ + 'links_updated_date_format' => __( 'F j, Y g:i a' ), + 'comment_moderation' => 0, + 'moderation_notify' => 1, + 'permalink_structure' => '', + 'rewrite_rules' => '', + 'hack_file' => 0, + 'blog_charset' => 'UTF-8', + 'moderation_keys' => '', + 'active_plugins' => array(), + 'category_base' => '', + 'ping_sites' => 'http://rpc.pingomatic.com/', + 'comment_max_links' => 2, + 'gmt_offset' => $gmt_offset, + + // 1.5.0 + 'default_email_category' => 1, + 'recently_edited' => '', + 'template' => $template, + 'stylesheet' => $stylesheet, + 'comment_registration' => 0, + 'html_type' => 'text/html', + + // 1.5.1 + 'use_trackback' => 0, + + // 2.0.0 + 'default_role' => 'subscriber', + 'db_version' => $wp_db_version, + + // 2.0.1 + 'uploads_use_yearmonth_folders' => 1, + 'upload_path' => '', + + // 2.1.0 + 'blog_public' => '1', + 'default_link_category' => 2, + 'show_on_front' => 'posts', + + // 2.2.0 + 'tag_base' => '', + + // 2.5.0 + 'show_avatars' => '1', + 'avatar_rating' => 'G', + 'upload_url_path' => '', + 'thumbnail_size_w' => 150, + 'thumbnail_size_h' => 150, + 'thumbnail_crop' => 1, + 'medium_size_w' => 300, + 'medium_size_h' => 300, + + // 2.6.0 + 'avatar_default' => 'mystery', + + // 2.7.0 + 'large_size_w' => 1024, + 'large_size_h' => 1024, + 'image_default_link_type' => 'none', + 'image_default_size' => '', + 'image_default_align' => '', + 'close_comments_for_old_posts' => 0, + 'close_comments_days_old' => 14, + 'thread_comments' => 1, + 'thread_comments_depth' => 5, + 'page_comments' => 0, + 'comments_per_page' => 50, + 'default_comments_page' => 'newest', + 'comment_order' => 'asc', + 'sticky_posts' => array(), + 'widget_categories' => array(), + 'widget_text' => array(), + 'widget_rss' => array(), + 'uninstall_plugins' => array(), + + // 2.8.0 + 'timezone_string' => $timezone_string, + + // 3.0.0 + 'page_for_posts' => 0, + 'page_on_front' => 0, + + // 3.1.0 + 'default_post_format' => 0, + + // 3.5.0 + 'link_manager_enabled' => 0, + + // 4.3.0 + 'finished_splitting_shared_terms' => 1, + 'site_icon' => 0, + + // 4.4.0 + 'medium_large_size_w' => 768, + 'medium_large_size_h' => 0, + + // 4.9.6 + 'wp_page_for_privacy_policy' => 0, + + // 4.9.8 + 'show_comments_cookies_opt_in' => 1, + + // 5.3.0 + 'admin_email_lifespan' => ( time() + 6 * MONTH_IN_SECONDS ), + + // 5.5.0 + 'disallowed_keys' => '', + 'comment_previously_approved' => 1, + 'auto_plugin_theme_update_emails' => array(), + + // 5.6.0 + 'auto_update_core_dev' => 'enabled', + 'auto_update_core_minor' => 'enabled', + /* + * Default to enabled for new installs. + * See https://core.trac.wordpress.org/ticket/51742. + */ + 'auto_update_core_major' => 'enabled', + + // 5.8.0 + 'wp_force_deactivated_plugins' => array(), + + // 6.4.0 + 'wp_attachment_pages_enabled' => 0, + ); + + // 3.3.0 + if ( ! is_multisite() ) { + $defaults['initial_db_version'] = ! empty( $wp_current_db_version ) && $wp_current_db_version < $wp_db_version + ? $wp_current_db_version : $wp_db_version; + } + + // 3.0.0 multisite. + if ( is_multisite() ) { + $defaults['permalink_structure'] = '/%year%/%monthnum%/%day%/%postname%/'; + } + + $options = wp_parse_args( $options, $defaults ); + + // Set autoload to no for these options. + $fat_options = array( + 'moderation_keys', + 'recently_edited', + 'disallowed_keys', + 'uninstall_plugins', + 'auto_plugin_theme_update_emails', + ); + + $keys = "'" . implode( "', '", array_keys( $options ) ) . "'"; + $existing_options = $wpdb->get_col( "SELECT option_name FROM $wpdb->options WHERE option_name in ( $keys )" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + $insert = ''; + + foreach ( $options as $option => $value ) { + if ( in_array( $option, $existing_options, true ) ) { + continue; + } + + if ( in_array( $option, $fat_options, true ) ) { + $autoload = 'no'; + } else { + $autoload = 'yes'; + } + + if ( ! empty( $insert ) ) { + $insert .= ', '; + } + + $value = maybe_serialize( sanitize_option( $option, $value ) ); + + $insert .= $wpdb->prepare( '(%s, %s, %s)', $option, $value, $autoload ); + } + + if ( ! empty( $insert ) ) { + $wpdb->query( "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES " . $insert ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + // In case it is set, but blank, update "home". + if ( ! __get_option( 'home' ) ) { + update_option( 'home', $guessurl ); + } + + // Delete unused options. + $unusedoptions = array( + 'blodotgsping_url', + 'bodyterminator', + 'emailtestonly', + 'phoneemail_separator', + 'smilies_directory', + 'subjectprefix', + 'use_bbcode', + 'use_blodotgsping', + 'use_phoneemail', + 'use_quicktags', + 'use_weblogsping', + 'weblogs_cache_file', + 'use_preview', + 'use_htmltrans', + 'smilies_directory', + 'fileupload_allowedusers', + 'use_phoneemail', + 'default_post_status', + 'default_post_category', + 'archive_mode', + 'time_difference', + 'links_minadminlevel', + 'links_use_adminlevels', + 'links_rating_type', + 'links_rating_char', + 'links_rating_ignore_zero', + 'links_rating_single_image', + 'links_rating_image0', + 'links_rating_image1', + 'links_rating_image2', + 'links_rating_image3', + 'links_rating_image4', + 'links_rating_image5', + 'links_rating_image6', + 'links_rating_image7', + 'links_rating_image8', + 'links_rating_image9', + 'links_recently_updated_time', + 'links_recently_updated_prepend', + 'links_recently_updated_append', + 'weblogs_cacheminutes', + 'comment_allowed_tags', + 'search_engine_friendly_urls', + 'default_geourl_lat', + 'default_geourl_lon', + 'use_default_geourl', + 'weblogs_xml_url', + 'new_users_can_blog', + '_wpnonce', + '_wp_http_referer', + 'Update', + 'action', + 'rich_editing', + 'autosave_interval', + 'deactivated_plugins', + 'can_compress_scripts', + 'page_uris', + 'update_core', + 'update_plugins', + 'update_themes', + 'doing_cron', + 'random_seed', + 'rss_excerpt_length', + 'secret', + 'use_linksupdate', + 'default_comment_status_page', + 'wporg_popular_tags', + 'what_to_show', + 'rss_language', + 'language', + 'enable_xmlrpc', + 'enable_app', + 'embed_autourls', + 'default_post_edit_rows', + 'gzipcompression', + 'advanced_edit', + ); + foreach ( $unusedoptions as $option ) { + delete_option( $option ); + } + + // Delete obsolete magpie stuff. + $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name REGEXP '^rss_[0-9a-f]{32}(_ts)?$'" ); + + // Clear expired transients. + delete_expired_transients( true ); +} + +/** + * Execute WordPress role creation for the various WordPress versions. + * + * @since 2.0.0 + */ +function populate_roles() { + populate_roles_160(); + populate_roles_210(); + populate_roles_230(); + populate_roles_250(); + populate_roles_260(); + populate_roles_270(); + populate_roles_280(); + populate_roles_300(); +} + +/** + * Create the roles for WordPress 2.0 + * + * @since 2.0.0 + */ +function populate_roles_160() { + // Add roles. + add_role( 'administrator', 'Administrator' ); + add_role( 'editor', 'Editor' ); + add_role( 'author', 'Author' ); + add_role( 'contributor', 'Contributor' ); + add_role( 'subscriber', 'Subscriber' ); + + // Add caps for Administrator role. + $role = get_role( 'administrator' ); + $role->add_cap( 'switch_themes' ); + $role->add_cap( 'edit_themes' ); + $role->add_cap( 'activate_plugins' ); + $role->add_cap( 'edit_plugins' ); + $role->add_cap( 'edit_users' ); + $role->add_cap( 'edit_files' ); + $role->add_cap( 'manage_options' ); + $role->add_cap( 'moderate_comments' ); + $role->add_cap( 'manage_categories' ); + $role->add_cap( 'manage_links' ); + $role->add_cap( 'upload_files' ); + $role->add_cap( 'import' ); + $role->add_cap( 'unfiltered_html' ); + $role->add_cap( 'edit_posts' ); + $role->add_cap( 'edit_others_posts' ); + $role->add_cap( 'edit_published_posts' ); + $role->add_cap( 'publish_posts' ); + $role->add_cap( 'edit_pages' ); + $role->add_cap( 'read' ); + $role->add_cap( 'level_10' ); + $role->add_cap( 'level_9' ); + $role->add_cap( 'level_8' ); + $role->add_cap( 'level_7' ); + $role->add_cap( 'level_6' ); + $role->add_cap( 'level_5' ); + $role->add_cap( 'level_4' ); + $role->add_cap( 'level_3' ); + $role->add_cap( 'level_2' ); + $role->add_cap( 'level_1' ); + $role->add_cap( 'level_0' ); + + // Add caps for Editor role. + $role = get_role( 'editor' ); + $role->add_cap( 'moderate_comments' ); + $role->add_cap( 'manage_categories' ); + $role->add_cap( 'manage_links' ); + $role->add_cap( 'upload_files' ); + $role->add_cap( 'unfiltered_html' ); + $role->add_cap( 'edit_posts' ); + $role->add_cap( 'edit_others_posts' ); + $role->add_cap( 'edit_published_posts' ); + $role->add_cap( 'publish_posts' ); + $role->add_cap( 'edit_pages' ); + $role->add_cap( 'read' ); + $role->add_cap( 'level_7' ); + $role->add_cap( 'level_6' ); + $role->add_cap( 'level_5' ); + $role->add_cap( 'level_4' ); + $role->add_cap( 'level_3' ); + $role->add_cap( 'level_2' ); + $role->add_cap( 'level_1' ); + $role->add_cap( 'level_0' ); + + // Add caps for Author role. + $role = get_role( 'author' ); + $role->add_cap( 'upload_files' ); + $role->add_cap( 'edit_posts' ); + $role->add_cap( 'edit_published_posts' ); + $role->add_cap( 'publish_posts' ); + $role->add_cap( 'read' ); + $role->add_cap( 'level_2' ); + $role->add_cap( 'level_1' ); + $role->add_cap( 'level_0' ); + + // Add caps for Contributor role. + $role = get_role( 'contributor' ); + $role->add_cap( 'edit_posts' ); + $role->add_cap( 'read' ); + $role->add_cap( 'level_1' ); + $role->add_cap( 'level_0' ); + + // Add caps for Subscriber role. + $role = get_role( 'subscriber' ); + $role->add_cap( 'read' ); + $role->add_cap( 'level_0' ); +} + +/** + * Create and modify WordPress roles for WordPress 2.1. + * + * @since 2.1.0 + */ +function populate_roles_210() { + $roles = array( 'administrator', 'editor' ); + foreach ( $roles as $role ) { + $role = get_role( $role ); + if ( empty( $role ) ) { + continue; + } + + $role->add_cap( 'edit_others_pages' ); + $role->add_cap( 'edit_published_pages' ); + $role->add_cap( 'publish_pages' ); + $role->add_cap( 'delete_pages' ); + $role->add_cap( 'delete_others_pages' ); + $role->add_cap( 'delete_published_pages' ); + $role->add_cap( 'delete_posts' ); + $role->add_cap( 'delete_others_posts' ); + $role->add_cap( 'delete_published_posts' ); + $role->add_cap( 'delete_private_posts' ); + $role->add_cap( 'edit_private_posts' ); + $role->add_cap( 'read_private_posts' ); + $role->add_cap( 'delete_private_pages' ); + $role->add_cap( 'edit_private_pages' ); + $role->add_cap( 'read_private_pages' ); + } + + $role = get_role( 'administrator' ); + if ( ! empty( $role ) ) { + $role->add_cap( 'delete_users' ); + $role->add_cap( 'create_users' ); + } + + $role = get_role( 'author' ); + if ( ! empty( $role ) ) { + $role->add_cap( 'delete_posts' ); + $role->add_cap( 'delete_published_posts' ); + } + + $role = get_role( 'contributor' ); + if ( ! empty( $role ) ) { + $role->add_cap( 'delete_posts' ); + } +} + +/** + * Create and modify WordPress roles for WordPress 2.3. + * + * @since 2.3.0 + */ +function populate_roles_230() { + $role = get_role( 'administrator' ); + + if ( ! empty( $role ) ) { + $role->add_cap( 'unfiltered_upload' ); + } +} + +/** + * Create and modify WordPress roles for WordPress 2.5. + * + * @since 2.5.0 + */ +function populate_roles_250() { + $role = get_role( 'administrator' ); + + if ( ! empty( $role ) ) { + $role->add_cap( 'edit_dashboard' ); + } +} + +/** + * Create and modify WordPress roles for WordPress 2.6. + * + * @since 2.6.0 + */ +function populate_roles_260() { + $role = get_role( 'administrator' ); + + if ( ! empty( $role ) ) { + $role->add_cap( 'update_plugins' ); + $role->add_cap( 'delete_plugins' ); + } +} + +/** + * Create and modify WordPress roles for WordPress 2.7. + * + * @since 2.7.0 + */ +function populate_roles_270() { + $role = get_role( 'administrator' ); + + if ( ! empty( $role ) ) { + $role->add_cap( 'install_plugins' ); + $role->add_cap( 'update_themes' ); + } +} + +/** + * Create and modify WordPress roles for WordPress 2.8. + * + * @since 2.8.0 + */ +function populate_roles_280() { + $role = get_role( 'administrator' ); + + if ( ! empty( $role ) ) { + $role->add_cap( 'install_themes' ); + } +} + +/** + * Create and modify WordPress roles for WordPress 3.0. + * + * @since 3.0.0 + */ +function populate_roles_300() { + $role = get_role( 'administrator' ); + + if ( ! empty( $role ) ) { + $role->add_cap( 'update_core' ); + $role->add_cap( 'list_users' ); + $role->add_cap( 'remove_users' ); + $role->add_cap( 'promote_users' ); + $role->add_cap( 'edit_theme_options' ); + $role->add_cap( 'delete_themes' ); + $role->add_cap( 'export' ); + } +} + +if ( ! function_exists( 'install_network' ) ) : + /** + * Install Network. + * + * @since 3.0.0 + */ + function install_network() { + if ( ! defined( 'WP_INSTALLING_NETWORK' ) ) { + define( 'WP_INSTALLING_NETWORK', true ); + } + + dbDelta( wp_get_db_schema( 'global' ) ); + } +endif; + +/** + * Populate network settings. + * + * @since 3.0.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * @global object $current_site + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. + * + * @param int $network_id ID of network to populate. + * @param string $domain The domain name for the network. Example: "example.com". + * @param string $email Email address for the network administrator. + * @param string $site_name The name of the network. + * @param string $path Optional. The path to append to the network's domain name. Default '/'. + * @param bool $subdomain_install Optional. Whether the network is a subdomain installation or a subdirectory installation. + * Default false, meaning the network is a subdirectory installation. + * @return true|WP_Error True on success, or WP_Error on warning (with the installation otherwise successful, + * so the error code must be checked) or failure. + */ +function populate_network( $network_id = 1, $domain = '', $email = '', $site_name = '', $path = '/', $subdomain_install = false ) { + global $wpdb, $current_site, $wp_rewrite; + + $network_id = (int) $network_id; + + $errors = new WP_Error(); + if ( '' === $domain ) { + $errors->add( 'empty_domain', __( 'You must provide a domain name.' ) ); + } + if ( '' === $site_name ) { + $errors->add( 'empty_sitename', __( 'You must provide a name for your network of sites.' ) ); + } + + // Check for network collision. + $network_exists = false; + if ( is_multisite() ) { + if ( get_network( $network_id ) ) { + $errors->add( 'siteid_exists', __( 'The network already exists.' ) ); + } + } else { + if ( $network_id === (int) $wpdb->get_var( + $wpdb->prepare( "SELECT id FROM $wpdb->site WHERE id = %d", $network_id ) + ) ) { + $errors->add( 'siteid_exists', __( 'The network already exists.' ) ); + } + } + + if ( ! is_email( $email ) ) { + $errors->add( 'invalid_email', __( 'You must provide a valid email address.' ) ); + } + + if ( $errors->has_errors() ) { + return $errors; + } + + if ( 1 === $network_id ) { + $wpdb->insert( + $wpdb->site, + array( + 'domain' => $domain, + 'path' => $path, + ) + ); + $network_id = $wpdb->insert_id; + } else { + $wpdb->insert( + $wpdb->site, + array( + 'domain' => $domain, + 'path' => $path, + 'id' => $network_id, + ) + ); + } + + populate_network_meta( + $network_id, + array( + 'admin_email' => $email, + 'site_name' => $site_name, + 'subdomain_install' => $subdomain_install, + ) + ); + + /* + * When upgrading from single to multisite, assume the current site will + * become the main site of the network. When using populate_network() + * to create another network in an existing multisite environment, skip + * these steps since the main site of the new network has not yet been + * created. + */ + if ( ! is_multisite() ) { + $current_site = new stdClass(); + $current_site->domain = $domain; + $current_site->path = $path; + $current_site->site_name = ucfirst( $domain ); + $wpdb->insert( + $wpdb->blogs, + array( + 'site_id' => $network_id, + 'blog_id' => 1, + 'domain' => $domain, + 'path' => $path, + 'registered' => current_time( 'mysql' ), + ) + ); + $current_site->blog_id = $wpdb->insert_id; + + $site_user_id = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT meta_value + FROM $wpdb->sitemeta + WHERE meta_key = %s AND site_id = %d", + 'admin_user_id', + $network_id + ) + ); + + update_user_meta( $site_user_id, 'source_domain', $domain ); + update_user_meta( $site_user_id, 'primary_blog', $current_site->blog_id ); + + // Unable to use update_network_option() while populating the network. + $wpdb->insert( + $wpdb->sitemeta, + array( + 'site_id' => $network_id, + 'meta_key' => 'main_site', + 'meta_value' => $current_site->blog_id, + ) + ); + + if ( $subdomain_install ) { + $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' ); + } else { + $wp_rewrite->set_permalink_structure( '/blog/%year%/%monthnum%/%day%/%postname%/' ); + } + + flush_rewrite_rules(); + + if ( ! $subdomain_install ) { + return true; + } + + $vhost_ok = false; + $errstr = ''; + $hostname = substr( md5( time() ), 0, 6 ) . '.' . $domain; // Very random hostname! + $page = wp_remote_get( + 'http://' . $hostname, + array( + 'timeout' => 5, + 'httpversion' => '1.1', + ) + ); + if ( is_wp_error( $page ) ) { + $errstr = $page->get_error_message(); + } elseif ( 200 === wp_remote_retrieve_response_code( $page ) ) { + $vhost_ok = true; + } + + if ( ! $vhost_ok ) { + $msg = '

    ' . __( 'Warning! Wildcard DNS may not be configured correctly!' ) . '

    '; + + $msg .= '

    ' . sprintf( + /* translators: %s: Host name. */ + __( 'The installer attempted to contact a random hostname (%s) on your domain.' ), + '' . $hostname . '' + ); + if ( ! empty( $errstr ) ) { + /* translators: %s: Error message. */ + $msg .= ' ' . sprintf( __( 'This resulted in an error message: %s' ), '' . $errstr . '' ); + } + $msg .= '

    '; + + $msg .= '

    ' . sprintf( + /* translators: %s: Asterisk symbol (*). */ + __( 'To use a subdomain configuration, you must have a wildcard entry in your DNS. This usually means adding a %s hostname record pointing at your web server in your DNS configuration tool.' ), + '*' + ) . '

    '; + + $msg .= '

    ' . __( 'You can still use your site but any subdomain you create may not be accessible. If you know your DNS is correct, ignore this message.' ) . '

    '; + + return new WP_Error( 'no_wildcard_dns', $msg ); + } + } + + return true; +} + +/** + * Creates WordPress network meta and sets the default values. + * + * @since 5.1.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * @global int $wp_db_version WordPress database version. + * + * @param int $network_id Network ID to populate meta for. + * @param array $meta Optional. Custom meta $key => $value pairs to use. Default empty array. + */ +function populate_network_meta( $network_id, array $meta = array() ) { + global $wpdb, $wp_db_version; + + $network_id = (int) $network_id; + + $email = ! empty( $meta['admin_email'] ) ? $meta['admin_email'] : ''; + $subdomain_install = isset( $meta['subdomain_install'] ) ? (int) $meta['subdomain_install'] : 0; + + // If a user with the provided email does not exist, default to the current user as the new network admin. + $site_user = ! empty( $email ) ? get_user_by( 'email', $email ) : false; + if ( false === $site_user ) { + $site_user = wp_get_current_user(); + } + + if ( empty( $email ) ) { + $email = $site_user->user_email; + } + + $template = get_option( 'template' ); + $stylesheet = get_option( 'stylesheet' ); + $allowed_themes = array( $stylesheet => true ); + + if ( $template !== $stylesheet ) { + $allowed_themes[ $template ] = true; + } + + if ( WP_DEFAULT_THEME !== $stylesheet && WP_DEFAULT_THEME !== $template ) { + $allowed_themes[ WP_DEFAULT_THEME ] = true; + } + + // If WP_DEFAULT_THEME doesn't exist, also include the latest core default theme. + if ( ! wp_get_theme( WP_DEFAULT_THEME )->exists() ) { + $core_default = WP_Theme::get_core_default_theme(); + if ( $core_default ) { + $allowed_themes[ $core_default->get_stylesheet() ] = true; + } + } + + if ( function_exists( 'clean_network_cache' ) ) { + clean_network_cache( $network_id ); + } else { + wp_cache_delete( $network_id, 'networks' ); + } + + if ( ! is_multisite() ) { + $site_admins = array( $site_user->user_login ); + $users = get_users( + array( + 'fields' => array( 'user_login' ), + 'role' => 'administrator', + ) + ); + if ( $users ) { + foreach ( $users as $user ) { + $site_admins[] = $user->user_login; + } + + $site_admins = array_unique( $site_admins ); + } + } else { + $site_admins = get_site_option( 'site_admins' ); + } + + /* translators: Do not translate USERNAME, SITE_NAME, BLOG_URL, PASSWORD: those are placeholders. */ + $welcome_email = __( + 'Howdy USERNAME, + +Your new SITE_NAME site has been successfully set up at: +BLOG_URL + +You can log in to the administrator account with the following information: + +Username: USERNAME +Password: PASSWORD +Log in here: BLOG_URLwp-login.php + +We hope you enjoy your new site. Thanks! + +--The Team @ SITE_NAME' + ); + + $misc_exts = array( + // Images. + 'jpg', + 'jpeg', + 'png', + 'gif', + 'webp', + // Video. + 'mov', + 'avi', + 'mpg', + '3gp', + '3g2', + // "audio". + 'midi', + 'mid', + // Miscellaneous. + 'pdf', + 'doc', + 'ppt', + 'odt', + 'pptx', + 'docx', + 'pps', + 'ppsx', + 'xls', + 'xlsx', + 'key', + ); + $audio_exts = wp_get_audio_extensions(); + $video_exts = wp_get_video_extensions(); + $upload_filetypes = array_unique( array_merge( $misc_exts, $audio_exts, $video_exts ) ); + + $sitemeta = array( + 'site_name' => __( 'My Network' ), + 'admin_email' => $email, + 'admin_user_id' => $site_user->ID, + 'registration' => 'none', + 'upload_filetypes' => implode( ' ', $upload_filetypes ), + 'blog_upload_space' => 100, + 'fileupload_maxk' => 1500, + 'site_admins' => $site_admins, + 'allowedthemes' => $allowed_themes, + 'illegal_names' => array( 'www', 'web', 'root', 'admin', 'main', 'invite', 'administrator', 'files' ), + 'wpmu_upgrade_site' => $wp_db_version, + 'welcome_email' => $welcome_email, + /* translators: %s: Site link. */ + 'first_post' => __( 'Welcome to %s. This is your first post. Edit or delete it, then start writing!' ), + // @todo - Network admins should have a method of editing the network siteurl (used for cookie hash). + 'siteurl' => get_option( 'siteurl' ) . '/', + 'add_new_users' => '0', + 'upload_space_check_disabled' => is_multisite() ? get_site_option( 'upload_space_check_disabled' ) : '1', + 'subdomain_install' => $subdomain_install, + 'ms_files_rewriting' => is_multisite() ? get_site_option( 'ms_files_rewriting' ) : '0', + 'user_count' => get_site_option( 'user_count' ), + 'initial_db_version' => get_option( 'initial_db_version' ), + 'active_sitewide_plugins' => array(), + 'WPLANG' => get_locale(), + ); + if ( ! $subdomain_install ) { + $sitemeta['illegal_names'][] = 'blog'; + } + + $sitemeta = wp_parse_args( $meta, $sitemeta ); + + /** + * Filters meta for a network on creation. + * + * @since 3.7.0 + * + * @param array $sitemeta Associative array of network meta keys and values to be inserted. + * @param int $network_id ID of network to populate. + */ + $sitemeta = apply_filters( 'populate_network_meta', $sitemeta, $network_id ); + + $insert = ''; + foreach ( $sitemeta as $meta_key => $meta_value ) { + if ( is_array( $meta_value ) ) { + $meta_value = serialize( $meta_value ); + } + if ( ! empty( $insert ) ) { + $insert .= ', '; + } + $insert .= $wpdb->prepare( '( %d, %s, %s)', $network_id, $meta_key, $meta_value ); + } + $wpdb->query( "INSERT INTO $wpdb->sitemeta ( site_id, meta_key, meta_value ) VALUES " . $insert ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared +} + +/** + * Creates WordPress site meta and sets the default values. + * + * @since 5.1.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param int $site_id Site ID to populate meta for. + * @param array $meta Optional. Custom meta $key => $value pairs to use. Default empty array. + */ +function populate_site_meta( $site_id, array $meta = array() ) { + global $wpdb; + + $site_id = (int) $site_id; + + if ( ! is_site_meta_supported() ) { + return; + } + + if ( empty( $meta ) ) { + return; + } + + /** + * Filters meta for a site on creation. + * + * @since 5.2.0 + * + * @param array $meta Associative array of site meta keys and values to be inserted. + * @param int $site_id ID of site to populate. + */ + $site_meta = apply_filters( 'populate_site_meta', $meta, $site_id ); + + $insert = ''; + foreach ( $site_meta as $meta_key => $meta_value ) { + if ( is_array( $meta_value ) ) { + $meta_value = serialize( $meta_value ); + } + if ( ! empty( $insert ) ) { + $insert .= ', '; + } + $insert .= $wpdb->prepare( '( %d, %s, %s)', $site_id, $meta_key, $meta_value ); + } + + $wpdb->query( "INSERT INTO $wpdb->blogmeta ( blog_id, meta_key, meta_value ) VALUES " . $insert ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + wp_cache_delete( $site_id, 'blog_meta' ); + wp_cache_set_sites_last_changed(); +} diff --git a/wp-admin/includes/screen.php b/wp-admin/includes/screen.php new file mode 100644 index 0000000..bf5aefc --- /dev/null +++ b/wp-admin/includes/screen.php @@ -0,0 +1,244 @@ +id ] ) ) { + /** + * Filters the column headers for a list table on a specific screen. + * + * The dynamic portion of the hook name, `$screen->id`, refers to the + * ID of a specific screen. For example, the screen ID for the Posts + * list table is edit-post, so the filter for that screen would be + * manage_edit-post_columns. + * + * @since 3.0.0 + * + * @param string[] $columns The column header labels keyed by column ID. + */ + $column_headers[ $screen->id ] = apply_filters( "manage_{$screen->id}_columns", array() ); + } + + return $column_headers[ $screen->id ]; +} + +/** + * Get a list of hidden columns. + * + * @since 2.7.0 + * + * @param string|WP_Screen $screen The screen you want the hidden columns for + * @return string[] Array of IDs of hidden columns. + */ +function get_hidden_columns( $screen ) { + if ( is_string( $screen ) ) { + $screen = convert_to_screen( $screen ); + } + + $hidden = get_user_option( 'manage' . $screen->id . 'columnshidden' ); + + $use_defaults = ! is_array( $hidden ); + + if ( $use_defaults ) { + $hidden = array(); + + /** + * Filters the default list of hidden columns. + * + * @since 4.4.0 + * + * @param string[] $hidden Array of IDs of columns hidden by default. + * @param WP_Screen $screen WP_Screen object of the current screen. + */ + $hidden = apply_filters( 'default_hidden_columns', $hidden, $screen ); + } + + /** + * Filters the list of hidden columns. + * + * @since 4.4.0 + * @since 4.4.1 Added the `use_defaults` parameter. + * + * @param string[] $hidden Array of IDs of hidden columns. + * @param WP_Screen $screen WP_Screen object of the current screen. + * @param bool $use_defaults Whether to show the default columns. + */ + return apply_filters( 'hidden_columns', $hidden, $screen, $use_defaults ); +} + +/** + * Prints the meta box preferences for screen meta. + * + * @since 2.7.0 + * + * @global array $wp_meta_boxes + * + * @param WP_Screen $screen + */ +function meta_box_prefs( $screen ) { + global $wp_meta_boxes; + + if ( is_string( $screen ) ) { + $screen = convert_to_screen( $screen ); + } + + if ( empty( $wp_meta_boxes[ $screen->id ] ) ) { + return; + } + + $hidden = get_hidden_meta_boxes( $screen ); + + foreach ( array_keys( $wp_meta_boxes[ $screen->id ] ) as $context ) { + foreach ( array( 'high', 'core', 'default', 'low' ) as $priority ) { + if ( ! isset( $wp_meta_boxes[ $screen->id ][ $context ][ $priority ] ) ) { + continue; + } + + foreach ( $wp_meta_boxes[ $screen->id ][ $context ][ $priority ] as $box ) { + if ( false === $box || ! $box['title'] ) { + continue; + } + + // Submit box cannot be hidden. + if ( 'submitdiv' === $box['id'] || 'linksubmitdiv' === $box['id'] ) { + continue; + } + + $widget_title = $box['title']; + + if ( is_array( $box['args'] ) && isset( $box['args']['__widget_basename'] ) ) { + $widget_title = $box['args']['__widget_basename']; + } + + $is_hidden = in_array( $box['id'], $hidden, true ); + + printf( + '', + esc_attr( $box['id'] ), + checked( $is_hidden, false, false ), + $widget_title + ); + } + } + } +} + +/** + * Gets an array of IDs of hidden meta boxes. + * + * @since 2.7.0 + * + * @param string|WP_Screen $screen Screen identifier + * @return string[] IDs of hidden meta boxes. + */ +function get_hidden_meta_boxes( $screen ) { + if ( is_string( $screen ) ) { + $screen = convert_to_screen( $screen ); + } + + $hidden = get_user_option( "metaboxhidden_{$screen->id}" ); + + $use_defaults = ! is_array( $hidden ); + + // Hide slug boxes by default. + if ( $use_defaults ) { + $hidden = array(); + + if ( 'post' === $screen->base ) { + if ( in_array( $screen->post_type, array( 'post', 'page', 'attachment' ), true ) ) { + $hidden = array( 'slugdiv', 'trackbacksdiv', 'postcustom', 'postexcerpt', 'commentstatusdiv', 'commentsdiv', 'authordiv', 'revisionsdiv' ); + } else { + $hidden = array( 'slugdiv' ); + } + } + + /** + * Filters the default list of hidden meta boxes. + * + * @since 3.1.0 + * + * @param string[] $hidden An array of IDs of meta boxes hidden by default. + * @param WP_Screen $screen WP_Screen object of the current screen. + */ + $hidden = apply_filters( 'default_hidden_meta_boxes', $hidden, $screen ); + } + + /** + * Filters the list of hidden meta boxes. + * + * @since 3.3.0 + * + * @param string[] $hidden An array of IDs of hidden meta boxes. + * @param WP_Screen $screen WP_Screen object of the current screen. + * @param bool $use_defaults Whether to show the default meta boxes. + * Default true. + */ + return apply_filters( 'hidden_meta_boxes', $hidden, $screen, $use_defaults ); +} + +/** + * Register and configure an admin screen option + * + * @since 3.1.0 + * + * @param string $option An option name. + * @param mixed $args Option-dependent arguments. + */ +function add_screen_option( $option, $args = array() ) { + $current_screen = get_current_screen(); + + if ( ! $current_screen ) { + return; + } + + $current_screen->add_option( $option, $args ); +} + +/** + * Get the current screen object + * + * @since 3.1.0 + * + * @global WP_Screen $current_screen WordPress current screen object. + * + * @return WP_Screen|null Current screen object or null when screen not defined. + */ +function get_current_screen() { + global $current_screen; + + if ( ! isset( $current_screen ) ) { + return null; + } + + return $current_screen; +} + +/** + * Set the current screen object + * + * @since 3.0.0 + * + * @param string|WP_Screen $hook_name Optional. The hook name (also known as the hook suffix) used to determine the screen, + * or an existing screen object. + */ +function set_current_screen( $hook_name = '' ) { + WP_Screen::get( $hook_name )->set_current_screen(); +} diff --git a/wp-admin/includes/taxonomy.php b/wp-admin/includes/taxonomy.php new file mode 100644 index 0000000..7765084 --- /dev/null +++ b/wp-admin/includes/taxonomy.php @@ -0,0 +1,316 @@ + $cat_name, + 'category_parent' => $category_parent, + ) + ); +} + +/** + * Creates categories for the given post. + * + * @since 2.0.0 + * + * @param string[] $categories Array of category names to create. + * @param int $post_id Optional. The post ID. Default empty. + * @return int[] Array of IDs of categories assigned to the given post. + */ +function wp_create_categories( $categories, $post_id = '' ) { + $cat_ids = array(); + foreach ( $categories as $category ) { + $id = category_exists( $category ); + if ( $id ) { + $cat_ids[] = $id; + } else { + $id = wp_create_category( $category ); + if ( $id ) { + $cat_ids[] = $id; + } + } + } + + if ( $post_id ) { + wp_set_post_categories( $post_id, $cat_ids ); + } + + return $cat_ids; +} + +/** + * Updates an existing Category or creates a new Category. + * + * @since 2.0.0 + * @since 2.5.0 $wp_error parameter was added. + * @since 3.0.0 The 'taxonomy' argument was added. + * + * @param array $catarr { + * Array of arguments for inserting a new category. + * + * @type int $cat_ID Category ID. A non-zero value updates an existing category. + * Default 0. + * @type string $taxonomy Taxonomy slug. Default 'category'. + * @type string $cat_name Category name. Default empty. + * @type string $category_description Category description. Default empty. + * @type string $category_nicename Category nice (display) name. Default empty. + * @type int|string $category_parent Category parent ID. Default empty. + * } + * @param bool $wp_error Optional. Default false. + * @return int|WP_Error The ID number of the new or updated Category on success. Zero or a WP_Error on failure, + * depending on param `$wp_error`. + */ +function wp_insert_category( $catarr, $wp_error = false ) { + $cat_defaults = array( + 'cat_ID' => 0, + 'taxonomy' => 'category', + 'cat_name' => '', + 'category_description' => '', + 'category_nicename' => '', + 'category_parent' => '', + ); + $catarr = wp_parse_args( $catarr, $cat_defaults ); + + if ( '' === trim( $catarr['cat_name'] ) ) { + if ( ! $wp_error ) { + return 0; + } else { + return new WP_Error( 'cat_name', __( 'You did not enter a category name.' ) ); + } + } + + $catarr['cat_ID'] = (int) $catarr['cat_ID']; + + // Are we updating or creating? + $update = ! empty( $catarr['cat_ID'] ); + + $name = $catarr['cat_name']; + $description = $catarr['category_description']; + $slug = $catarr['category_nicename']; + $parent = (int) $catarr['category_parent']; + if ( $parent < 0 ) { + $parent = 0; + } + + if ( empty( $parent ) + || ! term_exists( $parent, $catarr['taxonomy'] ) + || ( $catarr['cat_ID'] && term_is_ancestor_of( $catarr['cat_ID'], $parent, $catarr['taxonomy'] ) ) ) { + $parent = 0; + } + + $args = compact( 'name', 'slug', 'parent', 'description' ); + + if ( $update ) { + $catarr['cat_ID'] = wp_update_term( $catarr['cat_ID'], $catarr['taxonomy'], $args ); + } else { + $catarr['cat_ID'] = wp_insert_term( $catarr['cat_name'], $catarr['taxonomy'], $args ); + } + + if ( is_wp_error( $catarr['cat_ID'] ) ) { + if ( $wp_error ) { + return $catarr['cat_ID']; + } else { + return 0; + } + } + return $catarr['cat_ID']['term_id']; +} + +/** + * Aliases wp_insert_category() with minimal args. + * + * If you want to update only some fields of an existing category, call this + * function with only the new values set inside $catarr. + * + * @since 2.0.0 + * + * @param array $catarr The 'cat_ID' value is required. All other keys are optional. + * @return int|false The ID number of the new or updated Category on success. Zero or FALSE on failure. + */ +function wp_update_category( $catarr ) { + $cat_id = (int) $catarr['cat_ID']; + + if ( isset( $catarr['category_parent'] ) && ( $cat_id === (int) $catarr['category_parent'] ) ) { + return false; + } + + // First, get all of the original fields. + $category = get_term( $cat_id, 'category', ARRAY_A ); + _make_cat_compat( $category ); + + // Escape data pulled from DB. + $category = wp_slash( $category ); + + // Merge old and new fields with new fields overwriting old ones. + $catarr = array_merge( $category, $catarr ); + + return wp_insert_category( $catarr ); +} + +// +// Tags. +// + +/** + * Checks whether a post tag with a given name exists. + * + * @since 2.3.0 + * + * @param int|string $tag_name + * @return mixed Returns null if the term does not exist. + * Returns an array of the term ID and the term taxonomy ID if the pairing exists. + * Returns 0 if term ID 0 is passed to the function. + */ +function tag_exists( $tag_name ) { + return term_exists( $tag_name, 'post_tag' ); +} + +/** + * Adds a new tag to the database if it does not already exist. + * + * @since 2.3.0 + * + * @param int|string $tag_name + * @return array|WP_Error + */ +function wp_create_tag( $tag_name ) { + return wp_create_term( $tag_name, 'post_tag' ); +} + +/** + * Gets comma-separated list of tags available to edit. + * + * @since 2.3.0 + * + * @param int $post_id + * @param string $taxonomy Optional. The taxonomy for which to retrieve terms. Default 'post_tag'. + * @return string|false|WP_Error + */ +function get_tags_to_edit( $post_id, $taxonomy = 'post_tag' ) { + return get_terms_to_edit( $post_id, $taxonomy ); +} + +/** + * Gets comma-separated list of terms available to edit for the given post ID. + * + * @since 2.8.0 + * + * @param int $post_id + * @param string $taxonomy Optional. The taxonomy for which to retrieve terms. Default 'post_tag'. + * @return string|false|WP_Error + */ +function get_terms_to_edit( $post_id, $taxonomy = 'post_tag' ) { + $post_id = (int) $post_id; + if ( ! $post_id ) { + return false; + } + + $terms = get_object_term_cache( $post_id, $taxonomy ); + if ( false === $terms ) { + $terms = wp_get_object_terms( $post_id, $taxonomy ); + wp_cache_add( $post_id, wp_list_pluck( $terms, 'term_id' ), $taxonomy . '_relationships' ); + } + + if ( ! $terms ) { + return false; + } + if ( is_wp_error( $terms ) ) { + return $terms; + } + $term_names = array(); + foreach ( $terms as $term ) { + $term_names[] = $term->name; + } + + $terms_to_edit = esc_attr( implode( ',', $term_names ) ); + + /** + * Filters the comma-separated list of terms available to edit. + * + * @since 2.8.0 + * + * @see get_terms_to_edit() + * + * @param string $terms_to_edit A comma-separated list of term names. + * @param string $taxonomy The taxonomy name for which to retrieve terms. + */ + $terms_to_edit = apply_filters( 'terms_to_edit', $terms_to_edit, $taxonomy ); + + return $terms_to_edit; +} + +/** + * Adds a new term to the database if it does not already exist. + * + * @since 2.8.0 + * + * @param string $tag_name The term name. + * @param string $taxonomy Optional. The taxonomy within which to create the term. Default 'post_tag'. + * @return array|WP_Error + */ +function wp_create_term( $tag_name, $taxonomy = 'post_tag' ) { + $id = term_exists( $tag_name, $taxonomy ); + if ( $id ) { + return $id; + } + + return wp_insert_term( $tag_name, $taxonomy ); +} diff --git a/wp-admin/includes/template.php b/wp-admin/includes/template.php new file mode 100644 index 0000000..1d4a8e8 --- /dev/null +++ b/wp-admin/includes/template.php @@ -0,0 +1,2821 @@ + 'category', + 'descendants_and_self' => $descendants_and_self, + 'selected_cats' => $selected_cats, + 'popular_cats' => $popular_cats, + 'walker' => $walker, + 'checked_ontop' => $checked_ontop, + ) + ); +} + +/** + * Outputs an unordered list of checkbox input elements labelled with term names. + * + * Taxonomy-independent version of wp_category_checklist(). + * + * @since 3.0.0 + * @since 4.4.0 Introduced the `$echo` argument. + * + * @param int $post_id Optional. Post ID. Default 0. + * @param array|string $args { + * Optional. Array or string of arguments for generating a terms checklist. Default empty array. + * + * @type int $descendants_and_self ID of the category to output along with its descendants. + * Default 0. + * @type int[] $selected_cats Array of category IDs to mark as checked. Default false. + * @type int[] $popular_cats Array of category IDs to receive the "popular-category" class. + * Default false. + * @type Walker $walker Walker object to use to build the output. Default empty which + * results in a Walker_Category_Checklist instance being used. + * @type string $taxonomy Taxonomy to generate the checklist for. Default 'category'. + * @type bool $checked_ontop Whether to move checked items out of the hierarchy and to + * the top of the list. Default true. + * @type bool $echo Whether to echo the generated markup. False to return the markup instead + * of echoing it. Default true. + * } + * @return string HTML list of input elements. + */ +function wp_terms_checklist( $post_id = 0, $args = array() ) { + $defaults = array( + 'descendants_and_self' => 0, + 'selected_cats' => false, + 'popular_cats' => false, + 'walker' => null, + 'taxonomy' => 'category', + 'checked_ontop' => true, + 'echo' => true, + ); + + /** + * Filters the taxonomy terms checklist arguments. + * + * @since 3.4.0 + * + * @see wp_terms_checklist() + * + * @param array|string $args An array or string of arguments. + * @param int $post_id The post ID. + */ + $params = apply_filters( 'wp_terms_checklist_args', $args, $post_id ); + + $parsed_args = wp_parse_args( $params, $defaults ); + + if ( empty( $parsed_args['walker'] ) || ! ( $parsed_args['walker'] instanceof Walker ) ) { + $walker = new Walker_Category_Checklist(); + } else { + $walker = $parsed_args['walker']; + } + + $taxonomy = $parsed_args['taxonomy']; + $descendants_and_self = (int) $parsed_args['descendants_and_self']; + + $args = array( 'taxonomy' => $taxonomy ); + + $tax = get_taxonomy( $taxonomy ); + $args['disabled'] = ! current_user_can( $tax->cap->assign_terms ); + + $args['list_only'] = ! empty( $parsed_args['list_only'] ); + + if ( is_array( $parsed_args['selected_cats'] ) ) { + $args['selected_cats'] = array_map( 'intval', $parsed_args['selected_cats'] ); + } elseif ( $post_id ) { + $args['selected_cats'] = wp_get_object_terms( $post_id, $taxonomy, array_merge( $args, array( 'fields' => 'ids' ) ) ); + } else { + $args['selected_cats'] = array(); + } + + if ( is_array( $parsed_args['popular_cats'] ) ) { + $args['popular_cats'] = array_map( 'intval', $parsed_args['popular_cats'] ); + } else { + $args['popular_cats'] = get_terms( + array( + 'taxonomy' => $taxonomy, + 'fields' => 'ids', + 'orderby' => 'count', + 'order' => 'DESC', + 'number' => 10, + 'hierarchical' => false, + ) + ); + } + + if ( $descendants_and_self ) { + $categories = (array) get_terms( + array( + 'taxonomy' => $taxonomy, + 'child_of' => $descendants_and_self, + 'hierarchical' => 0, + 'hide_empty' => 0, + ) + ); + $self = get_term( $descendants_and_self, $taxonomy ); + array_unshift( $categories, $self ); + } else { + $categories = (array) get_terms( + array( + 'taxonomy' => $taxonomy, + 'get' => 'all', + ) + ); + } + + $output = ''; + + if ( $parsed_args['checked_ontop'] ) { + /* + * Post-process $categories rather than adding an exclude to the get_terms() query + * to keep the query the same across all posts (for any query cache). + */ + $checked_categories = array(); + $keys = array_keys( $categories ); + + foreach ( $keys as $k ) { + if ( in_array( $categories[ $k ]->term_id, $args['selected_cats'], true ) ) { + $checked_categories[] = $categories[ $k ]; + unset( $categories[ $k ] ); + } + } + + // Put checked categories on top. + $output .= $walker->walk( $checked_categories, 0, $args ); + } + // Then the rest of them. + $output .= $walker->walk( $categories, 0, $args ); + + if ( $parsed_args['echo'] ) { + echo $output; + } + + return $output; +} + +/** + * Retrieves a list of the most popular terms from the specified taxonomy. + * + * If the `$display` argument is true then the elements for a list of checkbox + * `` elements labelled with the names of the selected terms is output. + * If the `$post_ID` global is not empty then the terms associated with that + * post will be marked as checked. + * + * @since 2.5.0 + * + * @param string $taxonomy Taxonomy to retrieve terms from. + * @param int $default_term Optional. Not used. + * @param int $number Optional. Number of terms to retrieve. Default 10. + * @param bool $display Optional. Whether to display the list as well. Default true. + * @return int[] Array of popular term IDs. + */ +function wp_popular_terms_checklist( $taxonomy, $default_term = 0, $number = 10, $display = true ) { + $post = get_post(); + + if ( $post && $post->ID ) { + $checked_terms = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'ids' ) ); + } else { + $checked_terms = array(); + } + + $terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'orderby' => 'count', + 'order' => 'DESC', + 'number' => $number, + 'hierarchical' => false, + ) + ); + + $tax = get_taxonomy( $taxonomy ); + + $popular_ids = array(); + + foreach ( (array) $terms as $term ) { + $popular_ids[] = $term->term_id; + + if ( ! $display ) { // Hack for Ajax use. + continue; + } + + $id = "popular-$taxonomy-$term->term_id"; + $checked = in_array( $term->term_id, $checked_terms, true ) ? 'checked="checked"' : ''; + ?> + + + + 'link_category', + 'orderby' => 'name', + 'hide_empty' => 0, + ) + ); + + if ( empty( $categories ) ) { + return; + } + + foreach ( $categories as $category ) { + $cat_id = $category->term_id; + + /** This filter is documented in wp-includes/category-template.php */ + $name = esc_html( apply_filters( 'the_category', $category->name, '', '' ) ); + $checked = in_array( $cat_id, $checked_categories, true ) ? ' checked="checked"' : ''; + echo ''; + } +} + +/** + * Adds hidden fields with the data for use in the inline editor for posts and pages. + * + * @since 2.7.0 + * + * @param WP_Post $post Post object. + */ +function get_inline_data( $post ) { + $post_type_object = get_post_type_object( $post->post_type ); + if ( ! current_user_can( 'edit_post', $post->ID ) ) { + return; + } + + $title = esc_textarea( trim( $post->post_title ) ); + + echo ' +'; +} + +/** + * Outputs the in-line comment reply-to form in the Comments list table. + * + * @since 2.7.0 + * + * @global WP_List_Table $wp_list_table + * + * @param int $position Optional. The value of the 'position' input field. Default 1. + * @param bool $checkbox Optional. The value of the 'checkbox' input field. Default false. + * @param string $mode Optional. If set to 'single', will use WP_Post_Comments_List_Table, + * otherwise WP_Comments_List_Table. Default 'single'. + * @param bool $table_row Optional. Whether to use a table instead of a div element. Default true. + */ +function wp_comment_reply( $position = 1, $checkbox = false, $mode = 'single', $table_row = true ) { + global $wp_list_table; + /** + * Filters the in-line comment reply-to form output in the Comments + * list table. + * + * Returning a non-empty value here will short-circuit display + * of the in-line comment-reply form in the Comments list table, + * echoing the returned value instead. + * + * @since 2.7.0 + * + * @see wp_comment_reply() + * + * @param string $content The reply-to form content. + * @param array $args An array of default args. + */ + $content = apply_filters( + 'wp_comment_reply', + '', + array( + 'position' => $position, + 'checkbox' => $checkbox, + 'mode' => $mode, + ) + ); + + if ( ! empty( $content ) ) { + echo $content; + return; + } + + if ( ! $wp_list_table ) { + if ( 'single' === $mode ) { + $wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table' ); + } else { + $wp_list_table = _get_list_table( 'WP_Comments_List_Table' ); + } + } + + ?> +
    + +
    + + + +
    + + + + + + + ' . _x( 'Name', 'meta name' ) . ' + ' . __( 'Value' ) . ' + + + + + +'; // TBODY needed for list-manipulation JS. + return; + } + $count = 0; + ?> + + + + + + + + + + +
    + . + $entry['meta_id'] = (int) $entry['meta_id']; + + $delete_nonce = wp_create_nonce( 'delete-meta_' . $entry['meta_id'] ); + + $r .= "\n\t"; + $r .= "\n\t\t"; + + $r .= "\n\t\t
    "; + $r .= get_submit_button( __( 'Delete' ), 'deletemeta small', "deletemeta[{$entry['meta_id']}]", false, array( 'data-wp-lists' => "delete:the-list:meta-{$entry['meta_id']}::_ajax_nonce=$delete_nonce" ) ); + $r .= "\n\t\t"; + $r .= get_submit_button( __( 'Update' ), 'updatemeta small', "meta-{$entry['meta_id']}-submit", false, array( 'data-wp-lists' => "add:the-list:meta-{$entry['meta_id']}::_ajax_nonce-add-meta=$update_nonce" ) ); + $r .= '
    '; + $r .= wp_nonce_field( 'change-meta', '_ajax_nonce', false, false ); + $r .= ''; + + $r .= "\n\t\t\n\t"; + return $r; +} + +/** + * Prints the form in the Custom Fields meta box. + * + * @since 1.2.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param WP_Post $post Optional. The post being edited. + */ +function meta_form( $post = null ) { + global $wpdb; + $post = get_post( $post ); + + /** + * Filters values for the meta key dropdown in the Custom Fields meta box. + * + * Returning a non-null value will effectively short-circuit and avoid a + * potentially expensive query against postmeta. + * + * @since 4.4.0 + * + * @param array|null $keys Pre-defined meta keys to be used in place of a postmeta query. Default null. + * @param WP_Post $post The current post object. + */ + $keys = apply_filters( 'postmeta_form_keys', null, $post ); + + if ( null === $keys ) { + /** + * Filters the number of custom fields to retrieve for the drop-down + * in the Custom Fields meta box. + * + * @since 2.1.0 + * + * @param int $limit Number of custom fields to retrieve. Default 30. + */ + $limit = apply_filters( 'postmeta_form_limit', 30 ); + + $keys = $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT meta_key + FROM $wpdb->postmeta + WHERE meta_key NOT BETWEEN '_' AND '_z' + HAVING meta_key NOT LIKE %s + ORDER BY meta_key + LIMIT %d", + $wpdb->esc_like( '_' ) . '%', + $limit + ) + ); + } + + if ( $keys ) { + natcasesort( $keys ); + } + ?> +

    + + + + + + + + + + + + + + +
    + + + + + + + + + +
    +
    + 'newmeta-submit', + 'data-wp-lists' => 'add:the-list:newmeta', + ) + ); + ?> +
    + post_status, array( 'draft', 'pending' ), true ) && ( ! $post->post_date_gmt || '0000-00-00 00:00:00' === $post->post_date_gmt ) ); + } + + $tab_index_attribute = ''; + if ( (int) $tab_index > 0 ) { + $tab_index_attribute = " tabindex=\"$tab_index\""; + } + + // @todo Remove this? + // echo '
    '; + + $post_date = ( $for_post ) ? $post->post_date : get_comment()->comment_date; + $jj = ( $edit ) ? mysql2date( 'd', $post_date, false ) : current_time( 'd' ); + $mm = ( $edit ) ? mysql2date( 'm', $post_date, false ) : current_time( 'm' ); + $aa = ( $edit ) ? mysql2date( 'Y', $post_date, false ) : current_time( 'Y' ); + $hh = ( $edit ) ? mysql2date( 'H', $post_date, false ) : current_time( 'H' ); + $mn = ( $edit ) ? mysql2date( 'i', $post_date, false ) : current_time( 'i' ); + $ss = ( $edit ) ? mysql2date( 's', $post_date, false ) : current_time( 's' ); + + $cur_jj = current_time( 'd' ); + $cur_mm = current_time( 'm' ); + $cur_aa = current_time( 'Y' ); + $cur_hh = current_time( 'H' ); + $cur_mn = current_time( 'i' ); + + $month = ''; + + $day = ''; + $year = ''; + $hour = ''; + $minute = ''; + + echo '
    '; + /* translators: 1: Month, 2: Day, 3: Year, 4: Hour, 5: Minute. */ + printf( __( '%1$s %2$s, %3$s at %4$s:%5$s' ), $month, $day, $year, $hour, $minute ); + + echo '
    '; + + if ( $multi ) { + return; + } + + echo "\n\n"; + + $map = array( + 'mm' => array( $mm, $cur_mm ), + 'jj' => array( $jj, $cur_jj ), + 'aa' => array( $aa, $cur_aa ), + 'hh' => array( $hh, $cur_hh ), + 'mn' => array( $mn, $cur_mn ), + ); + + foreach ( $map as $timeunit => $value ) { + list( $unit, $curr ) = $value; + + echo '' . "\n"; + $cur_timeunit = 'cur_' . $timeunit; + echo '' . "\n"; + } + ?> + +

    + + +

    + " . esc_html( $template ) . ''; + } +} + +/** + * Prints out option HTML elements for the page parents drop-down. + * + * @since 1.5.0 + * @since 4.4.0 `$post` argument was added. + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param int $default_page Optional. The default page ID to be pre-selected. Default 0. + * @param int $parent_page Optional. The parent page ID. Default 0. + * @param int $level Optional. Page depth level. Default 0. + * @param int|WP_Post $post Post ID or WP_Post object. + * @return void|false Void on success, false if the page has no children. + */ +function parent_dropdown( $default_page = 0, $parent_page = 0, $level = 0, $post = null ) { + global $wpdb; + + $post = get_post( $post ); + $items = $wpdb->get_results( + $wpdb->prepare( + "SELECT ID, post_parent, post_title + FROM $wpdb->posts + WHERE post_parent = %d AND post_type = 'page' + ORDER BY menu_order", + $parent_page + ) + ); + + if ( $items ) { + foreach ( $items as $item ) { + // A page cannot be its own parent. + if ( $post && $post->ID && (int) $item->ID === $post->ID ) { + continue; + } + + $pad = str_repeat( ' ', $level * 3 ); + $selected = selected( $default_page, $item->ID, false ); + + echo "\n\t'; + parent_dropdown( $default_page, $item->ID, $level + 1 ); + } + } else { + return false; + } +} + +/** + * Prints out option HTML elements for role selectors. + * + * @since 2.1.0 + * + * @param string $selected Slug for the role that should be already selected. + */ +function wp_dropdown_roles( $selected = '' ) { + $r = ''; + + $editable_roles = array_reverse( get_editable_roles() ); + + foreach ( $editable_roles as $role => $details ) { + $name = translate_user_role( $details['name'] ); + // Preselect specified role. + if ( $selected === $role ) { + $r .= "\n\t"; + } else { + $r .= "\n\t"; + } + } + + echo $r; +} + +/** + * Outputs the form used by the importers to accept the data to be imported. + * + * @since 2.0.0 + * + * @param string $action The action attribute for the form. + */ +function wp_import_upload_form( $action ) { + + /** + * Filters the maximum allowed upload size for import files. + * + * @since 2.3.0 + * + * @see wp_max_upload_size() + * + * @param int $max_upload_size Allowed upload size. Default 1 MB. + */ + $bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); + $size = size_format( $bytes ); + $upload_dir = wp_upload_dir(); + if ( ! empty( $upload_dir['error'] ) ) : + $upload_directory_error = '

    ' . __( 'Before you can upload your import file, you will need to fix the following error:' ) . '

    '; + $upload_directory_error .= '

    ' . $upload_dir['error'] . '

    '; + wp_admin_notice( + $upload_directory_error, + array( + 'additional_classes' => array( 'error' ), + 'paragraph_wrap' => false, + ) + ); + else : + ?> +
    +

    + %s (%s)', + __( 'Choose a file from your computer:' ), + /* translators: %s: Maximum allowed file size. */ + sprintf( __( 'Maximum size: %s' ), $size ) + ); + ?> + + + +

    + +
    + id ) ) { + return; + } + + $page = $screen->id; + + if ( ! isset( $wp_meta_boxes ) ) { + $wp_meta_boxes = array(); + } + if ( ! isset( $wp_meta_boxes[ $page ] ) ) { + $wp_meta_boxes[ $page ] = array(); + } + if ( ! isset( $wp_meta_boxes[ $page ][ $context ] ) ) { + $wp_meta_boxes[ $page ][ $context ] = array(); + } + + foreach ( array_keys( $wp_meta_boxes[ $page ] ) as $a_context ) { + foreach ( array( 'high', 'core', 'default', 'low' ) as $a_priority ) { + if ( ! isset( $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ] ) ) { + continue; + } + + // If a core box was previously removed, don't add. + if ( ( 'core' === $priority || 'sorted' === $priority ) + && false === $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ] + ) { + return; + } + + // If a core box was previously added by a plugin, don't add. + if ( 'core' === $priority ) { + /* + * If the box was added with default priority, give it core priority + * to maintain sort order. + */ + if ( 'default' === $a_priority ) { + $wp_meta_boxes[ $page ][ $a_context ]['core'][ $id ] = $wp_meta_boxes[ $page ][ $a_context ]['default'][ $id ]; + unset( $wp_meta_boxes[ $page ][ $a_context ]['default'][ $id ] ); + } + return; + } + + // If no priority given and ID already present, use existing priority. + if ( empty( $priority ) ) { + $priority = $a_priority; + /* + * Else, if we're adding to the sorted priority, we don't know the title + * or callback. Grab them from the previously added context/priority. + */ + } elseif ( 'sorted' === $priority ) { + $title = $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ]['title']; + $callback = $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ]['callback']; + $callback_args = $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ]['args']; + } + + // An ID can be in only one priority and one context. + if ( $priority !== $a_priority || $context !== $a_context ) { + unset( $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ] ); + } + } + } + + if ( empty( $priority ) ) { + $priority = 'low'; + } + + if ( ! isset( $wp_meta_boxes[ $page ][ $context ][ $priority ] ) ) { + $wp_meta_boxes[ $page ][ $context ][ $priority ] = array(); + } + + $wp_meta_boxes[ $page ][ $context ][ $priority ][ $id ] = array( + 'id' => $id, + 'title' => $title, + 'callback' => $callback, + 'args' => $callback_args, + ); +} + + +/** + * Renders a "fake" meta box with an information message, + * shown on the block editor, when an incompatible meta box is found. + * + * @since 5.0.0 + * + * @param mixed $data_object The data object being rendered on this screen. + * @param array $box { + * Custom formats meta box arguments. + * + * @type string $id Meta box 'id' attribute. + * @type string $title Meta box title. + * @type callable $old_callback The original callback for this meta box. + * @type array $args Extra meta box arguments. + * } + */ +function do_block_editor_incompatible_meta_box( $data_object, $box ) { + $plugin = _get_plugin_from_callback( $box['old_callback'] ); + $plugins = get_plugins(); + echo '

    '; + if ( $plugin ) { + /* translators: %s: The name of the plugin that generated this meta box. */ + printf( __( 'This meta box, from the %s plugin, is not compatible with the block editor.' ), "{$plugin['Name']}" ); + } else { + _e( 'This meta box is not compatible with the block editor.' ); + } + echo '

    '; + + if ( empty( $plugins['classic-editor/classic-editor.php'] ) ) { + if ( current_user_can( 'install_plugins' ) ) { + $install_url = wp_nonce_url( + self_admin_url( 'plugin-install.php?tab=favorites&user=wordpressdotorg&save=0' ), + 'save_wporg_username_' . get_current_user_id() + ); + + echo '

    '; + /* translators: %s: A link to install the Classic Editor plugin. */ + printf( __( 'Please install the Classic Editor plugin to use this meta box.' ), esc_url( $install_url ) ); + echo '

    '; + } + } elseif ( is_plugin_inactive( 'classic-editor/classic-editor.php' ) ) { + if ( current_user_can( 'activate_plugins' ) ) { + $activate_url = wp_nonce_url( + self_admin_url( 'plugins.php?action=activate&plugin=classic-editor/classic-editor.php' ), + 'activate-plugin_classic-editor/classic-editor.php' + ); + + echo '

    '; + /* translators: %s: A link to activate the Classic Editor plugin. */ + printf( __( 'Please activate the Classic Editor plugin to use this meta box.' ), esc_url( $activate_url ) ); + echo '

    '; + } + } elseif ( $data_object instanceof WP_Post ) { + $edit_url = add_query_arg( + array( + 'classic-editor' => '', + 'classic-editor__forget' => '', + ), + get_edit_post_link( $data_object ) + ); + echo '

    '; + /* translators: %s: A link to use the Classic Editor plugin. */ + printf( __( 'Please open the classic editor to use this meta box.' ), esc_url( $edit_url ) ); + echo '

    '; + } +} + +/** + * Internal helper function to find the plugin from a meta box callback. + * + * @since 5.0.0 + * + * @access private + * + * @param callable $callback The callback function to check. + * @return array|null The plugin that the callback belongs to, or null if it doesn't belong to a plugin. + */ +function _get_plugin_from_callback( $callback ) { + try { + if ( is_array( $callback ) ) { + $reflection = new ReflectionMethod( $callback[0], $callback[1] ); + } elseif ( is_string( $callback ) && str_contains( $callback, '::' ) ) { + $reflection = new ReflectionMethod( $callback ); + } else { + $reflection = new ReflectionFunction( $callback ); + } + } catch ( ReflectionException $exception ) { + // We could not properly reflect on the callable, so we abort here. + return null; + } + + // Don't show an error if it's an internal PHP function. + if ( ! $reflection->isInternal() ) { + + // Only show errors if the meta box was registered by a plugin. + $filename = wp_normalize_path( $reflection->getFileName() ); + $plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ); + + if ( str_starts_with( $filename, $plugin_dir ) ) { + $filename = str_replace( $plugin_dir, '', $filename ); + $filename = preg_replace( '|^/([^/]*/).*$|', '\\1', $filename ); + + $plugins = get_plugins(); + + foreach ( $plugins as $name => $plugin ) { + if ( str_starts_with( $name, $filename ) ) { + return $plugin; + } + } + } + } + + return null; +} + +/** + * Meta-Box template function. + * + * @since 2.5.0 + * + * @global array $wp_meta_boxes + * + * @param string|WP_Screen $screen The screen identifier. If you have used add_menu_page() or + * add_submenu_page() to create a new screen (and hence screen_id) + * make sure your menu slug conforms to the limits of sanitize_key() + * otherwise the 'screen' menu may not correctly render on your page. + * @param string $context The screen context for which to display meta boxes. + * @param mixed $data_object Gets passed to the meta box callback function as the first parameter. + * Often this is the object that's the focus of the current screen, + * for example a `WP_Post` or `WP_Comment` object. + * @return int Number of meta_boxes. + */ +function do_meta_boxes( $screen, $context, $data_object ) { + global $wp_meta_boxes; + static $already_sorted = false; + + if ( empty( $screen ) ) { + $screen = get_current_screen(); + } elseif ( is_string( $screen ) ) { + $screen = convert_to_screen( $screen ); + } + + $page = $screen->id; + + $hidden = get_hidden_meta_boxes( $screen ); + + printf( '
    ', esc_attr( $context ) ); + + /* + * Grab the ones the user has manually sorted. + * Pull them out of their previous context/priority and into the one the user chose. + */ + $sorted = get_user_option( "meta-box-order_$page" ); + + if ( ! $already_sorted && $sorted ) { + foreach ( $sorted as $box_context => $ids ) { + foreach ( explode( ',', $ids ) as $id ) { + if ( $id && 'dashboard_browser_nag' !== $id ) { + add_meta_box( $id, null, null, $screen, $box_context, 'sorted' ); + } + } + } + } + + $already_sorted = true; + + $i = 0; + + if ( isset( $wp_meta_boxes[ $page ][ $context ] ) ) { + foreach ( array( 'high', 'sorted', 'core', 'default', 'low' ) as $priority ) { + if ( isset( $wp_meta_boxes[ $page ][ $context ][ $priority ] ) ) { + foreach ( (array) $wp_meta_boxes[ $page ][ $context ][ $priority ] as $box ) { + if ( false === $box || ! $box['title'] ) { + continue; + } + + $block_compatible = true; + if ( is_array( $box['args'] ) ) { + // If a meta box is just here for back compat, don't show it in the block editor. + if ( $screen->is_block_editor() && isset( $box['args']['__back_compat_meta_box'] ) && $box['args']['__back_compat_meta_box'] ) { + continue; + } + + if ( isset( $box['args']['__block_editor_compatible_meta_box'] ) ) { + $block_compatible = (bool) $box['args']['__block_editor_compatible_meta_box']; + unset( $box['args']['__block_editor_compatible_meta_box'] ); + } + + // If the meta box is declared as incompatible with the block editor, override the callback function. + if ( ! $block_compatible && $screen->is_block_editor() ) { + $box['old_callback'] = $box['callback']; + $box['callback'] = 'do_block_editor_incompatible_meta_box'; + } + + if ( isset( $box['args']['__back_compat_meta_box'] ) ) { + $block_compatible = $block_compatible || (bool) $box['args']['__back_compat_meta_box']; + unset( $box['args']['__back_compat_meta_box'] ); + } + } + + ++$i; + // get_hidden_meta_boxes() doesn't apply in the block editor. + $hidden_class = ( ! $screen->is_block_editor() && in_array( $box['id'], $hidden, true ) ) ? ' hide-if-js' : ''; + echo '
    ' . "\n"; + + echo '
    '; + echo '

    '; + if ( 'dashboard_php_nag' === $box['id'] ) { + echo ''; + echo '' . + /* translators: Hidden accessibility text. */ + __( 'Warning:' ) . + ' '; + } + echo $box['title']; + echo "

    \n"; + + if ( 'dashboard_browser_nag' !== $box['id'] ) { + $widget_title = $box['title']; + + if ( is_array( $box['args'] ) && isset( $box['args']['__widget_basename'] ) ) { + $widget_title = $box['args']['__widget_basename']; + // Do not pass this parameter to the user callback function. + unset( $box['args']['__widget_basename'] ); + } + + echo '
    '; + + echo ''; + echo ''; + + echo ''; + echo ''; + + echo ''; + + echo '
    '; + } + echo '
    '; + + echo '
    ' . "\n"; + + if ( WP_DEBUG && ! $block_compatible && 'edit' === $screen->parent_base && ! $screen->is_block_editor() && ! isset( $_GET['meta-box-loader'] ) ) { + $plugin = _get_plugin_from_callback( $box['callback'] ); + if ( $plugin ) { + $meta_box_not_compatible_message = sprintf( + /* translators: %s: The name of the plugin that generated this meta box. */ + __( 'This meta box, from the %s plugin, is not compatible with the block editor.' ), + "{$plugin['Name']}" + ); + wp_admin_notice( + $meta_box_not_compatible_message, + array( + 'additional_classes' => array( 'error', 'inline' ), + ) + ); + } + } + + call_user_func( $box['callback'], $data_object, $box ); + echo "
    \n"; + echo "
    \n"; + } + } + } + } + + echo '
    '; + + return $i; +} + +/** + * Removes a meta box from one or more screens. + * + * @since 2.6.0 + * @since 4.4.0 The `$screen` parameter now accepts an array of screen IDs. + * + * @global array $wp_meta_boxes + * + * @param string $id Meta box ID (used in the 'id' attribute for the meta box). + * @param string|array|WP_Screen $screen The screen or screens on which the meta box is shown (such as a + * post type, 'link', or 'comment'). Accepts a single screen ID, + * WP_Screen object, or array of screen IDs. + * @param string $context The context within the screen where the box is set to display. + * Contexts vary from screen to screen. Post edit screen contexts + * include 'normal', 'side', and 'advanced'. Comments screen contexts + * include 'normal' and 'side'. Menus meta boxes (accordion sections) + * all use the 'side' context. + */ +function remove_meta_box( $id, $screen, $context ) { + global $wp_meta_boxes; + + if ( empty( $screen ) ) { + $screen = get_current_screen(); + } elseif ( is_string( $screen ) ) { + $screen = convert_to_screen( $screen ); + } elseif ( is_array( $screen ) ) { + foreach ( $screen as $single_screen ) { + remove_meta_box( $id, $single_screen, $context ); + } + } + + if ( ! isset( $screen->id ) ) { + return; + } + + $page = $screen->id; + + if ( ! isset( $wp_meta_boxes ) ) { + $wp_meta_boxes = array(); + } + if ( ! isset( $wp_meta_boxes[ $page ] ) ) { + $wp_meta_boxes[ $page ] = array(); + } + if ( ! isset( $wp_meta_boxes[ $page ][ $context ] ) ) { + $wp_meta_boxes[ $page ][ $context ] = array(); + } + + foreach ( array( 'high', 'core', 'default', 'low' ) as $priority ) { + $wp_meta_boxes[ $page ][ $context ][ $priority ][ $id ] = false; + } +} + +/** + * Meta Box Accordion Template Function. + * + * Largely made up of abstracted code from do_meta_boxes(), this + * function serves to build meta boxes as list items for display as + * a collapsible accordion. + * + * @since 3.6.0 + * + * @uses global $wp_meta_boxes Used to retrieve registered meta boxes. + * + * @param string|object $screen The screen identifier. + * @param string $context The screen context for which to display accordion sections. + * @param mixed $data_object Gets passed to the section callback function as the first parameter. + * @return int Number of meta boxes as accordion sections. + */ +function do_accordion_sections( $screen, $context, $data_object ) { + global $wp_meta_boxes; + + wp_enqueue_script( 'accordion' ); + + if ( empty( $screen ) ) { + $screen = get_current_screen(); + } elseif ( is_string( $screen ) ) { + $screen = convert_to_screen( $screen ); + } + + $page = $screen->id; + + $hidden = get_hidden_meta_boxes( $screen ); + ?> +
    +
      + +
    • +

      + + + + +

      +
      +
      + +
      +
      +
    • + +
    +
    + $id, + 'title' => $title, + 'callback' => $callback, + 'before_section' => '', + 'after_section' => '', + 'section_class' => '', + ); + + $section = wp_parse_args( $args, $defaults ); + + if ( 'misc' === $page ) { + _deprecated_argument( + __FUNCTION__, + '3.0.0', + sprintf( + /* translators: %s: misc */ + __( 'The "%s" options group has been removed. Use another settings group.' ), + 'misc' + ) + ); + $page = 'general'; + } + + if ( 'privacy' === $page ) { + _deprecated_argument( + __FUNCTION__, + '3.5.0', + sprintf( + /* translators: %s: privacy */ + __( 'The "%s" options group has been removed. Use another settings group.' ), + 'privacy' + ) + ); + $page = 'reading'; + } + + $wp_settings_sections[ $page ][ $id ] = $section; +} + +/** + * Adds a new field to a section of a settings page. + * + * Part of the Settings API. Use this to define a settings field that will show + * as part of a settings section inside a settings page. The fields are shown using + * do_settings_fields() in do_settings_sections(). + * + * The $callback argument should be the name of a function that echoes out the + * HTML input tags for this setting field. Use get_option() to retrieve existing + * values to show. + * + * @since 2.7.0 + * @since 4.2.0 The `$class` argument was added. + * + * @global array $wp_settings_fields Storage array of settings fields and info about their pages/sections. + * + * @param string $id Slug-name to identify the field. Used in the 'id' attribute of tags. + * @param string $title Formatted title of the field. Shown as the label for the field + * during output. + * @param callable $callback Function that fills the field with the desired form inputs. The + * function should echo its output. + * @param string $page The slug-name of the settings page on which to show the section + * (general, reading, writing, ...). + * @param string $section Optional. The slug-name of the section of the settings page + * in which to show the box. Default 'default'. + * @param array $args { + * Optional. Extra arguments that get passed to the callback function. + * + * @type string $label_for When supplied, the setting title will be wrapped + * in a `