class-wp-links-list-table.php 0000644 00000022031 15025130033 0012150 0 ustar 00 'bookmarks', 'screen' => isset( $args['screen'] ) ? $args['screen'] : null, ) ); } /** * @return bool */ public function ajax_user_can() { return current_user_can( 'manage_links' ); } /** * @global int $cat_id * @global string $s * @global string $orderby * @global string $order */ public function prepare_items() { global $cat_id, $s, $orderby, $order; $cat_id = ! empty( $_REQUEST['cat_id'] ) ? absint( $_REQUEST['cat_id'] ) : 0; $orderby = ! empty( $_REQUEST['orderby'] ) ? sanitize_text_field( $_REQUEST['orderby'] ) : ''; $order = ! empty( $_REQUEST['order'] ) ? sanitize_text_field( $_REQUEST['order'] ) : ''; $s = ! empty( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : ''; $args = array( 'hide_invisible' => 0, 'hide_empty' => 0, ); if ( 'all' !== $cat_id ) { $args['category'] = $cat_id; } if ( ! empty( $s ) ) { $args['search'] = $s; } if ( ! empty( $orderby ) ) { $args['orderby'] = $orderby; } if ( ! empty( $order ) ) { $args['order'] = $order; } $this->items = get_bookmarks( $args ); } /** */ public function no_items() { _e( 'No links found.' ); } /** * @return array */ protected function get_bulk_actions() { $actions = array(); $actions['delete'] = __( 'Delete' ); return $actions; } /** * @global int $cat_id * @param string $which */ protected function extra_tablenav( $which ) { global $cat_id; if ( 'top' !== $which ) { return; } ?>
' . __( '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 'admin_image_div_callback ) { call_user_func( $this->admin_image_div_callback ); } else { $custom_header = get_custom_header(); $header_image = get_header_image(); if ( $header_image ) { $header_image_style = 'background-image:url(' . esc_url( $header_image ) . ');'; } else { $header_image_style = ''; } if ( $custom_header->width ) { $header_image_style .= 'max-width:' . $custom_header->width . 'px;'; } if ( $custom_header->height ) { $header_image_style .= 'height:' . $custom_header->height . 'px;'; } ?> | |
|
' . __( 'The active theme does not support uploading a custom header image. Please ensure your theme supports custom headers and try again.' ) . '
', 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; } ?>' . __( 'The active theme does not support uploading a custom header image. Please ensure your theme supports custom headers and try again.' ) . '
', 403 ); } if ( ! empty( $_POST['skip-cropping'] ) && ! current_theme_supports( 'custom-header', 'flex-height' ) && ! current_theme_supports( 'custom-header', 'flex-width' ) ) { wp_die( '' . __( '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 = wp_copy_parent_attachment_properties( $cropped, $attachment_id, 'custom-header' ); 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 * @deprecated 6.5.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 ) { _deprecated_function( __METHOD__, '6.5.0', 'wp_copy_parent_attachment_properties()' ); $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 = wp_copy_parent_attachment_properties( $cropped, $attachment_id, 'custom-header' ); $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; } } class-wp-ajax-upgrader-skin.php 0000644 00000010141 15025130033 0012465 0 ustar 00 errors = new WP_Error(); } /** * Retrieves the list of errors. * * @since 4.6.0 * * @return WP_Error Errors during an upgrade. */ public function get_errors() { return $this->errors; } /** * Retrieves a string for error messages. * * @since 4.6.0 * * @return string Error messages during an upgrade. */ public function get_error_messages() { $messages = array(); foreach ( $this->errors->get_error_codes() as $error_code ) { $error_data = $this->errors->get_error_data( $error_code ); if ( $error_data && is_string( $error_data ) ) { $messages[] = $this->errors->get_error_message( $error_code ) . ' ' . esc_html( strip_tags( $error_data ) ); } else { $messages[] = $this->errors->get_error_message( $error_code ); } } return implode( ', ', $messages ); } /** * Stores an error message about the upgrade. * * @since 4.6.0 * @since 5.3.0 Formalized the existing `...$args` parameter by adding it * to the function signature. * * @param string|WP_Error $errors Errors. * @param mixed ...$args Optional text replacements. */ public function error( $errors, ...$args ) { if ( is_string( $errors ) ) { $string = $errors; if ( ! empty( $this->upgrader->strings[ $string ] ) ) { $string = $this->upgrader->strings[ $string ]; } if ( str_contains( $string, '%' ) ) { if ( ! empty( $args ) ) { $string = vsprintf( $string, $args ); } } // Count existing errors to generate a unique error code. $errors_count = count( $this->errors->get_error_codes() ); $this->errors->add( 'unknown_upgrade_error_' . ( $errors_count + 1 ), $string ); } elseif ( is_wp_error( $errors ) ) { foreach ( $errors->get_error_codes() as $error_code ) { $this->errors->add( $error_code, $errors->get_error_message( $error_code ), $errors->get_error_data( $error_code ) ); } } parent::error( $errors, ...$args ); } /** * Stores a message about the upgrade. * * @since 4.6.0 * @since 5.3.0 Formalized the existing `...$args` parameter by adding it * to the function signature. * @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 ) ) { foreach ( $feedback->get_error_codes() as $error_code ) { $this->errors->add( $error_code, $feedback->get_error_message( $error_code ), $feedback->get_error_data( $error_code ) ); } } parent::feedback( $feedback, ...$args ); } } credits.php 0000644 00000013356 15025130033 0006712 0 ustar 00 'WordPress/' . $version . '; ' . home_url( '/' ) ); if ( wp_http_supports( array( 'ssl' ) ) ) { $url = set_url_scheme( $url, 'https' ); } $response = wp_remote_get( $url, $options ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } $results = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $results ) ) { return false; } set_site_transient( 'wordpress_credits_' . $locale, $results, DAY_IN_SECONDS ); } return $results; } /** * Retrieves the link to a contributor's WordPress.org profile page. * * @access private * @since 3.2.0 * * @param string $display_name The contributor's display name (passed by reference). * @param string $username The contributor's username. * @param string $profiles URL to the contributor's WordPress.org profile page. */ function _wp_credits_add_profile_link( &$display_name, $username, $profiles ) { $display_name = '' . esc_html( $display_name ) . ''; } /** * Retrieves the link to an external library used in WordPress. * * @access private * @since 3.2.0 * * @param string $data External library data (passed by reference). */ function _wp_credits_build_object_link( &$data ) { $data = '' . esc_html( $data[0] ) . ''; } /** * Displays the title for a given group of contributors. * * @since 5.3.0 * * @param array $group_data The current contributor group. */ function wp_credits_section_title( $group_data = array() ) { if ( ! count( $group_data ) ) { return; } if ( $group_data['name'] ) { if ( 'Translators' === $group_data['name'] ) { // Considered a special slug in the API response. (Also, will never be returned for en_US.) $title = _x( 'Translators', 'Translate this to be the equivalent of English Translators in your language for the credits page Translators section' ); } elseif ( isset( $group_data['placeholders'] ) ) { // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText $title = vsprintf( translate( $group_data['name'] ), $group_data['placeholders'] ); } else { // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText $title = translate( $group_data['name'] ); } echo '' . wp_sprintf( '%l.', $group_data['data'] ) . "
\n\n"; break; case 'libraries': array_walk( $group_data['data'], '_wp_credits_build_object_link' ); echo '' . wp_sprintf( '%l.', $group_data['data'] ) . "
\n\n"; break; default: $compact = 'compact' === $group_data['type']; $classes = 'wp-people-group ' . ( $compact ? 'compact' : '' ); echo '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 );
}
} elseif ( 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-in plugins 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 of data about the drop-in.
*
* @type array ...$0 {
* Data about the drop-in.
*
* @type string $0 The purpose of the drop-in.
* @type string|true $1 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' );
$wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.l10n.php' );
$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`, `Requires PHP` and `Requires Plugins` 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.
* @since 6.5.0 Added support for the 'Requires Plugins' header.
*
* @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'] : '',
'requires_plugins' => ! empty( $plugin_headers['RequiresPlugins'] ) ? $plugin_headers['RequiresPlugins'] : '',
);
$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'] ) . '
' ); } WP_Plugin_Dependencies::initialize(); if ( WP_Plugin_Dependencies::has_unmet_dependencies( $plugin ) ) { $dependency_names = WP_Plugin_Dependencies::get_dependency_names( $plugin ); $unmet_dependencies = array(); $unmet_dependency_names = array(); foreach ( $dependency_names as $dependency => $dependency_name ) { $dependency_file = WP_Plugin_Dependencies::get_dependency_filepath( $dependency ); if ( false === $dependency_file ) { $unmet_dependencies['not_installed'][ $dependency ] = $dependency_name; $unmet_dependency_names[] = $dependency_name; } elseif ( is_plugin_inactive( $dependency_file ) ) { $unmet_dependencies['inactive'][ $dependency ] = $dependency_name; $unmet_dependency_names[] = $dependency_name; } } $error_message = sprintf( /* translators: 1: Plugin name, 2: Number of plugins, 3: A comma-separated list of plugin names. */ _n( 'Error: %1$s requires %2$d plugin to be installed and activated: %3$s.', 'Error: %1$s requires %2$d plugins to be installed and activated: %3$s.', count( $unmet_dependency_names ) ), $plugin_headers['Name'], count( $unmet_dependency_names ), implode( wp_get_list_item_separator(), $unmet_dependency_names ) ); if ( is_multisite() ) { if ( current_user_can( 'manage_network_plugins' ) ) { $error_message .= ' ' . sprintf( /* translators: %s: Link to the plugins page. */ __( 'Manage plugins.' ), esc_url( network_admin_url( 'plugins.php' ) ) ); } else { $error_message .= ' ' . __( 'Please contact your network administrator.' ); } } else { $error_message .= ' ' . sprintf( /* translators: %s: Link to the plugins page. */ __( 'Manage plugins.' ), esc_url( admin_url( 'plugins.php' ) ) ); } return new WP_Error( 'plugin_missing_dependencies', "{$error_message}
", $unmet_dependencies ); } 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 The title of the current screen.
* @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
*
* @global WP_Paused_Extensions_Storage $_paused_plugins
*
* @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
*
* @global WP_Paused_Extensions_Storage $_paused_plugins
*
* @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.
* @global WP_Paused_Extensions_Storage $_paused_plugins
*/
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',
__( '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(), false );
}
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(), false ); if ( is_multisite() ) { update_site_option( 'wp_force_deactivated_plugins', array() ); } } file.php 0000644 00000277571 15025130033 0006207 0 ustar 00 __( 'Theme Functions' ), 'header.php' => __( 'Theme Header' ), 'footer.php' => __( 'Theme Footer' ), 'sidebar.php' => __( 'Sidebar' ), 'comments.php' => __( 'Comments' ), 'searchform.php' => __( 'Search Form' ), '404.php' => __( '404 Template' ), 'link.php' => __( 'Links Template' ), 'theme.json' => __( 'Theme Styles & Block Settings' ), // Archives. 'index.php' => __( 'Main Index Template' ), 'archive.php' => __( 'Archives' ), 'author.php' => __( 'Author Template' ), 'taxonomy.php' => __( 'Taxonomy Template' ), 'category.php' => __( 'Category Template' ), 'tag.php' => __( 'Tag Template' ), 'home.php' => __( 'Posts Page' ), 'search.php' => __( 'Search Results' ), 'date.php' => __( 'Date Template' ), // Content. 'singular.php' => __( 'Singular Template' ), 'single.php' => __( 'Single Post' ), 'page.php' => __( 'Single Page' ), 'front-page.php' => __( 'Homepage' ), 'privacy-policy.php' => __( 'Privacy Policy Page' ), // Attachments. 'attachment.php' => __( 'Attachment Template' ), 'image.php' => __( 'Image Attachment Template' ), 'video.php' => __( 'Video Attachment Template' ), 'audio.php' => __( 'Audio Attachment Template' ), 'application.php' => __( 'Application Attachment Template' ), // Embeds. 'embed.php' => __( 'Embed Template' ), 'embed-404.php' => __( 'Embed 404 Template' ), 'embed-content.php' => __( 'Embed Content Template' ), 'header-embed.php' => __( 'Embed Header Template' ), 'footer-embed.php' => __( 'Embed Footer Template' ), // Stylesheets. 'style.css' => __( 'Stylesheet' ), 'editor-style.css' => __( 'Visual Editor Stylesheet' ), 'editor-style-rtl.css' => __( 'Visual Editor RTL Stylesheet' ), 'rtl.css' => __( 'RTL Stylesheet' ), // Other. 'my-hacks.php' => __( 'my-hacks.php (legacy hacks support)' ), '.htaccess' => __( '.htaccess (for rewrite rules )' ), // Deprecated files. 'wp-layout.css' => __( 'Stylesheet' ), 'wp-comments.php' => __( 'Comments Template' ), 'wp-comments-popup.php' => __( 'Popup Comments Template' ), 'comments-popup.php' => __( 'Popup Comments' ), ); /** * Gets the description for standard WordPress theme files. * * @since 1.5.0 * * @global array $wp_file_descriptions Theme file descriptions. * @global array $allowed_files List of allowed files. * * @param string $file Filesystem path or filename. * @return string Description of file from $wp_file_descriptions or basename of $file if description doesn't exist. * Appends 'Page Template' to basename of $file if the file is a page template. */ function get_file_description( $file ) { global $wp_file_descriptions, $allowed_files; $dirname = pathinfo( $file, PATHINFO_DIRNAME ); $file_path = $allowed_files[ $file ]; if ( isset( $wp_file_descriptions[ basename( $file ) ] ) && '.' === $dirname ) { return $wp_file_descriptions[ basename( $file ) ]; } elseif ( file_exists( $file_path ) && is_file( $file_path ) ) { $template_data = implode( '', file( $file_path ) ); if ( preg_match( '|Template Name:(.*)$|mi', $template_data, $name ) ) { /* translators: %s: Template name. */ return sprintf( __( '%s Page Template' ), _cleanup_header_comment( $name[1] ) ); } } return trim( basename( $file ) ); } /** * Gets the absolute filesystem path to the root of the WordPress installation. * * @since 1.5.0 * * @return string Full filesystem path to the root of the WordPress installation. */ function get_home_path() { $home = set_url_scheme( get_option( 'home' ), 'http' ); $siteurl = set_url_scheme( get_option( 'siteurl' ), 'http' ); if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) { $wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */ $pos = strripos( str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ), trailingslashit( $wp_path_rel_to_home ) ); $home_path = substr( $_SERVER['SCRIPT_FILENAME'], 0, $pos ); $home_path = trailingslashit( $home_path ); } else { $home_path = ABSPATH; } return str_replace( '\\', '/', $home_path ); } /** * Returns a listing of all files in the specified folder and all subdirectories up to 100 levels deep. * * The depth of the recursiveness can be controlled by the $levels param. * * @since 2.6.0 * @since 4.9.0 Added the `$exclusions` parameter. * @since 6.3.0 Added the `$include_hidden` parameter. * * @param string $folder Optional. Full path to folder. Default empty. * @param int $levels Optional. Levels of folders to follow, Default 100 (PHP Loop limit). * @param string[] $exclusions Optional. List of folders and files to skip. * @param bool $include_hidden Optional. Whether to include details of hidden ("." prefixed) files. * Default false. * @return string[]|false Array of files on success, false on failure. */ function list_files( $folder = '', $levels = 100, $exclusions = array(), $include_hidden = false ) { if ( empty( $folder ) ) { return false; } $folder = trailingslashit( $folder ); if ( ! $levels ) { return false; } $files = array(); $dir = @opendir( $folder ); if ( $dir ) { while ( ( $file = readdir( $dir ) ) !== false ) { // Skip current and parent folder links. if ( in_array( $file, array( '.', '..' ), true ) ) { continue; } // Skip hidden and excluded files. if ( ( ! $include_hidden && '.' === $file[0] ) || in_array( $file, $exclusions, true ) ) { continue; } if ( is_dir( $folder . $file ) ) { $files2 = list_files( $folder . $file, $levels - 1, array(), $include_hidden ); if ( $files2 ) { $files = array_merge( $files, $files2 ); } else { $files[] = $folder . $file . '/'; } } else { $files[] = $folder . $file; } } closedir( $dir ); } return $files; } /** * Gets the list of file extensions that are editable in plugins. * * @since 4.9.0 * * @param string $plugin Path to the plugin file relative to the plugins directory. * @return string[] Array of editable file extensions. */ function wp_get_plugin_file_editable_extensions( $plugin ) { $default_types = array( 'bash', 'conf', 'css', 'diff', 'htm', 'html', 'http', 'inc', 'include', 'js', 'json', 'jsx', 'less', 'md', 'patch', 'php', 'php3', 'php4', 'php5', 'php7', 'phps', 'phtml', 'sass', 'scss', 'sh', 'sql', 'svg', 'text', 'txt', 'xml', 'yaml', 'yml', ); /** * Filters the list of file types allowed for editing in the plugin file editor. * * @since 2.8.0 * @since 4.9.0 Added the `$plugin` parameter. * * @param string[] $default_types An array of editable plugin file extensions. * @param string $plugin Path to the plugin file relative to the plugins directory. */ $file_types = (array) apply_filters( 'editable_extensions', $default_types, $plugin ); return $file_types; } /** * Gets the list of file extensions that are editable for a given theme. * * @since 4.9.0 * * @param WP_Theme $theme Theme object. * @return string[] Array of editable file extensions. */ function wp_get_theme_file_editable_extensions( $theme ) { $default_types = array( 'bash', 'conf', 'css', 'diff', 'htm', 'html', 'http', 'inc', 'include', 'js', 'json', 'jsx', 'less', 'md', 'patch', 'php', 'php3', 'php4', 'php5', 'php7', 'phps', 'phtml', 'sass', 'scss', 'sh', 'sql', 'svg', 'text', 'txt', 'xml', 'yaml', 'yml', ); /** * Filters the list of file types allowed for editing in the theme file editor. * * @since 4.4.0 * * @param string[] $default_types An array of editable theme file extensions. * @param WP_Theme $theme The active theme object. */ $file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme ); // Ensure that default types are still there. return array_unique( array_merge( $file_types, $default_types ) ); } /** * Prints file editor templates (for plugins and themes). * * @since 4.9.0 */ function wp_print_file_editor_templates() { ?> exists() ) { return new WP_Error( 'non_existent_theme', __( 'The requested theme does not exist.' ) ); } if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $stylesheet . '_' . $file ) ) { return new WP_Error( 'nonce_failure' ); } if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) { return new WP_Error( 'theme_no_stylesheet', __( 'The requested theme does not exist.' ) . ' ' . $theme->errors()->get_error_message() ); } $editable_extensions = wp_get_theme_file_editable_extensions( $theme ); $allowed_files = array(); foreach ( $editable_extensions as $type ) { switch ( $type ) { case 'php': $allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) ); break; case 'css': $style_files = $theme->get_files( 'css', -1 ); $allowed_files['style.css'] = $style_files['style.css']; $allowed_files = array_merge( $allowed_files, $style_files ); break; default: $allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) ); break; } } // Compare based on relative paths. if ( 0 !== validate_file( $file, array_keys( $allowed_files ) ) ) { return new WP_Error( 'disallowed_theme_file', __( 'Sorry, that file cannot be edited.' ) ); } $real_file = $theme->get_stylesheet_directory() . '/' . $file; $is_active = ( get_stylesheet() === $stylesheet || get_template() === $stylesheet ); } else { return new WP_Error( 'missing_theme_or_plugin' ); } // Ensure file is real. if ( ! is_file( $real_file ) ) { return new WP_Error( 'file_does_not_exist', __( 'File does not exist! Please double check the name and try again.' ) ); } // Ensure file extension is allowed. $extension = null; if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) { $extension = strtolower( $matches[1] ); if ( ! in_array( $extension, $editable_extensions, true ) ) { return new WP_Error( 'illegal_file_type', __( 'Files of this type are not editable.' ) ); } } $previous_content = file_get_contents( $real_file ); if ( ! is_writable( $real_file ) ) { return new WP_Error( 'file_not_writable' ); } $f = fopen( $real_file, 'w+' ); if ( false === $f ) { return new WP_Error( 'file_not_writable' ); } $written = fwrite( $f, $content ); fclose( $f ); if ( false === $written ) { return new WP_Error( 'unable_to_write', __( 'Unable to write to file.' ) ); } wp_opcache_invalidate( $real_file, true ); if ( $is_active && 'php' === $extension ) { $scrape_key = md5( rand() ); $transient = 'scrape_key_' . $scrape_key; $scrape_nonce = (string) rand(); // It shouldn't take more than 60 seconds to make the two loopback requests. set_transient( $transient, $scrape_nonce, 60 ); $cookies = wp_unslash( $_COOKIE ); $scrape_params = array( 'wp_scrape_key' => $scrape_key, 'wp_scrape_nonce' => $scrape_nonce, ); $headers = array( 'Cache-Control' => 'no-cache', ); /** This filter is documented in wp-includes/class-wp-http-streams.php */ $sslverify = apply_filters( 'https_local_ssl_verify', false ); // Include Basic auth in loopback requests. if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) { $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) ); } // Make sure PHP process doesn't die before loopback requests complete. if ( function_exists( 'set_time_limit' ) ) { set_time_limit( 5 * MINUTE_IN_SECONDS ); } // Time to wait for loopback requests to finish. $timeout = 100; // 100 seconds. $needle_start = "###### wp_scraping_result_start:$scrape_key ######"; $needle_end = "###### wp_scraping_result_end:$scrape_key ######"; // Attempt loopback request to editor to see if user just whitescreened themselves. if ( $plugin ) { $url = add_query_arg( compact( 'plugin', 'file' ), admin_url( 'plugin-editor.php' ) ); } elseif ( isset( $stylesheet ) ) { $url = add_query_arg( array( 'theme' => $stylesheet, 'file' => $file, ), admin_url( 'theme-editor.php' ) ); } else { $url = admin_url(); } if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) { /* * Close any active session to prevent HTTP requests from timing out * when attempting to connect back to the site. */ session_write_close(); } $url = add_query_arg( $scrape_params, $url ); $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) ); $body = wp_remote_retrieve_body( $r ); $scrape_result_position = strpos( $body, $needle_start ); $loopback_request_failure = array( 'code' => 'loopback_request_failed', 'message' => __( 'Unable to communicate back with site to check for fatal errors, so the PHP change was reverted. You will need to upload your PHP file change by some other means, such as by using SFTP.' ), ); $json_parse_failure = array( 'code' => 'json_parse_error', ); $result = null; if ( false === $scrape_result_position ) { $result = $loopback_request_failure; } else { $error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) ); $error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) ); $result = json_decode( trim( $error_output ), true ); if ( empty( $result ) ) { $result = $json_parse_failure; } } // Try making request to homepage as well to see if visitors have been whitescreened. if ( true === $result ) { $url = home_url( '/' ); $url = add_query_arg( $scrape_params, $url ); $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) ); $body = wp_remote_retrieve_body( $r ); $scrape_result_position = strpos( $body, $needle_start ); if ( false === $scrape_result_position ) { $result = $loopback_request_failure; } else { $error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) ); $error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) ); $result = json_decode( trim( $error_output ), true ); if ( empty( $result ) ) { $result = $json_parse_failure; } } } delete_transient( $transient ); if ( true !== $result ) { // Roll-back file change. file_put_contents( $real_file, $previous_content ); wp_opcache_invalidate( $real_file, true ); if ( ! isset( $result['message'] ) ) { $message = __( 'An error occurred. Please try again later.' ); } else { $message = $result['message']; unset( $result['message'] ); } return new WP_Error( 'php_error', $message, $result ); } } if ( $theme instanceof WP_Theme ) { $theme->cache_delete(); } return true; } /** * Returns a filename of a temporary unique file. * * Please note that the calling function must delete or move the file. * * The filename is based off the passed parameter or defaults to the current unix timestamp, * while the directory can either be passed as well, or by leaving it blank, default to a writable * temporary directory. * * @since 2.6.0 * * @param string $filename Optional. Filename to base the Unique file off. Default empty. * @param string $dir Optional. Directory to store the file in. Default empty. * @return string A writable filename. */ function wp_tempnam( $filename = '', $dir = '' ) { if ( empty( $dir ) ) { $dir = get_temp_dir(); } if ( empty( $filename ) || in_array( $filename, array( '.', '/', '\\' ), true ) ) { $filename = uniqid(); } // Use the basename of the given file without the extension as the name for the temporary directory. $temp_filename = basename( $filename ); $temp_filename = preg_replace( '|\.[^.]*$|', '', $temp_filename ); // If the folder is falsey, use its parent directory name instead. if ( ! $temp_filename ) { return wp_tempnam( dirname( $filename ), $dir ); } // Suffix some random data to avoid filename conflicts. $temp_filename .= '-' . wp_generate_password( 6, false ); $temp_filename .= '.tmp'; $temp_filename = wp_unique_filename( $dir, $temp_filename ); /* * Filesystems typically have a limit of 255 characters for a filename. * * If the generated unique filename exceeds this, truncate the initial * filename and try again. * * As it's possible that the truncated filename may exist, producing a * suffix of "-1" or "-10" which could exceed the limit again, truncate * it to 252 instead. */ $characters_over_limit = strlen( $temp_filename ) - 252; if ( $characters_over_limit > 0 ) { $filename = substr( $filename, 0, -$characters_over_limit ); return wp_tempnam( $filename, $dir ); } $temp_filename = $dir . $temp_filename; $fp = @fopen( $temp_filename, 'x' ); if ( ! $fp && is_writable( $dir ) && file_exists( $temp_filename ) ) { return wp_tempnam( $filename, $dir ); } if ( $fp ) { fclose( $fp ); } return $temp_filename; } /** * Makes sure that the file that was requested to be edited is allowed to be edited. * * Function will die if you are not allowed to edit the file. * * @since 1.5.0 * * @param string $file File the user is attempting to edit. * @param string[] $allowed_files Optional. Array of allowed files to edit. * `$file` must match an entry exactly. * @return string|void Returns the file name on success, dies on failure. */ function validate_file_to_edit( $file, $allowed_files = array() ) { $code = validate_file( $file, $allowed_files ); if ( ! $code ) { return $file; } switch ( $code ) { case 1: wp_die( __( 'Sorry, that file cannot be edited.' ) ); // case 2 : // wp_die( __('Sorry, cannot call files with their real path.' )); case 3: wp_die( __( 'Sorry, that file cannot be edited.' ) ); } } /** * Handles PHP uploads in WordPress. * * Sanitizes file names, checks extensions for mime type, and moves the file * to the appropriate directory within the uploads directory. * * @access private * @since 4.0.0 * * @see wp_handle_upload_error * * @param array $file { * Reference to a single element from `$_FILES`. Call the function once for each uploaded file. * * @type string $name The original name of the file on the client machine. * @type string $type The mime type of the file, if the browser provided this information. * @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server. * @type int $size The size, in bytes, of the uploaded file. * @type int $error The error code associated with this file upload. * } * @param array|false $overrides { * An array of override parameters for this file, or boolean false if none are provided. * * @type callable $upload_error_handler Function to call when there is an error during the upload process. * See {@see wp_handle_upload_error()}. * @type callable $unique_filename_callback Function to call when determining a unique file name for the file. * See {@see wp_unique_filename()}. * @type string[] $upload_error_strings The strings that describe the error indicated in * `$_FILES[{form field}]['error']`. * @type bool $test_form Whether to test that the `$_POST['action']` parameter is as expected. * @type bool $test_size Whether to test that the file size is greater than zero bytes. * @type bool $test_type Whether to test that the mime type of the file is as expected. * @type string[] $mimes Array of allowed mime types keyed by their file extension regex. * } * @param string $time Time formatted in 'yyyy/mm'. * @param string $action Expected value for `$_POST['action']`. * @return array { * On success, returns an associative array of file attributes. * On failure, returns `$overrides['upload_error_handler']( &$file, $message )` * or `array( 'error' => $message )`. * * @type string $file Filename of the newly-uploaded file. * @type string $url URL of the newly-uploaded file. * @type string $type Mime type of the newly-uploaded file. * } */ function _wp_handle_upload( &$file, $overrides, $time, $action ) { // The default error handler. if ( ! function_exists( 'wp_handle_upload_error' ) ) { function wp_handle_upload_error( &$file, $message ) { return array( 'error' => $message ); } } /** * Filters the data for a file before it is uploaded to WordPress. * * The dynamic portion of the hook name, `$action`, refers to the post action. * * Possible hook names include: * * - `wp_handle_sideload_prefilter` * - `wp_handle_upload_prefilter` * * @since 2.9.0 as 'wp_handle_upload_prefilter'. * @since 4.0.0 Converted to a dynamic hook with `$action`. * * @param array $file { * Reference to a single element from `$_FILES`. * * @type string $name The original name of the file on the client machine. * @type string $type The mime type of the file, if the browser provided this information. * @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server. * @type int $size The size, in bytes, of the uploaded file. * @type int $error The error code associated with this file upload. * } */ $file = apply_filters( "{$action}_prefilter", $file ); /** * Filters the override parameters for a file before it is uploaded to WordPress. * * The dynamic portion of the hook name, `$action`, refers to the post action. * * Possible hook names include: * * - `wp_handle_sideload_overrides` * - `wp_handle_upload_overrides` * * @since 5.7.0 * * @param array|false $overrides An array of override parameters for this file. Boolean false if none are * provided. See {@see _wp_handle_upload()}. * @param array $file { * Reference to a single element from `$_FILES`. * * @type string $name The original name of the file on the client machine. * @type string $type The mime type of the file, if the browser provided this information. * @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server. * @type int $size The size, in bytes, of the uploaded file. * @type int $error The error code associated with this file upload. * } */ $overrides = apply_filters( "{$action}_overrides", $overrides, $file ); // You may define your own function and pass the name in $overrides['upload_error_handler']. $upload_error_handler = 'wp_handle_upload_error'; if ( isset( $overrides['upload_error_handler'] ) ) { $upload_error_handler = $overrides['upload_error_handler']; } // You may have had one or more 'wp_handle_upload_prefilter' functions error out the file. Handle that gracefully. if ( isset( $file['error'] ) && ! is_numeric( $file['error'] ) && $file['error'] ) { return call_user_func_array( $upload_error_handler, array( &$file, $file['error'] ) ); } // Install user overrides. Did we mention that this voids your warranty? // You may define your own function and pass the name in $overrides['unique_filename_callback']. $unique_filename_callback = null; if ( isset( $overrides['unique_filename_callback'] ) ) { $unique_filename_callback = $overrides['unique_filename_callback']; } /* * This may not have originally been intended to be overridable, * but historically has been. */ if ( isset( $overrides['upload_error_strings'] ) ) { $upload_error_strings = $overrides['upload_error_strings']; } else { // Courtesy of php.net, the strings that describe the error indicated in $_FILES[{form field}]['error']. $upload_error_strings = array( false, sprintf( /* translators: 1: upload_max_filesize, 2: php.ini */ __( 'The uploaded file exceeds the %1$s directive in %2$s.' ), 'upload_max_filesize', 'php.ini' ), sprintf( /* translators: %s: MAX_FILE_SIZE */ __( 'The uploaded file exceeds the %s directive that was specified in the HTML form.' ), 'MAX_FILE_SIZE' ), __( 'The uploaded file was only partially uploaded.' ), __( 'No file was uploaded.' ), '', __( 'Missing a temporary folder.' ), __( 'Failed to write file to disk.' ), __( 'File upload stopped by extension.' ), ); } // All tests are on by default. Most can be turned off by $overrides[{test_name}] = false; $test_form = isset( $overrides['test_form'] ) ? $overrides['test_form'] : true; $test_size = isset( $overrides['test_size'] ) ? $overrides['test_size'] : true; // If you override this, you must provide $ext and $type!! $test_type = isset( $overrides['test_type'] ) ? $overrides['test_type'] : true; $mimes = isset( $overrides['mimes'] ) ? $overrides['mimes'] : null; // A correct form post will pass this test. if ( $test_form && ( ! isset( $_POST['action'] ) || $_POST['action'] !== $action ) ) { return call_user_func_array( $upload_error_handler, array( &$file, __( 'Invalid form submission.' ) ) ); } // A successful upload will pass this test. It makes no sense to override this one. if ( isset( $file['error'] ) && $file['error'] > 0 ) { return call_user_func_array( $upload_error_handler, array( &$file, $upload_error_strings[ $file['error'] ] ) ); } // A properly uploaded file will pass this test. There should be no reason to override this one. $test_uploaded_file = 'wp_handle_upload' === $action ? is_uploaded_file( $file['tmp_name'] ) : @is_readable( $file['tmp_name'] ); if ( ! $test_uploaded_file ) { return call_user_func_array( $upload_error_handler, array( &$file, __( 'Specified file failed upload test.' ) ) ); } $test_file_size = 'wp_handle_upload' === $action ? $file['size'] : filesize( $file['tmp_name'] ); // A non-empty file will pass this test. if ( $test_size && ! ( $test_file_size > 0 ) ) { if ( is_multisite() ) { $error_msg = __( 'File is empty. Please upload something more substantial.' ); } else { $error_msg = sprintf( /* translators: 1: php.ini, 2: post_max_size, 3: upload_max_filesize */ __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your %1$s file or by %2$s being defined as smaller than %3$s in %1$s.' ), 'php.ini', 'post_max_size', 'upload_max_filesize' ); } return call_user_func_array( $upload_error_handler, array( &$file, $error_msg ) ); } // A correct MIME type will pass this test. Override $mimes or use the upload_mimes filter. if ( $test_type ) { $wp_filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'], $mimes ); $ext = empty( $wp_filetype['ext'] ) ? '' : $wp_filetype['ext']; $type = empty( $wp_filetype['type'] ) ? '' : $wp_filetype['type']; $proper_filename = empty( $wp_filetype['proper_filename'] ) ? '' : $wp_filetype['proper_filename']; // Check to see if wp_check_filetype_and_ext() determined the filename was incorrect. if ( $proper_filename ) { $file['name'] = $proper_filename; } if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) { return call_user_func_array( $upload_error_handler, array( &$file, __( 'Sorry, you are not allowed to upload this file type.' ) ) ); } if ( ! $type ) { $type = $file['type']; } } else { $type = ''; } /* * A writable uploads dir will pass this test. Again, there's no point * overriding this one. */ $uploads = wp_upload_dir( $time ); if ( ! ( $uploads && false === $uploads['error'] ) ) { return call_user_func_array( $upload_error_handler, array( &$file, $uploads['error'] ) ); } $filename = wp_unique_filename( $uploads['path'], $file['name'], $unique_filename_callback ); // Move the file to the uploads dir. $new_file = $uploads['path'] . "/$filename"; /** * Filters whether to short-circuit moving the uploaded file after passing all checks. * * If a non-null value is returned from the filter, moving the file and any related * error reporting will be completely skipped. * * @since 4.9.0 * * @param mixed $move_new_file If null (default) move the file after the upload. * @param array $file { * Reference to a single element from `$_FILES`. * * @type string $name The original name of the file on the client machine. * @type string $type The mime type of the file, if the browser provided this information. * @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server. * @type int $size The size, in bytes, of the uploaded file. * @type int $error The error code associated with this file upload. * } * @param string $new_file Filename of the newly-uploaded file. * @param string $type Mime type of the newly-uploaded file. */ $move_new_file = apply_filters( 'pre_move_uploaded_file', null, $file, $new_file, $type ); if ( null === $move_new_file ) { if ( 'wp_handle_upload' === $action ) { $move_new_file = @move_uploaded_file( $file['tmp_name'], $new_file ); } else { // Use copy and unlink because rename breaks streams. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $move_new_file = @copy( $file['tmp_name'], $new_file ); unlink( $file['tmp_name'] ); } if ( false === $move_new_file ) { if ( str_starts_with( $uploads['basedir'], ABSPATH ) ) { $error_path = str_replace( ABSPATH, '', $uploads['basedir'] ) . $uploads['subdir']; } else { $error_path = basename( $uploads['basedir'] ) . $uploads['subdir']; } return $upload_error_handler( $file, sprintf( /* translators: %s: Destination file path. */ __( 'The uploaded file could not be moved to %s.' ), $error_path ) ); } } // Set correct file permissions. $stat = stat( dirname( $new_file ) ); $perms = $stat['mode'] & 0000666; chmod( $new_file, $perms ); // Compute the URL. $url = $uploads['url'] . "/$filename"; if ( is_multisite() ) { clean_dirsize_cache( $new_file ); } /** * Filters the data array for the uploaded file. * * @since 2.1.0 * * @param array $upload { * Array of upload data. * * @type string $file Filename of the newly-uploaded file. * @type string $url URL of the newly-uploaded file. * @type string $type Mime type of the newly-uploaded file. * } * @param string $context The type of upload action. Values include 'upload' or 'sideload'. */ return apply_filters( 'wp_handle_upload', array( 'file' => $new_file, 'url' => $url, 'type' => $type, ), 'wp_handle_sideload' === $action ? 'sideload' : 'upload' ); } /** * Wrapper for _wp_handle_upload(). * * Passes the {@see 'wp_handle_upload'} action. * * @since 2.0.0 * * @see _wp_handle_upload() * * @param array $file Reference to a single element of `$_FILES`. * Call the function once for each uploaded file. * See _wp_handle_upload() for accepted values. * @param array|false $overrides Optional. An associative array of names => values * to override default variables. Default false. * See _wp_handle_upload() for accepted values. * @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null. * @return array See _wp_handle_upload() for return value. */ function wp_handle_upload( &$file, $overrides = false, $time = null ) { /* * $_POST['action'] must be set and its value must equal $overrides['action'] * or this: */ $action = 'wp_handle_upload'; if ( isset( $overrides['action'] ) ) { $action = $overrides['action']; } return _wp_handle_upload( $file, $overrides, $time, $action ); } /** * Wrapper for _wp_handle_upload(). * * Passes the {@see 'wp_handle_sideload'} action. * * @since 2.6.0 * * @see _wp_handle_upload() * * @param array $file Reference to a single element of `$_FILES`. * Call the function once for each uploaded file. * See _wp_handle_upload() for accepted values. * @param array|false $overrides Optional. An associative array of names => values * to override default variables. Default false. * See _wp_handle_upload() for accepted values. * @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null. * @return array See _wp_handle_upload() for return value. */ function wp_handle_sideload( &$file, $overrides = false, $time = null ) { /* * $_POST['action'] must be set and its value must equal $overrides['action'] * or this: */ $action = 'wp_handle_sideload'; if ( isset( $overrides['action'] ) ) { $action = $overrides['action']; } return _wp_handle_upload( $file, $overrides, $time, $action ); } /** * Downloads a URL to a local temporary file using the WordPress HTTP API. * * Please note that the calling function must delete or move the file. * * @since 2.5.0 * @since 5.2.0 Signature Verification with SoftFail was added. * @since 5.9.0 Support for Content-Disposition filename was added. * * @param string $url The URL of the file to download. * @param int $timeout The timeout for the request to download the file. * Default 300 seconds. * @param bool $signature_verification Whether to perform Signature Verification. * Default false. * @return string|WP_Error Filename on success, WP_Error on failure. */ function download_url( $url, $timeout = 300, $signature_verification = false ) { // WARNING: The file is not automatically deleted, the script must delete or move the file. if ( ! $url ) { return new WP_Error( 'http_no_url', __( 'No URL Provided.' ) ); } $url_path = parse_url( $url, PHP_URL_PATH ); $url_filename = ''; if ( is_string( $url_path ) && '' !== $url_path ) { $url_filename = basename( $url_path ); } $tmpfname = wp_tempnam( $url_filename ); if ( ! $tmpfname ) { return new WP_Error( 'http_no_file', __( 'Could not create temporary file.' ) ); } $response = wp_safe_remote_get( $url, array( 'timeout' => $timeout, 'stream' => true, 'filename' => $tmpfname, ) ); if ( is_wp_error( $response ) ) { unlink( $tmpfname ); return $response; } $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $response_code ) { $data = array( 'code' => $response_code, ); // Retrieve a sample of the response body for debugging purposes. $tmpf = fopen( $tmpfname, 'rb' ); if ( $tmpf ) { /** * Filters the maximum error response body size in `download_url()`. * * @since 5.1.0 * * @see download_url() * * @param int $size The maximum error response body size. Default 1 KB. */ $response_size = apply_filters( 'download_url_error_max_body_size', KB_IN_BYTES ); $data['body'] = fread( $tmpf, $response_size ); fclose( $tmpf ); } unlink( $tmpfname ); return new WP_Error( 'http_404', trim( wp_remote_retrieve_response_message( $response ) ), $data ); } $content_disposition = wp_remote_retrieve_header( $response, 'Content-Disposition' ); if ( $content_disposition ) { $content_disposition = strtolower( $content_disposition ); if ( str_starts_with( $content_disposition, 'attachment; filename=' ) ) { $tmpfname_disposition = sanitize_file_name( substr( $content_disposition, 21 ) ); } else { $tmpfname_disposition = ''; } // Potential file name must be valid string. if ( $tmpfname_disposition && is_string( $tmpfname_disposition ) && ( 0 === validate_file( $tmpfname_disposition ) ) ) { $tmpfname_disposition = dirname( $tmpfname ) . '/' . $tmpfname_disposition; if ( rename( $tmpfname, $tmpfname_disposition ) ) { $tmpfname = $tmpfname_disposition; } if ( ( $tmpfname !== $tmpfname_disposition ) && file_exists( $tmpfname_disposition ) ) { unlink( $tmpfname_disposition ); } } } $mime_type = wp_remote_retrieve_header( $response, 'content-type' ); if ( $mime_type && 'tmp' === pathinfo( $tmpfname, PATHINFO_EXTENSION ) ) { $valid_mime_types = array_flip( get_allowed_mime_types() ); if ( ! empty( $valid_mime_types[ $mime_type ] ) ) { $extensions = explode( '|', $valid_mime_types[ $mime_type ] ); $new_image_name = substr( $tmpfname, 0, -4 ) . ".{$extensions[0]}"; if ( 0 === validate_file( $new_image_name ) ) { if ( rename( $tmpfname, $new_image_name ) ) { $tmpfname = $new_image_name; } if ( ( $tmpfname !== $new_image_name ) && file_exists( $new_image_name ) ) { unlink( $new_image_name ); } } } } $content_md5 = wp_remote_retrieve_header( $response, 'Content-MD5' ); if ( $content_md5 ) { $md5_check = verify_file_md5( $tmpfname, $content_md5 ); if ( is_wp_error( $md5_check ) ) { unlink( $tmpfname ); return $md5_check; } } // If the caller expects signature verification to occur, check to see if this URL supports it. if ( $signature_verification ) { /** * Filters the list of hosts which should have Signature Verification attempted on. * * @since 5.2.0 * * @param string[] $hostnames List of hostnames. */ $signed_hostnames = apply_filters( 'wp_signature_hosts', array( 'wordpress.org', 'downloads.wordpress.org', 's.w.org' ) ); $signature_verification = in_array( parse_url( $url, PHP_URL_HOST ), $signed_hostnames, true ); } // Perform signature validation if supported. if ( $signature_verification ) { $signature = wp_remote_retrieve_header( $response, 'X-Content-Signature' ); if ( ! $signature ) { /* * Retrieve signatures from a file if the header wasn't included. * WordPress.org stores signatures at $package_url.sig. */ $signature_url = false; if ( is_string( $url_path ) && ( str_ends_with( $url_path, '.zip' ) || str_ends_with( $url_path, '.tar.gz' ) ) ) { $signature_url = str_replace( $url_path, $url_path . '.sig', $url ); } /** * Filters the URL where the signature for a file is located. * * @since 5.2.0 * * @param false|string $signature_url The URL where signatures can be found for a file, or false if none are known. * @param string $url The URL being verified. */ $signature_url = apply_filters( 'wp_signature_url', $signature_url, $url ); if ( $signature_url ) { $signature_request = wp_safe_remote_get( $signature_url, array( 'limit_response_size' => 10 * KB_IN_BYTES, // 10KB should be large enough for quite a few signatures. ) ); if ( ! is_wp_error( $signature_request ) && 200 === wp_remote_retrieve_response_code( $signature_request ) ) { $signature = explode( "\n", wp_remote_retrieve_body( $signature_request ) ); } } } // Perform the checks. $signature_verification = verify_file_signature( $tmpfname, $signature, $url_filename ); } if ( is_wp_error( $signature_verification ) ) { if ( /** * Filters whether Signature Verification failures should be allowed to soft fail. * * WARNING: This may be removed from a future release. * * @since 5.2.0 * * @param bool $signature_softfail If a softfail is allowed. * @param string $url The url being accessed. */ apply_filters( 'wp_signature_softfail', true, $url ) ) { $signature_verification->add_data( $tmpfname, 'softfail-filename' ); } else { // Hard-fail. unlink( $tmpfname ); } return $signature_verification; } return $tmpfname; } /** * Calculates and compares the MD5 of a file to its expected value. * * @since 3.7.0 * * @param string $filename The filename to check the MD5 of. * @param string $expected_md5 The expected MD5 of the file, either a base64-encoded raw md5, * or a hex-encoded md5. * @return bool|WP_Error True on success, false when the MD5 format is unknown/unexpected, * WP_Error on failure. */ function verify_file_md5( $filename, $expected_md5 ) { if ( 32 === strlen( $expected_md5 ) ) { $expected_raw_md5 = pack( 'H*', $expected_md5 ); } elseif ( 24 === strlen( $expected_md5 ) ) { $expected_raw_md5 = base64_decode( $expected_md5 ); } else { return false; // Unknown format. } $file_md5 = md5_file( $filename, true ); if ( $file_md5 === $expected_raw_md5 ) { return true; } return new WP_Error( 'md5_mismatch', sprintf( /* translators: 1: File checksum, 2: Expected checksum value. */ __( 'The checksum of the file (%1$s) does not match the expected checksum value (%2$s).' ), bin2hex( $file_md5 ), bin2hex( $expected_raw_md5 ) ) ); } /** * Verifies the contents of a file against its ED25519 signature. * * @since 5.2.0 * * @param string $filename The file to validate. * @param string|array $signatures A Signature provided for the file. * @param string|false $filename_for_errors Optional. A friendly filename for errors. * @return bool|WP_Error True on success, false if verification not attempted, * or WP_Error describing an error condition. */ function verify_file_signature( $filename, $signatures, $filename_for_errors = false ) { if ( ! $filename_for_errors ) { $filename_for_errors = wp_basename( $filename ); } // Check we can process signatures. if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) || ! in_array( 'sha384', array_map( 'strtolower', hash_algos() ), true ) ) { return new WP_Error( 'signature_verification_unsupported', sprintf( /* translators: %s: The filename of the package. */ __( 'The authenticity of %s could not be verified as signature verification is unavailable on this system.' ), '' . esc_html( $filename_for_errors ) . '' ), ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ? 'sodium_crypto_sign_verify_detached' : 'sha384' ) ); } // Verify runtime speed of Sodium_Compat is acceptable. if ( ! extension_loaded( 'sodium' ) && ! ParagonIE_Sodium_Compat::polyfill_is_fast() ) { $sodium_compat_is_fast = false; // Allow for an old version of Sodium_Compat being loaded before the bundled WordPress one. if ( method_exists( 'ParagonIE_Sodium_Compat', 'runtime_speed_test' ) ) { /* * Run `ParagonIE_Sodium_Compat::runtime_speed_test()` in optimized integer mode, * as that's what WordPress utilizes during signing verifications. */ // phpcs:disable WordPress.NamingConventions.ValidVariableName $old_fastMult = ParagonIE_Sodium_Compat::$fastMult; ParagonIE_Sodium_Compat::$fastMult = true; $sodium_compat_is_fast = ParagonIE_Sodium_Compat::runtime_speed_test( 100, 10 ); ParagonIE_Sodium_Compat::$fastMult = $old_fastMult; // phpcs:enable } /* * This cannot be performed in a reasonable amount of time. * https://github.com/paragonie/sodium_compat#help-sodium_compat-is-slow-how-can-i-make-it-fast */ if ( ! $sodium_compat_is_fast ) { return new WP_Error( 'signature_verification_unsupported', sprintf( /* translators: %s: The filename of the package. */ __( 'The authenticity of %s could not be verified as signature verification is unavailable on this system.' ), '' . esc_html( $filename_for_errors ) . '' ), array( 'php' => PHP_VERSION, 'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ), 'polyfill_is_fast' => false, 'max_execution_time' => ini_get( 'max_execution_time' ), ) ); } } if ( ! $signatures ) { return new WP_Error( 'signature_verification_no_signature', sprintf( /* translators: %s: The filename of the package. */ __( 'The authenticity of %s could not be verified as no signature was found.' ), '' . esc_html( $filename_for_errors ) . '' ), array( 'filename' => $filename_for_errors, ) ); } $trusted_keys = wp_trusted_keys(); $file_hash = hash_file( 'sha384', $filename, true ); mbstring_binary_safe_encoding(); $skipped_key = 0; $skipped_signature = 0; foreach ( (array) $signatures as $signature ) { $signature_raw = base64_decode( $signature ); // Ensure only valid-length signatures are considered. if ( SODIUM_CRYPTO_SIGN_BYTES !== strlen( $signature_raw ) ) { ++$skipped_signature; continue; } foreach ( (array) $trusted_keys as $key ) { $key_raw = base64_decode( $key ); // Only pass valid public keys through. if ( SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES !== strlen( $key_raw ) ) { ++$skipped_key; continue; } if ( sodium_crypto_sign_verify_detached( $signature_raw, $file_hash, $key_raw ) ) { reset_mbstring_encoding(); return true; } } } reset_mbstring_encoding(); return new WP_Error( 'signature_verification_failed', sprintf( /* translators: %s: The filename of the package. */ __( 'The authenticity of %s could not be verified.' ), '' . esc_html( $filename_for_errors ) . '' ), // Error data helpful for debugging: array( 'filename' => $filename_for_errors, 'keys' => $trusted_keys, 'signatures' => $signatures, 'hash' => bin2hex( $file_hash ), 'skipped_key' => $skipped_key, 'skipped_sig' => $skipped_signature, 'php' => PHP_VERSION, 'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ), ) ); } /** * Retrieves the list of signing keys trusted by WordPress. * * @since 5.2.0 * * @return string[] Array of base64-encoded signing keys. */ function wp_trusted_keys() { $trusted_keys = array(); if ( time() < 1617235200 ) { // WordPress.org Key #1 - This key is only valid before April 1st, 2021. $trusted_keys[] = 'fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0='; } // TODO: Add key #2 with longer expiration. /** * Filters the valid signing keys used to verify the contents of files. * * @since 5.2.0 * * @param string[] $trusted_keys The trusted keys that may sign packages. */ return apply_filters( 'wp_trusted_keys', $trusted_keys ); } /** * Determines whether the given file is a valid ZIP file. * * This function does not test to ensure that a file exists. Non-existent files * are not valid ZIPs, so those will also return false. * * @since 6.4.4 * * @param string $file Full path to the ZIP file. * @return bool Whether the file is a valid ZIP file. */ function wp_zip_file_is_valid( $file ) { /** 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, ZipArchive::CHECKCONS ); if ( true === $archive_is_valid ) { $archive->close(); return true; } } // Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file. require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; $archive = new PclZip( $file ); $archive_is_valid = is_array( $archive->properties() ); return $archive_is_valid; } /** * Unzips a specified ZIP file to a location on the filesystem via the WordPress * Filesystem Abstraction. * * Assumes that WP_Filesystem() has already been called and set up. Does not extract * a root-level __MACOSX directory, if present. * * Attempts to increase the PHP memory limit to 256M before uncompressing. However, * the most memory required shouldn't be much larger than the archive itself. * * @since 2.5.0 * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * * @param string $file Full path and filename of ZIP archive. * @param string $to Full path on the filesystem to extract archive to. * @return true|WP_Error True on success, WP_Error on failure. */ function unzip_file( $file, $to ) { global $wp_filesystem; if ( ! $wp_filesystem || ! is_object( $wp_filesystem ) ) { return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) ); } // Unzip can use a lot of memory, but not this much hopefully. wp_raise_memory_limit( 'admin' ); $needed_dirs = array(); $to = trailingslashit( $to ); // Determine any parent directories needed (of the upgrade directory). if ( ! $wp_filesystem->is_dir( $to ) ) { // Only do parents if no children exist. $path = preg_split( '![/\\\]!', untrailingslashit( $to ) ); for ( $i = count( $path ); $i >= 0; $i-- ) { if ( empty( $path[ $i ] ) ) { continue; } $dir = implode( '/', array_slice( $path, 0, $i + 1 ) ); if ( preg_match( '!^[a-z]:$!i', $dir ) ) { // Skip it if it looks like a Windows Drive letter. continue; } if ( ! $wp_filesystem->is_dir( $dir ) ) { $needed_dirs[] = $dir; } else { break; // A folder exists, therefore we don't need to check the levels below this. } } } /** * Filters whether to use ZipArchive to unzip archives. * * @since 3.0.0 * * @param bool $ziparchive Whether to use ZipArchive. Default true. */ if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) { $result = _unzip_file_ziparchive( $file, $to, $needed_dirs ); if ( true === $result ) { return $result; } elseif ( is_wp_error( $result ) ) { if ( 'incompatible_archive' !== $result->get_error_code() ) { return $result; } } } // Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file. return _unzip_file_pclzip( $file, $to, $needed_dirs ); } /** * Attempts to unzip an archive using the ZipArchive class. * * This function should not be called directly, use `unzip_file()` instead. * * Assumes that WP_Filesystem() has already been called and set up. * * @since 3.0.0 * @access private * * @see unzip_file() * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * * @param string $file Full path and filename of ZIP archive. * @param string $to Full path on the filesystem to extract archive to. * @param string[] $needed_dirs A partial list of required folders needed to be created. * @return true|WP_Error True on success, WP_Error on failure. */ function _unzip_file_ziparchive( $file, $to, $needed_dirs = array() ) { global $wp_filesystem; $z = new ZipArchive(); $zopen = $z->open( $file, ZIPARCHIVE::CHECKCONS ); if ( true !== $zopen ) { return new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.' ), array( 'ziparchive_error' => $zopen ) ); } $uncompressed_size = 0; for ( $i = 0; $i < $z->numFiles; $i++ ) { $info = $z->statIndex( $i ); if ( ! $info ) { $z->close(); return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) ); } if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory. continue; } // Don't extract invalid files: if ( 0 !== validate_file( $info['name'] ) ) { continue; } $uncompressed_size += $info['size']; $dirname = dirname( $info['name'] ); if ( str_ends_with( $info['name'], '/' ) ) { // Directory. $needed_dirs[] = $to . untrailingslashit( $info['name'] ); } elseif ( '.' !== $dirname ) { // Path to a file. $needed_dirs[] = $to . untrailingslashit( $dirname ); } } // Enough space to unzip the file and copy its contents, with a 10% buffer. $required_space = $uncompressed_size * 2.1; /* * disk_free_space() could return false. Assume that any falsey value is an error. * A disk that has zero free bytes has bigger problems. * Require we have enough space to unzip the file and copy its contents, with a 10% buffer. */ if ( wp_doing_cron() ) { $available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false; if ( $available_space && ( $required_space > $available_space ) ) { $z->close(); return new WP_Error( 'disk_full_unzip_file', __( 'Could not copy files. You may have run out of disk space.' ), compact( 'uncompressed_size', 'available_space' ) ); } } $needed_dirs = array_unique( $needed_dirs ); foreach ( $needed_dirs as $dir ) { // Check the parent folders of the folders all exist within the creation array. if ( untrailingslashit( $to ) === $dir ) { // Skip over the working directory, we know this exists (or will exist). continue; } if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it. continue; } $parent_folder = dirname( $dir ); while ( ! empty( $parent_folder ) && untrailingslashit( $to ) !== $parent_folder && ! in_array( $parent_folder, $needed_dirs, true ) ) { $needed_dirs[] = $parent_folder; $parent_folder = dirname( $parent_folder ); } } asort( $needed_dirs ); // Create those directories if need be: foreach ( $needed_dirs as $_dir ) { // Only check to see if the Dir exists upon creation failure. Less I/O this way. if ( ! $wp_filesystem->mkdir( $_dir, FS_CHMOD_DIR ) && ! $wp_filesystem->is_dir( $_dir ) ) { $z->close(); return new WP_Error( 'mkdir_failed_ziparchive', __( 'Could not create directory.' ), $_dir ); } } /** * Filters archive unzipping to override with a custom process. * * @since 6.4.0 * * @param null|true|WP_Error $result The result of the override. True on success, otherwise WP Error. Default null. * @param string $file Full path and filename of ZIP archive. * @param string $to Full path on the filesystem to extract archive to. * @param string[] $needed_dirs A full list of required folders that need to be created. * @param float $required_space The space required to unzip the file and copy its contents, with a 10% buffer. */ $pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space ); if ( null !== $pre ) { // Ensure the ZIP file archive has been closed. $z->close(); return $pre; } for ( $i = 0; $i < $z->numFiles; $i++ ) { $info = $z->statIndex( $i ); if ( ! $info ) { $z->close(); return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) ); } if ( str_ends_with( $info['name'], '/' ) ) { // Directory. continue; } if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files. continue; } // Don't extract invalid files: if ( 0 !== validate_file( $info['name'] ) ) { continue; } $contents = $z->getFromIndex( $i ); if ( false === $contents ) { $z->close(); return new WP_Error( 'extract_failed_ziparchive', __( 'Could not extract file from archive.' ), $info['name'] ); } if ( ! $wp_filesystem->put_contents( $to . $info['name'], $contents, FS_CHMOD_FILE ) ) { $z->close(); return new WP_Error( 'copy_failed_ziparchive', __( 'Could not copy file.' ), $info['name'] ); } } $z->close(); /** * Filters the result of unzipping an archive. * * @since 6.4.0 * * @param true|WP_Error $result The result of unzipping the archive. True on success, otherwise WP_Error. Default true. * @param string $file Full path and filename of ZIP archive. * @param string $to Full path on the filesystem the archive was extracted to. * @param string[] $needed_dirs A full list of required folders that were created. * @param float $required_space The space required to unzip the file and copy its contents, with a 10% buffer. */ $result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space ); unset( $needed_dirs ); return $result; } /** * Attempts to unzip an archive using the PclZip library. * * This function should not be called directly, use `unzip_file()` instead. * * Assumes that WP_Filesystem() has already been called and set up. * * @since 3.0.0 * @access private * * @see unzip_file() * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * * @param string $file Full path and filename of ZIP archive. * @param string $to Full path on the filesystem to extract archive to. * @param string[] $needed_dirs A partial list of required folders needed to be created. * @return true|WP_Error True on success, WP_Error on failure. */ function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) { global $wp_filesystem; mbstring_binary_safe_encoding(); require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; $archive = new PclZip( $file ); $archive_files = $archive->extract( PCLZIP_OPT_EXTRACT_AS_STRING ); reset_mbstring_encoding(); // Is the archive valid? if ( ! is_array( $archive_files ) ) { return new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.' ), $archive->errorInfo( true ) ); } if ( 0 === count( $archive_files ) ) { return new WP_Error( 'empty_archive_pclzip', __( 'Empty archive.' ) ); } $uncompressed_size = 0; // Determine any children directories needed (From within the archive). foreach ( $archive_files as $file ) { if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory. continue; } $uncompressed_size += $file['size']; $needed_dirs[] = $to . untrailingslashit( $file['folder'] ? $file['filename'] : dirname( $file['filename'] ) ); } // Enough space to unzip the file and copy its contents, with a 10% buffer. $required_space = $uncompressed_size * 2.1; /* * disk_free_space() could return false. Assume that any falsey value is an error. * A disk that has zero free bytes has bigger problems. * Require we have enough space to unzip the file and copy its contents, with a 10% buffer. */ if ( wp_doing_cron() ) { $available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false; if ( $available_space && ( $required_space > $available_space ) ) { return new WP_Error( 'disk_full_unzip_file', __( 'Could not copy files. You may have run out of disk space.' ), compact( 'uncompressed_size', 'available_space' ) ); } } $needed_dirs = array_unique( $needed_dirs ); foreach ( $needed_dirs as $dir ) { // Check the parent folders of the folders all exist within the creation array. if ( untrailingslashit( $to ) === $dir ) { // Skip over the working directory, we know this exists (or will exist). continue; } if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it. continue; } $parent_folder = dirname( $dir ); while ( ! empty( $parent_folder ) && untrailingslashit( $to ) !== $parent_folder && ! in_array( $parent_folder, $needed_dirs, true ) ) { $needed_dirs[] = $parent_folder; $parent_folder = dirname( $parent_folder ); } } asort( $needed_dirs ); // Create those directories if need be: foreach ( $needed_dirs as $_dir ) { // Only check to see if the dir exists upon creation failure. Less I/O this way. if ( ! $wp_filesystem->mkdir( $_dir, FS_CHMOD_DIR ) && ! $wp_filesystem->is_dir( $_dir ) ) { return new WP_Error( 'mkdir_failed_pclzip', __( 'Could not create directory.' ), $_dir ); } } /** This filter is documented in src/wp-admin/includes/file.php */ $pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space ); if ( null !== $pre ) { return $pre; } // Extract the files from the zip. foreach ( $archive_files as $file ) { if ( $file['folder'] ) { continue; } if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files. continue; } // Don't extract invalid files: if ( 0 !== validate_file( $file['filename'] ) ) { continue; } if ( ! $wp_filesystem->put_contents( $to . $file['filename'], $file['content'], FS_CHMOD_FILE ) ) { return new WP_Error( 'copy_failed_pclzip', __( 'Could not copy file.' ), $file['filename'] ); } } /** This action is documented in src/wp-admin/includes/file.php */ $result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space ); unset( $needed_dirs ); return $result; } /** * Copies a directory from one location to another via the WordPress Filesystem * Abstraction. * * Assumes that WP_Filesystem() has already been called and setup. * * @since 2.5.0 * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * * @param string $from Source directory. * @param string $to Destination directory. * @param string[] $skip_list An array of files/folders to skip copying. * @return true|WP_Error True on success, WP_Error on failure. */ function copy_dir( $from, $to, $skip_list = array() ) { global $wp_filesystem; $dirlist = $wp_filesystem->dirlist( $from ); if ( false === $dirlist ) { return new WP_Error( 'dirlist_failed_copy_dir', __( 'Directory listing failed.' ), basename( $from ) ); } $from = trailingslashit( $from ); $to = trailingslashit( $to ); if ( ! $wp_filesystem->exists( $to ) && ! $wp_filesystem->mkdir( $to ) ) { return new WP_Error( 'mkdir_destination_failed_copy_dir', __( 'Could not create the destination directory.' ), basename( $to ) ); } foreach ( (array) $dirlist as $filename => $fileinfo ) { if ( in_array( $filename, $skip_list, true ) ) { continue; } if ( 'f' === $fileinfo['type'] ) { if ( ! $wp_filesystem->copy( $from . $filename, $to . $filename, true, FS_CHMOD_FILE ) ) { // If copy failed, chmod file to 0644 and try again. $wp_filesystem->chmod( $to . $filename, FS_CHMOD_FILE ); if ( ! $wp_filesystem->copy( $from . $filename, $to . $filename, true, FS_CHMOD_FILE ) ) { return new WP_Error( 'copy_failed_copy_dir', __( 'Could not copy file.' ), $to . $filename ); } } wp_opcache_invalidate( $to . $filename ); } elseif ( 'd' === $fileinfo['type'] ) { if ( ! $wp_filesystem->is_dir( $to . $filename ) ) { if ( ! $wp_filesystem->mkdir( $to . $filename, FS_CHMOD_DIR ) ) { return new WP_Error( 'mkdir_failed_copy_dir', __( 'Could not create directory.' ), $to . $filename ); } } // Generate the $sub_skip_list for the subdirectory as a sub-set of the existing $skip_list. $sub_skip_list = array(); foreach ( $skip_list as $skip_item ) { if ( str_starts_with( $skip_item, $filename . '/' ) ) { $sub_skip_list[] = preg_replace( '!^' . preg_quote( $filename, '!' ) . '/!i', '', $skip_item ); } } $result = copy_dir( $from . $filename, $to . $filename, $sub_skip_list ); if ( is_wp_error( $result ) ) { return $result; } } } return true; } /** * Moves a directory from one location to another. * * Recursively invalidates OPcache on success. * * If the renaming failed, falls back to copy_dir(). * * Assumes that WP_Filesystem() has already been called and setup. * * This function is not designed to merge directories, copy_dir() should be used instead. * * @since 6.2.0 * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * * @param string $from Source directory. * @param string $to Destination directory. * @param bool $overwrite Optional. Whether to overwrite the destination directory if it exists. * Default false. * @return true|WP_Error True on success, WP_Error on failure. */ function move_dir( $from, $to, $overwrite = false ) { global $wp_filesystem; if ( trailingslashit( strtolower( $from ) ) === trailingslashit( strtolower( $to ) ) ) { return new WP_Error( 'source_destination_same_move_dir', __( 'The source and destination are the same.' ) ); } if ( $wp_filesystem->exists( $to ) ) { if ( ! $overwrite ) { return new WP_Error( 'destination_already_exists_move_dir', __( 'The destination folder already exists.' ), $to ); } elseif ( ! $wp_filesystem->delete( $to, true ) ) { // Can't overwrite if the destination couldn't be deleted. return new WP_Error( 'destination_not_deleted_move_dir', __( 'The destination directory already exists and could not be removed.' ) ); } } if ( $wp_filesystem->move( $from, $to ) ) { /* * When using an environment with shared folders, * there is a delay in updating the filesystem's cache. * * This is a known issue in environments with a VirtualBox provider. * * A 200ms delay gives time for the filesystem to update its cache, * prevents "Operation not permitted", and "No such file or directory" warnings. * * This delay is used in other projects, including Composer. * @link https://github.com/composer/composer/blob/2.5.1/src/Composer/Util/Platform.php#L228-L233 */ usleep( 200000 ); wp_opcache_invalidate_directory( $to ); return true; } // Fall back to a recursive copy. if ( ! $wp_filesystem->is_dir( $to ) ) { if ( ! $wp_filesystem->mkdir( $to, FS_CHMOD_DIR ) ) { return new WP_Error( 'mkdir_failed_move_dir', __( 'Could not create directory.' ), $to ); } } $result = copy_dir( $from, $to, array( basename( $to ) ) ); // Clear the source directory. if ( true === $result ) { $wp_filesystem->delete( $from, true ); } return $result; } /** * Initializes and connects the WordPress Filesystem Abstraction classes. * * This function will include the chosen transport and attempt connecting. * * Plugins may add extra transports, And force WordPress to use them by returning * the filename via the {@see 'filesystem_method_file'} filter. * * @since 2.5.0 * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * * @param array|false $args Optional. Connection args, These are passed * directly to the `WP_Filesystem_*()` classes. * Default false. * @param string|false $context Optional. Context for get_filesystem_method(). * Default false. * @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable. * Default false. * @return bool|null True on success, false on failure, * null if the filesystem method class file does not exist. */ function WP_Filesystem( $args = false, $context = false, $allow_relaxed_file_ownership = false ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid global $wp_filesystem; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; $method = get_filesystem_method( $args, $context, $allow_relaxed_file_ownership ); if ( ! $method ) { return false; } if ( ! class_exists( "WP_Filesystem_$method" ) ) { /** * Filters the path for a specific filesystem method class file. * * @since 2.6.0 * * @see get_filesystem_method() * * @param string $path Path to the specific filesystem method class file. * @param string $method The filesystem method to use. */ $abstraction_file = apply_filters( 'filesystem_method_file', ABSPATH . 'wp-admin/includes/class-wp-filesystem-' . $method . '.php', $method ); if ( ! file_exists( $abstraction_file ) ) { return; } require_once $abstraction_file; } $method = "WP_Filesystem_$method"; $wp_filesystem = new $method( $args ); /* * Define the timeouts for the connections. Only available after the constructor is called * to allow for per-transport overriding of the default. */ if ( ! defined( 'FS_CONNECT_TIMEOUT' ) ) { define( 'FS_CONNECT_TIMEOUT', 30 ); // 30 seconds. } if ( ! defined( 'FS_TIMEOUT' ) ) { define( 'FS_TIMEOUT', 30 ); // 30 seconds. } if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { return false; } if ( ! $wp_filesystem->connect() ) { return false; // There was an error connecting to the server. } // Set the permission constants if not already set. if ( ! defined( 'FS_CHMOD_DIR' ) ) { define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) ); } if ( ! defined( 'FS_CHMOD_FILE' ) ) { define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) ); } return true; } /** * Determines which method to use for reading, writing, modifying, or deleting * files on the filesystem. * * The priority of the transports are: Direct, SSH2, FTP PHP Extension, FTP Sockets * (Via Sockets class, or `fsockopen()`). Valid values for these are: 'direct', 'ssh2', * 'ftpext' or 'ftpsockets'. * * The return value can be overridden by defining the `FS_METHOD` constant in `wp-config.php`, * or filtering via {@see 'filesystem_method'}. * * @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/#wordpress-upgrade-constants * * Plugins may define a custom transport handler, See WP_Filesystem(). * * @since 2.5.0 * * @global callable $_wp_filesystem_direct_method * * @param array $args Optional. Connection details. Default empty array. * @param string $context Optional. Full path to the directory that is tested * for being writable. Default empty. * @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable. * Default false. * @return string The transport to use, see description for valid return values. */ function get_filesystem_method( $args = array(), $context = '', $allow_relaxed_file_ownership = false ) { // Please ensure that this is either 'direct', 'ssh2', 'ftpext', or 'ftpsockets'. $method = defined( 'FS_METHOD' ) ? FS_METHOD : false; if ( ! $context ) { $context = WP_CONTENT_DIR; } // If the directory doesn't exist (wp-content/languages) then use the parent directory as we'll create it. if ( WP_LANG_DIR === $context && ! is_dir( $context ) ) { $context = dirname( $context ); } $context = trailingslashit( $context ); if ( ! $method ) { $temp_file_name = $context . 'temp-write-test-' . str_replace( '.', '-', uniqid( '', true ) ); $temp_handle = @fopen( $temp_file_name, 'w' ); if ( $temp_handle ) { // Attempt to determine the file owner of the WordPress files, and that of newly created files. $wp_file_owner = false; $temp_file_owner = false; if ( function_exists( 'fileowner' ) ) { $wp_file_owner = @fileowner( __FILE__ ); $temp_file_owner = @fileowner( $temp_file_name ); } if ( false !== $wp_file_owner && $wp_file_owner === $temp_file_owner ) { /* * WordPress is creating files as the same owner as the WordPress files, * this means it's safe to modify & create new files via PHP. */ $method = 'direct'; $GLOBALS['_wp_filesystem_direct_method'] = 'file_owner'; } elseif ( $allow_relaxed_file_ownership ) { /* * The $context directory is writable, and $allow_relaxed_file_ownership is set, * this means we can modify files safely in this directory. * This mode doesn't create new files, only alter existing ones. */ $method = 'direct'; $GLOBALS['_wp_filesystem_direct_method'] = 'relaxed_ownership'; } fclose( $temp_handle ); @unlink( $temp_file_name ); } } if ( ! $method && isset( $args['connection_type'] ) && 'ssh' === $args['connection_type'] && extension_loaded( 'ssh2' ) ) { $method = 'ssh2'; } if ( ! $method && extension_loaded( 'ftp' ) ) { $method = 'ftpext'; } if ( ! $method && ( extension_loaded( 'sockets' ) || function_exists( 'fsockopen' ) ) ) { $method = 'ftpsockets'; // Sockets: Socket extension; PHP Mode: FSockopen / fwrite / fread. } /** * Filters the filesystem method to use. * * @since 2.6.0 * * @param string $method Filesystem method to return. * @param array $args An array of connection details for the method. * @param string $context Full path to the directory that is tested for being writable. * @param bool $allow_relaxed_file_ownership Whether to allow Group/World writable. */ return apply_filters( 'filesystem_method', $method, $args, $context, $allow_relaxed_file_ownership ); } /** * Displays a form to the user to request for their FTP/SSH details in order * to connect to the filesystem. * * All chosen/entered details are saved, excluding the password. * * Hostnames may be in the form of hostname:portnumber (eg: wordpress.org:2467) * to specify an alternate FTP/SSH port. * * Plugins may override this form by returning true|false via the {@see 'request_filesystem_credentials'} filter. * * @since 2.5.0 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string. * * @global string $pagenow The filename of the current screen. * * @param string $form_post The URL to post the form to. * @param string $type Optional. Chosen type of filesystem. Default empty. * @param bool|WP_Error $error Optional. Whether the current request has failed * to connect, or an error object. Default false. * @param string $context Optional. Full path to the directory that is tested * for being writable. Default empty. * @param array $extra_fields Optional. Extra `POST` fields to be checked * for inclusion in the post. Default null. * @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable. * Default false. * @return bool|array True if no filesystem credentials are required, * false if they are required but have not been provided, * array of credentials if they are required and have been provided. */ function request_filesystem_credentials( $form_post, $type = '', $error = false, $context = '', $extra_fields = null, $allow_relaxed_file_ownership = false ) { global $pagenow; /** * Filters the filesystem credentials. * * Returning anything other than an empty string will effectively short-circuit * output of the filesystem credentials form, returning that value instead. * * A filter should return true if no filesystem credentials are required, false if they are required but have not been * provided, or an array of credentials if they are required and have been provided. * * @since 2.5.0 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string. * * @param mixed $credentials Credentials to return instead. Default empty string. * @param string $form_post The URL to post the form to. * @param string $type Chosen type of filesystem. * @param bool|WP_Error $error Whether the current request has failed to connect, * or an error object. * @param string $context Full path to the directory that is tested for * being writable. * @param array $extra_fields Extra POST fields. * @param bool $allow_relaxed_file_ownership Whether to allow Group/World writable. */ $req_cred = apply_filters( 'request_filesystem_credentials', '', $form_post, $type, $error, $context, $extra_fields, $allow_relaxed_file_ownership ); if ( '' !== $req_cred ) { return $req_cred; } if ( empty( $type ) ) { $type = get_filesystem_method( array(), $context, $allow_relaxed_file_ownership ); } if ( 'direct' === $type ) { return true; } if ( is_null( $extra_fields ) ) { $extra_fields = array( 'version', 'locale' ); } $credentials = get_option( 'ftp_credentials', array( 'hostname' => '', 'username' => '', ) ); $submitted_form = wp_unslash( $_POST ); // Verify nonce, or unset submitted form field values on failure. if ( ! isset( $_POST['_fs_nonce'] ) || ! wp_verify_nonce( $_POST['_fs_nonce'], 'filesystem-credentials' ) ) { unset( $submitted_form['hostname'], $submitted_form['username'], $submitted_form['password'], $submitted_form['public_key'], $submitted_form['private_key'], $submitted_form['connection_type'] ); } $ftp_constants = array( 'hostname' => 'FTP_HOST', 'username' => 'FTP_USER', 'password' => 'FTP_PASS', 'public_key' => 'FTP_PUBKEY', 'private_key' => 'FTP_PRIKEY', ); /* * If defined, set it to that. Else, if POST'd, set it to that. If not, set it to an empty string. * Otherwise, keep it as it previously was (saved details in option). */ foreach ( $ftp_constants as $key => $constant ) { if ( defined( $constant ) ) { $credentials[ $key ] = constant( $constant ); } elseif ( ! empty( $submitted_form[ $key ] ) ) { $credentials[ $key ] = $submitted_form[ $key ]; } elseif ( ! isset( $credentials[ $key ] ) ) { $credentials[ $key ] = ''; } } // Sanitize the hostname, some people might pass in odd data. $credentials['hostname'] = preg_replace( '|\w+://|', '', $credentials['hostname'] ); // Strip any schemes off. if ( strpos( $credentials['hostname'], ':' ) ) { list( $credentials['hostname'], $credentials['port'] ) = explode( ':', $credentials['hostname'], 2 ); if ( ! is_numeric( $credentials['port'] ) ) { unset( $credentials['port'] ); } } else { unset( $credentials['port'] ); } if ( ( defined( 'FTP_SSH' ) && FTP_SSH ) || ( defined( 'FS_METHOD' ) && 'ssh2' === FS_METHOD ) ) { $credentials['connection_type'] = 'ssh'; } elseif ( ( defined( 'FTP_SSL' ) && FTP_SSL ) && 'ftpext' === $type ) { // Only the FTP Extension understands SSL. $credentials['connection_type'] = 'ftps'; } elseif ( ! empty( $submitted_form['connection_type'] ) ) { $credentials['connection_type'] = $submitted_form['connection_type']; } elseif ( ! isset( $credentials['connection_type'] ) ) { // All else fails (and it's not defaulted to something else saved), default to FTP. $credentials['connection_type'] = 'ftp'; } if ( ! $error && ( ! empty( $credentials['hostname'] ) && ! empty( $credentials['username'] ) && ! empty( $credentials['password'] ) || 'ssh' === $credentials['connection_type'] && ! empty( $credentials['public_key'] ) && ! empty( $credentials['private_key'] ) ) ) { $stored_credentials = $credentials; if ( ! empty( $stored_credentials['port'] ) ) { // Save port as part of hostname to simplify above code. $stored_credentials['hostname'] .= ':' . $stored_credentials['port']; } unset( $stored_credentials['password'], $stored_credentials['port'], $stored_credentials['private_key'], $stored_credentials['public_key'] ); if ( ! wp_installing() ) { update_option( 'ftp_credentials', $stored_credentials, false ); } return $credentials; } $hostname = isset( $credentials['hostname'] ) ? $credentials['hostname'] : ''; $username = isset( $credentials['username'] ) ? $credentials['username'] : ''; $public_key = isset( $credentials['public_key'] ) ? $credentials['public_key'] : ''; $private_key = isset( $credentials['private_key'] ) ? $credentials['private_key'] : ''; $port = isset( $credentials['port'] ) ? $credentials['port'] : ''; $connection_type = isset( $credentials['connection_type'] ) ? $credentials['connection_type'] : ''; if ( $error ) { $error_string = __( 'Error: Could not connect to the server. Please verify the settings are correct.' ); if ( is_wp_error( $error ) ) { $error_string = esc_html( $error->get_error_message() ); } wp_admin_notice( $error_string, array( 'id' => 'message', 'additional_classes' => array( 'error' ), ) ); } $types = array(); if ( extension_loaded( 'ftp' ) || extension_loaded( 'sockets' ) || function_exists( 'fsockopen' ) ) { $types['ftp'] = __( 'FTP' ); } if ( extension_loaded( 'ftp' ) ) { // Only this supports FTPS. $types['ftps'] = __( 'FTPS (SSL)' ); } if ( extension_loaded( 'ssh2' ) ) { $types['ssh'] = __( 'SSH2' ); } /** * Filters the connection types to output to the filesystem credentials form. * * @since 2.9.0 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string. * * @param string[] $types Types of connections. * @param array $credentials Credentials to connect with. * @param string $type Chosen filesystem method. * @param bool|WP_Error $error Whether the current request has failed to connect, * or an error object. * @param string $context Full path to the directory that is tested for being writable. */ $types = apply_filters( 'fs_ftp_connection_types', $types, $credentials, $type, $error, $context ); ?>