elseif ( ! empty( $data['media_details']['sizes'] ) ) { foreach ( $data['media_details']['sizes'] as $size => &$size_data ) { if ( isset( $size_data['mime-type'] ) ) { $size_data['mime_type'] = $size_data['mime-type']; unset( $size_data['mime-type'] ); } // Use the same method image_downsize() does. $image_src = wp_get_attachment_image_src( $post->ID, $size ); if ( ! $image_src ) { continue; } $size_data['source_url'] = $image_src[0]; } $full_src = wp_get_attachment_image_src( $post->ID, 'full' ); if ( ! empty( $full_src ) ) { $data['media_details']['sizes']['full'] = array( 'file' => wp_basename( $full_src[0] ), 'width' => $full_src[1], 'height' => $full_src[2], 'mime_type' => $post->post_mime_type, 'source_url' => $full_src[0], ); } } else { $data['media_details']['sizes'] = new stdClass(); } } if ( in_array( 'post', $fields, true ) ) { $data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null; } if ( in_array( 'source_url', $fields, true ) ) { $data['source_url'] = wp_get_attachment_url( $post->ID ); } if ( in_array( 'missing_image_sizes', $fields, true ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; $data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->filter_response_by_context( $data, $context ); $links = $response->get_links(); // Wrap the data in a response object. $response = rest_ensure_response( $data ); foreach ( $links as $rel => $rel_links ) { foreach ( $rel_links as $link ) { $response->add_link( $rel, $link['href'], $link['attributes'] ); } } /** * Filters an attachment returned from the REST API. * * Allows modification of the attachment right before it is returned. * * @since 4.7.0 * * @param WP_REST_Response $response The response object. * @param WP_Post $post The original attachment post. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'rest_prepare_attachment', $response, $post, $request ); } /** * Retrieves the attachment's schema, conforming to JSON Schema. * * @since 4.7.0 * * @return array Item schema as an array. */ public function get_item_schema() { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $schema = parent::get_item_schema(); $schema['properties']['alt_text'] = array( 'description' => __( 'Alternative text to display when attachment is not displayed.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ); $schema['properties']['caption'] = array( 'description' => __( 'The attachment caption.' ), 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). ), 'properties' => array( 'raw' => array( 'description' => __( 'Caption for the attachment, as it exists in the database.' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'rendered' => array( 'description' => __( 'HTML caption for the attachment, transformed for display.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), ), ); $schema['properties']['description'] = array( 'description' => __( 'The attachment description.' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). ), 'properties' => array( 'raw' => array( 'description' => __( 'Description for the attachment, as it exists in the database.' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'rendered' => array( 'description' => __( 'HTML description for the attachment, transformed for display.' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ), ); $schema['properties']['media_type'] = array( 'description' => __( 'Attachment type.' ), 'type' => 'string', 'enum' => array( 'image', 'file' ), 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['mime_type'] = array( 'description' => __( 'The attachment MIME type.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['media_details'] = array( 'description' => __( 'Details about the media file, specific to its type.' ), 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['post'] = array( 'description' => __( 'The ID for the associated post of the attachment.' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ); $schema['properties']['source_url'] = array( 'description' => __( 'URL to the original attachment file.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['missing_image_sizes'] = array( 'description' => __( 'List of the missing image sizes of the attachment.' ), 'type' => 'array', 'items' => array( 'type' => 'string' ), 'context' => array( 'edit' ), 'readonly' => true, ); unset( $schema['properties']['password'] ); $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); } /** * Handles an upload via raw POST data. * * @since 4.7.0 * * @param string $data Supplied file data. * @param array $headers HTTP headers from the request. * @return array|WP_Error Data from wp_handle_sideload(). */ protected function upload_from_data( $data, $headers ) { if ( empty( $data ) ) { return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) ); } if ( empty( $headers['content_type'] ) ) { return new WP_Error( 'rest_upload_no_content_type', __( 'No Content-Type supplied.' ), array( 'status' => 400 ) ); } if ( empty( $headers['content_disposition'] ) ) { return new WP_Error( 'rest_upload_no_content_disposition', __( 'No Content-Disposition supplied.' ), array( 'status' => 400 ) ); } $filename = self::get_filename_from_disposition( $headers['content_disposition'] ); if ( empty( $filename ) ) { return new WP_Error( 'rest_upload_invalid_disposition', __( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ), array( 'status' => 400 ) ); } if ( ! empty( $headers['content_md5'] ) ) { $content_md5 = array_shift( $headers['content_md5'] ); $expected = trim( $content_md5 ); $actual = md5( $data ); if ( $expected !== $actual ) { return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) ); } } // Get the content-type. $type = array_shift( $headers['content_type'] ); // Include filesystem functions to get access to wp_tempnam() and wp_handle_sideload(). require_once ABSPATH . 'wp-admin/includes/file.php'; // Save the file. $tmpfname = wp_tempnam( $filename ); $fp = fopen( $tmpfname, 'w+' ); if ( ! $fp ) { return new WP_Error( 'rest_upload_file_error', __( 'Could not open file handle.' ), array( 'status' => 500 ) ); } fwrite( $fp, $data ); fclose( $fp ); // Now, sideload it in. $file_data = array( 'error' => null, 'tmp_name' => $tmpfname, 'name' => $filename, 'type' => $type, ); $size_check = self::check_upload_size( $file_data ); if ( is_wp_error( $size_check ) ) { return $size_check; } $overrides = array( 'test_form' => false, ); $sideloaded = wp_handle_sideload( $file_data, $overrides ); if ( isset( $sideloaded['error'] ) ) { @unlink( $tmpfname ); return new WP_Error( 'rest_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) ); } return $sideloaded; } /** * Parses filename from a Content-Disposition header value. * * As per RFC6266: * * content-disposition = "Content-Disposition" ":" * disposition-type *( ";" disposition-parm ) * * disposition-type = "inline" | "attachment" | disp-ext-type * ; case-insensitive * disp-ext-type = token * * disposition-parm = filename-parm | disp-ext-parm * * filename-parm = "filename" "=" value * | "filename*" "=" ext-value * * disp-ext-parm = token "=" value * | ext-token "=" ext-value * ext-token = * * @since 4.7.0 * * @link https://tools.ietf.org/html/rfc2388 * @link https://tools.ietf.org/html/rfc6266 * * @param string[] $disposition_header List of Content-Disposition header values. * @return string|null Filename if available, or null if not found. */ public static function get_filename_from_disposition( $disposition_header ) { // Get the filename. $filename = null; foreach ( $disposition_header as $value ) { $value = trim( $value ); if ( ! str_contains( $value, ';' ) ) { continue; } list( $type, $attr_parts ) = explode( ';', $value, 2 ); $attr_parts = explode( ';', $attr_parts ); $attributes = array(); foreach ( $attr_parts as $part ) { if ( ! str_contains( $part, '=' ) ) { continue; } list( $key, $value ) = explode( '=', $part, 2 ); $attributes[ trim( $key ) ] = trim( $value ); } if ( empty( $attributes['filename'] ) ) { continue; } $filename = trim( $attributes['filename'] ); // Unquote quoted filename, but after trimming. if ( str_starts_with( $filename, '"' ) && str_ends_with( $filename, '"' ) ) { $filename = substr( $filename, 1, -1 ); } } return $filename; } /** * Retrieves the query params for collections of attachments. * * @since 4.7.0 * * @return array Query parameters for the attachment collection as an array. */ public function get_collection_params() { $params = parent::get_collection_params(); $params['status']['default'] = 'inherit'; $params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' ); $media_types = $this->get_media_types(); $params['media_type'] = array( 'default' => null, 'description' => __( 'Limit result set to attachments of a particular media type.' ), 'type' => 'string', 'enum' => array_keys( $media_types ), ); $params['mime_type'] = array( 'default' => null, 'description' => __( 'Limit result set to attachments of a particular MIME type.' ), 'type' => 'string', ); return $params; } /** * Handles an upload via multipart/form-data ($_FILES). * * @since 4.7.0 * * @param array $files Data from the `$_FILES` superglobal. * @param array $headers HTTP headers from the request. * @return array|WP_Error Data from wp_handle_upload(). */ protected function upload_from_file( $files, $headers ) { if ( empty( $files ) ) { return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) ); } // Verify hash, if given. if ( ! empty( $headers['content_md5'] ) ) { $content_md5 = array_shift( $headers['content_md5'] ); $expected = trim( $content_md5 ); $actual = md5_file( $files['file']['tmp_name'] ); if ( $expected !== $actual ) { return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) ); } } // Pass off to WP to handle the actual upload. $overrides = array( 'test_form' => false, ); // Bypasses is_uploaded_file() when running unit tests. if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) { $overrides['action'] = 'wp_handle_mock_upload'; } $size_check = self::check_upload_size( $files['file'] ); if ( is_wp_error( $size_check ) ) { return $size_check; } // Include filesystem functions to get access to wp_handle_upload(). require_once ABSPATH . 'wp-admin/includes/file.php'; $file = wp_handle_upload( $files['file'], $overrides ); if ( isset( $file['error'] ) ) { return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) ); } return $file; } /** * Retrieves the supported media types. * * Media types are considered the MIME type category. * * @since 4.7.0 * * @return array Array of supported media types. */ protected function get_media_types() { $media_types = array(); foreach ( get_allowed_mime_types() as $mime_type ) { $parts = explode( '/', $mime_type ); if ( ! isset( $media_types[ $parts[0] ] ) ) { $media_types[ $parts[0] ] = array(); } $media_types[ $parts[0] ][] = $mime_type; } return $media_types; } /** * Determine if uploaded file exceeds space quota on multisite. * * Replicates check_upload_size(). * * @since 4.9.8 * * @param array $file $_FILES array for a given file. * @return true|WP_Error True if can upload, error for errors. */ protected function check_upload_size( $file ) { if ( ! is_multisite() ) { return true; } if ( get_site_option( 'upload_space_check_disabled' ) ) { return true; } $space_left = get_upload_space_available(); $file_size = filesize( $file['tmp_name'] ); if ( $space_left < $file_size ) { return new WP_Error( 'rest_upload_limited_space', /* translators: %s: Required disk space in kilobytes. */ sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ), array( 'status' => 400 ) ); } if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) { return new WP_Error( 'rest_upload_file_too_big', /* translators: %s: Maximum allowed file size in kilobytes. */ sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ), array( 'status' => 400 ) ); } // Include multisite admin functions to get access to upload_is_user_over_quota(). require_once ABSPATH . 'wp-admin/includes/ms.php'; if ( upload_is_user_over_quota( false ) ) { return new WP_Error( 'rest_upload_user_quota_exceeded', __( 'You have used your space quota. Please delete files before uploading.' ), array( 'status' => 400 ) ); } return true; } /** * Gets the request args for the edit item route. * * @since 5.5.0 * * @return array */ protected function get_edit_media_item_args() { return array( 'src' => array( 'description' => __( 'URL to the edited image file.' ), 'type' => 'string', 'format' => 'uri', 'required' => true, ), 'modifiers' => array( 'description' => __( 'Array of image edits.' ), 'type' => 'array', 'minItems' => 1, 'items' => array( 'description' => __( 'Image edit.' ), 'type' => 'object', 'required' => array( 'type', 'args', ), 'oneOf' => array( array( 'title' => __( 'Rotation' ), 'properties' => array( 'type' => array( 'description' => __( 'Rotation type.' ), 'type' => 'string', 'enum' => array( 'rotate' ), ), 'args' => array( 'description' => __( 'Rotation arguments.' ), 'type' => 'object', 'required' => array( 'angle', ), 'properties' => array( 'angle' => array( 'description' => __( 'Angle to rotate clockwise in degrees.' ), 'type' => 'number', ), ), ), ), ), array( 'title' => __( 'Crop' ), 'properties' => array( 'type' => array( 'description' => __( 'Crop type.' ), 'type' => 'string', 'enum' => array( 'crop' ), ), 'args' => array( 'description' => __( 'Crop arguments.' ), 'type' => 'object', 'required' => array( 'left', 'top', 'width', 'height', ), 'properties' => array( 'left' => array( 'description' => __( 'Horizontal position from the left to begin the crop as a percentage of the image width.' ), 'type' => 'number', ), 'top' => array( 'description' => __( 'Vertical position from the top to begin the crop as a percentage of the image height.' ), 'type' => 'number', ), 'width' => array( 'description' => __( 'Width of the crop as a percentage of the image width.' ), 'type' => 'number', ), 'height' => array( 'description' => __( 'Height of the crop as a percentage of the image height.' ), 'type' => 'number', ), ), ), ), ), ), ), ), 'rotation' => array( 'description' => __( 'The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'integer', 'minimum' => 0, 'exclusiveMinimum' => true, 'maximum' => 360, 'exclusiveMaximum' => true, ), 'x' => array( 'description' => __( 'As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), 'y' => array( 'description' => __( 'As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), 'width' => array( 'description' => __( 'As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), 'height' => array( 'description' => __( 'As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), ); } } etaBox' => [ 'type' => 'boolean', 'default' => true ], 'keywords' => [ 'type' => 'string', 'localized' => true ] ] ] ] ], 'searchStatistics' => [ 'postTypes' => [ 'all' => [ 'type' => 'boolean', 'default' => true ], 'included' => [ 'type' => 'array', 'default' => [ 'post', 'page' ] ], ] ], 'tools' => [ 'robots' => [ 'enable' => [ 'type' => 'boolean', 'default' => false ], 'rules' => [ 'type' => 'array', 'default' => [] ], 'robotsDetected' => [ 'type' => 'boolean', 'default' => true ], ], 'importExport' => [ 'backup' => [ 'lastTime' => [ 'type' => 'string' ], 'data' => [ 'type' => 'string' ], ] ] ], 'deprecated' => [ 'searchAppearance' => [ 'global' => [ 'descriptionFormat' => [ 'type' => 'string' ], 'schema' => [ 'enableSchemaMarkup' => [ 'type' => 'boolean', 'default' => true ] ] ], 'advanced' => [ 'autogenerateDescriptions' => [ 'type' => 'boolean', 'default' => true ], 'runShortcodesInDescription' => [ 'type' => 'boolean', 'default' => true ], // TODO: Remove this in a future update. 'useContentForAutogeneratedDescriptions' => [ 'type' => 'boolean', 'default' => false ], 'excludePosts' => [ 'type' => 'array', 'default' => [] ], 'excludeTerms' => [ 'type' => 'array', 'default' => [] ], 'noPaginationForCanonical' => [ 'type' => 'boolean', 'default' => true ] ] ], 'sitemap' => [ 'general' => [ 'advancedSettings' => [ 'dynamic' => [ 'type' => 'boolean', 'default' => true ] ] ] ], 'tools' => [ 'blocker' => [ 'blockBots' => [ 'type' => 'boolean' ], 'blockReferer' => [ 'type' => 'boolean' ], 'track' => [ 'type' => 'boolean' ], 'custom' => [ 'enable' => [ 'type' => 'boolean' ], 'bots' => [ 'type' => 'html', 'default' => '' ], 'referer' => [ 'type' => 'html', 'default' => '' ] ] ] ] ] // phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound ]; /** * The Construct method. * * @since 4.0.0 * * @param string $optionsName An array of options. */ public function __construct( $optionsName = 'aioseo_options' ) { $this->optionsName = $optionsName; $this->init(); add_action( 'shutdown', [ $this, 'save' ] ); } /** * Initializes the options. * * @since 4.0.0 * * @return void */ public function init() { $this->setInitialDefaults(); $this->translateDefaults(); $this->setDbOptions(); add_action( 'wp_loaded', [ $this, 'maybeFlushRewriteRules' ] ); } /** * Sets the DB options to the class after merging in new defaults and dropping unknown values. * * @since 4.0.14 * * @return void */ public function setDbOptions() { // Refactor options. $this->defaultsMerged = array_replace_recursive( $this->defaults, $this->defaultsMerged ); $dbOptions = $this->getDbOptions( $this->optionsName ); $options = array_replace_recursive( $this->defaultsMerged, $this->addValueToValuesArray( $this->defaultsMerged, $dbOptions ) ); aioseo()->core->optionsCache->setOptions( $this->optionsName, apply_filters( 'aioseo_get_options', $options ) ); // Get the localized options. $dbOptionsLocalized = get_option( $this->optionsName . '_localized' ); if ( empty( $dbOptionsLocalized ) ) { $dbOptionsLocalized = []; } $this->localized = $dbOptionsLocalized; } /** * Sets the initial defaults that can't be defined in the property because of PHP 5.4. * * @since 4.1.4 * * @return void */ protected function setInitialDefaults() { static $hasInitialized = false; if ( $hasInitialized ) { return; } $hasInitialized = true; $this->defaults['searchAppearance']['global']['schema']['organizationName']['default'] = aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ); $this->defaults['deprecated']['tools']['blocker']['custom']['bots']['default'] = implode( "\n", aioseo()->badBotBlocker->getBotList() ); $this->defaults['deprecated']['tools']['blocker']['custom']['referer']['default'] = implode( "\n", aioseo()->badBotBlocker->getRefererList() ); $this->defaults['searchAppearance']['global']['schema']['organizationName']['default'] = aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ); $this->defaults['searchAppearance']['global']['schema']['websiteName']['default'] = aioseo()->helpers->decodeHtmlEntities( get_bloginfo( 'name' ) ); $this->defaults['searchAppearance']['global']['schema']['organizationLogo']['default'] = aioseo()->helpers->getSiteLogoUrl() ? aioseo()->helpers->getSiteLogoUrl() : ''; } /** * For our defaults array, some options need to be translated, so we do that here. * * @since 4.0.0 * * @return void */ public function translateDefaults() { static $hasInitialized = false; if ( $hasInitialized ) { return; } $hasInitialized = true; $default = sprintf( '{"label":"%1$s","value":"default"}', __( 'default', 'all-in-one-seo-pack' ) ); $this->defaults['sitemap']['general']['advancedSettings']['priority']['homePage']['priority']['default'] = $default; $this->defaults['sitemap']['general']['advancedSettings']['priority']['homePage']['frequency']['default'] = $default; $this->defaults['sitemap']['general']['advancedSettings']['priority']['postTypes']['priority']['default'] = $default; $this->defaults['sitemap']['general']['advancedSettings']['priority']['postTypes']['frequency']['default'] = $default; $this->defaults['sitemap']['general']['advancedSettings']['priority']['taxonomies']['priority']['default'] = $default; $this->defaults['sitemap']['general']['advancedSettings']['priority']['taxonomies']['frequency']['default'] = $default; $this->defaults['breadcrumbs']['homepageLabel']['default'] = __( 'Home', 'all-in-one-seo-pack' ); $this->defaults['breadcrumbs']['archiveFormat']['default'] = sprintf( '%1$s #breadcrumb_archive_post_type_name', __( 'Archives for', 'all-in-one-seo-pack' ) ); $this->defaults['breadcrumbs']['searchResultFormat']['default'] = sprintf( '%1$s \'#breadcrumb_search_string\'', __( 'Search Results for', 'all-in-one-seo-pack' ) ); $this->defaults['breadcrumbs']['errorFormat404']['default'] = __( '404 - Page Not Found', 'all-in-one-seo-pack' ); } /** * Sanitizes, then saves the options to the database. * * @since 4.0.0 * * @param array $options An array of options to sanitize, then save. * @return void */ public function sanitizeAndSave( $options ) { $sitemapOptions = ! empty( $options['sitemap'] ) ? $options['sitemap'] : null; $oldSitemapOptions = aioseo()->options->sitemap->all(); $generalSitemapOptions = ! empty( $options['sitemap']['general'] ) ? $options['sitemap']['general'] : null; $oldGeneralSitemapOptions = aioseo()->options->sitemap->general->all(); $deprecatedGeneralSitemapOptions = ! empty( $options['deprecated']['sitemap']['general'] ) ? $options['deprecated']['sitemap']['general'] : null; $oldDeprecatedGeneralSitemapOptions = aioseo()->options->deprecated->sitemap->general->all(); $oldPhoneOption = aioseo()->options->searchAppearance->global->schema->phone; $phoneNumberOptions = isset( $options['searchAppearance']['global']['schema']['phone'] ) ? $options['searchAppearance']['global']['schema']['phone'] : null; $oldHtmlSitemapUrl = aioseo()->options->sitemap->html->pageUrl; $logsRetention = isset( $options['searchAppearance']['advanced']['blockArgs']['logsRetention'] ) ? $options['searchAppearance']['advanced']['blockArgs']['logsRetention'] : null; $oldLogsRetention = aioseo()->options->searchAppearance->advanced->blockArgs->logsRetention; $options = $this->maybeRemoveUnfilteredHtmlFields( $options ); $this->init(); if ( ! is_array( $options ) ) { return; } // First, recursively replace the new options into the cached state. // It's important we use the helper method since we want to replace populated arrays with empty ones if needed (when a setting was cleared out). $cachedOptions = aioseo()->core->optionsCache->getOptions( $this->optionsName ); $dbOptions = aioseo()->helpers->arrayReplaceRecursive( $cachedOptions, $this->addValueToValuesArray( $cachedOptions, $options, [], true ) ); // Now, we must also intersect both arrays to delete any individual keys that were unset. // We must do this because, while arrayReplaceRecursive will update the values for keys or empty them out, // it will keys that aren't present in the replacement array unaffected in the target array. $dbOptions = aioseo()->helpers->arrayIntersectRecursive( $dbOptions, $this->addValueToValuesArray( $cachedOptions, $options, [], true ), 'value' ); if ( isset( $options['social']['profiles']['additionalUrls'] ) ) { $dbOptions['social']['profiles']['additionalUrls'] = preg_replace( '/\h/', "\n", $options['social']['profiles']['additionalUrls'] ); } $newOptions = ! empty( $options['sitemap']['html'] ) ? $options['sitemap']['html'] : null; if ( ! empty( $newOptions ) && aioseo()->options->sitemap->html->enable ) { $newOptions = ! empty( $options['sitemap']['html'] ) ? $options['sitemap']['html'] : null; $pageUrl = wp_parse_url( $newOptions['pageUrl'] ); $path = ! empty( $pageUrl['path'] ) ? untrailingslashit( $pageUrl['path'] ) : ''; if ( $path ) { $existingPage = get_page_by_path( $path, OBJECT ); if ( is_object( $existingPage ) ) { // If the page exists, don't override the previous URL. $options['sitemap']['html']['pageUrl'] = $oldHtmlSitemapUrl; } } } // Update the cache state. aioseo()->core->optionsCache->setOptions( $this->optionsName, $dbOptions ); // Update localized options. update_option( $this->optionsName . '_localized', $this->localized ); // Finally, save the new values to the DB. $this->save( true ); // If phone settings have changed, let's see if we need to dump the phone number notice. if ( $phoneNumberOptions && $phoneNumberOptions !== $oldPhoneOption ) { $notification = Models\Notification::getNotificationByName( 'v3-migration-schema-number' ); if ( $notification->exists() ) { Models\Notification::deleteNotificationByName( 'v3-migration-schema-number' ); } } // If sitemap settings were changed, static files need to be regenerated. if ( ! empty( $deprecatedGeneralSitemapOptions ) && ! empty( $generalSitemapOptions ) ) { if ( ( aioseo()->helpers->arraysDifferent( $oldGeneralSitemapOptions, $generalSitemapOptions ) || aioseo()->helpers->arraysDifferent( $oldDeprecatedGeneralSitemapOptions, $deprecatedGeneralSitemapOptions ) ) && $generalSitemapOptions['advancedSettings']['enable'] && ! $deprecatedGeneralSitemapOptions['advancedSettings']['dynamic'] ) { aioseo()->sitemap->scheduleRegeneration(); } } // Add or remove schedule for clearing crawl cleanup logs. if ( ! empty( $logsRetention ) && $oldLogsRetention !== $logsRetention ) { aioseo()->crawlCleanup->scheduleClearingLogs(); } if ( ! empty( $sitemapOptions ) ) { aioseo()->searchStatistics->sitemap->maybeSync( $oldSitemapOptions, $sitemapOptions ); } // This is required in order for the Pro options to be refreshed before they save data again. $this->refresh(); } /** * If the user does not have access to unfiltered HTML, we need to remove them from saving. * * @since 4.0.0 * * @param array $options An array of options. * @return array An array of options. */ private function maybeRemoveUnfilteredHtmlFields( $options ) { if ( current_user_can( 'unfiltered_html' ) ) { return $options; } if ( ! empty( $options['webmasterTools'] ) && isset( $options['webmasterTools']['miscellaneousVerification'] ) ) { unset( $options['webmasterTools']['miscellaneousVerification'] ); } if ( ! empty( $options['rssContent'] ) && isset( $options['rssContent']['before'] ) ) { unset( $options['rssContent']['before'] ); } if ( ! empty( $options['rssContent'] ) && isset( $options['rssContent']['after'] ) ) { unset( $options['rssContent']['after'] ); } return $options; } /** * Indicate we need to flush rewrite rules on next load. * * @since 4.0.17 * * @return void */ public function flushRewriteRules() { update_option( 'aioseo_flush_rewrite_rules_flag', true ); } /** * Flush rewrite rules if needed. * * @since 4.0.17 * * @return void */ public function maybeFlushRewriteRules() { if ( get_option( 'aioseo_flush_rewrite_rules_flag' ) ) { flush_rewrite_rules(); delete_option( 'aioseo_flush_rewrite_rules_flag' ); } } }