| Server IP : 170.10.162.208 / Your IP : 216.73.216.181 Web Server : LiteSpeed System : Linux altar19.supremepanel19.com 4.18.0-553.69.1.lve.el8.x86_64 #1 SMP Wed Aug 13 19:53:59 UTC 2025 x86_64 User : deltahospital ( 1806) PHP Version : 7.4.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : OFF | Pkexec : OFF Directory : /tmp/ |
Upload File : |
<?php
/**
* API for easily embedding rich media such as videos and images into content.
*
* @package WordPress
* @subpackage Embed
* @since 2.9.0
*/
class WP_Embed {
public $handlers = array();
public $post_ID;
public $usecache = true;
public $linkifunknown = true;
public $last_attr = array();
public $last_url = '';
/**
* When a URL cannot be embedded, return false instead of returning a link
* or the URL.
*
* Bypasses the {@see 'embed_maybe_make_link'} filter.
*
* @var bool
*/
public $return_false_on_fail = false;
/**
* Constructor
*/
public function __construct() {
// Hack to get the [embed] shortcode to run before wpautop()
add_filter( 'the_content', array( $this, 'run_shortcode' ), 8 );
add_filter( 'widget_text_content', array( $this, 'run_shortcode' ), 8 );
// Shortcode placeholder for strip_shortcodes()
add_shortcode( 'embed', '__return_false' );
// Attempts to embed all URLs in a post
add_filter( 'the_content', array( $this, 'autoembed' ), 8 );
add_filter( 'widget_text_content', array( $this, 'autoembed' ), 8 );
// After a post is saved, cache oEmbed items via Ajax
add_action( 'edit_form_advanced', array( $this, 'maybe_run_ajax_cache' ) );
add_action( 'edit_page_form', array( $this, 'maybe_run_ajax_cache' ) );
}
/**
* Process the [embed] shortcode.
*
* Since the [embed] shortcode needs to be run earlier than other shortcodes,
* this function removes all existing shortcodes, registers the [embed] shortcode,
* calls do_shortcode(), and then re-registers the old shortcodes.
*
* @global array $shortcode_tags
*
* @param string $content Content to parse
* @return string Content with shortcode parsed
*/
public function run_shortcode( $content ) {
global $shortcode_tags;
// Back up current registered shortcodes and clear them all out
$orig_shortcode_tags = $shortcode_tags;
remove_all_shortcodes();
add_shortcode( 'embed', array( $this, 'shortcode' ) );
// Do the shortcode (only the [embed] one is registered)
$content = do_shortcode( $content, true );
// Put the original shortcodes back
$shortcode_tags = $orig_shortcode_tags;
return $content;
}
/**
* If a post/page was saved, then output JavaScript to make
* an Ajax request that will call WP_Embed::cache_oembed().
*/
public function maybe_run_ajax_cache() {
$post = get_post();
if ( ! $post || empty( $_GET['message'] ) ) {
return;
}
?>
<script type="text/javascript">
jQuery(document).ready(function($){
$.get("<?php echo admin_url( 'admin-ajax.php?action=oembed-cache&post=' . $post->ID, 'relative' ); ?>");
});
</script>
<?php
}
/**
* Registers an embed handler.
*
* Do not use this function directly, use wp_embed_register_handler() instead.
*
* This function should probably also only be used for sites that do not support oEmbed.
*
* @param string $id An internal ID/name for the handler. Needs to be unique.
* @param string $regex The regex that will be used to see if this handler should be used for a URL.
* @param callable $callback The callback function that will be called if the regex is matched.
* @param int $priority Optional. Used to specify the order in which the registered handlers will be tested (default: 10). Lower numbers correspond with earlier testing, and handlers with the same priority are tested in the order in which they were added to the action.
*/
public function register_handler( $id, $regex, $callback, $priority = 10 ) {
$this->handlers[ $priority ][ $id ] = array(
'regex' => $regex,
'callback' => $callback,
);
}
/**
* Unregisters a previously-registered embed handler.
*
* Do not use this function directly, use wp_embed_unregister_handler() instead.
*
* @param string $id The handler ID that should be removed.
* @param int $priority Optional. The priority of the handler to be removed (default: 10).
*/
public function unregister_handler( $id, $priority = 10 ) {
unset( $this->handlers[ $priority ][ $id ] );
}
/**
* The do_shortcode() callback function.
*
* Attempts to convert a URL into embed HTML. Starts by checking the URL against the regex of
* the registered embed handlers. If none of the regex matches and it's enabled, then the URL
* will be given to the WP_oEmbed class.
*
* @param array $attr {
* Shortcode attributes. Optional.
*
* @type int $width Width of the embed in pixels.
* @type int $height Height of the embed in pixels.
* }
* @param string $url The URL attempting to be embedded.
* @return string|false The embed HTML on success, otherwise the original URL.
* `->maybe_make_link()` can return false on failure.
*/
public function shortcode( $attr, $url = '' ) {
$post = get_post();
if ( empty( $url ) && ! empty( $attr['src'] ) ) {
$url = $attr['src'];
}
$this->last_url = $url;
if ( empty( $url ) ) {
$this->last_attr = $attr;
return '';
}
$rawattr = $attr;
$attr = wp_parse_args( $attr, wp_embed_defaults( $url ) );
$this->last_attr = $attr;
// kses converts & into & and we need to undo this
// See https://core.trac.wordpress.org/ticket/11311
$url = str_replace( '&', '&', $url );
// Look for known internal handlers
ksort( $this->handlers );
foreach ( $this->handlers as $priority => $handlers ) {
foreach ( $handlers as $id => $handler ) {
if ( preg_match( $handler['regex'], $url, $matches ) && is_callable( $handler['callback'] ) ) {
if ( false !== $return = call_user_func( $handler['callback'], $matches, $attr, $url, $rawattr ) ) {
/**
* Filters the returned embed handler.
*
* @since 2.9.0
*
* @see WP_Embed::shortcode()
*
* @param mixed $return The shortcode callback function to call.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
*/
return apply_filters( 'embed_handler_html', $return, $url, $attr );
}
}
}
}
$post_ID = ( ! empty( $post->ID ) ) ? $post->ID : null;
// Potentially set by WP_Embed::cache_oembed().
if ( ! empty( $this->post_ID ) ) {
$post_ID = $this->post_ID;
}
// Check for a cached result (stored as custom post or in the post meta).
$key_suffix = md5( $url . serialize( $attr ) );
$cachekey = '_oembed_' . $key_suffix;
$cachekey_time = '_oembed_time_' . $key_suffix;
/**
* Filters the oEmbed TTL value (time to live).
*
* @since 4.0.0
*
* @param int $time Time to live (in seconds).
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @param int $post_ID Post ID.
*/
$ttl = apply_filters( 'oembed_ttl', DAY_IN_SECONDS, $url, $attr, $post_ID );
$cache = '';
$cache_time = 0;
$cached_post_id = $this->find_oembed_post_id( $key_suffix );
if ( $post_ID ) {
$cache = get_post_meta( $post_ID, $cachekey, true );
$cache_time = get_post_meta( $post_ID, $cachekey_time, true );
if ( ! $cache_time ) {
$cache_time = 0;
}
} elseif ( $cached_post_id ) {
$cached_post = get_post( $cached_post_id );
$cache = $cached_post->post_content;
$cache_time = strtotime( $cached_post->post_modified_gmt );
}
$cached_recently = ( time() - $cache_time ) < $ttl;
if ( $this->usecache || $cached_recently ) {
// Failures are cached. Serve one if we're using the cache.
if ( '{{unknown}}' === $cache ) {
return $this->maybe_make_link( $url );
}
if ( ! empty( $cache ) ) {
/**
* Filters the cached oEmbed HTML.
*
* @since 2.9.0
*
* @see WP_Embed::shortcode()
*
* @param mixed $cache The cached HTML result, stored in post meta.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @param int $post_ID Post ID.
*/
return apply_filters( 'embed_oembed_html', $cache, $url, $attr, $post_ID );
}
}
/**
* Filters whether to inspect the given URL for discoverable link tags.
*
* @since 2.9.0
* @since 4.4.0 The default value changed to true.
*
* @see WP_oEmbed::discover()
*
* @param bool $enable Whether to enable `<link>` tag discovery. Default true.
*/
$attr['discover'] = apply_filters( 'embed_oembed_discover', true );
// Use oEmbed to get the HTML.
$html = wp_oembed_get( $url, $attr );
if ( $post_ID ) {
if ( $html ) {
update_post_meta( $post_ID, $cachekey, $html );
update_post_meta( $post_ID, $cachekey_time, time() );
} elseif ( ! $cache ) {
update_post_meta( $post_ID, $cachekey, '{{unknown}}' );
}
} else {
$has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
if ( $has_kses ) {
// Prevent KSES from corrupting JSON in post_content.
kses_remove_filters();
}
$insert_post_args = array(
'post_name' => $key_suffix,
'post_status' => 'publish',
'post_type' => 'oembed_cache',
);
if ( $html ) {
if ( $cached_post_id ) {
wp_update_post(
wp_slash(
array(
'ID' => $cached_post_id,
'post_content' => $html,
)
)
);
} else {
wp_insert_post(
wp_slash(
array_merge(
$insert_post_args,
array(
'post_content' => $html,
)
)
)
);
}
} elseif ( ! $cache ) {
wp_insert_post(
wp_slash(
array_merge(
$insert_post_args,
array(
'post_content' => '{{unknown}}',
)
)
)
);
}
if ( $has_kses ) {
kses_init_filters();
}
}
// If there was a result, return it.
if ( $html ) {
/** This filter is documented in site-inc/class-wp-embed.php */
return apply_filters( 'embed_oembed_html', $html, $url, $attr, $post_ID );
}
// Still unknown
return $this->maybe_make_link( $url );
}
/**
* Delete all oEmbed caches. Unused by core as of 4.0.0.
*
* @param int $post_ID Post ID to delete the caches for.
*/
public function delete_oembed_caches( $post_ID ) {
$post_metas = get_post_custom_keys( $post_ID );
if ( empty( $post_metas ) ) {
return;
}
foreach ( $post_metas as $post_meta_key ) {
if ( '_oembed_' == substr( $post_meta_key, 0, 8 ) ) {
delete_post_meta( $post_ID, $post_meta_key );
}
}
}
/**
* Triggers a caching of all oEmbed results.
*
* @param int $post_ID Post ID to do the caching for.
*/
public function cache_oembed( $post_ID ) {
$post = get_post( $post_ID );
$post_types = get_post_types( array( 'show_ui' => true ) );
/**
* Filters the array of post types to cache oEmbed results for.
*
* @since 2.9.0
*
* @param string[] $post_types Array of post type names to cache oEmbed results for. Defaults to post types with `show_ui` set to true.
*/
if ( empty( $post->ID ) || ! in_array( $post->post_type, apply_filters( 'embed_cache_oembed_types', $post_types ) ) ) {
return;
}
// Trigger a caching
if ( ! empty( $post->post_content ) ) {
$this->post_ID = $post->ID;
$this->usecache = false;
$content = $this->run_shortcode( $post->post_content );
$this->autoembed( $content );
$this->usecache = true;
}
}
/**
* Passes any unlinked URLs that are on their own line to WP_Embed::shortcode() for potential embedding.
*
* @see WP_Embed::autoembed_callback()
*
* @param string $content The content to be searched.
* @return string Potentially modified $content.
*/
public function autoembed( $content ) {
// Replace line breaks from all HTML elements with placeholders.
$content = wp_replace_in_html_tags( $content, array( "\n" => '<!-- wp-line-break -->' ) );
if ( preg_match( '#(^|\s|>)https?://#i', $content ) ) {
// Find URLs on their own line.
$content = preg_replace_callback( '|^(\s*)(https?://[^\s<>"]+)(\s*)$|im', array( $this, 'autoembed_callback' ), $content );
// Find URLs in their own paragraph.
$content = preg_replace_callback( '|(<p(?: [^>]*)?>\s*)(https?://[^\s<>"]+)(\s*<\/p>)|i', array( $this, 'autoembed_callback' ), $content );
}
// Put the line breaks back.
return str_replace( '<!-- wp-line-break -->', "\n", $content );
}
/**
* Callback function for WP_Embed::autoembed().
*
* @param array $match A regex match array.
* @return string The embed HTML on success, otherwise the original URL.
*/
public function autoembed_callback( $match ) {
$oldval = $this->linkifunknown;
$this->linkifunknown = false;
$return = $this->shortcode( array(), $match[2] );
$this->linkifunknown = $oldval;
return $match[1] . $return . $match[3];
}
/**
* Conditionally makes a hyperlink based on an internal class variable.
*
* @param string $url URL to potentially be linked.
* @return false|string Linked URL or the original URL. False if 'return_false_on_fail' is true.
*/
public function maybe_make_link( $url ) {
if ( $this->return_false_on_fail ) {
return false;
}
$output = ( $this->linkifunknown ) ? '<a href="' . esc_url( $url ) . '">' . esc_html( $url ) . '</a>' : $url;
/**
* Filters the returned, maybe-linked embed URL.
*
* @since 2.9.0
*
* @param string $output The linked or original URL.
* @param string $url The original URL.
*/
return apply_filters( 'embed_maybe_make_link', $output, $url );
}
/**
* Find the oEmbed cache post ID for a given cache key.
*
* @since 4.9.0
*
* @param string $cache_key oEmbed cache key.
* @return int|null Post ID on success, null on failure.
*/
public function find_oembed_post_id( $cache_key ) {
$cache_group = 'oembed_cache_post';
$oembed_post_id = wp_cache_get( $cache_key, $cache_group );
if ( $oembed_post_id && 'oembed_cache' === get_post_type( $oembed_post_id ) ) {
return $oembed_post_id;
}
$oembed_post_query = new WP_Query(
array(
'post_type' => 'oembed_cache',
'post_status' => 'publish',
'name' => $cache_key,
'posts_per_page' => 1,
'no_found_rows' => true,
'cache_results' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'lazy_load_term_meta' => false,
)
);
if ( ! empty( $oembed_post_query->posts ) ) {
// Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
$oembed_post_id = $oembed_post_query->posts[0]->ID;
wp_cache_set( $cache_key, $oembed_post_id, $cache_group );
return $oembed_post_id;
}
return null;
}
}
<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
if (!is_callable('random_bytes')) {
/**
* Windows with PHP < 5.3.0 will not have the function
* openssl_random_pseudo_bytes() available, so let's use
* CAPICOM to work around this deficiency.
*
* @param int $bytes
*
* @throws Exception
*
* @return string
*/
function random_bytes($bytes)
{
try {
$bytes = RandomCompat_intval($bytes);
} catch (TypeError $ex) {
throw new TypeError(
'random_bytes(): $bytes must be an integer'
);
}
if ($bytes < 1) {
throw new Error(
'Length must be greater than 0'
);
}
$buf = '';
if (!class_exists('COM')) {
throw new Error(
'COM does not exist'
);
}
$util = new COM('CAPICOM.Utilities.1');
$execCount = 0;
/**
* Let's not let it loop forever. If we run N times and fail to
* get N bytes of random data, then CAPICOM has failed us.
*/
do {
$buf .= base64_decode($util->GetRandom($bytes, 0));
if (RandomCompat_strlen($buf) >= $bytes) {
/**
* Return our random entropy buffer here:
*/
return RandomCompat_substr($buf, 0, $bytes);
}
++$execCount;
} while ($execCount < $bytes);
/**
* If we reach here, PHP has failed us.
*/
throw new Exception(
'Could not gather sufficient random data'
);
}
}<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
if (!is_callable('RandomCompat_strlen')) {
if (
defined('MB_OVERLOAD_STRING') &&
ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING
) {
/**
* strlen() implementation that isn't brittle to mbstring.func_overload
*
* This version uses mb_strlen() in '8bit' mode to treat strings as raw
* binary rather than UTF-8, ISO-8859-1, etc
*
* @param string $binary_string
*
* @throws TypeError
*
* @return int
*/
function RandomCompat_strlen($binary_string)
{
if (!is_string($binary_string)) {
throw new TypeError(
'RandomCompat_strlen() expects a string'
);
}
return (int) mb_strlen($binary_string, '8bit');
}
} else {
/**
* strlen() implementation that isn't brittle to mbstring.func_overload
*
* This version just used the default strlen()
*
* @param string $binary_string
*
* @throws TypeError
*
* @return int
*/
function RandomCompat_strlen($binary_string)
{
if (!is_string($binary_string)) {
throw new TypeError(
'RandomCompat_strlen() expects a string'
);
}
return (int) strlen($binary_string);
}
}
}
if (!is_callable('RandomCompat_substr')) {
if (
defined('MB_OVERLOAD_STRING')
&&
ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING
) {
/**
* substr() implementation that isn't brittle to mbstring.func_overload
*
* This version uses mb_substr() in '8bit' mode to treat strings as raw
* binary rather than UTF-8, ISO-8859-1, etc
*
* @param string $binary_string
* @param int $start
* @param int $length (optional)
*
* @throws TypeError
*
* @return string
*/
function RandomCompat_substr($binary_string, $start, $length = null)
{
if (!is_string($binary_string)) {
throw new TypeError(
'RandomCompat_substr(): First argument should be a string'
);
}
if (!is_int($start)) {
throw new TypeError(
'RandomCompat_substr(): Second argument should be an integer'
);
}
if ($length === null) {
/**
* mb_substr($str, 0, NULL, '8bit') returns an empty string on
* PHP 5.3, so we have to find the length ourselves.
*/
$length = RandomCompat_strlen($binary_string) - $start;
} elseif (!is_int($length)) {
throw new TypeError(
'RandomCompat_substr(): Third argument should be an integer, or omitted'
);
}
// Consistency with PHP's behavior
if ($start === RandomCompat_strlen($binary_string) && $length === 0) {
return '';
}
if ($start > RandomCompat_strlen($binary_string)) {
return '';
}
return (string) mb_substr($binary_string, $start, $length, '8bit');
}
} else {
/**
* substr() implementation that isn't brittle to mbstring.func_overload
*
* This version just uses the default substr()
*
* @param string $binary_string
* @param int $start
* @param int $length (optional)
*
* @throws TypeError
*
* @return string
*/
function RandomCompat_substr($binary_string, $start, $length = null)
{
if (!is_string($binary_string)) {
throw new TypeError(
'RandomCompat_substr(): First argument should be a string'
);
}
if (!is_int($start)) {
throw new TypeError(
'RandomCompat_substr(): Second argument should be an integer'
);
}
if ($length !== null) {
if (!is_int($length)) {
throw new TypeError(
'RandomCompat_substr(): Third argument should be an integer, or omitted'
);
}
return (string) substr($binary_string, $start, $length);
}
return (string) substr($binary_string, $start);
}
}
}
<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
if (!is_callable('RandomCompat_intval')) {
/**
* Cast to an integer if we can, safely.
*
* If you pass it a float in the range (~PHP_INT_MAX, PHP_INT_MAX)
* (non-inclusive), it will sanely cast it to an int. If you it's equal to
* ~PHP_INT_MAX or PHP_INT_MAX, we let it fail as not an integer. Floats
* lose precision, so the <= and => operators might accidentally let a float
* through.
*
* @param int|float $number The number we want to convert to an int
* @param bool $fail_open Set to true to not throw an exception
*
* @return float|int
* @psalm-suppress InvalidReturnType
*
* @throws TypeError
*/
function RandomCompat_intval($number, $fail_open = false)
{
if (is_int($number) || is_float($number)) {
$number += 0;
} elseif (is_numeric($number)) {
$number += 0;
}
if (
is_float($number)
&&
$number > ~PHP_INT_MAX
&&
$number < PHP_INT_MAX
) {
$number = (int) $number;
}
if (is_int($number)) {
return (int) $number;
} elseif (!$fail_open) {
throw new TypeError(
'Expected an integer.'
);
}
return $number;
}
}
<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* @version 2.0.10
* @released 2017-03-13
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
if (!defined('PHP_VERSION_ID')) {
// This constant was introduced in PHP 5.2.7
$RandomCompatversion = array_map('intval', explode('.', PHP_VERSION));
define(
'PHP_VERSION_ID',
$RandomCompatversion[0] * 10000
+ $RandomCompatversion[1] * 100
+ $RandomCompatversion[2]
);
$RandomCompatversion = null;
}
/**
* PHP 7.0.0 and newer have these functions natively.
*/
if (PHP_VERSION_ID >= 70000) {
return;
}
if (!defined('RANDOM_COMPAT_READ_BUFFER')) {
define('RANDOM_COMPAT_READ_BUFFER', 8);
}
$RandomCompatDIR = dirname(__FILE__);
require_once $RandomCompatDIR . '/byte_safe_strings.php';
require_once $RandomCompatDIR . '/cast_to_int.php';
require_once $RandomCompatDIR . '/error_polyfill.php';
if (!is_callable('random_bytes')) {
/**
* PHP 5.2.0 - 5.6.x way to implement random_bytes()
*
* We use conditional statements here to define the function in accordance
* to the operating environment. It's a micro-optimization.
*
* In order of preference:
* 1. Use libsodium if available.
* 2. fread() /dev/urandom if available (never on Windows)
* 3. mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM)
* 4. COM('CAPICOM.Utilities.1')->GetRandom()
*
* See RATIONALE.md for our reasoning behind this particular order
*/
if (extension_loaded('libsodium')) {
// See random_bytes_libsodium.php
if (PHP_VERSION_ID >= 50300 && is_callable('\\Sodium\\randombytes_buf')) {
require_once $RandomCompatDIR . '/random_bytes_libsodium.php';
} elseif (method_exists('Sodium', 'randombytes_buf')) {
require_once $RandomCompatDIR . '/random_bytes_libsodium_legacy.php';
}
}
/**
* Reading directly from /dev/urandom:
*/
if (DIRECTORY_SEPARATOR === '/') {
// DIRECTORY_SEPARATOR === '/' on Unix-like OSes -- this is a fast
// way to exclude Windows.
$RandomCompatUrandom = true;
$RandomCompat_basedir = ini_get('open_basedir');
if (!empty($RandomCompat_basedir)) {
$RandomCompat_open_basedir = explode(
PATH_SEPARATOR,
strtolower($RandomCompat_basedir)
);
$RandomCompatUrandom = (array() !== array_intersect(
array('/dev', '/dev/', '/dev/urandom'),
$RandomCompat_open_basedir
));
$RandomCompat_open_basedir = null;
}
if (
!is_callable('random_bytes')
&&
$RandomCompatUrandom
&&
@is_readable('/dev/urandom')
) {
// Error suppression on is_readable() in case of an open_basedir
// or safe_mode failure. All we care about is whether or not we
// can read it at this point. If the PHP environment is going to
// panic over trying to see if the file can be read in the first
// place, that is not helpful to us here.
// See random_bytes_dev_urandom.php
require_once $RandomCompatDIR . '/random_bytes_dev_urandom.php';
}
// Unset variables after use
$RandomCompat_basedir = null;
} else {
$RandomCompatUrandom = false;
}
/**
* mcrypt_create_iv()
*
* We only want to use mcypt_create_iv() if:
*
* - random_bytes() hasn't already been defined
* - the mcrypt extensions is loaded
* - One of these two conditions is true:
* - We're on Windows (DIRECTORY_SEPARATOR !== '/')
* - We're not on Windows and /dev/urandom is readabale
* (i.e. we're not in a chroot jail)
* - Special case:
* - If we're not on Windows, but the PHP version is between
* 5.6.10 and 5.6.12, we don't want to use mcrypt. It will
* hang indefinitely. This is bad.
* - If we're on Windows, we want to use PHP >= 5.3.7 or else
* we get insufficient entropy errors.
*/
if (
!is_callable('random_bytes')
&&
// Windows on PHP < 5.3.7 is broken, but non-Windows is not known to be.
(DIRECTORY_SEPARATOR === '/' || PHP_VERSION_ID >= 50307)
&&
// Prevent this code from hanging indefinitely on non-Windows;
// see https://bugs.php.net/bug.php?id=69833
(
DIRECTORY_SEPARATOR !== '/' ||
(PHP_VERSION_ID <= 50609 || PHP_VERSION_ID >= 50613)
)
&&
extension_loaded('mcrypt')
) {
// See random_bytes_mcrypt.php
require_once $RandomCompatDIR . '/random_bytes_mcrypt.php';
}
$RandomCompatUrandom = null;
/**
* This is a Windows-specific fallback, for when the mcrypt extension
* isn't loaded.
*/
if (
!is_callable('random_bytes')
&&
extension_loaded('com_dotnet')
&&
class_exists('COM')
) {
$RandomCompat_disabled_classes = preg_split(
'#\s*,\s*#',
strtolower(ini_get('disable_classes'))
);
if (!in_array('com', $RandomCompat_disabled_classes)) {
try {
$RandomCompatCOMtest = new COM('CAPICOM.Utilities.1');
if (method_exists($RandomCompatCOMtest, 'GetRandom')) {
// See random_bytes_com_dotnet.php
require_once $RandomCompatDIR . '/random_bytes_com_dotnet.php';
}
} catch (com_exception $e) {
// Don't try to use it.
}
}
$RandomCompat_disabled_classes = null;
$RandomCompatCOMtest = null;
}
/**
* throw new Exception
*/
if (!is_callable('random_bytes')) {
/**
* We don't have any more options, so let's throw an exception right now
* and hope the developer won't let it fail silently.
*
* @param mixed $length
* @return void
* @throws Exception
*/
function random_bytes($length)
{
unset($length); // Suppress "variable not used" warnings.
throw new Exception(
'There is no suitable CSPRNG installed on your system'
);
}
}
}
if (!is_callable('random_int')) {
require_once $RandomCompatDIR . '/random_int.php';
}
$RandomCompatDIR = null;
<?php
if (!is_callable('random_int')) {
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Fetch a random integer between $min and $max inclusive
*
* @param int $min
* @param int $max
*
* @throws Exception
*
* @return int
*/
function random_int($min, $max)
{
/**
* Type and input logic checks
*
* If you pass it a float in the range (~PHP_INT_MAX, PHP_INT_MAX)
* (non-inclusive), it will sanely cast it to an int. If you it's equal to
* ~PHP_INT_MAX or PHP_INT_MAX, we let it fail as not an integer. Floats
* lose precision, so the <= and => operators might accidentally let a float
* through.
*/
try {
$min = RandomCompat_intval($min);
} catch (TypeError $ex) {
throw new TypeError(
'random_int(): $min must be an integer'
);
}
try {
$max = RandomCompat_intval($max);
} catch (TypeError $ex) {
throw new TypeError(
'random_int(): $max must be an integer'
);
}
/**
* Now that we've verified our weak typing system has given us an integer,
* let's validate the logic then we can move forward with generating random
* integers along a given range.
*/
if ($min > $max) {
throw new Error(
'Minimum value must be less than or equal to the maximum value'
);
}
if ($max === $min) {
return (int) $min;
}
/**
* Initialize variables to 0
*
* We want to store:
* $bytes => the number of random bytes we need
* $mask => an integer bitmask (for use with the &) operator
* so we can minimize the number of discards
*/
$attempts = $bits = $bytes = $mask = $valueShift = 0;
/**
* At this point, $range is a positive number greater than 0. It might
* overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to
* a float and we will lose some precision.
*/
$range = $max - $min;
/**
* Test for integer overflow:
*/
if (!is_int($range)) {
/**
* Still safely calculate wider ranges.
* Provided by @CodesInChaos, @oittaa
*
* @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435
*
* We use ~0 as a mask in this case because it generates all 1s
*
* @ref https://eval.in/400356 (32-bit)
* @ref http://3v4l.org/XX9r5 (64-bit)
*/
$bytes = PHP_INT_SIZE;
$mask = ~0;
} else {
/**
* $bits is effectively ceil(log($range, 2)) without dealing with
* type juggling
*/
while ($range > 0) {
if ($bits % 8 === 0) {
++$bytes;
}
++$bits;
$range >>= 1;
$mask = $mask << 1 | 1;
}
$valueShift = $min;
}
$val = 0;
/**
* Now that we have our parameters set up, let's begin generating
* random integers until one falls between $min and $max
*/
do {
/**
* The rejection probability is at most 0.5, so this corresponds
* to a failure probability of 2^-128 for a working RNG
*/
if ($attempts > 128) {
throw new Exception(
'random_int: RNG is broken - too many rejections'
);
}
/**
* Let's grab the necessary number of random bytes
*/
$randomByteString = random_bytes($bytes);
/**
* Let's turn $randomByteString into an integer
*
* This uses bitwise operators (<< and |) to build an integer
* out of the values extracted from ord()
*
* Example: [9F] | [6D] | [32] | [0C] =>
* 159 + 27904 + 3276800 + 201326592 =>
* 204631455
*/
$val &= 0;
for ($i = 0; $i < $bytes; ++$i) {
$val |= ord($randomByteString[$i]) << ($i * 8);
}
/**
* Apply mask
*/
$val &= $mask;
$val += $valueShift;
++$attempts;
/**
* If $val overflows to a floating point number,
* ... or is larger than $max,
* ... or smaller than $min,
* then try again.
*/
} while (!is_int($val) || $val > $max || $val < $min);
return (int) $val;
}
}
<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
if (!defined('RANDOM_COMPAT_READ_BUFFER')) {
define('RANDOM_COMPAT_READ_BUFFER', 8);
}
if (!is_callable('random_bytes')) {
/**
* Unless open_basedir is enabled, use /dev/urandom for
* random numbers in accordance with best practices
*
* Why we use /dev/urandom and not /dev/random
* @ref http://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers
*
* @param int $bytes
*
* @throws Exception
*
* @return string
*/
function random_bytes($bytes)
{
static $fp = null;
/**
* This block should only be run once
*/
if (empty($fp)) {
/**
* We use /dev/urandom if it is a char device.
* We never fall back to /dev/random
*/
$fp = fopen('/dev/urandom', 'rb');
if (!empty($fp)) {
$st = fstat($fp);
if (($st['mode'] & 0170000) !== 020000) {
fclose($fp);
$fp = false;
}
}
if (!empty($fp)) {
/**
* stream_set_read_buffer() does not exist in HHVM
*
* If we don't set the stream's read buffer to 0, PHP will
* internally buffer 8192 bytes, which can waste entropy
*
* stream_set_read_buffer returns 0 on success
*/
if (is_callable('stream_set_read_buffer')) {
stream_set_read_buffer($fp, RANDOM_COMPAT_READ_BUFFER);
}
if (is_callable('stream_set_chunk_size')) {
stream_set_chunk_size($fp, RANDOM_COMPAT_READ_BUFFER);
}
}
}
try {
$bytes = RandomCompat_intval($bytes);
} catch (TypeError $ex) {
throw new TypeError(
'random_bytes(): $bytes must be an integer'
);
}
if ($bytes < 1) {
throw new Error(
'Length must be greater than 0'
);
}
/**
* This if() block only runs if we managed to open a file handle
*
* It does not belong in an else {} block, because the above
* if (empty($fp)) line is logic that should only be run once per
* page load.
*/
if (!empty($fp)) {
/**
* @var int
*/
$remaining = $bytes;
/**
* @var string|bool
*/
$buf = '';
/**
* We use fread() in a loop to protect against partial reads
*/
do {
/**
* @var string|bool
*/
$read = fread($fp, $remaining);
if (!is_string($read)) {
if ($read === false) {
/**
* We cannot safely read from the file. Exit the
* do-while loop and trigger the exception condition
*
* @var string|bool
*/
$buf = false;
break;
}
}
/**
* Decrease the number of bytes returned from remaining
*/
$remaining -= RandomCompat_strlen($read);
/**
* @var string|bool
*/
$buf = $buf . $read;
} while ($remaining > 0);
/**
* Is our result valid?
*/
if (is_string($buf)) {
if (RandomCompat_strlen($buf) === $bytes) {
/**
* Return our random entropy buffer here:
*/
return $buf;
}
}
}
/**
* If we reach here, PHP has failed us.
*/
throw new Exception(
'Error reading from source device'
);
}
}
<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
if (!is_callable('random_bytes')) {
/**
* If the libsodium PHP extension is loaded, we'll use it above any other
* solution.
*
* libsodium-php project:
* @ref https://github.com/jedisct1/libsodium-php
*
* @param int $bytes
*
* @throws Exception
*
* @return string
*/
function random_bytes($bytes)
{
try {
$bytes = RandomCompat_intval($bytes);
} catch (TypeError $ex) {
throw new TypeError(
'random_bytes(): $bytes must be an integer'
);
}
if ($bytes < 1) {
throw new Error(
'Length must be greater than 0'
);
}
/**
* @var string
*/
$buf = '';
/**
* \Sodium\randombytes_buf() doesn't allow more than 2147483647 bytes to be
* generated in one invocation.
*/
if ($bytes > 2147483647) {
for ($i = 0; $i < $bytes; $i += 1073741824) {
$n = ($bytes - $i) > 1073741824
? 1073741824
: $bytes - $i;
$buf .= Sodium::randombytes_buf((int) $n);
}
} else {
$buf .= Sodium::randombytes_buf((int) $bytes);
}
if (is_string($buf)) {
if (RandomCompat_strlen($buf) === $bytes) {
return $buf;
}
}
/**
* If we reach here, PHP has failed us.
*/
throw new Exception(
'Could not gather sufficient random data'
);
}
}
<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
if (!class_exists('Error', false)) {
// We can't really avoid making this extend Exception in PHP 5.
class Error extends Exception
{
}
}
if (!class_exists('TypeError', false)) {
if (is_subclass_of('Error', 'Exception')) {
class TypeError extends Error
{
}
} else {
class TypeError extends Exception
{
}
}
}
<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
if (!is_callable('random_bytes')) {
/**
* Powered by ext/mcrypt (and thankfully NOT libmcrypt)
*
* @ref https://bugs.php.net/bug.php?id=55169
* @ref https://github.com/php/php-src/blob/c568ffe5171d942161fc8dda066bce844bdef676/ext/mcrypt/mcrypt.c#L1321-L1386
*
* @param int $bytes
*
* @throws Exception
*
* @return string
*/
function random_bytes($bytes)
{
try {
$bytes = RandomCompat_intval($bytes);
} catch (TypeError $ex) {
throw new TypeError(
'random_bytes(): $bytes must be an integer'
);
}
if ($bytes < 1) {
throw new Error(
'Length must be greater than 0'
);
}
$buf = @mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
if (
$buf !== false
&&
RandomCompat_strlen($buf) === $bytes
) {
/**
* Return our random entropy buffer here:
*/
return $buf;
}
/**
* If we reach here, PHP has failed us.
*/
throw new Exception(
'Could not gather sufficient random data'
);
}
}
<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2017 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
if (!is_callable('random_bytes')) {
/**
* If the libsodium PHP extension is loaded, we'll use it above any other
* solution.
*
* libsodium-php project:
* @ref https://github.com/jedisct1/libsodium-php
*
* @param int $bytes
*
* @throws Exception
*
* @return string
*/
function random_bytes($bytes)
{
try {
$bytes = RandomCompat_intval($bytes);
} catch (TypeError $ex) {
throw new TypeError(
'random_bytes(): $bytes must be an integer'
);
}
if ($bytes < 1) {
throw new Error(
'Length must be greater than 0'
);
}
/**
* \Sodium\randombytes_buf() doesn't allow more than 2147483647 bytes to be
* generated in one invocation.
*/
if ($bytes > 2147483647) {
$buf = '';
for ($i = 0; $i < $bytes; $i += 1073741824) {
$n = ($bytes - $i) > 1073741824
? 1073741824
: $bytes - $i;
$buf .= \Sodium\randombytes_buf($n);
}
} else {
$buf = \Sodium\randombytes_buf($bytes);
}
if ($buf !== false) {
if (RandomCompat_strlen($buf) === $bytes) {
return $buf;
}
}
/**
* If we reach here, PHP has failed us.
*/
throw new Exception(
'Could not gather sufficient random data'
);
}
}
<?php
/**
* Object Cache API
*
* @link https://sitepad.com/docs/Class_Reference/WP_Object_Cache
*
* @package WordPress
* @subpackage Cache
*/
/**
* Adds data to the cache, if the cache key doesn't already exist.
*
* @since 2.0.0
*
* @see WP_Object_Cache::add()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The cache key to use for retrieval later.
* @param mixed $data The data to add to the cache.
* @param string $group Optional. The group to add the cache to. Enables the same key
* to be used across groups. Default empty.
* @param int $expire Optional. When the cache data should expire, in seconds.
* Default 0 (no expiration).
* @return bool False if cache key and group already exist, true on success.
*/
function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->add( $key, $data, $group, (int) $expire );
}
/**
* Closes the cache.
*
* This function has ceased to do anything since WordPress 2.5. The
* functionality was removed along with the rest of the persistent cache.
*
* This does not mean that plugins can't implement this function when they need
* to make sure that the cache is cleaned up after WordPress no longer needs it.
*
* @since 2.0.0
*
* @return true Always returns true.
*/
function wp_cache_close() {
return true;
}
/**
* Decrements numeric cache item's value.
*
* @since 3.3.0
*
* @see WP_Object_Cache::decr()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The cache key to decrement.
* @param int $offset Optional. The amount by which to decrement the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default empty.
* @return false|int False on failure, the item's new value on success.
*/
function wp_cache_decr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->decr( $key, $offset, $group );
}
/**
* Removes the cache contents matching key and group.
*
* @since 2.0.0
*
* @see WP_Object_Cache::delete()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key What the contents in the cache are called.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @return bool True on successful removal, false on failure.
*/
function wp_cache_delete( $key, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->delete( $key, $group );
}
/**
* Removes all cache items.
*
* @since 2.0.0
*
* @see WP_Object_Cache::flush()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @return bool False on failure, true on success
*/
function wp_cache_flush() {
global $wp_object_cache;
return $wp_object_cache->flush();
}
/**
* Retrieves the cache contents from the cache by key and group.
*
* @since 2.0.0
*
* @see WP_Object_Cache::get()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The key under which the cache contents are stored.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @param bool $force Optional. Whether to force an update of the local cache from the persistent
* cache. Default false.
* @param bool $found Optional. Whether the key was found in the cache (passed by reference).
* Disambiguates a return of false, a storable value. Default null.
* @return bool|mixed False on failure to retrieve contents or the cache
* contents on success
*/
function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
global $wp_object_cache;
return $wp_object_cache->get( $key, $group, $force, $found );
}
/**
* Increment numeric cache item's value
*
* @since 3.3.0
*
* @see WP_Object_Cache::incr()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The key for the cache contents that should be incremented.
* @param int $offset Optional. The amount by which to increment the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default empty.
* @return false|int False on failure, the item's new value on success.
*/
function wp_cache_incr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->incr( $key, $offset, $group );
}
/**
* Sets up Object Cache Global and assigns it.
*
* @since 2.0.0
*
* @global WP_Object_Cache $wp_object_cache
*/
function wp_cache_init() {
$GLOBALS['wp_object_cache'] = new WP_Object_Cache();
}
/**
* Replaces the contents of the cache with new data.
*
* @since 2.0.0
*
* @see WP_Object_Cache::replace()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The key for the cache data that should be replaced.
* @param mixed $data The new data to store in the cache.
* @param string $group Optional. The group for the cache data that should be replaced.
* Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool False if original value does not exist, true if contents were replaced
*/
function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
}
/**
* Saves the data to the cache.
*
* Differs from wp_cache_add() and wp_cache_replace() in that it will always write data.
*
* @since 2.0.0
*
* @see WP_Object_Cache::set()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The cache key to use for retrieval later.
* @param mixed $data The contents to store in the cache.
* @param string $group Optional. Where to group the cache contents. Enables the same key
* to be used across groups. Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool False on failure, true on success
*/
function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->set( $key, $data, $group, (int) $expire );
}
/**
* Switches the internal blog ID.
*
* This changes the blog id used to create keys in blog specific groups.
*
* @since 3.5.0
*
* @see WP_Object_Cache::switch_to_blog()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int $blog_id Site ID.
*/
function wp_cache_switch_to_blog( $blog_id ) {
global $wp_object_cache;
$wp_object_cache->switch_to_blog( $blog_id );
}
/**
* Adds a group or set of groups to the list of global groups.
*
* @since 2.6.0
*
* @see WP_Object_Cache::add_global_groups()
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param string|array $groups A group or an array of groups to add.
*/
function wp_cache_add_global_groups( $groups ) {
global $wp_object_cache;
$wp_object_cache->add_global_groups( $groups );
}
/**
* Adds a group or set of groups to the list of non-persistent groups.
*
* @since 2.6.0
*
* @param string|array $groups A group or an array of groups to add.
*/
function wp_cache_add_non_persistent_groups( $groups ) {
// Default cache doesn't persist so nothing to do here.
}
/**
* Reset internal cache keys and structures.
*
* If the cache back end uses global blog or site IDs as part of its cache keys,
* this function instructs the back end to reset those keys and perform any cleanup
* since blog or site IDs have changed since cache init.
*
* This function is deprecated. Use wp_cache_switch_to_blog() instead of this
* function when preparing the cache for a blog switch. For clearing the cache
* during unit tests, consider using wp_cache_init(). wp_cache_init() is not
* recommended outside of unit tests as the performance penalty for using it is
* high.
*
* @since 2.6.0
* @deprecated 3.5.0 WP_Object_Cache::reset()
* @see WP_Object_Cache::reset()
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*/
function wp_cache_reset() {
_deprecated_function( __FUNCTION__, '3.5.0', 'WP_Object_Cache::reset()' );
global $wp_object_cache;
$wp_object_cache->reset();
}
/**
* Core class that implements an object cache.
*
* The WordPress Object Cache is used to save on trips to the database. The
* Object Cache stores all of the cache data to memory and makes the cache
* contents available by using a key, which is used to name and later retrieve
* the cache contents.
*
* The Object Cache can be replaced by other caching mechanisms by placing files
* in the wp-content folder which is looked at in wp-settings. If that file
* exists, then this file will not be included.
*
* @since 2.0.0
*/
class WP_Object_Cache {
/**
* Holds the cached objects.
*
* @since 2.0.0
* @var array
*/
private $cache = array();
/**
* The amount of times the cache data was already stored in the cache.
*
* @since 2.5.0
* @var int
*/
public $cache_hits = 0;
/**
* Amount of times the cache did not have the request in cache.
*
* @since 2.0.0
* @var int
*/
public $cache_misses = 0;
/**
* List of global cache groups.
*
* @since 3.0.0
* @var array
*/
protected $global_groups = array();
/**
* The blog prefix to prepend to keys in non-global groups.
*
* @since 3.5.0
* @var int
*/
private $blog_prefix;
/**
* Holds the value of is_multisite().
*
* @since 3.5.0
* @var bool
*/
private $multisite;
/**
* Makes private properties readable for backward compatibility.
*
* @since 4.0.0
*
* @param string $name Property to get.
* @return mixed Property.
*/
public function __get( $name ) {
return $this->$name;
}
/**
* Makes private properties settable for backward compatibility.
*
* @since 4.0.0
*
* @param string $name Property to set.
* @param mixed $value Property value.
* @return mixed Newly-set property.
*/
public function __set( $name, $value ) {
return $this->$name = $value;
}
/**
* Makes private properties checkable for backward compatibility.
*
* @since 4.0.0
*
* @param string $name Property to check if set.
* @return bool Whether the property is set.
*/
public function __isset( $name ) {
return isset( $this->$name );
}
/**
* Makes private properties un-settable for backward compatibility.
*
* @since 4.0.0
*
* @param string $name Property to unset.
*/
public function __unset( $name ) {
unset( $this->$name );
}
/**
* Adds data to the cache if it doesn't already exist.
*
* @since 2.0.0
*
* @uses WP_Object_Cache::_exists() Checks to see if the cache already has data.
* @uses WP_Object_Cache::set() Sets the data after the checking the cache
* contents existence.
*
* @param int|string $key What to call the contents in the cache.
* @param mixed $data The contents to store in the cache.
* @param string $group Optional. Where to group the cache contents. Default 'default'.
* @param int $expire Optional. When to expire the cache contents. Default 0 (no expiration).
* @return bool False if cache key and group already exist, true on success
*/
public function add( $key, $data, $group = 'default', $expire = 0 ) {
if ( wp_suspend_cache_addition() ) {
return false;
}
if ( empty( $group ) ) {
$group = 'default';
}
$id = $key;
if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
$id = $this->blog_prefix . $key;
}
if ( $this->_exists( $id, $group ) ) {
return false;
}
return $this->set( $key, $data, $group, (int) $expire );
}
/**
* Sets the list of global cache groups.
*
* @since 3.0.0
*
* @param array $groups List of groups that are global.
*/
public function add_global_groups( $groups ) {
$groups = (array) $groups;
$groups = array_fill_keys( $groups, true );
$this->global_groups = array_merge( $this->global_groups, $groups );
}
/**
* Decrements numeric cache item's value.
*
* @since 3.3.0
*
* @param int|string $key The cache key to decrement.
* @param int $offset Optional. The amount by which to decrement the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default 'default'.
* @return false|int False on failure, the item's new value on success.
*/
public function decr( $key, $offset = 1, $group = 'default' ) {
if ( empty( $group ) ) {
$group = 'default';
}
if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
$key = $this->blog_prefix . $key;
}
if ( ! $this->_exists( $key, $group ) ) {
return false;
}
if ( ! is_numeric( $this->cache[ $group ][ $key ] ) ) {
$this->cache[ $group ][ $key ] = 0;
}
$offset = (int) $offset;
$this->cache[ $group ][ $key ] -= $offset;
if ( $this->cache[ $group ][ $key ] < 0 ) {
$this->cache[ $group ][ $key ] = 0;
}
return $this->cache[ $group ][ $key ];
}
/**
* Removes the contents of the cache key in the group.
*
* If the cache key does not exist in the group, then nothing will happen.
*
* @since 2.0.0
*
* @param int|string $key What the contents in the cache are called.
* @param string $group Optional. Where the cache contents are grouped. Default 'default'.
* @param bool $deprecated Optional. Unused. Default false.
* @return bool False if the contents weren't deleted and true on success.
*/
public function delete( $key, $group = 'default', $deprecated = false ) {
if ( empty( $group ) ) {
$group = 'default';
}
if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
$key = $this->blog_prefix . $key;
}
if ( ! $this->_exists( $key, $group ) ) {
return false;
}
unset( $this->cache[ $group ][ $key ] );
return true;
}
/**
* Clears the object cache of all data.
*
* @since 2.0.0
*
* @return true Always returns true.
*/
public function flush() {
$this->cache = array();
return true;
}
/**
* Retrieves the cache contents, if it exists.
*
* The contents will be first attempted to be retrieved by searching by the
* key in the cache group. If the cache is hit (success) then the contents
* are returned.
*
* On failure, the number of cache misses will be incremented.
*
* @since 2.0.0
*
* @param int|string $key What the contents in the cache are called.
* @param string $group Optional. Where the cache contents are grouped. Default 'default'.
* @param bool $force Optional. Unused. Whether to force a refetch rather than relying on the local
* cache. Default false.
* @param bool $found Optional. Whether the key was found in the cache (passed by reference).
* Disambiguates a return of false, a storable value. Default null.
* @return false|mixed False on failure to retrieve contents or the cache contents on success.
*/
public function get( $key, $group = 'default', $force = false, &$found = null ) {
if ( empty( $group ) ) {
$group = 'default';
}
if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
$key = $this->blog_prefix . $key;
}
if ( $this->_exists( $key, $group ) ) {
$found = true;
$this->cache_hits += 1;
if ( is_object( $this->cache[ $group ][ $key ] ) ) {
return clone $this->cache[ $group ][ $key ];
} else {
return $this->cache[ $group ][ $key ];
}
}
$found = false;
$this->cache_misses += 1;
return false;
}
/**
* Increments numeric cache item's value.
*
* @since 3.3.0
*
* @param int|string $key The cache key to increment
* @param int $offset Optional. The amount by which to increment the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default 'default'.
* @return false|int False on failure, the item's new value on success.
*/
public function incr( $key, $offset = 1, $group = 'default' ) {
if ( empty( $group ) ) {
$group = 'default';
}
if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
$key = $this->blog_prefix . $key;
}
if ( ! $this->_exists( $key, $group ) ) {
return false;
}
if ( ! is_numeric( $this->cache[ $group ][ $key ] ) ) {
$this->cache[ $group ][ $key ] = 0;
}
$offset = (int) $offset;
$this->cache[ $group ][ $key ] += $offset;
if ( $this->cache[ $group ][ $key ] < 0 ) {
$this->cache[ $group ][ $key ] = 0;
}
return $this->cache[ $group ][ $key ];
}
/**
* Replaces the contents in the cache, if contents already exist.
*
* @since 2.0.0
*
* @see WP_Object_Cache::set()
*
* @param int|string $key What to call the contents in the cache.
* @param mixed $data The contents to store in the cache.
* @param string $group Optional. Where to group the cache contents. Default 'default'.
* @param int $expire Optional. When to expire the cache contents. Default 0 (no expiration).
* @return bool False if not exists, true if contents were replaced.
*/
public function replace( $key, $data, $group = 'default', $expire = 0 ) {
if ( empty( $group ) ) {
$group = 'default';
}
$id = $key;
if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
$id = $this->blog_prefix . $key;
}
if ( ! $this->_exists( $id, $group ) ) {
return false;
}
return $this->set( $key, $data, $group, (int) $expire );
}
/**
* Resets cache keys.
*
* @since 3.0.0
*
* @deprecated 3.5.0 Use switch_to_blog()
* @see switch_to_blog()
*/
public function reset() {
_deprecated_function( __FUNCTION__, '3.5.0', 'switch_to_blog()' );
// Clear out non-global caches since the blog ID has changed.
foreach ( array_keys( $this->cache ) as $group ) {
if ( ! isset( $this->global_groups[ $group ] ) ) {
unset( $this->cache[ $group ] );
}
}
}
/**
* Sets the data contents into the cache.
*
* The cache contents is grouped by the $group parameter followed by the
* $key. This allows for duplicate ids in unique groups. Therefore, naming of
* the group should be used with care and should follow normal function
* naming guidelines outside of core WordPress usage.
*
* The $expire parameter is not used, because the cache will automatically
* expire for each time a page is accessed and PHP finishes. The method is
* more for cache plugins which use files.
*
* @since 2.0.0
*
* @param int|string $key What to call the contents in the cache.
* @param mixed $data The contents to store in the cache.
* @param string $group Optional. Where to group the cache contents. Default 'default'.
* @param int $expire Not Used.
* @return true Always returns true.
*/
public function set( $key, $data, $group = 'default', $expire = 0 ) {
if ( empty( $group ) ) {
$group = 'default';
}
if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
$key = $this->blog_prefix . $key;
}
if ( is_object( $data ) ) {
$data = clone $data;
}
$this->cache[ $group ][ $key ] = $data;
return true;
}
/**
* Echoes the stats of the caching.
*
* Gives the cache hits, and cache misses. Also prints every cached group,
* key and the data.
*
* @since 2.0.0
*/
public function stats() {
echo '<p>';
echo "<strong>Cache Hits:</strong> {$this->cache_hits}<br />";
echo "<strong>Cache Misses:</strong> {$this->cache_misses}<br />";
echo '</p>';
echo '<ul>';
foreach ( $this->cache as $group => $cache ) {
echo '<li><strong>Group:</strong> ' . esc_html( $group ) . ' - ( ' . number_format( strlen( serialize( $cache ) ) / KB_IN_BYTES, 2 ) . 'k )</li>';
}
echo '</ul>';
}
/**
* Switches the internal blog ID.
*
* This changes the blog ID used to create keys in blog specific groups.
*
* @since 3.5.0
*
* @param int $blog_id Blog ID.
*/
public function switch_to_blog( $blog_id ) {
$blog_id = (int) $blog_id;
$this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
}
/**
* Serves as a utility function to determine whether a key exists in the cache.
*
* @since 3.4.0
*
* @param int|string $key Cache key to check for existence.
* @param string $group Cache group for the key existence check.
* @return bool Whether the key exists in the cache for the given group.
*/
protected function _exists( $key, $group ) {
return isset( $this->cache[ $group ] ) && ( isset( $this->cache[ $group ][ $key ] ) || array_key_exists( $key, $this->cache[ $group ] ) );
}
/**
* Sets up object properties; PHP 5 style constructor.
*
* @since 2.0.8
*/
public function __construct() {
$this->multisite = is_multisite();
$this->blog_prefix = $this->multisite ? get_current_blog_id() . ':' : '';
/**
* @todo This should be moved to the PHP4 style constructor, PHP5
* already calls __destruct()
*/
register_shutdown_function( array( $this, '__destruct' ) );
}
/**
* Saves the object cache before object is completely destroyed.
*
* Called upon object destruction, which should be when PHP ends.
*
* @since 2.0.8
*
* @return true Always returns true.
*/
public function __destruct() {
return true;
}
}
<?php
/**
* The plugin API is located in this file, which allows for creating actions
* and filters and hooking functions, and methods. The functions or methods will
* then be run when the action or filter is called.
*
* The API callback examples reference functions, but can be methods of classes.
* To hook methods, you'll need to pass an array one of two ways.
*
* Any of the syntaxes explained in the PHP documentation for the
* {@link https://secure.php.net/manual/en/language.pseudo-types.php#language.types.callback 'callback'}
* type are valid.
*
* Also see the {@link https://sitepad.com/docs/Plugin_API Plugin API} for
* more information and examples on how to use a lot of these functions.
*
* This file should have no external dependencies.
*
* @package WordPress
* @subpackage Plugin
* @since 1.5.0
*/
// Initialize the filter globals.
require( dirname( __FILE__ ) . '/class-wp-hook.php' );
/** @var WP_Hook[] $wp_filter */
global $wp_filter, $wp_actions, $wp_current_filter;
if ( $wp_filter ) {
$wp_filter = WP_Hook::build_preinitialized_hooks( $wp_filter );
} else {
$wp_filter = array();
}
if ( ! isset( $wp_actions ) ) {
$wp_actions = array();
}
if ( ! isset( $wp_current_filter ) ) {
$wp_current_filter = array();
}
/**
* Hook a function or method to a specific filter action.
*
* WordPress offers filter hooks to allow plugins to modify
* various types of internal data at runtime.
*
* A plugin can modify data by binding a callback to a filter hook. When the filter
* is later applied, each bound callback is run in order of priority, and given
* the opportunity to modify a value by returning a new value.
*
* The following example shows how a callback function is bound to a filter hook.
*
* Note that `$example` is passed to the callback, (maybe) modified, then returned:
*
* function example_callback( $example ) {
* // Maybe modify $example in some way.
* return $example;
* }
* add_filter( 'example_filter', 'example_callback' );
*
* Bound callbacks can accept from none to the total number of arguments passed as parameters
* in the corresponding apply_filters() call.
*
* In other words, if an apply_filters() call passes four total arguments, callbacks bound to
* it can accept none (the same as 1) of the arguments or up to four. The important part is that
* the `$accepted_args` value must reflect the number of arguments the bound callback *actually*
* opted to accept. If no arguments were accepted by the callback that is considered to be the
* same as accepting 1 argument. For example:
*
* // Filter call.
* $value = apply_filters( 'hook', $value, $arg2, $arg3 );
*
* // Accepting zero/one arguments.
* function example_callback() {
* ...
* return 'some value';
* }
* add_filter( 'hook', 'example_callback' ); // Where $priority is default 10, $accepted_args is default 1.
*
* // Accepting two arguments (three possible).
* function example_callback( $value, $arg2 ) {
* ...
* return $maybe_modified_value;
* }
* add_filter( 'hook', 'example_callback', 10, 2 ); // Where $priority is 10, $accepted_args is 2.
*
* *Note:* The function will return true whether or not the callback is valid.
* It is up to you to take care. This is done for optimization purposes, so
* everything is as quick as possible.
*
* @since 0.71
*
* @global array $wp_filter A multidimensional array of all hooks and the callbacks hooked to them.
*
* @param string $tag The name of the filter to hook the $function_to_add callback to.
* @param callable $function_to_add The callback to be run when the filter is applied.
* @param int $priority Optional. Used to specify the order in which the functions
* associated with a particular action are executed. Default 10.
* Lower numbers correspond with earlier execution,
* and functions with the same priority are executed
* in the order in which they were added to the action.
* @param int $accepted_args Optional. The number of arguments the function accepts. Default 1.
* @return true
*/
function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
global $wp_filter;
if ( ! isset( $wp_filter[ $tag ] ) ) {
$wp_filter[ $tag ] = new WP_Hook();
}
$wp_filter[ $tag ]->add_filter( $tag, $function_to_add, $priority, $accepted_args );
return true;
}
/**
* Check if any filter has been registered for a hook.
*
* @since 2.5.0
*
* @global array $wp_filter Stores all of the filters.
*
* @param string $tag The name of the filter hook.
* @param callable|bool $function_to_check Optional. The callback to check for. Default false.
* @return false|int If $function_to_check is omitted, returns boolean for whether the hook has
* anything registered. When checking a specific function, the priority of that
* hook is returned, or false if the function is not attached. When using the
* $function_to_check argument, this function may return a non-boolean value
* that evaluates to false (e.g.) 0, so use the === operator for testing the
* return value.
*/
function has_filter( $tag, $function_to_check = false ) {
global $wp_filter;
if ( ! isset( $wp_filter[ $tag ] ) ) {
return false;
}
return $wp_filter[ $tag ]->has_filter( $tag, $function_to_check );
}
/**
* Call the functions added to a filter hook.
*
* The callback functions attached to filter hook $tag are invoked by calling
* this function. This function can be used to create a new filter hook by
* simply calling this function with the name of the new hook specified using
* the $tag parameter.
*
* The function allows for additional arguments to be added and passed to hooks.
*
* // Our filter callback function
* function example_callback( $string, $arg1, $arg2 ) {
* // (maybe) modify $string
* return $string;
* }
* add_filter( 'example_filter', 'example_callback', 10, 3 );
*
* /*
* * Apply the filters by calling the 'example_callback' function we
* * "hooked" to 'example_filter' using the add_filter() function above.
* * - 'example_filter' is the filter hook $tag
* * - 'filter me' is the value being filtered
* * - $arg1 and $arg2 are the additional arguments passed to the callback.
* $value = apply_filters( 'example_filter', 'filter me', $arg1, $arg2 );
*
* @since 0.71
*
* @global array $wp_filter Stores all of the filters.
* @global array $wp_current_filter Stores the list of current filters with the current one last.
*
* @param string $tag The name of the filter hook.
* @param mixed $value The value on which the filters hooked to `$tag` are applied on.
* @param mixed $var,... Additional variables passed to the functions hooked to `$tag`.
* @return mixed The filtered value after all hooked functions are applied to it.
*/
function apply_filters( $tag, $value ) {
global $wp_filter, $wp_current_filter;
$args = array();
// Do 'all' actions first.
if ( isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $tag;
$args = func_get_args();
_wp_call_all_hook( $args );
}
if ( ! isset( $wp_filter[ $tag ] ) ) {
if ( isset( $wp_filter['all'] ) ) {
array_pop( $wp_current_filter );
}
return $value;
}
if ( ! isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $tag;
}
if ( empty( $args ) ) {
$args = func_get_args();
}
// don't pass the tag name to WP_Hook
array_shift( $args );
$filtered = $wp_filter[ $tag ]->apply_filters( $value, $args );
array_pop( $wp_current_filter );
return $filtered;
}
/**
* Execute functions hooked on a specific filter hook, specifying arguments in an array.
*
* @since 3.0.0
*
* @see apply_filters() This function is identical, but the arguments passed to the
* functions hooked to `$tag` are supplied using an array.
*
* @global array $wp_filter Stores all of the filters
* @global array $wp_current_filter Stores the list of current filters with the current one last
*
* @param string $tag The name of the filter hook.
* @param array $args The arguments supplied to the functions hooked to $tag.
* @return mixed The filtered value after all hooked functions are applied to it.
*/
function apply_filters_ref_array( $tag, $args ) {
global $wp_filter, $wp_current_filter;
// Do 'all' actions first
if ( isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $tag;
$all_args = func_get_args();
_wp_call_all_hook( $all_args );
}
if ( ! isset( $wp_filter[ $tag ] ) ) {
if ( isset( $wp_filter['all'] ) ) {
array_pop( $wp_current_filter );
}
return $args[0];
}
if ( ! isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $tag;
}
$filtered = $wp_filter[ $tag ]->apply_filters( $args[0], $args );
array_pop( $wp_current_filter );
return $filtered;
}
/**
* Removes a function from a specified filter hook.
*
* This function removes a function attached to a specified filter hook. This
* method can be used to remove default functions attached to a specific filter
* hook and possibly replace them with a substitute.
*
* To remove a hook, the $function_to_remove and $priority arguments must match
* when the hook was added. This goes for both filters and actions. No warning
* will be given on removal failure.
*
* @since 1.2.0
*
* @global array $wp_filter Stores all of the filters
*
* @param string $tag The filter hook to which the function to be removed is hooked.
* @param callable $function_to_remove The name of the function which should be removed.
* @param int $priority Optional. The priority of the function. Default 10.
* @return bool Whether the function existed before it was removed.
*/
function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
global $wp_filter;
$r = false;
if ( isset( $wp_filter[ $tag ] ) ) {
$r = $wp_filter[ $tag ]->remove_filter( $tag, $function_to_remove, $priority );
if ( ! $wp_filter[ $tag ]->callbacks ) {
unset( $wp_filter[ $tag ] );
}
}
return $r;
}
/**
* Remove all of the hooks from a filter.
*
* @since 2.7.0
*
* @global array $wp_filter Stores all of the filters
*
* @param string $tag The filter to remove hooks from.
* @param int|bool $priority Optional. The priority number to remove. Default false.
* @return true True when finished.
*/
function remove_all_filters( $tag, $priority = false ) {
global $wp_filter;
if ( isset( $wp_filter[ $tag ] ) ) {
$wp_filter[ $tag ]->remove_all_filters( $priority );
if ( ! $wp_filter[ $tag ]->has_filters() ) {
unset( $wp_filter[ $tag ] );
}
}
return true;
}
/**
* Retrieve the name of the current filter or action.
*
* @since 2.5.0
*
* @global array $wp_current_filter Stores the list of current filters with the current one last
*
* @return string Hook name of the current filter or action.
*/
function current_filter() {
global $wp_current_filter;
return end( $wp_current_filter );
}
/**
* Retrieve the name of the current action.
*
* @since 3.9.0
*
* @return string Hook name of the current action.
*/
function current_action() {
return current_filter();
}
/**
* Retrieve the name of a filter currently being processed.
*
* The function current_filter() only returns the most recent filter or action
* being executed. did_action() returns true once the action is initially
* processed.
*
* This function allows detection for any filter currently being
* executed (despite not being the most recent filter to fire, in the case of
* hooks called from hook callbacks) to be verified.
*
* @since 3.9.0
*
* @see current_filter()
* @see did_action()
* @global array $wp_current_filter Current filter.
*
* @param null|string $filter Optional. Filter to check. Defaults to null, which
* checks if any filter is currently being run.
* @return bool Whether the filter is currently in the stack.
*/
function doing_filter( $filter = null ) {
global $wp_current_filter;
if ( null === $filter ) {
return ! empty( $wp_current_filter );
}
return in_array( $filter, $wp_current_filter );
}
/**
* Retrieve the name of an action currently being processed.
*
* @since 3.9.0
*
* @param string|null $action Optional. Action to check. Defaults to null, which checks
* if any action is currently being run.
* @return bool Whether the action is currently in the stack.
*/
function doing_action( $action = null ) {
return doing_filter( $action );
}
/**
* Hooks a function on to a specific action.
*
* Actions are the hooks that the WordPress core launches at specific points
* during execution, or when specific events occur. Plugins can specify that
* one or more of its PHP functions are executed at these points, using the
* Action API.
*
* @since 1.2.0
*
* @param string $tag The name of the action to which the $function_to_add is hooked.
* @param callable $function_to_add The name of the function you wish to be called.
* @param int $priority Optional. Used to specify the order in which the functions
* associated with a particular action are executed. Default 10.
* Lower numbers correspond with earlier execution,
* and functions with the same priority are executed
* in the order in which they were added to the action.
* @param int $accepted_args Optional. The number of arguments the function accepts. Default 1.
* @return true Will always return true.
*/
function add_action( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
return add_filter( $tag, $function_to_add, $priority, $accepted_args );
}
/**
* Execute functions hooked on a specific action hook.
*
* This function invokes all functions attached to action hook `$tag`. It is
* possible to create new action hooks by simply calling this function,
* specifying the name of the new hook using the `$tag` parameter.
*
* You can pass extra arguments to the hooks, much like you can with apply_filters().
*
* @since 1.2.0
*
* @global array $wp_filter Stores all of the filters
* @global array $wp_actions Increments the amount of times action was triggered.
* @global array $wp_current_filter Stores the list of current filters with the current one last
*
* @param string $tag The name of the action to be executed.
* @param mixed $arg,... Optional. Additional arguments which are passed on to the
* functions hooked to the action. Default empty.
*/
function do_action( $tag, $arg = '' ) {
global $wp_filter, $wp_actions, $wp_current_filter;
if ( ! isset( $wp_actions[ $tag ] ) ) {
$wp_actions[ $tag ] = 1;
} else {
++$wp_actions[ $tag ];
}
// Do 'all' actions first
if ( isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $tag;
$all_args = func_get_args();
_wp_call_all_hook( $all_args );
}
if ( ! isset( $wp_filter[ $tag ] ) ) {
if ( isset( $wp_filter['all'] ) ) {
array_pop( $wp_current_filter );
}
return;
}
if ( ! isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $tag;
}
$args = array();
if ( is_array( $arg ) && 1 == count( $arg ) && isset( $arg[0] ) && is_object( $arg[0] ) ) { // array(&$this)
$args[] =& $arg[0];
} else {
$args[] = $arg;
}
for ( $a = 2, $num = func_num_args(); $a < $num; $a++ ) {
$args[] = func_get_arg( $a );
}
$wp_filter[ $tag ]->do_action( $args );
array_pop( $wp_current_filter );
}
/**
* Retrieve the number of times an action is fired.
*
* @since 2.1.0
*
* @global array $wp_actions Increments the amount of times action was triggered.
*
* @param string $tag The name of the action hook.
* @return int The number of times action hook $tag is fired.
*/
function did_action( $tag ) {
global $wp_actions;
if ( ! isset( $wp_actions[ $tag ] ) ) {
return 0;
}
return $wp_actions[ $tag ];
}
/**
* Execute functions hooked on a specific action hook, specifying arguments in an array.
*
* @since 2.1.0
*
* @see do_action() This function is identical, but the arguments passed to the
* functions hooked to $tag< are supplied using an array.
* @global array $wp_filter Stores all of the filters
* @global array $wp_actions Increments the amount of times action was triggered.
* @global array $wp_current_filter Stores the list of current filters with the current one last
*
* @param string $tag The name of the action to be executed.
* @param array $args The arguments supplied to the functions hooked to `$tag`.
*/
function do_action_ref_array( $tag, $args ) {
global $wp_filter, $wp_actions, $wp_current_filter;
if ( ! isset( $wp_actions[ $tag ] ) ) {
$wp_actions[ $tag ] = 1;
} else {
++$wp_actions[ $tag ];
}
// Do 'all' actions first
if ( isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $tag;
$all_args = func_get_args();
_wp_call_all_hook( $all_args );
}
if ( ! isset( $wp_filter[ $tag ] ) ) {
if ( isset( $wp_filter['all'] ) ) {
array_pop( $wp_current_filter );
}
return;
}
if ( ! isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $tag;
}
$wp_filter[ $tag ]->do_action( $args );
array_pop( $wp_current_filter );
}
/**
* Check if any action has been registered for a hook.
*
* @since 2.5.0
*
* @see has_filter() has_action() is an alias of has_filter().
*
* @param string $tag The name of the action hook.
* @param callable|bool $function_to_check Optional. The callback to check for. Default false.
* @return bool|int If $function_to_check is omitted, returns boolean for whether the hook has
* anything registered. When checking a specific function, the priority of that
* hook is returned, or false if the function is not attached. When using the
* $function_to_check argument, this function may return a non-boolean value
* that evaluates to false (e.g.) 0, so use the === operator for testing the
* return value.
*/
function has_action( $tag, $function_to_check = false ) {
return has_filter( $tag, $function_to_check );
}
/**
* Removes a function from a specified action hook.
*
* This function removes a function attached to a specified action hook. This
* method can be used to remove default functions attached to a specific filter
* hook and possibly replace them with a substitute.
*
* @since 1.2.0
*
* @param string $tag The action hook to which the function to be removed is hooked.
* @param callable $function_to_remove The name of the function which should be removed.
* @param int $priority Optional. The priority of the function. Default 10.
* @return bool Whether the function is removed.
*/
function remove_action( $tag, $function_to_remove, $priority = 10 ) {
return remove_filter( $tag, $function_to_remove, $priority );
}
/**
* Remove all of the hooks from an action.
*
* @since 2.7.0
*
* @param string $tag The action to remove hooks from.
* @param int|bool $priority The priority number to remove them from. Default false.
* @return true True when finished.
*/
function remove_all_actions( $tag, $priority = false ) {
return remove_all_filters( $tag, $priority );
}
/**
* Fires functions attached to a deprecated filter hook.
*
* When a filter hook is deprecated, the apply_filters() call is replaced with
* apply_filters_deprecated(), which triggers a deprecation notice and then fires
* the original filter hook.
*
* Note: the value and extra arguments passed to the original apply_filters() call
* must be passed here to `$args` as an array. For example:
*
* // Old filter.
* return apply_filters( 'wpdocs_filter', $value, $extra_arg );
*
* // Deprecated.
* return apply_filters_deprecated( 'wpdocs_filter', array( $value, $extra_arg ), '4.9', 'wpdocs_new_filter' );
*
* @since 4.6.0
*
* @see _deprecated_hook()
*
* @param string $tag The name of the filter hook.
* @param array $args Array of additional function arguments to be passed to apply_filters().
* @param string $version The version of WordPress that deprecated the hook.
* @param string $replacement Optional. The hook that should have been used. Default false.
* @param string $message Optional. A message regarding the change. Default null.
*/
function apply_filters_deprecated( $tag, $args, $version, $replacement = false, $message = null ) {
if ( ! has_filter( $tag ) ) {
return $args[0];
}
_deprecated_hook( $tag, $version, $replacement, $message );
return apply_filters_ref_array( $tag, $args );
}
/**
* Fires functions attached to a deprecated action hook.
*
* When an action hook is deprecated, the do_action() call is replaced with
* do_action_deprecated(), which triggers a deprecation notice and then fires
* the original hook.
*
* @since 4.6.0
*
* @see _deprecated_hook()
*
* @param string $tag The name of the action hook.
* @param array $args Array of additional function arguments to be passed to do_action().
* @param string $version The version of WordPress that deprecated the hook.
* @param string $replacement Optional. The hook that should have been used.
* @param string $message Optional. A message regarding the change.
*/
function do_action_deprecated( $tag, $args, $version, $replacement = false, $message = null ) {
if ( ! has_action( $tag ) ) {
return;
}
_deprecated_hook( $tag, $version, $replacement, $message );
do_action_ref_array( $tag, $args );
}
//
// Functions for handling plugins.
//
/**
* Gets the basename of a plugin.
*
* This method extracts the name of a plugin from its filename.
*
* @since 1.5.0
*
* @global array $wp_plugin_paths
*
* @param string $file The filename of plugin.
* @return string The name of a plugin.
*/
function plugin_basename( $file ) {
global $wp_plugin_paths;
// $wp_plugin_paths contains normalized paths.
$file = wp_normalize_path( (realpath($file) ? realpath($file) : $file) );
$plugin_dir = wp_normalize_path( (realpath(WP_PLUGIN_DIR) ? realpath(WP_PLUGIN_DIR) : WP_PLUGIN_DIR) );
$sp_plugin_dir = wp_normalize_path( (realpath(SP_PLUGIN_DIR) ? realpath(SP_PLUGIN_DIR) : SP_PLUGIN_DIR) );
$mu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR );
$file = preg_replace( '#^' . preg_quote( $plugin_dir, '#' ) . '/|^' . preg_quote( $sp_plugin_dir, '#' ) . '/|^' . preg_quote( $mu_plugin_dir, '#' ) . '/#', '', $file ); // get relative path from plugins dir
$file = trim( $file, '/' );
return $file;
}
/**
* Register a plugin's real path.
*
* This is used in plugin_basename() to resolve symlinked paths.
*
* @since 3.9.0
*
* @see wp_normalize_path()
*
* @global array $wp_plugin_paths
*
* @staticvar string $wp_plugin_path
* @staticvar string $wpmu_plugin_path
*
* @param string $file Known path to the file.
* @return bool Whether the path was able to be registered.
*/
function wp_register_plugin_realpath( $file ) {
global $wp_plugin_paths;
// Normalize, but store as static to avoid recalculation of a constant value
static $wp_plugin_path = null, $wpmu_plugin_path = null;
if ( ! isset( $wp_plugin_path ) ) {
$wp_plugin_path = wp_normalize_path( WP_PLUGIN_DIR );
$wpmu_plugin_path = wp_normalize_path( WPMU_PLUGIN_DIR );
}
$plugin_path = wp_normalize_path( dirname( $file ) );
$plugin_realpath = wp_normalize_path( dirname( realpath( $file ) ) );
if ( $plugin_path === $wp_plugin_path || $plugin_path === $wpmu_plugin_path ) {
return false;
}
if ( $plugin_path !== $plugin_realpath ) {
$wp_plugin_paths[ $plugin_path ] = $plugin_realpath;
}
return true;
}
/**
* Get the filesystem directory path (with trailing slash) for the plugin __FILE__ passed in.
*
* @since 2.8.0
*
* @param string $file The filename of the plugin (__FILE__).
* @return string the filesystem path of the directory that contains the plugin.
*/
function plugin_dir_path( $file ) {
return trailingslashit( dirname( $file ) );
}
/**
* Get the URL directory path (with trailing slash) for the plugin __FILE__ passed in.
*
* @since 2.8.0
*
* @param string $file The filename of the plugin (__FILE__).
* @return string the URL path of the directory that contains the plugin.
*/
function plugin_dir_url( $file ) {
return trailingslashit( plugins_url( '', $file ) );
}
/**
* Set the activation hook for a plugin.
*
* When a plugin is activated, the action 'activate_PLUGINNAME' hook is
* called. In the name of this hook, PLUGINNAME is replaced with the name
* of the plugin, including the optional subdirectory. For example, when the
* plugin is located in wp-content/plugins/sampleplugin/sample.php, then
* the name of this hook will become 'activate_sampleplugin/sample.php'.
*
* When the plugin consists of only one file and is (as by default) located at
* wp-content/plugins/sample.php the name of this hook will be
* 'activate_sample.php'.
*
* @since 2.0.0
*
* @param string $file The filename of the plugin including the path.
* @param callable $function The function hooked to the 'activate_PLUGIN' action.
*/
function register_activation_hook( $file, $function ) {
$file = plugin_basename( $file );
add_action( 'activate_' . $file, $function );
}
/**
* Set the deactivation hook for a plugin.
*
* When a plugin is deactivated, the action 'deactivate_PLUGINNAME' hook is
* called. In the name of this hook, PLUGINNAME is replaced with the name
* of the plugin, including the optional subdirectory. For example, when the
* plugin is located in wp-content/plugins/sampleplugin/sample.php, then
* the name of this hook will become 'deactivate_sampleplugin/sample.php'.
*
* When the plugin consists of only one file and is (as by default) located at
* wp-content/plugins/sample.php the name of this hook will be
* 'deactivate_sample.php'.
*
* @since 2.0.0
*
* @param string $file The filename of the plugin including the path.
* @param callable $function The function hooked to the 'deactivate_PLUGIN' action.
*/
function register_deactivation_hook( $file, $function ) {
$file = plugin_basename( $file );
add_action( 'deactivate_' . $file, $function );
}
/**
* Set the uninstallation hook for a plugin.
*
* Registers the uninstall hook that will be called when the user clicks on the
* uninstall link that calls for the plugin to uninstall itself. The link won't
* be active unless the plugin hooks into the action.
*
* The plugin should not run arbitrary code outside of functions, when
* registering the uninstall hook. In order to run using the hook, the plugin
* will have to be included, which means that any code laying outside of a
* function will be run during the uninstallation process. The plugin should not
* hinder the uninstallation process.
*
* If the plugin can not be written without running code within the plugin, then
* the plugin should create a file named 'uninstall.php' in the base plugin
* folder. This file will be called, if it exists, during the uninstallation process
* bypassing the uninstall hook. The plugin, when using the 'uninstall.php'
* should always check for the 'WP_UNINSTALL_PLUGIN' constant, before
* executing.
*
* @since 2.7.0
*
* @param string $file Plugin file.
* @param callable $callback The callback to run when the hook is called. Must be
* a static method or function.
*/
function register_uninstall_hook( $file, $callback ) {
if ( is_array( $callback ) && is_object( $callback[0] ) ) {
_doing_it_wrong( __FUNCTION__, __( 'Only a static class method or function can be used in an uninstall hook.' ), '3.1.0' );
return;
}
/*
* The option should not be autoloaded, because it is not needed in most
* cases. Emphasis should be put on using the 'uninstall.php' way of
* uninstalling the plugin.
*/
$uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
$uninstallable_plugins[ plugin_basename( $file ) ] = $callback;
update_option( 'uninstall_plugins', $uninstallable_plugins );
}
/**
* Call the 'all' hook, which will process the functions hooked into it.
*
* The 'all' hook passes all of the arguments or parameters that were used for
* the hook, which this function was called for.
*
* This function is used internally for apply_filters(), do_action(), and
* do_action_ref_array() and is not meant to be used from outside those
* functions. This function does not check for the existence of the all hook, so
* it will fail unless the all hook exists prior to this function call.
*
* @since 2.5.0
* @access private
*
* @global array $wp_filter Stores all of the filters
*
* @param array $args The collected parameters from the hook that was called.
*/
function _wp_call_all_hook( $args ) {
global $wp_filter;
$wp_filter['all']->do_all_hook( $args );
}
/**
* Build Unique ID for storage and retrieval.
*
* The old way to serialize the callback caused issues and this function is the
* solution. It works by checking for objects and creating a new property in
* the class to keep track of the object and new objects of the same class that
* need to be added.
*
* It also allows for the removal of actions and filters for objects after they
* change class properties. It is possible to include the property $wp_filter_id
* in your class and set it to "null" or a number to bypass the workaround.
* However this will prevent you from adding new classes and any new classes
* will overwrite the previous hook by the same class.
*
* Functions and static method callbacks are just returned as strings and
* shouldn't have any speed penalty.
*
* @link https://core.trac.wordpress.org/ticket/3875
*
* @since 2.2.3
* @access private
*
* @global array $wp_filter Storage for all of the filters and actions.
* @staticvar int $filter_id_count
*
* @param string $tag Used in counting how many hooks were applied
* @param callable $function Used for creating unique id
* @param int|bool $priority Used in counting how many hooks were applied. If === false
* and $function is an object reference, we return the unique
* id only if it already has one, false otherwise.
* @return string|false Unique ID for usage as array key or false if $priority === false
* and $function is an object reference, and it does not already have
* a unique id.
*/
function _wp_filter_build_unique_id( $tag, $function, $priority ) {
global $wp_filter;
static $filter_id_count = 0;
if ( is_string( $function ) ) {
return $function;
}
if ( is_object( $function ) ) {
// Closures are currently implemented as objects
$function = array( $function, '' );
} else {
$function = (array) $function;
}
if ( is_object( $function[0] ) ) {
// Object Class Calling
if ( function_exists( 'spl_object_hash' ) ) {
return spl_object_hash( $function[0] ) . $function[1];
} else {
$obj_idx = get_class( $function[0] ) . $function[1];
if ( ! isset( $function[0]->wp_filter_id ) ) {
if ( false === $priority ) {
return false;
}
$obj_idx .= isset( $wp_filter[ $tag ][ $priority ] ) ? count( (array) $wp_filter[ $tag ][ $priority ] ) : $filter_id_count;
$function[0]->wp_filter_id = $filter_id_count;
++$filter_id_count;
} else {
$obj_idx .= $function[0]->wp_filter_id;
}
return $obj_idx;
}
} elseif ( is_string( $function[0] ) ) {
// Static Calling
return $function[0] . '::' . $function[1];
}
}
<?php
/**
* Atom Feed Template for displaying Atom Comments feed.
*
* @package WordPress
*/
header( 'Content-Type: ' . feed_content_type( 'atom' ) . '; charset=' . get_option( 'blog_charset' ), true );
echo '<?xml version="1.0" encoding="' . get_option( 'blog_charset' ) . '" ?' . '>';
/** This action is documented in site-inc/feed-rss2.php */
do_action( 'rss_tag_pre', 'atom-comments' );
?>
<feed
xmlns="http://www.w3.org/2005/Atom"
xml:lang="<?php bloginfo_rss( 'language' ); ?>"
xmlns:thr="http://purl.org/syndication/thread/1.0"
<?php
/** This action is documented in site-inc/feed-atom.php */
do_action( 'atom_ns' );
/**
* Fires inside the feed tag in the Atom comment feed.
*
* @since 2.8.0
*/
do_action( 'atom_comments_ns' );
?>
>
<title type="text">
<?php
if ( is_singular() ) {
/* translators: Comments feed title. %s: Post title */
printf( ent2ncr( __( 'Comments on %s' ) ), get_the_title_rss() );
} elseif ( is_search() ) {
/* translators: Comments feed title. 1: Site name, 2: Search query */
printf( ent2ncr( __( 'Comments for %1$s searching on %2$s' ) ), get_bloginfo_rss( 'name' ), get_search_query() );
} else {
/* translators: Comments feed title. %s: Site name */
printf( ent2ncr( __( 'Comments for %s' ) ), get_wp_title_rss() );
}
?>
</title>
<subtitle type="text"><?php bloginfo_rss( 'description' ); ?></subtitle>
<updated>
<?php
$date = get_lastcommentmodified( 'GMT' );
echo $date ? mysql2date( 'Y-m-d\TH:i:s\Z', $date, false ) : date( 'Y-m-d\TH:i:s\Z' );
?>
</updated>
<?php if ( is_singular() ) { ?>
<link rel="alternate" type="<?php bloginfo_rss( 'html_type' ); ?>" href="<?php comments_link_feed(); ?>" />
<link rel="self" type="application/atom+xml" href="<?php echo esc_url( get_post_comments_feed_link( '', 'atom' ) ); ?>" />
<id><?php echo esc_url( get_post_comments_feed_link( '', 'atom' ) ); ?></id>
<?php } elseif ( is_search() ) { ?>
<link rel="alternate" type="<?php bloginfo_rss( 'html_type' ); ?>" href="<?php echo home_url() . '?s=' . get_search_query(); ?>" />
<link rel="self" type="application/atom+xml" href="<?php echo get_search_comments_feed_link( '', 'atom' ); ?>" />
<id><?php echo get_search_comments_feed_link( '', 'atom' ); ?></id>
<?php } else { ?>
<link rel="alternate" type="<?php bloginfo_rss( 'html_type' ); ?>" href="<?php bloginfo_rss( 'url' ); ?>" />
<link rel="self" type="application/atom+xml" href="<?php bloginfo_rss( 'comments_atom_url' ); ?>" />
<id><?php bloginfo_rss( 'comments_atom_url' ); ?></id>
<?php } ?>
<?php
/**
* Fires at the end of the Atom comment feed header.
*
* @since 2.8.0
*/
do_action( 'comments_atom_head' );
?>
<?php
if ( have_comments() ) :
while ( have_comments() ) :
the_comment();
$comment_post = $GLOBALS['post'] = get_post( $comment->comment_post_ID );
?>
<entry>
<title>
<?php
if ( ! is_singular() ) {
$title = get_the_title( $comment_post->ID );
/** This filter is documented in site-inc/feed.php */
$title = apply_filters( 'the_title_rss', $title );
/* translators: Individual comment title. 1: Post title, 2: Comment author name */
printf( ent2ncr( __( 'Comment on %1$s by %2$s' ) ), $title, get_comment_author_rss() );
} else {
/* translators: Comment author title. %s: Comment author name */
printf( ent2ncr( __( 'By: %s' ) ), get_comment_author_rss() );
}
?>
</title>
<link rel="alternate" href="<?php comment_link(); ?>" type="<?php bloginfo_rss( 'html_type' ); ?>" />
<author>
<name><?php comment_author_rss(); ?></name>
<?php
if ( get_comment_author_url() ) {
echo '<uri>' . get_comment_author_url() . '</uri>';}
?>
</author>
<id><?php comment_guid(); ?></id>
<updated><?php echo mysql2date( 'Y-m-d\TH:i:s\Z', get_comment_time( 'Y-m-d H:i:s', true, false ), false ); ?></updated>
<published><?php echo mysql2date( 'Y-m-d\TH:i:s\Z', get_comment_time( 'Y-m-d H:i:s', true, false ), false ); ?></published>
<?php if ( post_password_required( $comment_post ) ) : ?>
<content type="html" xml:base="<?php comment_link(); ?>"><![CDATA[<?php echo get_the_password_form(); ?>]]></content>
<?php else : // post pass ?>
<content type="html" xml:base="<?php comment_link(); ?>"><![CDATA[<?php comment_text(); ?>]]></content>
<?php
endif; // post pass
// Return comment threading information (https://www.ietf.org/rfc/rfc4685.txt)
if ( $comment->comment_parent == 0 ) : // This comment is top level
?>
<thr:in-reply-to ref="<?php the_guid(); ?>" href="<?php the_permalink_rss(); ?>" type="<?php bloginfo_rss( 'html_type' ); ?>" />
<?php
else : // This comment is in reply to another comment
$parent_comment = get_comment( $comment->comment_parent );
// The rel attribute below and the id tag above should be GUIDs, but WP doesn't create them for comments (unlike posts). Either way, it's more important that they both use the same system
?>
<thr:in-reply-to ref="<?php comment_guid( $parent_comment ); ?>" href="<?php echo get_comment_link( $parent_comment ); ?>" type="<?php bloginfo_rss( 'html_type' ); ?>" />
<?php
endif;
/**
* Fires at the end of each Atom comment feed item.
*
* @since 2.2.0
*
* @param int $comment_id ID of the current comment.
* @param int $comment_post_id ID of the post the current comment is connected to.
*/
do_action( 'comment_atom_entry', $comment->comment_ID, $comment_post->ID );
?>
</entry>
<?php
endwhile;
endif;
?>
</feed>
<?php
/**
* Option API
*
* @package WordPress
* @subpackage Option
*/
/**
* Retrieves an option value based on an option name.
*
* If the option does not exist or does not have a value, then the return value
* will be false. This is useful to check whether you need to install an option
* and is commonly used during installation of plugin options and to test
* whether upgrading is required.
*
* If the option was serialized then it will be unserialized when it is returned.
*
* Any scalar values will be returned as strings. You may coerce the return type of
* a given option by registering an {@see 'option_$option'} filter callback.
*
* @since 1.5.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $option Name of option to retrieve. Expected to not be SQL-escaped.
* @param mixed $default Optional. Default value to return if the option does not exist.
* @return mixed Value set for the option.
*/
function get_option( $option, $default = false ) {
global $wpdb;
$option = trim( $option );
if ( empty( $option ) ) {
return false;
}
/**
* Filters the value of an existing option before it is retrieved.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* Passing a truthy value to the filter will short-circuit retrieving
* the option value, returning the passed value instead.
*
* @since 1.5.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.9.0 The `$default` parameter was added.
*
* @param bool|mixed $pre_option The value to return instead of the option value. This differs from
* `$default`, which is used as the fallback value in the event the option
* doesn't exist elsewhere in get_option(). Default false (to skip past the
* short-circuit).
* @param string $option Option name.
* @param mixed $default The fallback value to return if the option does not exist.
* Default is false.
*/
$pre = apply_filters( "pre_option_{$option}", false, $option, $default );
if ( false !== $pre ) {
return $pre;
}
if ( defined( 'WP_SETUP_CONFIG' ) ) {
return false;
}
// Distinguish between `false` as a default, and not passing one.
$passed_default = func_num_args() > 1;
if ( ! wp_installing() ) {
// prevent non-existent options from triggering multiple queries
$notoptions = wp_cache_get( 'notoptions', 'options' );
if ( isset( $notoptions[ $option ] ) ) {
/**
* Filters the default value for an option.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 3.4.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$passed_default` parameter was added to distinguish between a `false` value and the default parameter value.
*
* @param mixed $default The default value to return if the option does not exist
* in the database.
* @param string $option Option name.
* @param bool $passed_default Was `get_option()` passed a default value?
*/
return apply_filters( "default_option_{$option}", $default, $option, $passed_default );
}
$alloptions = wp_load_alloptions();
if ( isset( $alloptions[ $option ] ) ) {
$value = $alloptions[ $option ];
} else {
$value = wp_cache_get( $option, 'options' );
if ( false === $value ) {
$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );
// Has to be get_row instead of get_var because of funkiness with 0, false, null values
if ( is_object( $row ) ) {
$value = $row->option_value;
wp_cache_add( $option, $value, 'options' );
} else { // option does not exist, so we must cache its non-existence
if ( ! is_array( $notoptions ) ) {
$notoptions = array();
}
$notoptions[ $option ] = true;
wp_cache_set( 'notoptions', $notoptions, 'options' );
/** This filter is documented in site-inc/option.php */
return apply_filters( "default_option_{$option}", $default, $option, $passed_default );
}
}
}
} else {
$suppress = $wpdb->suppress_errors();
$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );
$wpdb->suppress_errors( $suppress );
if ( is_object( $row ) ) {
$value = $row->option_value;
} else {
/** This filter is documented in site-inc/option.php */
return apply_filters( "default_option_{$option}", $default, $option, $passed_default );
}
}
// If home is not set use siteurl.
if ( 'home' == $option && '' == $value ) {
return get_option( 'siteurl' );
}
if ( in_array( $option, array( 'siteurl', 'home', 'category_base', 'tag_base' ) ) ) {
$value = untrailingslashit( $value );
}
/**
* Filters the value of an existing option.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 1.5.0 As 'option_' . $setting
* @since 3.0.0
* @since 4.4.0 The `$option` parameter was added.
*
* @param mixed $value Value of the option. If stored serialized, it will be
* unserialized prior to being returned.
* @param string $option Option name.
*/
return apply_filters( "option_{$option}", maybe_unserialize( $value ), $option );
}
/**
* Protect WordPress special option from being modified.
*
* Will die if $option is in protected list. Protected options are 'alloptions'
* and 'notoptions' options.
*
* @since 2.2.0
*
* @param string $option Option name.
*/
function wp_protect_special_option( $option ) {
if ( 'alloptions' === $option || 'notoptions' === $option ) {
wp_die( sprintf( __( '%s is a protected WP option and may not be modified' ), esc_html( $option ) ) );
}
}
/**
* Print option value after sanitizing for forms.
*
* @since 1.5.0
*
* @param string $option Option name.
*/
function form_option( $option ) {
echo esc_attr( get_option( $option ) );
}
/**
* Loads and caches all autoloaded options, if available or all options.
*
* @since 2.2.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @return array List of all options.
*/
function wp_load_alloptions() {
global $wpdb;
if ( ! wp_installing() || ! is_multisite() ) {
$alloptions = wp_cache_get( 'alloptions', 'options' );
} else {
$alloptions = false;
}
if ( ! $alloptions ) {
$suppress = $wpdb->suppress_errors();
if ( ! $alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE autoload = 'yes'" ) ) {
$alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options" );
}
$wpdb->suppress_errors( $suppress );
$alloptions = array();
foreach ( (array) $alloptions_db as $o ) {
$alloptions[ $o->option_name ] = $o->option_value;
}
if ( ! wp_installing() || ! is_multisite() ) {
/**
* Filters all options before caching them.
*
* @since 4.9.0
*
* @param array $alloptions Array with all options.
*/
$alloptions = apply_filters( 'pre_cache_alloptions', $alloptions );
wp_cache_add( 'alloptions', $alloptions, 'options' );
}
}
/**
* Filters all options after retrieving them.
*
* @since 4.9.0
*
* @param array $alloptions Array with all options.
*/
return apply_filters( 'alloptions', $alloptions );
}
/**
* Loads and caches certain often requested site options if is_multisite() and a persistent cache is not being used.
*
* @since 3.0.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param int $network_id Optional site ID for which to query the options. Defaults to the current site.
*/
function wp_load_core_site_options( $network_id = null ) {
global $wpdb;
if ( ! is_multisite() || wp_using_ext_object_cache() || wp_installing() ) {
return;
}
if ( empty( $network_id ) ) {
$network_id = get_current_network_id();
}
$core_options = array( 'site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled', 'ms_files_rewriting' );
$core_options_in = "'" . implode( "', '", $core_options ) . "'";
$options = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM $wpdb->sitemeta WHERE meta_key IN ($core_options_in) AND site_id = %d", $network_id ) );
foreach ( $options as $option ) {
$key = $option->meta_key;
$cache_key = "{$network_id}:$key";
$option->meta_value = maybe_unserialize( $option->meta_value );
wp_cache_set( $cache_key, $option->meta_value, 'site-options' );
}
}
/**
* Update the value of an option that was already added.
*
* You do not need to serialize values. If the value needs to be serialized, then
* it will be serialized before it is inserted into the database. Remember,
* resources can not be serialized or added as an option.
*
* If the option does not exist, then the option will be added with the option value,
* with an `$autoload` value of 'yes'.
*
* @since 1.0.0
* @since 4.2.0 The `$autoload` parameter was added.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $option Option name. Expected to not be SQL-escaped.
* @param mixed $value Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
* @param string|bool $autoload Optional. Whether to load the option when WordPress starts up. For existing options,
* `$autoload` can only be updated using `update_option()` if `$value` is also changed.
* Accepts 'yes'|true to enable or 'no'|false to disable. For non-existent options,
* the default value is 'yes'. Default null.
* @return bool False if value was not updated and true if value was updated.
*/
function update_option( $option, $value, $autoload = null ) {
global $wpdb;
$option = trim( $option );
if ( empty( $option ) ) {
return false;
}
wp_protect_special_option( $option );
if ( is_object( $value ) ) {
$value = clone $value;
}
$value = sanitize_option( $option, $value );
$old_value = get_option( $option );
/**
* Filters a specific option before its value is (maybe) serialized and updated.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 2.6.0
* @since 4.4.0 The `$option` parameter was added.
*
* @param mixed $value The new, unserialized option value.
* @param mixed $old_value The old option value.
* @param string $option Option name.
*/
$value = apply_filters( "pre_update_option_{$option}", $value, $old_value, $option );
/**
* Filters an option before its value is (maybe) serialized and updated.
*
* @since 3.9.0
*
* @param mixed $value The new, unserialized option value.
* @param string $option Name of the option.
* @param mixed $old_value The old option value.
*/
$value = apply_filters( 'pre_update_option', $value, $option, $old_value );
/*
* If the new and old values are the same, no need to update.
*
* Unserialized values will be adequate in most cases. If the unserialized
* data differs, the (maybe) serialized data is checked to avoid
* unnecessary database calls for otherwise identical object instances.
*
* See https://core.trac.wordpress.org/ticket/38903
*/
if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) {
return false;
}
/** This filter is documented in site-inc/option.php */
if ( apply_filters( "default_option_{$option}", false, $option, false ) === $old_value ) {
// Default setting for new options is 'yes'.
if ( null === $autoload ) {
$autoload = 'yes';
}
return add_option( $option, $value, '', $autoload );
}
$serialized_value = maybe_serialize( $value );
/**
* Fires immediately before an option value is updated.
*
* @since 2.9.0
*
* @param string $option Name of the option to update.
* @param mixed $old_value The old option value.
* @param mixed $value The new option value.
*/
do_action( 'update_option', $option, $old_value, $value );
$update_args = array(
'option_value' => $serialized_value,
);
if ( null !== $autoload ) {
$update_args['autoload'] = ( 'no' === $autoload || false === $autoload ) ? 'no' : 'yes';
}
$result = $wpdb->update( $wpdb->options, $update_args, array( 'option_name' => $option ) );
if ( ! $result ) {
return false;
}
$notoptions = wp_cache_get( 'notoptions', 'options' );
if ( is_array( $notoptions ) && isset( $notoptions[ $option ] ) ) {
unset( $notoptions[ $option ] );
wp_cache_set( 'notoptions', $notoptions, 'options' );
}
if ( ! wp_installing() ) {
$alloptions = wp_load_alloptions();
if ( isset( $alloptions[ $option ] ) ) {
$alloptions[ $option ] = $serialized_value;
wp_cache_set( 'alloptions', $alloptions, 'options' );
} else {
wp_cache_set( $option, $serialized_value, 'options' );
}
}
/**
* Fires after the value of a specific option has been successfully updated.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 2.0.1
* @since 4.4.0 The `$option` parameter was added.
*
* @param mixed $old_value The old option value.
* @param mixed $value The new option value.
* @param string $option Option name.
*/
do_action( "update_option_{$option}", $old_value, $value, $option );
/**
* Fires after the value of an option has been successfully updated.
*
* @since 2.9.0
*
* @param string $option Name of the updated option.
* @param mixed $old_value The old option value.
* @param mixed $value The new option value.
*/
do_action( 'updated_option', $option, $old_value, $value );
return true;
}
/**
* Add a new option.
*
* You do not need to serialize values. If the value needs to be serialized, then
* it will be serialized before it is inserted into the database. Remember,
* resources can not be serialized or added as an option.
*
* You can create options without values and then update the values later.
* Existing options will not be updated and checks are performed to ensure that you
* aren't adding a protected WordPress option. Care should be taken to not name
* options the same as the ones which are protected.
*
* @since 1.0.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $option Name of option to add. Expected to not be SQL-escaped.
* @param mixed $value Optional. Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
* @param string $deprecated Optional. Description. Not used anymore.
* @param string|bool $autoload Optional. Whether to load the option when WordPress starts up.
* Default is enabled. Accepts 'no' to disable for legacy reasons.
* @return bool False if option was not added and true if option was added.
*/
function add_option( $option, $value = '', $deprecated = '', $autoload = 'yes' ) {
global $wpdb;
if ( ! empty( $deprecated ) ) {
_deprecated_argument( __FUNCTION__, '2.3.0' );
}
$option = trim( $option );
if ( empty( $option ) ) {
return false;
}
wp_protect_special_option( $option );
if ( is_object( $value ) ) {
$value = clone $value;
}
$value = sanitize_option( $option, $value );
// Make sure the option doesn't already exist. We can check the 'notoptions' cache before we ask for a db query
$notoptions = wp_cache_get( 'notoptions', 'options' );
if ( ! is_array( $notoptions ) || ! isset( $notoptions[ $option ] ) ) {
/** This filter is documented in site-inc/option.php */
if ( apply_filters( "default_option_{$option}", false, $option, false ) !== get_option( $option ) ) {
return false;
}
}
$serialized_value = maybe_serialize( $value );
$autoload = ( 'no' === $autoload || false === $autoload ) ? 'no' : 'yes';
/**
* Fires before an option is added.
*
* @since 2.9.0
*
* @param string $option Name of the option to add.
* @param mixed $value Value of the option.
*/
do_action( 'add_option', $option, $value );
$result = $wpdb->query( $wpdb->prepare( "INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE `option_name` = VALUES(`option_name`), `option_value` = VALUES(`option_value`), `autoload` = VALUES(`autoload`)", $option, $serialized_value, $autoload ) );
if ( ! $result ) {
return false;
}
if ( ! wp_installing() ) {
if ( 'yes' == $autoload ) {
$alloptions = wp_load_alloptions();
$alloptions[ $option ] = $serialized_value;
wp_cache_set( 'alloptions', $alloptions, 'options' );
} else {
wp_cache_set( $option, $serialized_value, 'options' );
}
}
// This option exists now
$notoptions = wp_cache_get( 'notoptions', 'options' ); // yes, again... we need it to be fresh
if ( is_array( $notoptions ) && isset( $notoptions[ $option ] ) ) {
unset( $notoptions[ $option ] );
wp_cache_set( 'notoptions', $notoptions, 'options' );
}
/**
* Fires after a specific option has been added.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 2.5.0 As "add_option_{$name}"
* @since 3.0.0
*
* @param string $option Name of the option to add.
* @param mixed $value Value of the option.
*/
do_action( "add_option_{$option}", $option, $value );
/**
* Fires after an option has been added.
*
* @since 2.9.0
*
* @param string $option Name of the added option.
* @param mixed $value Value of the option.
*/
do_action( 'added_option', $option, $value );
return true;
}
/**
* Removes option by name. Prevents removal of protected WordPress options.
*
* @since 1.2.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $option Name of option to remove. Expected to not be SQL-escaped.
* @return bool True, if option is successfully deleted. False on failure.
*/
function delete_option( $option ) {
global $wpdb;
$option = trim( $option );
if ( empty( $option ) ) {
return false;
}
wp_protect_special_option( $option );
// Get the ID, if no ID then return
$row = $wpdb->get_row( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) );
if ( is_null( $row ) ) {
return false;
}
/**
* Fires immediately before an option is deleted.
*
* @since 2.9.0
*
* @param string $option Name of the option to delete.
*/
do_action( 'delete_option', $option );
$result = $wpdb->delete( $wpdb->options, array( 'option_name' => $option ) );
if ( ! wp_installing() ) {
if ( 'yes' == $row->autoload ) {
$alloptions = wp_load_alloptions();
if ( is_array( $alloptions ) && isset( $alloptions[ $option ] ) ) {
unset( $alloptions[ $option ] );
wp_cache_set( 'alloptions', $alloptions, 'options' );
}
} else {
wp_cache_delete( $option, 'options' );
}
}
if ( $result ) {
/**
* Fires after a specific option has been deleted.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 3.0.0
*
* @param string $option Name of the deleted option.
*/
do_action( "delete_option_{$option}", $option );
/**
* Fires after an option has been deleted.
*
* @since 2.9.0
*
* @param string $option Name of the deleted option.
*/
do_action( 'deleted_option', $option );
return true;
}
return false;
}
/**
* Delete a transient.
*
* @since 2.8.0
*
* @param string $transient Transient name. Expected to not be SQL-escaped.
* @return bool true if successful, false otherwise
*/
function delete_transient( $transient ) {
/**
* Fires immediately before a specific transient is deleted.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 3.0.0
*
* @param string $transient Transient name.
*/
do_action( "delete_transient_{$transient}", $transient );
if ( wp_using_ext_object_cache() ) {
$result = wp_cache_delete( $transient, 'transient' );
} else {
$option_timeout = '_transient_timeout_' . $transient;
$option = '_transient_' . $transient;
$result = delete_option( $option );
if ( $result ) {
delete_option( $option_timeout );
}
}
if ( $result ) {
/**
* Fires after a transient is deleted.
*
* @since 3.0.0
*
* @param string $transient Deleted transient name.
*/
do_action( 'deleted_transient', $transient );
}
return $result;
}
/**
* Get the value of a transient.
*
* If the transient does not exist, does not have a value, or has expired,
* then the return value will be false.
*
* @since 2.8.0
*
* @param string $transient Transient name. Expected to not be SQL-escaped.
* @return mixed Value of transient.
*/
function get_transient( $transient ) {
/**
* Filters the value of an existing transient.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* Passing a truthy value to the filter will effectively short-circuit retrieval
* of the transient, returning the passed value instead.
*
* @since 2.8.0
* @since 4.4.0 The `$transient` parameter was added
*
* @param mixed $pre_transient The default value to return if the transient does not exist.
* Any value other than false will short-circuit the retrieval
* of the transient, and return the returned value.
* @param string $transient Transient name.
*/
$pre = apply_filters( "pre_transient_{$transient}", false, $transient );
if ( false !== $pre ) {
return $pre;
}
if ( wp_using_ext_object_cache() ) {
$value = wp_cache_get( $transient, 'transient' );
} else {
$transient_option = '_transient_' . $transient;
if ( ! wp_installing() ) {
// If option is not in alloptions, it is not autoloaded and thus has a timeout
$alloptions = wp_load_alloptions();
if ( ! isset( $alloptions[ $transient_option ] ) ) {
$transient_timeout = '_transient_timeout_' . $transient;
$timeout = get_option( $transient_timeout );
if ( false !== $timeout && $timeout < time() ) {
delete_option( $transient_option );
delete_option( $transient_timeout );
$value = false;
}
}
}
if ( ! isset( $value ) ) {
$value = get_option( $transient_option );
}
}
/**
* Filters an existing transient's value.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 2.8.0
* @since 4.4.0 The `$transient` parameter was added
*
* @param mixed $value Value of transient.
* @param string $transient Transient name.
*/
return apply_filters( "transient_{$transient}", $value, $transient );
}
/**
* Set/update the value of a transient.
*
* You do not need to serialize values. If the value needs to be serialized, then
* it will be serialized before it is set.
*
* @since 2.8.0
*
* @param string $transient Transient name. Expected to not be SQL-escaped. Must be
* 172 characters or fewer in length.
* @param mixed $value Transient value. Must be serializable if non-scalar.
* Expected to not be SQL-escaped.
* @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration).
* @return bool False if value was not set and true if value was set.
*/
function set_transient( $transient, $value, $expiration = 0 ) {
$expiration = (int) $expiration;
/**
* Filters a specific transient before its value is set.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 3.0.0
* @since 4.2.0 The `$expiration` parameter was added.
* @since 4.4.0 The `$transient` parameter was added.
*
* @param mixed $value New value of transient.
* @param int $expiration Time until expiration in seconds.
* @param string $transient Transient name.
*/
$value = apply_filters( "pre_set_transient_{$transient}", $value, $expiration, $transient );
/**
* Filters the expiration for a transient before its value is set.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 4.4.0
*
* @param int $expiration Time until expiration in seconds. Use 0 for no expiration.
* @param mixed $value New value of transient.
* @param string $transient Transient name.
*/
$expiration = apply_filters( "expiration_of_transient_{$transient}", $expiration, $value, $transient );
if ( wp_using_ext_object_cache() ) {
$result = wp_cache_set( $transient, $value, 'transient', $expiration );
} else {
$transient_timeout = '_transient_timeout_' . $transient;
$transient_option = '_transient_' . $transient;
if ( false === get_option( $transient_option ) ) {
$autoload = 'yes';
if ( $expiration ) {
$autoload = 'no';
add_option( $transient_timeout, time() + $expiration, '', 'no' );
}
$result = add_option( $transient_option, $value, '', $autoload );
} else {
// If expiration is requested, but the transient has no timeout option,
// delete, then re-create transient rather than update.
$update = true;
if ( $expiration ) {
if ( false === get_option( $transient_timeout ) ) {
delete_option( $transient_option );
add_option( $transient_timeout, time() + $expiration, '', 'no' );
$result = add_option( $transient_option, $value, '', 'no' );
$update = false;
} else {
update_option( $transient_timeout, time() + $expiration );
}
}
if ( $update ) {
$result = update_option( $transient_option, $value );
}
}
}
if ( $result ) {
/**
* Fires after the value for a specific transient has been set.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 3.0.0
* @since 3.6.0 The `$value` and `$expiration` parameters were added.
* @since 4.4.0 The `$transient` parameter was added.
*
* @param mixed $value Transient value.
* @param int $expiration Time until expiration in seconds.
* @param string $transient The name of the transient.
*/
do_action( "set_transient_{$transient}", $value, $expiration, $transient );
/**
* Fires after the value for a transient has been set.
*
* @since 3.0.0
* @since 3.6.0 The `$value` and `$expiration` parameters were added.
*
* @param string $transient The name of the transient.
* @param mixed $value Transient value.
* @param int $expiration Time until expiration in seconds.
*/
do_action( 'setted_transient', $transient, $value, $expiration );
}
return $result;
}
/**
* Deletes all expired transients.
*
* The multi-table delete syntax is used to delete the transient record
* from table a, and the corresponding transient_timeout record from table b.
*
* @since 4.9.0
*
* @param bool $force_db Optional. Force cleanup to run against the database even when an external object cache is used.
*/
function delete_expired_transients( $force_db = false ) {
global $wpdb;
if ( ! $force_db && wp_using_ext_object_cache() ) {
return;
}
$wpdb->query(
$wpdb->prepare(
"DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
WHERE a.option_name LIKE %s
AND a.option_name NOT LIKE %s
AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) )
AND b.option_value < %d",
$wpdb->esc_like( '_transient_' ) . '%',
$wpdb->esc_like( '_transient_timeout_' ) . '%',
time()
)
);
if ( ! is_multisite() ) {
// non-Multisite stores site transients in the options table.
$wpdb->query(
$wpdb->prepare(
"DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
WHERE a.option_name LIKE %s
AND a.option_name NOT LIKE %s
AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) )
AND b.option_value < %d",
$wpdb->esc_like( '_site_transient_' ) . '%',
$wpdb->esc_like( '_site_transient_timeout_' ) . '%',
time()
)
);
} elseif ( is_multisite() && is_main_site() && is_main_network() ) {
// Multisite stores site transients in the sitemeta table.
$wpdb->query(
$wpdb->prepare(
"DELETE a, b FROM {$wpdb->sitemeta} a, {$wpdb->sitemeta} b
WHERE a.meta_key LIKE %s
AND a.meta_key NOT LIKE %s
AND b.meta_key = CONCAT( '_site_transient_timeout_', SUBSTRING( a.meta_key, 17 ) )
AND b.meta_value < %d",
$wpdb->esc_like( '_site_transient_' ) . '%',
$wpdb->esc_like( '_site_transient_timeout_' ) . '%',
time()
)
);
}
}
/**
* Saves and restores user interface settings stored in a cookie.
*
* Checks if the current user-settings cookie is updated and stores it. When no
* cookie exists (different browser used), adds the last saved cookie restoring
* the settings.
*
* @since 2.7.0
*/
function wp_user_settings() {
if ( ! is_admin() || wp_doing_ajax() ) {
return;
}
if ( ! $user_id = get_current_user_id() ) {
return;
}
if ( ! is_user_member_of_blog() ) {
return;
}
$settings = (string) get_user_option( 'user-settings', $user_id );
if ( isset( $_COOKIE[ 'wp-settings-' . $user_id ] ) ) {
$cookie = preg_replace( '/[^A-Za-z0-9=&_]/', '', $_COOKIE[ 'wp-settings-' . $user_id ] );
// No change or both empty
if ( $cookie == $settings ) {
return;
}
$last_saved = (int) get_user_option( 'user-settings-time', $user_id );
$current = isset( $_COOKIE[ 'wp-settings-time-' . $user_id ] ) ? preg_replace( '/[^0-9]/', '', $_COOKIE[ 'wp-settings-time-' . $user_id ] ) : 0;
// The cookie is newer than the saved value. Update the user_option and leave the cookie as-is
if ( $current > $last_saved ) {
update_user_option( $user_id, 'user-settings', $cookie, false );
update_user_option( $user_id, 'user-settings-time', time() - 5, false );
return;
}
}
// The cookie is not set in the current browser or the saved value is newer.
$secure = ( 'https' === parse_url( admin_url(), PHP_URL_SCHEME ) );
setcookie( 'wp-settings-' . $user_id, $settings, time() + YEAR_IN_SECONDS, SITECOOKIEPATH, null, $secure );
setcookie( 'wp-settings-time-' . $user_id, time(), time() + YEAR_IN_SECONDS, SITECOOKIEPATH, null, $secure );
$_COOKIE[ 'wp-settings-' . $user_id ] = $settings;
}
/**
* Retrieve user interface setting value based on setting name.
*
* @since 2.7.0
*
* @param string $name The name of the setting.
* @param string $default Optional default value to return when $name is not set.
* @return mixed the last saved user setting or the default value/false if it doesn't exist.
*/
function get_user_setting( $name, $default = false ) {
$all_user_settings = get_all_user_settings();
return isset( $all_user_settings[ $name ] ) ? $all_user_settings[ $name ] : $default;
}
/**
* Add or update user interface setting.
*
* Both $name and $value can contain only ASCII letters, numbers and underscores.
*
* This function has to be used before any output has started as it calls setcookie().
*
* @since 2.8.0
*
* @param string $name The name of the setting.
* @param string $value The value for the setting.
* @return bool|null True if set successfully, false if not. Null if the current user can't be established.
*/
function set_user_setting( $name, $value ) {
if ( headers_sent() ) {
return false;
}
$all_user_settings = get_all_user_settings();
$all_user_settings[ $name ] = $value;
return wp_set_all_user_settings( $all_user_settings );
}
/**
* Delete user interface settings.
*
* Deleting settings would reset them to the defaults.
*
* This function has to be used before any output has started as it calls setcookie().
*
* @since 2.7.0
*
* @param string $names The name or array of names of the setting to be deleted.
* @return bool|null True if deleted successfully, false if not. Null if the current user can't be established.
*/
function delete_user_setting( $names ) {
if ( headers_sent() ) {
return false;
}
$all_user_settings = get_all_user_settings();
$names = (array) $names;
$deleted = false;
foreach ( $names as $name ) {
if ( isset( $all_user_settings[ $name ] ) ) {
unset( $all_user_settings[ $name ] );
$deleted = true;
}
}
if ( $deleted ) {
return wp_set_all_user_settings( $all_user_settings );
}
return false;
}
/**
* Retrieve all user interface settings.
*
* @since 2.7.0
*
* @global array $_updated_user_settings
*
* @return array the last saved user settings or empty array.
*/
function get_all_user_settings() {
global $_updated_user_settings;
if ( ! $user_id = get_current_user_id() ) {
return array();
}
if ( isset( $_updated_user_settings ) && is_array( $_updated_user_settings ) ) {
return $_updated_user_settings;
}
$user_settings = array();
if ( isset( $_COOKIE[ 'wp-settings-' . $user_id ] ) ) {
$cookie = preg_replace( '/[^A-Za-z0-9=&_-]/', '', $_COOKIE[ 'wp-settings-' . $user_id ] );
if ( strpos( $cookie, '=' ) ) { // '=' cannot be 1st char
parse_str( $cookie, $user_settings );
}
} else {
$option = get_user_option( 'user-settings', $user_id );
if ( $option && is_string( $option ) ) {
parse_str( $option, $user_settings );
}
}
$_updated_user_settings = $user_settings;
return $user_settings;
}
/**
* Private. Set all user interface settings.
*
* @since 2.8.0
* @access private
*
* @global array $_updated_user_settings
*
* @param array $user_settings User settings.
* @return bool|null False if the current user can't be found, null if the current
* user is not a super admin or a member of the site, otherwise true.
*/
function wp_set_all_user_settings( $user_settings ) {
global $_updated_user_settings;
if ( ! $user_id = get_current_user_id() ) {
return false;
}
if ( ! is_user_member_of_blog() ) {
return;
}
$settings = '';
foreach ( $user_settings as $name => $value ) {
$_name = preg_replace( '/[^A-Za-z0-9_-]+/', '', $name );
$_value = preg_replace( '/[^A-Za-z0-9_-]+/', '', $value );
if ( ! empty( $_name ) ) {
$settings .= $_name . '=' . $_value . '&';
}
}
$settings = rtrim( $settings, '&' );
parse_str( $settings, $_updated_user_settings );
update_user_option( $user_id, 'user-settings', $settings, false );
update_user_option( $user_id, 'user-settings-time', time(), false );
return true;
}
/**
* Delete the user settings of the current user.
*
* @since 2.7.0
*/
function delete_all_user_settings() {
if ( ! $user_id = get_current_user_id() ) {
return;
}
update_user_option( $user_id, 'user-settings', '', false );
setcookie( 'wp-settings-' . $user_id, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH );
}
/**
* Retrieve an option value for the current network based on name of option.
*
* @since 2.8.0
* @since 4.4.0 The `$use_cache` parameter was deprecated.
* @since 4.4.0 Modified into wrapper for get_network_option()
*
* @see get_network_option()
*
* @param string $option Name of option to retrieve. Expected to not be SQL-escaped.
* @param mixed $default Optional value to return if option doesn't exist. Default false.
* @param bool $deprecated Whether to use cache. Multisite only. Always set to true.
* @return mixed Value set for the option.
*/
function get_site_option( $option, $default = false, $deprecated = true ) {
return get_network_option( null, $option, $default );
}
/**
* Add a new option for the current network.
*
* Existing options will not be updated. Note that prior to 3.3 this wasn't the case.
*
* @since 2.8.0
* @since 4.4.0 Modified into wrapper for add_network_option()
*
* @see add_network_option()
*
* @param string $option Name of option to add. Expected to not be SQL-escaped.
* @param mixed $value Option value, can be anything. Expected to not be SQL-escaped.
* @return bool False if the option was not added. True if the option was added.
*/
function add_site_option( $option, $value ) {
return add_network_option( null, $option, $value );
}
/**
* Removes a option by name for the current network.
*
* @since 2.8.0
* @since 4.4.0 Modified into wrapper for delete_network_option()
*
* @see delete_network_option()
*
* @param string $option Name of option to remove. Expected to not be SQL-escaped.
* @return bool True, if succeed. False, if failure.
*/
function delete_site_option( $option ) {
return delete_network_option( null, $option );
}
/**
* Update the value of an option that was already added for the current network.
*
* @since 2.8.0
* @since 4.4.0 Modified into wrapper for update_network_option()
*
* @see update_network_option()
*
* @param string $option Name of option. Expected to not be SQL-escaped.
* @param mixed $value Option value. Expected to not be SQL-escaped.
* @return bool False if value was not updated. True if value was updated.
*/
function update_site_option( $option, $value ) {
return update_network_option( null, $option, $value );
}
/**
* Retrieve a network's option value based on the option name.
*
* @since 4.4.0
*
* @see get_option()
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param int $network_id ID of the network. Can be null to default to the current network ID.
* @param string $option Name of option to retrieve. Expected to not be SQL-escaped.
* @param mixed $default Optional. Value to return if the option doesn't exist. Default false.
* @return mixed Value set for the option.
*/
function get_network_option( $network_id, $option, $default = false ) {
global $wpdb;
if ( $network_id && ! is_numeric( $network_id ) ) {
return false;
}
$network_id = (int) $network_id;
// Fallback to the current network if a network ID is not specified.
if ( ! $network_id ) {
$network_id = get_current_network_id();
}
/**
* Filters an existing network option before it is retrieved.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* Passing a truthy value to the filter will effectively short-circuit retrieval,
* returning the passed value instead.
*
* @since 2.9.0 As 'pre_site_option_' . $key
* @since 3.0.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$network_id` parameter was added.
* @since 4.9.0 The `$default` parameter was added.
*
* @param mixed $pre_option The value to return instead of the option value. This differs from
* `$default`, which is used as the fallback value in the event the
* option doesn't exist elsewhere in get_network_option(). Default
* is false (to skip past the short-circuit).
* @param string $option Option name.
* @param int $network_id ID of the network.
* @param mixed $default The fallback value to return if the option does not exist.
* Default is false.
*/
$pre = apply_filters( "pre_site_option_{$option}", false, $option, $network_id, $default );
if ( false !== $pre ) {
return $pre;
}
// prevent non-existent options from triggering multiple queries
$notoptions_key = "$network_id:notoptions";
$notoptions = wp_cache_get( $notoptions_key, 'site-options' );
if ( is_array( $notoptions ) && isset( $notoptions[ $option ] ) ) {
/**
* Filters a specific default network option.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 3.4.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param mixed $default The value to return if the site option does not exist
* in the database.
* @param string $option Option name.
* @param int $network_id ID of the network.
*/
return apply_filters( "default_site_option_{$option}", $default, $option, $network_id );
}
if ( ! is_multisite() ) {
/** This filter is documented in site-inc/option.php */
$default = apply_filters( 'default_site_option_' . $option, $default, $option, $network_id );
$value = get_option( $option, $default );
} else {
$cache_key = "$network_id:$option";
$value = wp_cache_get( $cache_key, 'site-options' );
if ( ! isset( $value ) || false === $value ) {
$row = $wpdb->get_row( $wpdb->prepare( "SELECT meta_value FROM $wpdb->sitemeta WHERE meta_key = %s AND site_id = %d", $option, $network_id ) );
// Has to be get_row instead of get_var because of funkiness with 0, false, null values
if ( is_object( $row ) ) {
$value = $row->meta_value;
$value = maybe_unserialize( $value );
wp_cache_set( $cache_key, $value, 'site-options' );
} else {
if ( ! is_array( $notoptions ) ) {
$notoptions = array();
}
$notoptions[ $option ] = true;
wp_cache_set( $notoptions_key, $notoptions, 'site-options' );
/** This filter is documented in site-inc/option.php */
$value = apply_filters( 'default_site_option_' . $option, $default, $option, $network_id );
}
}
}
if ( ! is_array( $notoptions ) ) {
$notoptions = array();
wp_cache_set( $notoptions_key, $notoptions, 'site-options' );
}
/**
* Filters the value of an existing network option.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 2.9.0 As 'site_option_' . $key
* @since 3.0.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param mixed $value Value of network option.
* @param string $option Option name.
* @param int $network_id ID of the network.
*/
return apply_filters( "site_option_{$option}", $value, $option, $network_id );
}
/**
* Add a new network option.
*
* Existing options will not be updated.
*
* @since 4.4.0
*
* @see add_option()
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param int $network_id ID of the network. Can be null to default to the current network ID.
* @param string $option Name of option to add. Expected to not be SQL-escaped.
* @param mixed $value Option value, can be anything. Expected to not be SQL-escaped.
* @return bool False if option was not added and true if option was added.
*/
function add_network_option( $network_id, $option, $value ) {
global $wpdb;
if ( $network_id && ! is_numeric( $network_id ) ) {
return false;
}
$network_id = (int) $network_id;
// Fallback to the current network if a network ID is not specified.
if ( ! $network_id ) {
$network_id = get_current_network_id();
}
wp_protect_special_option( $option );
/**
* Filters the value of a specific network option before it is added.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 2.9.0 As 'pre_add_site_option_' . $key
* @since 3.0.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param mixed $value Value of network option.
* @param string $option Option name.
* @param int $network_id ID of the network.
*/
$value = apply_filters( "pre_add_site_option_{$option}", $value, $option, $network_id );
$notoptions_key = "$network_id:notoptions";
if ( ! is_multisite() ) {
$result = add_option( $option, $value, '', 'no' );
} else {
$cache_key = "$network_id:$option";
// Make sure the option doesn't already exist. We can check the 'notoptions' cache before we ask for a db query
$notoptions = wp_cache_get( $notoptions_key, 'site-options' );
if ( ! is_array( $notoptions ) || ! isset( $notoptions[ $option ] ) ) {
if ( false !== get_network_option( $network_id, $option, false ) ) {
return false;
}
}
$value = sanitize_option( $option, $value );
$serialized_value = maybe_serialize( $value );
$result = $wpdb->insert(
$wpdb->sitemeta,
array(
'site_id' => $network_id,
'meta_key' => $option,
'meta_value' => $serialized_value,
)
);
if ( ! $result ) {
return false;
}
wp_cache_set( $cache_key, $value, 'site-options' );
// This option exists now
$notoptions = wp_cache_get( $notoptions_key, 'site-options' ); // yes, again... we need it to be fresh
if ( is_array( $notoptions ) && isset( $notoptions[ $option ] ) ) {
unset( $notoptions[ $option ] );
wp_cache_set( $notoptions_key, $notoptions, 'site-options' );
}
}
if ( $result ) {
/**
* Fires after a specific network option has been successfully added.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 2.9.0 As "add_site_option_{$key}"
* @since 3.0.0
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param string $option Name of the network option.
* @param mixed $value Value of the network option.
* @param int $network_id ID of the network.
*/
do_action( "add_site_option_{$option}", $option, $value, $network_id );
/**
* Fires after a network option has been successfully added.
*
* @since 3.0.0
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param string $option Name of the network option.
* @param mixed $value Value of the network option.
* @param int $network_id ID of the network.
*/
do_action( 'add_site_option', $option, $value, $network_id );
return true;
}
return false;
}
/**
* Removes a network option by name.
*
* @since 4.4.0
*
* @see delete_option()
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param int $network_id ID of the network. Can be null to default to the current network ID.
* @param string $option Name of option to remove. Expected to not be SQL-escaped.
* @return bool True, if succeed. False, if failure.
*/
function delete_network_option( $network_id, $option ) {
global $wpdb;
if ( $network_id && ! is_numeric( $network_id ) ) {
return false;
}
$network_id = (int) $network_id;
// Fallback to the current network if a network ID is not specified.
if ( ! $network_id ) {
$network_id = get_current_network_id();
}
/**
* Fires immediately before a specific network option is deleted.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 3.0.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param string $option Option name.
* @param int $network_id ID of the network.
*/
do_action( "pre_delete_site_option_{$option}", $option, $network_id );
if ( ! is_multisite() ) {
$result = delete_option( $option );
} else {
$row = $wpdb->get_row( $wpdb->prepare( "SELECT meta_id FROM {$wpdb->sitemeta} WHERE meta_key = %s AND site_id = %d", $option, $network_id ) );
if ( is_null( $row ) || ! $row->meta_id ) {
return false;
}
$cache_key = "$network_id:$option";
wp_cache_delete( $cache_key, 'site-options' );
$result = $wpdb->delete(
$wpdb->sitemeta,
array(
'meta_key' => $option,
'site_id' => $network_id,
)
);
}
if ( $result ) {
/**
* Fires after a specific network option has been deleted.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 2.9.0 As "delete_site_option_{$key}"
* @since 3.0.0
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param string $option Name of the network option.
* @param int $network_id ID of the network.
*/
do_action( "delete_site_option_{$option}", $option, $network_id );
/**
* Fires after a network option has been deleted.
*
* @since 3.0.0
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param string $option Name of the network option.
* @param int $network_id ID of the network.
*/
do_action( 'delete_site_option', $option, $network_id );
return true;
}
return false;
}
/**
* Update the value of a network option that was already added.
*
* @since 4.4.0
*
* @see update_option()
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param int $network_id ID of the network. Can be null to default to the current network ID.
* @param string $option Name of option. Expected to not be SQL-escaped.
* @param mixed $value Option value. Expected to not be SQL-escaped.
* @return bool False if value was not updated and true if value was updated.
*/
function update_network_option( $network_id, $option, $value ) {
global $wpdb;
if ( $network_id && ! is_numeric( $network_id ) ) {
return false;
}
$network_id = (int) $network_id;
// Fallback to the current network if a network ID is not specified.
if ( ! $network_id ) {
$network_id = get_current_network_id();
}
wp_protect_special_option( $option );
$old_value = get_network_option( $network_id, $option, false );
/**
* Filters a specific network option before its value is updated.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 2.9.0 As 'pre_update_site_option_' . $key
* @since 3.0.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param mixed $value New value of the network option.
* @param mixed $old_value Old value of the network option.
* @param string $option Option name.
* @param int $network_id ID of the network.
*/
$value = apply_filters( "pre_update_site_option_{$option}", $value, $old_value, $option, $network_id );
/*
* If the new and old values are the same, no need to update.
*
* Unserialized values will be adequate in most cases. If the unserialized
* data differs, the (maybe) serialized data is checked to avoid
* unnecessary database calls for otherwise identical object instances.
*
* See https://core.trac.wordpress.org/ticket/44956
*/
if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) {
return false;
}
if ( false === $old_value ) {
return add_network_option( $network_id, $option, $value );
}
$notoptions_key = "$network_id:notoptions";
$notoptions = wp_cache_get( $notoptions_key, 'site-options' );
if ( is_array( $notoptions ) && isset( $notoptions[ $option ] ) ) {
unset( $notoptions[ $option ] );
wp_cache_set( $notoptions_key, $notoptions, 'site-options' );
}
if ( ! is_multisite() ) {
$result = update_option( $option, $value, 'no' );
} else {
$value = sanitize_option( $option, $value );
$serialized_value = maybe_serialize( $value );
$result = $wpdb->update(
$wpdb->sitemeta,
array( 'meta_value' => $serialized_value ),
array(
'site_id' => $network_id,
'meta_key' => $option,
)
);
if ( $result ) {
$cache_key = "$network_id:$option";
wp_cache_set( $cache_key, $value, 'site-options' );
}
}
if ( $result ) {
/**
* Fires after the value of a specific network option has been successfully updated.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 2.9.0 As "update_site_option_{$key}"
* @since 3.0.0
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param string $option Name of the network option.
* @param mixed $value Current value of the network option.
* @param mixed $old_value Old value of the network option.
* @param int $network_id ID of the network.
*/
do_action( "update_site_option_{$option}", $option, $value, $old_value, $network_id );
/**
* Fires after the value of a network option has been successfully updated.
*
* @since 3.0.0
* @since 4.7.0 The `$network_id` parameter was added.
*
* @param string $option Name of the network option.
* @param mixed $value Current value of the network option.
* @param mixed $old_value Old value of the network option.
* @param int $network_id ID of the network.
*/
do_action( 'update_site_option', $option, $value, $old_value, $network_id );
return true;
}
return false;
}
/**
* Delete a site transient.
*
* @since 2.9.0
*
* @param string $transient Transient name. Expected to not be SQL-escaped.
* @return bool True if successful, false otherwise
*/
function delete_site_transient( $transient ) {
/**
* Fires immediately before a specific site transient is deleted.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 3.0.0
*
* @param string $transient Transient name.
*/
do_action( "delete_site_transient_{$transient}", $transient );
if ( wp_using_ext_object_cache() ) {
$result = wp_cache_delete( $transient, 'site-transient' );
} else {
$option_timeout = '_site_transient_timeout_' . $transient;
$option = '_site_transient_' . $transient;
$result = delete_site_option( $option );
if ( $result ) {
delete_site_option( $option_timeout );
}
}
if ( $result ) {
/**
* Fires after a transient is deleted.
*
* @since 3.0.0
*
* @param string $transient Deleted transient name.
*/
do_action( 'deleted_site_transient', $transient );
}
return $result;
}
/**
* Get the value of a site transient.
*
* If the transient does not exist, does not have a value, or has expired,
* then the return value will be false.
*
* @since 2.9.0
*
* @see get_transient()
*
* @param string $transient Transient name. Expected to not be SQL-escaped.
* @return mixed Value of transient.
*/
function get_site_transient( $transient ) {
/**
* Filters the value of an existing site transient.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* Passing a truthy value to the filter will effectively short-circuit retrieval,
* returning the passed value instead.
*
* @since 2.9.0
* @since 4.4.0 The `$transient` parameter was added.
*
* @param mixed $pre_site_transient The default value to return if the site transient does not exist.
* Any value other than false will short-circuit the retrieval
* of the transient, and return the returned value.
* @param string $transient Transient name.
*/
$pre = apply_filters( "pre_site_transient_{$transient}", false, $transient );
if ( false !== $pre ) {
return $pre;
}
if ( wp_using_ext_object_cache() ) {
$value = wp_cache_get( $transient, 'site-transient' );
} else {
// Core transients that do not have a timeout. Listed here so querying timeouts can be avoided.
$no_timeout = array( 'update_core', 'update_plugins', 'update_themes' );
$transient_option = '_site_transient_' . $transient;
if ( ! in_array( $transient, $no_timeout ) ) {
$transient_timeout = '_site_transient_timeout_' . $transient;
$timeout = get_site_option( $transient_timeout );
if ( false !== $timeout && $timeout < time() ) {
delete_site_option( $transient_option );
delete_site_option( $transient_timeout );
$value = false;
}
}
if ( ! isset( $value ) ) {
$value = get_site_option( $transient_option );
}
}
/**
* Filters the value of an existing site transient.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 2.9.0
* @since 4.4.0 The `$transient` parameter was added.
*
* @param mixed $value Value of site transient.
* @param string $transient Transient name.
*/
return apply_filters( "site_transient_{$transient}", $value, $transient );
}
/**
* Set/update the value of a site transient.
*
* You do not need to serialize values, if the value needs to be serialize, then
* it will be serialized before it is set.
*
* @since 2.9.0
*
* @see set_transient()
*
* @param string $transient Transient name. Expected to not be SQL-escaped. Must be
* 167 characters or fewer in length.
* @param mixed $value Transient value. Expected to not be SQL-escaped.
* @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration).
* @return bool False if value was not set and true if value was set.
*/
function set_site_transient( $transient, $value, $expiration = 0 ) {
/**
* Filters the value of a specific site transient before it is set.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 3.0.0
* @since 4.4.0 The `$transient` parameter was added.
*
* @param mixed $value New value of site transient.
* @param string $transient Transient name.
*/
$value = apply_filters( "pre_set_site_transient_{$transient}", $value, $transient );
$expiration = (int) $expiration;
/**
* Filters the expiration for a site transient before its value is set.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 4.4.0
*
* @param int $expiration Time until expiration in seconds. Use 0 for no expiration.
* @param mixed $value New value of site transient.
* @param string $transient Transient name.
*/
$expiration = apply_filters( "expiration_of_site_transient_{$transient}", $expiration, $value, $transient );
if ( wp_using_ext_object_cache() ) {
$result = wp_cache_set( $transient, $value, 'site-transient', $expiration );
} else {
$transient_timeout = '_site_transient_timeout_' . $transient;
$option = '_site_transient_' . $transient;
if ( false === get_site_option( $option ) ) {
if ( $expiration ) {
add_site_option( $transient_timeout, time() + $expiration );
}
$result = add_site_option( $option, $value );
} else {
if ( $expiration ) {
update_site_option( $transient_timeout, time() + $expiration );
}
$result = update_site_option( $option, $value );
}
}
if ( $result ) {
/**
* Fires after the value for a specific site transient has been set.
*
* The dynamic portion of the hook name, `$transient`, refers to the transient name.
*
* @since 3.0.0
* @since 4.4.0 The `$transient` parameter was added
*
* @param mixed $value Site transient value.
* @param int $expiration Time until expiration in seconds.
* @param string $transient Transient name.
*/
do_action( "set_site_transient_{$transient}", $value, $expiration, $transient );
/**
* Fires after the value for a site transient has been set.
*
* @since 3.0.0
*
* @param string $transient The name of the site transient.
* @param mixed $value Site transient value.
* @param int $expiration Time until expiration in seconds.
*/
do_action( 'setted_site_transient', $transient, $value, $expiration );
}
return $result;
}
/**
* Register default settings available in WordPress.
*
* The settings registered here are primarily useful for the REST API, so this
* does not encompass all settings available in WordPress.
*
* @since 4.7.0
*/
function register_initial_settings() {
register_setting(
'general',
'blogname',
array(
'show_in_rest' => array(
'name' => 'title',
),
'type' => 'string',
'description' => __( 'Site title.' ),
)
);
register_setting(
'general',
'blogdescription',
array(
'show_in_rest' => array(
'name' => 'description',
),
'type' => 'string',
'description' => __( 'Site tagline.' ),
)
);
if ( ! is_multisite() ) {
register_setting(
'general',
'siteurl',
array(
'show_in_rest' => array(
'name' => 'url',
'schema' => array(
'format' => 'uri',
),
),
'type' => 'string',
'description' => __( 'Site URL.' ),
)
);
}
if ( ! is_multisite() ) {
register_setting(
'general',
'admin_email',
array(
'show_in_rest' => array(
'name' => 'email',
'schema' => array(
'format' => 'email',
),
),
'type' => 'string',
'description' => __( 'This address is used for admin purposes, like new user notification.' ),
)
);
}
register_setting(
'general',
'timezone_string',
array(
'show_in_rest' => array(
'name' => 'timezone',
),
'type' => 'string',
'description' => __( 'A city in the same timezone as you.' ),
)
);
register_setting(
'general',
'date_format',
array(
'show_in_rest' => true,
'type' => 'string',
'description' => __( 'A date format for all date strings.' ),
)
);
register_setting(
'general',
'time_format',
array(
'show_in_rest' => true,
'type' => 'string',
'description' => __( 'A time format for all time strings.' ),
)
);
register_setting(
'general',
'start_of_week',
array(
'show_in_rest' => true,
'type' => 'integer',
'description' => __( 'A day number of the week that the week should start on.' ),
)
);
register_setting(
'general',
'WPLANG',
array(
'show_in_rest' => array(
'name' => 'language',
),
'type' => 'string',
'description' => __( 'WordPress locale code.' ),
'default' => 'en_US',
)
);
register_setting(
'writing',
'use_smilies',
array(
'show_in_rest' => true,
'type' => 'boolean',
'description' => __( 'Convert emoticons like :-) and :-P to graphics on display.' ),
'default' => true,
)
);
register_setting(
'writing',
'default_category',
array(
'show_in_rest' => true,
'type' => 'integer',
'description' => __( 'Default post category.' ),
)
);
register_setting(
'writing',
'default_post_format',
array(
'show_in_rest' => true,
'type' => 'string',
'description' => __( 'Default post format.' ),
)
);
register_setting(
'reading',
'posts_per_page',
array(
'show_in_rest' => true,
'type' => 'integer',
'description' => __( 'Blog pages show at most.' ),
'default' => 10,
)
);
register_setting(
'discussion',
'default_ping_status',
array(
'show_in_rest' => array(
'schema' => array(
'enum' => array( 'open', 'closed' ),
),
),
'type' => 'string',
'description' => __( 'Allow link notifications from other blogs (pingbacks and trackbacks) on new articles.' ),
)
);
register_setting(
'discussion',
'default_comment_status',
array(
'show_in_rest' => array(
'schema' => array(
'enum' => array( 'open', 'closed' ),
),
),
'type' => 'string',
'description' => __( 'Allow people to post comments on new articles.' ),
)
);
}
/**
* Register a setting and its data.
*
* @since 2.7.0
* @since 4.7.0 `$args` can be passed to set flags on the setting, similar to `register_meta()`.
*
* @global array $new_whitelist_options
* @global array $wp_registered_settings
*
* @param string $option_group A settings group name. Should correspond to a whitelisted option key name.
* Default whitelisted option key names include "general," "discussion," and "reading," among others.
* @param string $option_name The name of an option to sanitize and save.
* @param array $args {
* Data used to describe the setting when registered.
*
* @type string $type The type of data associated with this setting.
* Valid values are 'string', 'boolean', 'integer', and 'number'.
* @type string $description A description of the data attached to this setting.
* @type callable $sanitize_callback A callback function that sanitizes the option's value.
* @type bool $show_in_rest Whether data associated with this setting should be included in the REST API.
* @type mixed $default Default value when calling `get_option()`.
* }
*/
function register_setting( $option_group, $option_name, $args = array() ) {
global $new_whitelist_options, $wp_registered_settings;
$defaults = array(
'type' => 'string',
'group' => $option_group,
'description' => '',
'sanitize_callback' => null,
'show_in_rest' => false,
);
// Back-compat: old sanitize callback is added.
if ( is_callable( $args ) ) {
$args = array(
'sanitize_callback' => $args,
);
}
/**
* Filters the registration arguments when registering a setting.
*
* @since 4.7.0
*
* @param array $args Array of setting registration arguments.
* @param array $defaults Array of default arguments.
* @param string $option_group Setting group.
* @param string $option_name Setting name.
*/
$args = apply_filters( 'register_setting_args', $args, $defaults, $option_group, $option_name );
$args = wp_parse_args( $args, $defaults );
if ( ! is_array( $wp_registered_settings ) ) {
$wp_registered_settings = array();
}
if ( 'misc' == $option_group ) {
_deprecated_argument(
__FUNCTION__,
'3.0.0',
/* translators: %s: misc */
sprintf(
__( 'The "%s" options group has been removed. Use another settings group.' ),
'misc'
)
);
$option_group = 'general';
}
if ( 'privacy' == $option_group ) {
_deprecated_argument(
__FUNCTION__,
'3.5.0',
/* translators: %s: privacy */
sprintf(
__( 'The "%s" options group has been removed. Use another settings group.' ),
'privacy'
)
);
$option_group = 'reading';
}
$new_whitelist_options[ $option_group ][] = $option_name;
if ( ! empty( $args['sanitize_callback'] ) ) {
add_filter( "sanitize_option_{$option_name}", $args['sanitize_callback'] );
}
if ( array_key_exists( 'default', $args ) ) {
add_filter( "default_option_{$option_name}", 'filter_default_option', 10, 3 );
}
$wp_registered_settings[ $option_name ] = $args;
}
/**
* Unregister a setting.
*
* @since 2.7.0
* @since 4.7.0 `$sanitize_callback` was deprecated. The callback from `register_setting()` is now used instead.
*
* @global array $new_whitelist_options
* @global array $wp_registered_settings
*
* @param string $option_group The settings group name used during registration.
* @param string $option_name The name of the option to unregister.
* @param callable $deprecated Deprecated.
*/
function unregister_setting( $option_group, $option_name, $deprecated = '' ) {
global $new_whitelist_options, $wp_registered_settings;
if ( 'misc' == $option_group ) {
_deprecated_argument(
__FUNCTION__,
'3.0.0',
/* translators: %s: misc */
sprintf(
__( 'The "%s" options group has been removed. Use another settings group.' ),
'misc'
)
);
$option_group = 'general';
}
if ( 'privacy' == $option_group ) {
_deprecated_argument(
__FUNCTION__,
'3.5.0',
/* translators: %s: privacy */
sprintf(
__( 'The "%s" options group has been removed. Use another settings group.' ),
'privacy'
)
);
$option_group = 'reading';
}
$pos = array_search( $option_name, (array) $new_whitelist_options[ $option_group ] );
if ( $pos !== false ) {
unset( $new_whitelist_options[ $option_group ][ $pos ] );
}
if ( '' !== $deprecated ) {
_deprecated_argument(
__FUNCTION__,
'4.7.0',
/* translators: 1: $sanitize_callback, 2: register_setting() */
sprintf(
__( '%1$s is deprecated. The callback from %2$s is used instead.' ),
'<code>$sanitize_callback</code>',
'<code>register_setting()</code>'
)
);
remove_filter( "sanitize_option_{$option_name}", $deprecated );
}
if ( isset( $wp_registered_settings[ $option_name ] ) ) {
// Remove the sanitize callback if one was set during registration.
if ( ! empty( $wp_registered_settings[ $option_name ]['sanitize_callback'] ) ) {
remove_filter( "sanitize_option_{$option_name}", $wp_registered_settings[ $option_name ]['sanitize_callback'] );
}
// Remove the default filter if a default was provided during registration.
if ( array_key_exists( 'default', $wp_registered_settings[ $option_name ] ) ) {
remove_filter( "default_option_{$option_name}", 'filter_default_option', 10 );
}
unset( $wp_registered_settings[ $option_name ] );
}
}
/**
* Retrieves an array of registered settings.
*
* @since 4.7.0
*
* @global array $wp_registered_settings
*
* @return array List of registered settings, keyed by option name.
*/
function get_registered_settings() {
global $wp_registered_settings;
if ( ! is_array( $wp_registered_settings ) ) {
return array();
}
return $wp_registered_settings;
}
/**
* Filter the default value for the option.
*
* For settings which register a default setting in `register_setting()`, this
* function is added as a filter to `default_option_{$option}`.
*
* @since 4.7.0
*
* @param mixed $default Existing default value to return.
* @param string $option Option name.
* @param bool $passed_default Was `get_option()` passed a default value?
* @return mixed Filtered default value.
*/
function filter_default_option( $default, $option, $passed_default ) {
if ( $passed_default ) {
return $default;
}
$registered = get_registered_settings();
if ( empty( $registered[ $option ] ) ) {
return $default;
}
return $registered[ $option ]['default'];
}
<?php
/**
* PHPMailer Exception class.
* PHP Version 5.5.
*
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
*
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
* @author Brent R. Matzelle (original founder)
* @copyright 2012 - 2020 Marcus Bointon
* @copyright 2010 - 2012 Jim Jagielski
* @copyright 2004 - 2009 Andy Prevost
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
* @note This program is distributed in the hope that it will be useful - WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE.
*/
namespace PHPMailer\PHPMailer;
/**
* PHPMailer exception handler.
*
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
*/
class Exception extends \Exception
{
/**
* Prettify error message output.
*
* @return string
*/
public function errorMessage()
{
return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n";
}
}
<?php
/**
* PHPMailer - PHP email creation and transport class.
* PHP Version 5.5.
*
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
*
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
* @author Brent R. Matzelle (original founder)
* @copyright 2012 - 2020 Marcus Bointon
* @copyright 2010 - 2012 Jim Jagielski
* @copyright 2004 - 2009 Andy Prevost
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
* @note This program is distributed in the hope that it will be useful - WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE.
*/
namespace PHPMailer\PHPMailer;
/**
* PHPMailer - PHP email creation and transport class.
*
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
* @author Brent R. Matzelle (original founder)
*/
class PHPMailer
{
const CHARSET_ASCII = 'us-ascii';
const CHARSET_ISO88591 = 'iso-8859-1';
const CHARSET_UTF8 = 'utf-8';
const CONTENT_TYPE_PLAINTEXT = 'text/plain';
const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar';
const CONTENT_TYPE_TEXT_HTML = 'text/html';
const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';
const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';
const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';
const ENCODING_7BIT = '7bit';
const ENCODING_8BIT = '8bit';
const ENCODING_BASE64 = 'base64';
const ENCODING_BINARY = 'binary';
const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
const ENCRYPTION_STARTTLS = 'tls';
const ENCRYPTION_SMTPS = 'ssl';
const ICAL_METHOD_REQUEST = 'REQUEST';
const ICAL_METHOD_PUBLISH = 'PUBLISH';
const ICAL_METHOD_REPLY = 'REPLY';
const ICAL_METHOD_ADD = 'ADD';
const ICAL_METHOD_CANCEL = 'CANCEL';
const ICAL_METHOD_REFRESH = 'REFRESH';
const ICAL_METHOD_COUNTER = 'COUNTER';
const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER';
/**
* Email priority.
* Options: null (default), 1 = High, 3 = Normal, 5 = low.
* When null, the header is not set at all.
*
* @var int|null
*/
public $Priority;
/**
* The character set of the message.
*
* @var string
*/
public $CharSet = self::CHARSET_ISO88591;
/**
* The MIME Content-type of the message.
*
* @var string
*/
public $ContentType = self::CONTENT_TYPE_PLAINTEXT;
/**
* The message encoding.
* Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
*
* @var string
*/
public $Encoding = self::ENCODING_8BIT;
/**
* Holds the most recent mailer error message.
*
* @var string
*/
public $ErrorInfo = '';
/**
* The From email address for the message.
*
* @var string
*/
public $From = 'root@localhost';
/**
* The From name of the message.
*
* @var string
*/
public $FromName = 'Root User';
/**
* The envelope sender of the message.
* This will usually be turned into a Return-Path header by the receiver,
* and is the address that bounces will be sent to.
* If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.
*
* @var string
*/
public $Sender = '';
/**
* The Subject of the message.
*
* @var string
*/
public $Subject = '';
/**
* An HTML or plain text message body.
* If HTML then call isHTML(true).
*
* @var string
*/
public $Body = '';
/**
* The plain-text message body.
* This body can be read by mail clients that do not have HTML email
* capability such as mutt & Eudora.
* Clients that can read HTML will view the normal Body.
*
* @var string
*/
public $AltBody = '';
/**
* An iCal message part body.
* Only supported in simple alt or alt_inline message types
* To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.
*
* @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/
* @see http://kigkonsult.se/iCalcreator/
*
* @var string
*/
public $Ical = '';
/**
* Value-array of "method" in Contenttype header "text/calendar"
*
* @var string[]
*/
protected static $IcalMethods = [
self::ICAL_METHOD_REQUEST,
self::ICAL_METHOD_PUBLISH,
self::ICAL_METHOD_REPLY,
self::ICAL_METHOD_ADD,
self::ICAL_METHOD_CANCEL,
self::ICAL_METHOD_REFRESH,
self::ICAL_METHOD_COUNTER,
self::ICAL_METHOD_DECLINECOUNTER,
];
/**
* The complete compiled MIME message body.
*
* @var string
*/
protected $MIMEBody = '';
/**
* The complete compiled MIME message headers.
*
* @var string
*/
protected $MIMEHeader = '';
/**
* Extra headers that createHeader() doesn't fold in.
*
* @var string
*/
protected $mailHeader = '';
/**
* Word-wrap the message body to this number of chars.
* Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
*
* @see static::STD_LINE_LENGTH
*
* @var int
*/
public $WordWrap = 0;
/**
* Which method to use to send mail.
* Options: "mail", "sendmail", or "smtp".
*
* @var string
*/
public $Mailer = 'mail';
/**
* The path to the sendmail program.
*
* @var string
*/
public $Sendmail = '/usr/sbin/sendmail';
/**
* Whether mail() uses a fully sendmail-compatible MTA.
* One which supports sendmail's "-oi -f" options.
*
* @var bool
*/
public $UseSendmailOptions = true;
/**
* The email address that a reading confirmation should be sent to, also known as read receipt.
*
* @var string
*/
public $ConfirmReadingTo = '';
/**
* The hostname to use in the Message-ID header and as default HELO string.
* If empty, PHPMailer attempts to find one with, in order,
* $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
* 'localhost.localdomain'.
*
* @see PHPMailer::$Helo
*
* @var string
*/
public $Hostname = '';
/**
* An ID to be used in the Message-ID header.
* If empty, a unique id will be generated.
* You can set your own, but it must be in the format "<id@domain>",
* as defined in RFC5322 section 3.6.4 or it will be ignored.
*
* @see https://tools.ietf.org/html/rfc5322#section-3.6.4
*
* @var string
*/
public $MessageID = '';
/**
* The message Date to be used in the Date header.
* If empty, the current date will be added.
*
* @var string
*/
public $MessageDate = '';
/**
* SMTP hosts.
* Either a single hostname or multiple semicolon-delimited hostnames.
* You can also specify a different port
* for each host by using this format: [hostname:port]
* (e.g. "smtp1.example.com:25;smtp2.example.com").
* You can also specify encryption type, for example:
* (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
* Hosts will be tried in order.
*
* @var string
*/
public $Host = 'localhost';
/**
* The default SMTP server port.
*
* @var int
*/
public $Port = 25;
/**
* The SMTP HELO/EHLO name used for the SMTP connection.
* Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
* one with the same method described above for $Hostname.
*
* @see PHPMailer::$Hostname
*
* @var string
*/
public $Helo = '';
/**
* What kind of encryption to use on the SMTP connection.
* Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.
*
* @var string
*/
public $SMTPSecure = '';
/**
* Whether to enable TLS encryption automatically if a server supports it,
* even if `SMTPSecure` is not set to 'tls'.
* Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
*
* @var bool
*/
public $SMTPAutoTLS = true;
/**
* Whether to use SMTP authentication.
* Uses the Username and Password properties.
*
* @see PHPMailer::$Username
* @see PHPMailer::$Password
*
* @var bool
*/
public $SMTPAuth = false;
/**
* Options array passed to stream_context_create when connecting via SMTP.
*
* @var array
*/
public $SMTPOptions = [];
/**
* SMTP username.
*
* @var string
*/
public $Username = '';
/**
* SMTP password.
*
* @var string
*/
public $Password = '';
/**
* SMTP authentication type. Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2.
* If not specified, the first one from that list that the server supports will be selected.
*
* @var string
*/
public $AuthType = '';
/**
* An implementation of the PHPMailer OAuthTokenProvider interface.
*
* @var OAuthTokenProvider
*/
protected $oauth;
/**
* The SMTP server timeout in seconds.
* Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
*
* @var int
*/
public $Timeout = 300;
/**
* Comma separated list of DSN notifications
* 'NEVER' under no circumstances a DSN must be returned to the sender.
* If you use NEVER all other notifications will be ignored.
* 'SUCCESS' will notify you when your mail has arrived at its destination.
* 'FAILURE' will arrive if an error occurred during delivery.
* 'DELAY' will notify you if there is an unusual delay in delivery, but the actual
* delivery's outcome (success or failure) is not yet decided.
*
* @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY
*/
public $dsn = '';
/**
* SMTP class debug output mode.
* Debug output level.
* Options:
* @see SMTP::DEBUG_OFF: No output
* @see SMTP::DEBUG_CLIENT: Client messages
* @see SMTP::DEBUG_SERVER: Client and server messages
* @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status
* @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
*
* @see SMTP::$do_debug
*
* @var int
*/
public $SMTPDebug = 0;
/**
* How to handle debug output.
* Options:
* * `echo` Output plain-text as-is, appropriate for CLI
* * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
* * `error_log` Output to error log as configured in php.ini
* By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
* Alternatively, you can provide a callable expecting two params: a message string and the debug level:
*
* ```php
* $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
* ```
*
* Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
* level output is used:
*
* ```php
* $mail->Debugoutput = new myPsr3Logger;
* ```
*
* @see SMTP::$Debugoutput
*
* @var string|callable|\Psr\Log\LoggerInterface
*/
public $Debugoutput = 'echo';
/**
* Whether to keep the SMTP connection open after each message.
* If this is set to true then the connection will remain open after a send,
* and closing the connection will require an explicit call to smtpClose().
* It's a good idea to use this if you are sending multiple messages as it reduces overhead.
* See the mailing list example for how to use it.
*
* @var bool
*/
public $SMTPKeepAlive = false;
/**
* Whether to split multiple to addresses into multiple messages
* or send them all in one message.
* Only supported in `mail` and `sendmail` transports, not in SMTP.
*
* @var bool
*
* @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
*/
public $SingleTo = false;
/**
* Storage for addresses when SingleTo is enabled.
*
* @var array
*/
protected $SingleToArray = [];
/**
* Whether to generate VERP addresses on send.
* Only applicable when sending via SMTP.
*
* @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
* @see http://www.postfix.org/VERP_README.html Postfix VERP info
*
* @var bool
*/
public $do_verp = false;
/**
* Whether to allow sending messages with an empty body.
*
* @var bool
*/
public $AllowEmpty = false;
/**
* DKIM selector.
*
* @var string
*/
public $DKIM_selector = '';
/**
* DKIM Identity.
* Usually the email address used as the source of the email.
*
* @var string
*/
public $DKIM_identity = '';
/**
* DKIM passphrase.
* Used if your key is encrypted.
*
* @var string
*/
public $DKIM_passphrase = '';
/**
* DKIM signing domain name.
*
* @example 'example.com'
*
* @var string
*/
public $DKIM_domain = '';
/**
* DKIM Copy header field values for diagnostic use.
*
* @var bool
*/
public $DKIM_copyHeaderFields = true;
/**
* DKIM Extra signing headers.
*
* @example ['List-Unsubscribe', 'List-Help']
*
* @var array
*/
public $DKIM_extraHeaders = [];
/**
* DKIM private key file path.
*
* @var string
*/
public $DKIM_private = '';
/**
* DKIM private key string.
*
* If set, takes precedence over `$DKIM_private`.
*
* @var string
*/
public $DKIM_private_string = '';
/**
* Callback Action function name.
*
* The function that handles the result of the send email action.
* It is called out by send() for each email sent.
*
* Value can be any php callable: http://www.php.net/is_callable
*
* Parameters:
* bool $result result of the send action
* array $to email addresses of the recipients
* array $cc cc email addresses
* array $bcc bcc email addresses
* string $subject the subject
* string $body the email body
* string $from email address of sender
* string $extra extra information of possible use
* "smtp_transaction_id' => last smtp transaction id
*
* @var string
*/
public $action_function = '';
/**
* What to put in the X-Mailer header.
* Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.
*
* @var string|null
*/
public $XMailer = '';
/**
* Which validator to use by default when validating email addresses.
* May be a callable to inject your own validator, but there are several built-in validators.
* The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
*
* @see PHPMailer::validateAddress()
*
* @var string|callable
*/
public static $validator = 'auto';
/**
* An instance of the SMTP sender class.
*
* @var SMTP
*/
protected $smtp;
/**
* The array of 'to' names and addresses.
*
* @var array
*/
protected $to = [];
/**
* The array of 'cc' names and addresses.
*
* @var array
*/
protected $cc = [];
/**
* The array of 'bcc' names and addresses.
*
* @var array
*/
protected $bcc = [];
/**
* The array of reply-to names and addresses.
*
* @var array
*/
protected $ReplyTo = [];
/**
* An array of all kinds of addresses.
* Includes all of $to, $cc, $bcc.
*
* @see PHPMailer::$to
* @see PHPMailer::$cc
* @see PHPMailer::$bcc
*
* @var array
*/
protected $all_recipients = [];
/**
* An array of names and addresses queued for validation.
* In send(), valid and non duplicate entries are moved to $all_recipients
* and one of $to, $cc, or $bcc.
* This array is used only for addresses with IDN.
*
* @see PHPMailer::$to
* @see PHPMailer::$cc
* @see PHPMailer::$bcc
* @see PHPMailer::$all_recipients
*
* @var array
*/
protected $RecipientsQueue = [];
/**
* An array of reply-to names and addresses queued for validation.
* In send(), valid and non duplicate entries are moved to $ReplyTo.
* This array is used only for addresses with IDN.
*
* @see PHPMailer::$ReplyTo
*
* @var array
*/
protected $ReplyToQueue = [];
/**
* The array of attachments.
*
* @var array
*/
protected $attachment = [];
/**
* The array of custom headers.
*
* @var array
*/
protected $CustomHeader = [];
/**
* The most recent Message-ID (including angular brackets).
*
* @var string
*/
protected $lastMessageID = '';
/**
* The message's MIME type.
*
* @var string
*/
protected $message_type = '';
/**
* The array of MIME boundary strings.
*
* @var array
*/
protected $boundary = [];
/**
* The array of available text strings for the current language.
*
* @var array
*/
protected $language = [];
/**
* The number of errors encountered.
*
* @var int
*/
protected $error_count = 0;
/**
* The S/MIME certificate file path.
*
* @var string
*/
protected $sign_cert_file = '';
/**
* The S/MIME key file path.
*
* @var string
*/
protected $sign_key_file = '';
/**
* The optional S/MIME extra certificates ("CA Chain") file path.
*
* @var string
*/
protected $sign_extracerts_file = '';
/**
* The S/MIME password for the key.
* Used only if the key is encrypted.
*
* @var string
*/
protected $sign_key_pass = '';
/**
* Whether to throw exceptions for errors.
*
* @var bool
*/
protected $exceptions = false;
/**
* Unique ID used for message ID and boundaries.
*
* @var string
*/
protected $uniqueid = '';
/**
* The PHPMailer Version number.
*
* @var string
*/
const VERSION = '6.6.5';
/**
* Error severity: message only, continue processing.
*
* @var int
*/
const STOP_MESSAGE = 0;
/**
* Error severity: message, likely ok to continue processing.
*
* @var int
*/
const STOP_CONTINUE = 1;
/**
* Error severity: message, plus full stop, critical error reached.
*
* @var int
*/
const STOP_CRITICAL = 2;
/**
* The SMTP standard CRLF line break.
* If you want to change line break format, change static::$LE, not this.
*/
const CRLF = "\r\n";
/**
* "Folding White Space" a white space string used for line folding.
*/
const FWS = ' ';
/**
* SMTP RFC standard line ending; Carriage Return, Line Feed.
*
* @var string
*/
protected static $LE = self::CRLF;
/**
* The maximum line length supported by mail().
*
* Background: mail() will sometimes corrupt messages
* with headers headers longer than 65 chars, see #818.
*
* @var int
*/
const MAIL_MAX_LINE_LENGTH = 63;
/**
* The maximum line length allowed by RFC 2822 section 2.1.1.
*
* @var int
*/
const MAX_LINE_LENGTH = 998;
/**
* The lower maximum line length allowed by RFC 2822 section 2.1.1.
* This length does NOT include the line break
* 76 means that lines will be 77 or 78 chars depending on whether
* the line break format is LF or CRLF; both are valid.
*
* @var int
*/
const STD_LINE_LENGTH = 76;
/**
* Constructor.
*
* @param bool $exceptions Should we throw external exceptions?
*/
public function __construct($exceptions = null)
{
if (null !== $exceptions) {
$this->exceptions = (bool) $exceptions;
}
//Pick an appropriate debug output format automatically
$this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
}
/**
* Destructor.
*/
public function __destruct()
{
//Close any open SMTP connection nicely
$this->smtpClose();
}
/**
* Call mail() in a safe_mode-aware fashion.
* Also, unless sendmail_path points to sendmail (or something that
* claims to be sendmail), don't pass params (not a perfect fix,
* but it will do).
*
* @param string $to To
* @param string $subject Subject
* @param string $body Message Body
* @param string $header Additional Header(s)
* @param string|null $params Params
*
* @return bool
*/
private function mailPassthru($to, $subject, $body, $header, $params)
{
//Check overloading of mail function to avoid double-encoding
if (ini_get('mbstring.func_overload') & 1) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.mbstring_func_overloadDeprecated
$subject = $this->secureHeader($subject);
} else {
$subject = $this->encodeHeader($this->secureHeader($subject));
}
//Calling mail() with null params breaks
$this->edebug('Sending with mail()');
$this->edebug('Sendmail path: ' . ini_get('sendmail_path'));
$this->edebug("Envelope sender: {$this->Sender}");
$this->edebug("To: {$to}");
$this->edebug("Subject: {$subject}");
$this->edebug("Headers: {$header}");
if (!$this->UseSendmailOptions || null === $params) {
$result = @mail($to, $subject, $body, $header);
} else {
$this->edebug("Additional params: {$params}");
$result = @mail($to, $subject, $body, $header, $params);
}
$this->edebug('Result: ' . ($result ? 'true' : 'false'));
return $result;
}
/**
* Output debugging info via a user-defined method.
* Only generates output if debug output is enabled.
*
* @see PHPMailer::$Debugoutput
* @see PHPMailer::$SMTPDebug
*
* @param string $str
*/
protected function edebug($str)
{
if ($this->SMTPDebug <= 0) {
return;
}
//Is this a PSR-3 logger?
if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
$this->Debugoutput->debug($str);
return;
}
//Avoid clash with built-in function names
if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
return;
}
switch ($this->Debugoutput) {
case 'error_log':
//Don't output, just log
/** @noinspection ForgottenDebugOutputInspection */
error_log($str);
break;
case 'html':
//Cleans up output a bit for a better looking, HTML-safe output
echo htmlentities(
preg_replace('/[\r\n]+/', '', $str),
ENT_QUOTES,
'UTF-8'
), "<br>\n";
break;
case 'echo':
default:
//Normalize line breaks
$str = preg_replace('/\r\n|\r/m', "\n", $str);
echo gmdate('Y-m-d H:i:s'),
"\t",
//Trim trailing space
trim(
//Indent for readability, except for trailing break
str_replace(
"\n",
"\n \t ",
trim($str)
)
),
"\n";
}
}
/**
* Sets message type to HTML or plain.
*
* @param bool $isHtml True for HTML mode
*/
public function isHTML($isHtml = true)
{
if ($isHtml) {
$this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
} else {
$this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
}
}
/**
* Send messages using SMTP.
*/
public function isSMTP()
{
$this->Mailer = 'smtp';
}
/**
* Send messages using PHP's mail() function.
*/
public function isMail()
{
$this->Mailer = 'mail';
}
/**
* Send messages using $Sendmail.
*/
public function isSendmail()
{
$ini_sendmail_path = ini_get('sendmail_path');
if (false === stripos($ini_sendmail_path, 'sendmail')) {
$this->Sendmail = '/usr/sbin/sendmail';
} else {
$this->Sendmail = $ini_sendmail_path;
}
$this->Mailer = 'sendmail';
}
/**
* Send messages using qmail.
*/
public function isQmail()
{
$ini_sendmail_path = ini_get('sendmail_path');
if (false === stripos($ini_sendmail_path, 'qmail')) {
$this->Sendmail = '/var/qmail/bin/qmail-inject';
} else {
$this->Sendmail = $ini_sendmail_path;
}
$this->Mailer = 'qmail';
}
/**
* Add a "To" address.
*
* @param string $address The email address to send to
* @param string $name
*
* @throws Exception
*
* @return bool true on success, false if address already used or invalid in some way
*/
public function addAddress($address, $name = '')
{
return $this->addOrEnqueueAnAddress('to', $address, $name);
}
/**
* Add a "CC" address.
*
* @param string $address The email address to send to
* @param string $name
*
* @throws Exception
*
* @return bool true on success, false if address already used or invalid in some way
*/
public function addCC($address, $name = '')
{
return $this->addOrEnqueueAnAddress('cc', $address, $name);
}
/**
* Add a "BCC" address.
*
* @param string $address The email address to send to
* @param string $name
*
* @throws Exception
*
* @return bool true on success, false if address already used or invalid in some way
*/
public function addBCC($address, $name = '')
{
return $this->addOrEnqueueAnAddress('bcc', $address, $name);
}
/**
* Add a "Reply-To" address.
*
* @param string $address The email address to reply to
* @param string $name
*
* @throws Exception
*
* @return bool true on success, false if address already used or invalid in some way
*/
public function addReplyTo($address, $name = '')
{
return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
}
/**
* Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
* can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
* be modified after calling this function), addition of such addresses is delayed until send().
* Addresses that have been added already return false, but do not throw exceptions.
*
* @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo'
* @param string $address The email address
* @param string $name An optional username associated with the address
*
* @throws Exception
*
* @return bool true on success, false if address already used or invalid in some way
*/
protected function addOrEnqueueAnAddress($kind, $address, $name)
{
$pos = false;
if ($address !== null) {
$address = trim($address);
$pos = strrpos($address, '@');
}
if (false === $pos) {
//At-sign is missing.
$error_message = sprintf(
'%s (%s): %s',
$this->lang('invalid_address'),
$kind,
$address
);
$this->setError($error_message);
$this->edebug($error_message);
if ($this->exceptions) {
throw new Exception($error_message);
}
return false;
}
if ($name !== null && is_string($name)) {
$name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
} else {
$name = '';
}
$params = [$kind, $address, $name];
//Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
//Domain is assumed to be whatever is after the last @ symbol in the address
if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
if ('Reply-To' !== $kind) {
if (!array_key_exists($address, $this->RecipientsQueue)) {
$this->RecipientsQueue[$address] = $params;
return true;
}
} elseif (!array_key_exists($address, $this->ReplyToQueue)) {
$this->ReplyToQueue[$address] = $params;
return true;
}
return false;
}
//Immediately add standard addresses without IDN.
return call_user_func_array([$this, 'addAnAddress'], $params);
}
/**
* Add an address to one of the recipient arrays or to the ReplyTo array.
* Addresses that have been added already return false, but do not throw exceptions.
*
* @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo'
* @param string $address The email address to send, resp. to reply to
* @param string $name
*
* @throws Exception
*
* @return bool true on success, false if address already used or invalid in some way
*/
protected function addAnAddress($kind, $address, $name = '')
{
if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
$error_message = sprintf(
'%s: %s',
$this->lang('Invalid recipient kind'),
$kind
);
$this->setError($error_message);
$this->edebug($error_message);
if ($this->exceptions) {
throw new Exception($error_message);
}
return false;
}
if (!static::validateAddress($address)) {
$error_message = sprintf(
'%s (%s): %s',
$this->lang('invalid_address'),
$kind,
$address
);
$this->setError($error_message);
$this->edebug($error_message);
if ($this->exceptions) {
throw new Exception($error_message);
}
return false;
}
if ('Reply-To' !== $kind) {
if (!array_key_exists(strtolower($address), $this->all_recipients)) {
$this->{$kind}[] = [$address, $name];
$this->all_recipients[strtolower($address)] = true;
return true;
}
} elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
$this->ReplyTo[strtolower($address)] = [$address, $name];
return true;
}
return false;
}
/**
* Parse and validate a string containing one or more RFC822-style comma-separated email addresses
* of the form "display name <address>" into an array of name/address pairs.
* Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
* Note that quotes in the name part are removed.
*
* @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
*
* @param string $addrstr The address list string
* @param bool $useimap Whether to use the IMAP extension to parse the list
* @param string $charset The charset to use when decoding the address list string.
*
* @return array
*/
public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591)
{
$addresses = [];
if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
//Use this built-in parser if it's available
$list = imap_rfc822_parse_adrlist($addrstr, '');
// Clear any potential IMAP errors to get rid of notices being thrown at end of script.
imap_errors();
foreach ($list as $address) {
if (
'.SYNTAX-ERROR.' !== $address->host &&
static::validateAddress($address->mailbox . '@' . $address->host)
) {
//Decode the name part if it's present and encoded
if (
property_exists($address, 'personal') &&
//Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
defined('MB_CASE_UPPER') &&
preg_match('/^=\?.*\?=$/s', $address->personal)
) {
$origCharset = mb_internal_encoding();
mb_internal_encoding($charset);
//Undo any RFC2047-encoded spaces-as-underscores
$address->personal = str_replace('_', '=20', $address->personal);
//Decode the name
$address->personal = mb_decode_mimeheader($address->personal);
mb_internal_encoding($origCharset);
}
$addresses[] = [
'name' => (property_exists($address, 'personal') ? $address->personal : ''),
'address' => $address->mailbox . '@' . $address->host,
];
}
}
} else {
//Use this simpler parser
$list = explode(',', $addrstr);
foreach ($list as $address) {
$address = trim($address);
//Is there a separate name part?
if (strpos($address, '<') === false) {
//No separate name, just use the whole thing
if (static::validateAddress($address)) {
$addresses[] = [
'name' => '',
'address' => $address,
];
}
} else {
list($name, $email) = explode('<', $address);
$email = trim(str_replace('>', '', $email));
$name = trim($name);
if (static::validateAddress($email)) {
//Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
//If this name is encoded, decode it
if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) {
$origCharset = mb_internal_encoding();
mb_internal_encoding($charset);
//Undo any RFC2047-encoded spaces-as-underscores
$name = str_replace('_', '=20', $name);
//Decode the name
$name = mb_decode_mimeheader($name);
mb_internal_encoding($origCharset);
}
$addresses[] = [
//Remove any surrounding quotes and spaces from the name
'name' => trim($name, '\'" '),
'address' => $email,
];
}
}
}
}
return $addresses;
}
/**
* Set the From and FromName properties.
*
* @param string $address
* @param string $name
* @param bool $auto Whether to also set the Sender address, defaults to true
*
* @throws Exception
*
* @return bool
*/
public function setFrom($address, $name = '', $auto = true)
{
$address = trim((string)$address);
$name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
//Don't validate now addresses with IDN. Will be done in send().
$pos = strrpos($address, '@');
if (
(false === $pos)
|| ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
&& !static::validateAddress($address))
) {
$error_message = sprintf(
'%s (From): %s',
$this->lang('invalid_address'),
$address
);
$this->setError($error_message);
$this->edebug($error_message);
if ($this->exceptions) {
throw new Exception($error_message);
}
return false;
}
$this->From = $address;
$this->FromName = $name;
if ($auto && empty($this->Sender)) {
$this->Sender = $address;
}
return true;
}
/**
* Return the Message-ID header of the last email.
* Technically this is the value from the last time the headers were created,
* but it's also the message ID of the last sent message except in
* pathological cases.
*
* @return string
*/
public function getLastMessageID()
{
return $this->lastMessageID;
}
/**
* Check that a string looks like an email address.
* Validation patterns supported:
* * `auto` Pick best pattern automatically;
* * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
* * `pcre` Use old PCRE implementation;
* * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
* * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
* * `noregex` Don't use a regex: super fast, really dumb.
* Alternatively you may pass in a callable to inject your own validator, for example:
*
* ```php
* PHPMailer::validateAddress('user@example.com', function($address) {
* return (strpos($address, '@') !== false);
* });
* ```
*
* You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
*
* @param string $address The email address to check
* @param string|callable $patternselect Which pattern to use
*
* @return bool
*/
public static function validateAddress($address, $patternselect = null)
{
if (null === $patternselect) {
$patternselect = static::$validator;
}
//Don't allow strings as callables, see SECURITY.md and CVE-2021-3603
if (is_callable($patternselect) && !is_string($patternselect)) {
return call_user_func($patternselect, $address);
}
//Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
return false;
}
switch ($patternselect) {
case 'pcre': //Kept for BC
case 'pcre8':
/*
* A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
* is based.
* In addition to the addresses allowed by filter_var, also permits:
* * dotless domains: `a@b`
* * comments: `1234 @ local(blah) .machine .example`
* * quoted elements: `'"test blah"@example.org'`
* * numeric TLDs: `a@b.123`
* * unbracketed IPv4 literals: `a@192.168.0.1`
* * IPv6 literals: 'first.last@[IPv6:a1::]'
* Not all of these will necessarily work for sending!
*
* @see http://squiloople.com/2009/12/20/email-address-validation/
* @copyright 2009-2010 Michael Rushton
* Feel free to use and redistribute this code. But please keep this copyright notice.
*/
return (bool) preg_match(
'/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
'((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
'(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
'([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
'(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
'(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
'|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
'|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
'|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
$address
);
case 'html5':
/*
* This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
*
* @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
*/
return (bool) preg_match(
'/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
'[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
$address
);
case 'php':
default:
return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
}
}
/**
* Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
* `intl` and `mbstring` PHP extensions.
*
* @return bool `true` if required functions for IDN support are present
*/
public static function idnSupported()
{
return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');
}
/**
* Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
* Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
* This function silently returns unmodified address if:
* - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
* - Conversion to punycode is impossible (e.g. required PHP functions are not available)
* or fails for any reason (e.g. domain contains characters not allowed in an IDN).
*
* @see PHPMailer::$CharSet
*
* @param string $address The email address to convert
*
* @return string The encoded address in ASCII form
*/
public function punyencodeAddress($address)
{
//Verify we have required functions, CharSet, and at-sign.
$pos = strrpos($address, '@');
if (
!empty($this->CharSet) &&
false !== $pos &&
static::idnSupported()
) {
$domain = substr($address, ++$pos);
//Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
//Convert the domain from whatever charset it's in to UTF-8
$domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet);
//Ignore IDE complaints about this line - method signature changed in PHP 5.4
$errorcode = 0;
if (defined('INTL_IDNA_VARIANT_UTS46')) {
//Use the current punycode standard (appeared in PHP 7.2)
$punycode = idn_to_ascii(
$domain,
\IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI |
\IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII,
\INTL_IDNA_VARIANT_UTS46
);
} elseif (defined('INTL_IDNA_VARIANT_2003')) {
//Fall back to this old, deprecated/removed encoding
// phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated
$punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003);
} else {
//Fall back to a default we don't know about
// phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
$punycode = idn_to_ascii($domain, $errorcode);
}
if (false !== $punycode) {
return substr($address, 0, $pos) . $punycode;
}
}
}
return $address;
}
/**
* Create a message and send it.
* Uses the sending method specified by $Mailer.
*
* @throws Exception
*
* @return bool false on error - See the ErrorInfo property for details of the error
*/
public function send()
{
try {
if (!$this->preSend()) {
return false;
}
return $this->postSend();
} catch (Exception $exc) {
$this->mailHeader = '';
$this->setError($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return false;
}
}
/**
* Prepare a message for sending.
*
* @throws Exception
*
* @return bool
*/
public function preSend()
{
if (
'smtp' === $this->Mailer
|| ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))
) {
//SMTP mandates RFC-compliant line endings
//and it's also used with mail() on Windows
static::setLE(self::CRLF);
} else {
//Maintain backward compatibility with legacy Linux command line mailers
static::setLE(PHP_EOL);
}
//Check for buggy PHP versions that add a header with an incorrect line break
if (
'mail' === $this->Mailer
&& ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017)
|| (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103))
&& ini_get('mail.add_x_header') === '1'
&& stripos(PHP_OS, 'WIN') === 0
) {
trigger_error($this->lang('buggy_php'), E_USER_WARNING);
}
try {
$this->error_count = 0; //Reset errors
$this->mailHeader = '';
//Dequeue recipient and Reply-To addresses with IDN
foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
$params[1] = $this->punyencodeAddress($params[1]);
call_user_func_array([$this, 'addAnAddress'], $params);
}
if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
}
//Validate From, Sender, and ConfirmReadingTo addresses
foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
$this->{$address_kind} = trim($this->{$address_kind});
if (empty($this->{$address_kind})) {
continue;
}
$this->{$address_kind} = $this->punyencodeAddress($this->{$address_kind});
if (!static::validateAddress($this->{$address_kind})) {
$error_message = sprintf(
'%s (%s): %s',
$this->lang('invalid_address'),
$address_kind,
$this->{$address_kind}
);
$this->setError($error_message);
$this->edebug($error_message);
if ($this->exceptions) {
throw new Exception($error_message);
}
return false;
}
}
//Set whether the message is multipart/alternative
if ($this->alternativeExists()) {
$this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
}
$this->setMessageType();
//Refuse to send an empty message unless we are specifically allowing it
if (!$this->AllowEmpty && empty($this->Body)) {
throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
}
//Trim subject consistently
$this->Subject = trim($this->Subject);
//Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
$this->MIMEHeader = '';
$this->MIMEBody = $this->createBody();
//createBody may have added some headers, so retain them
$tempheaders = $this->MIMEHeader;
$this->MIMEHeader = $this->createHeader();
$this->MIMEHeader .= $tempheaders;
//To capture the complete message when using mail(), create
//an extra header list which createHeader() doesn't fold in
if ('mail' === $this->Mailer) {
if (count($this->to) > 0) {
$this->mailHeader .= $this->addrAppend('To', $this->to);
} else {
$this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
}
$this->mailHeader .= $this->headerLine(
'Subject',
$this->encodeHeader($this->secureHeader($this->Subject))
);
}
//Sign with DKIM if enabled
if (
!empty($this->DKIM_domain)
&& !empty($this->DKIM_selector)
&& (!empty($this->DKIM_private_string)
|| (!empty($this->DKIM_private)
&& static::isPermittedPath($this->DKIM_private)
&& file_exists($this->DKIM_private)
)
)
) {
$header_dkim = $this->DKIM_Add(
$this->MIMEHeader . $this->mailHeader,
$this->encodeHeader($this->secureHeader($this->Subject)),
$this->MIMEBody
);
$this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE .
static::normalizeBreaks($header_dkim) . static::$LE;
}
return true;
} catch (Exception $exc) {
$this->setError($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return false;
}
}
/**
* Actually send a message via the selected mechanism.
*
* @throws Exception
*
* @return bool
*/
public function postSend()
{
try {
//Choose the mailer and send through it
switch ($this->Mailer) {
case 'sendmail':
case 'qmail':
return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
case 'smtp':
return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
case 'mail':
return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
default:
$sendMethod = $this->Mailer . 'Send';
if (method_exists($this, $sendMethod)) {
return $this->{$sendMethod}($this->MIMEHeader, $this->MIMEBody);
}
return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
}
} catch (Exception $exc) {
if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true && $this->smtp->connected()) {
$this->smtp->reset();
}
$this->setError($exc->getMessage());
$this->edebug($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
}
return false;
}
/**
* Send mail using the $Sendmail program.
*
* @see PHPMailer::$Sendmail
*
* @param string $header The message headers
* @param string $body The message body
*
* @throws Exception
*
* @return bool
*/
protected function sendmailSend($header, $body)
{
if ($this->Mailer === 'qmail') {
$this->edebug('Sending with qmail');
} else {
$this->edebug('Sending with sendmail');
}
$header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
//This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
//A space after `-f` is optional, but there is a long history of its presence
//causing problems, so we don't use one
//Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
//Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
//Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
//Example problem: https://www.drupal.org/node/1057954
//PHP 5.6 workaround
$sendmail_from_value = ini_get('sendmail_from');
if (empty($this->Sender) && !empty($sendmail_from_value)) {
//PHP config has a sender address we can use
$this->Sender = ini_get('sendmail_from');
}
//CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
if ($this->Mailer === 'qmail') {
$sendmailFmt = '%s -f%s';
} else {
$sendmailFmt = '%s -oi -f%s -t';
}
} else {
//allow sendmail to choose a default envelope sender. It may
//seem preferable to force it to use the From header as with
//SMTP, but that introduces new problems (see
//<https://github.com/PHPMailer/PHPMailer/issues/2298>), and
//it has historically worked this way.
$sendmailFmt = '%s -oi -t';
}
$sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
$this->edebug('Sendmail path: ' . $this->Sendmail);
$this->edebug('Sendmail command: ' . $sendmail);
$this->edebug('Envelope sender: ' . $this->Sender);
$this->edebug("Headers: {$header}");
if ($this->SingleTo) {
foreach ($this->SingleToArray as $toAddr) {
$mail = @popen($sendmail, 'w');
if (!$mail) {
throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
}
$this->edebug("To: {$toAddr}");
fwrite($mail, 'To: ' . $toAddr . "\n");
fwrite($mail, $header);
fwrite($mail, $body);
$result = pclose($mail);
$addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
$this->doCallback(
($result === 0),
[[$addrinfo['address'], $addrinfo['name']]],
$this->cc,
$this->bcc,
$this->Subject,
$body,
$this->From,
[]
);
$this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
if (0 !== $result) {
throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
}
}
} else {
$mail = @popen($sendmail, 'w');
if (!$mail) {
throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
}
fwrite($mail, $header);
fwrite($mail, $body);
$result = pclose($mail);
$this->doCallback(
($result === 0),
$this->to,
$this->cc,
$this->bcc,
$this->Subject,
$body,
$this->From,
[]
);
$this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
if (0 !== $result) {
throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
}
}
return true;
}
/**
* Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
* Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
*
* @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
*
* @param string $string The string to be validated
*
* @return bool
*/
protected static function isShellSafe($string)
{
//It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg,
//but some hosting providers disable it, creating a security problem that we don't want to have to deal with,
//so we don't.
if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) {
return false;
}
if (
escapeshellcmd($string) !== $string
|| !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
) {
return false;
}
$length = strlen($string);
for ($i = 0; $i < $length; ++$i) {
$c = $string[$i];
//All other characters have a special meaning in at least one common shell, including = and +.
//Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
//Note that this does permit non-Latin alphanumeric characters based on the current locale.
if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
return false;
}
}
return true;
}
/**
* Check whether a file path is of a permitted type.
* Used to reject URLs and phar files from functions that access local file paths,
* such as addAttachment.
*
* @param string $path A relative or absolute path to a file
*
* @return bool
*/
protected static function isPermittedPath($path)
{
//Matches scheme definition from https://tools.ietf.org/html/rfc3986#section-3.1
return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path);
}
/**
* Check whether a file path is safe, accessible, and readable.
*
* @param string $path A relative or absolute path to a file
*
* @return bool
*/
protected static function fileIsAccessible($path)
{
if (!static::isPermittedPath($path)) {
return false;
}
$readable = is_file($path);
//If not a UNC path (expected to start with \\), check read permission, see #2069
if (strpos($path, '\\\\') !== 0) {
$readable = $readable && is_readable($path);
}
return $readable;
}
/**
* Send mail using the PHP mail() function.
*
* @see http://www.php.net/manual/en/book.mail.php
*
* @param string $header The message headers
* @param string $body The message body
*
* @throws Exception
*
* @return bool
*/
protected function mailSend($header, $body)
{
$header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
$toArr = [];
foreach ($this->to as $toaddr) {
$toArr[] = $this->addrFormat($toaddr);
}
$to = trim(implode(', ', $toArr));
//If there are no To-addresses (e.g. when sending only to BCC-addresses)
//the following should be added to get a correct DKIM-signature.
//Compare with $this->preSend()
if ($to === '') {
$to = 'undisclosed-recipients:;';
}
$params = null;
//This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
//A space after `-f` is optional, but there is a long history of its presence
//causing problems, so we don't use one
//Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
//Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
//Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
//Example problem: https://www.drupal.org/node/1057954
//CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
//PHP 5.6 workaround
$sendmail_from_value = ini_get('sendmail_from');
if (empty($this->Sender) && !empty($sendmail_from_value)) {
//PHP config has a sender address we can use
$this->Sender = ini_get('sendmail_from');
}
if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
if (self::isShellSafe($this->Sender)) {
$params = sprintf('-f%s', $this->Sender);
}
$old_from = ini_get('sendmail_from');
ini_set('sendmail_from', $this->Sender);
}
$result = false;
if ($this->SingleTo && count($toArr) > 1) {
foreach ($toArr as $toAddr) {
$result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
$addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
$this->doCallback(
$result,
[[$addrinfo['address'], $addrinfo['name']]],
$this->cc,
$this->bcc,
$this->Subject,
$body,
$this->From,
[]
);
}
} else {
$result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
$this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
}
if (isset($old_from)) {
ini_set('sendmail_from', $old_from);
}
if (!$result) {
throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
}
return true;
}
/**
* Get an instance to use for SMTP operations.
* Override this function to load your own SMTP implementation,
* or set one with setSMTPInstance.
*
* @return SMTP
*/
public function getSMTPInstance()
{
if (!is_object($this->smtp)) {
$this->smtp = new SMTP();
}
return $this->smtp;
}
/**
* Provide an instance to use for SMTP operations.
*
* @return SMTP
*/
public function setSMTPInstance(SMTP $smtp)
{
$this->smtp = $smtp;
return $this->smtp;
}
/**
* Send mail via SMTP.
* Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
*
* @see PHPMailer::setSMTPInstance() to use a different class.
*
* @uses \PHPMailer\PHPMailer\SMTP
*
* @param string $header The message headers
* @param string $body The message body
*
* @throws Exception
*
* @return bool
*/
protected function smtpSend($header, $body)
{
$header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
$bad_rcpt = [];
if (!$this->smtpConnect($this->SMTPOptions)) {
throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
}
//Sender already validated in preSend()
if ('' === $this->Sender) {
$smtp_from = $this->From;
} else {
$smtp_from = $this->Sender;
}
if (!$this->smtp->mail($smtp_from)) {
$this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
}
$callbacks = [];
//Attempt to send to all recipients
foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
foreach ($togroup as $to) {
if (!$this->smtp->recipient($to[0], $this->dsn)) {
$error = $this->smtp->getError();
$bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
$isSent = false;
} else {
$isSent = true;
}
$callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]];
}
}
//Only send the DATA command if we have viable recipients
if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {
throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
}
$smtp_transaction_id = $this->smtp->getLastTransactionID();
if ($this->SMTPKeepAlive) {
$this->smtp->reset();
} else {
$this->smtp->quit();
$this->smtp->close();
}
foreach ($callbacks as $cb) {
$this->doCallback(
$cb['issent'],
[[$cb['to'], $cb['name']]],
[],
[],
$this->Subject,
$body,
$this->From,
['smtp_transaction_id' => $smtp_transaction_id]
);
}
//Create error message for any bad addresses
if (count($bad_rcpt) > 0) {
$errstr = '';
foreach ($bad_rcpt as $bad) {
$errstr .= $bad['to'] . ': ' . $bad['error'];
}
throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE);
}
return true;
}
/**
* Initiate a connection to an SMTP server.
* Returns false if the operation failed.
*
* @param array $options An array of options compatible with stream_context_create()
*
* @throws Exception
*
* @uses \PHPMailer\PHPMailer\SMTP
*
* @return bool
*/
public function smtpConnect($options = null)
{
if (null === $this->smtp) {
$this->smtp = $this->getSMTPInstance();
}
//If no options are provided, use whatever is set in the instance
if (null === $options) {
$options = $this->SMTPOptions;
}
//Already connected?
if ($this->smtp->connected()) {
return true;
}
$this->smtp->setTimeout($this->Timeout);
$this->smtp->setDebugLevel($this->SMTPDebug);
$this->smtp->setDebugOutput($this->Debugoutput);
$this->smtp->setVerp($this->do_verp);
if ($this->Host === null) {
$this->Host = 'localhost';
}
$hosts = explode(';', $this->Host);
$lastexception = null;
foreach ($hosts as $hostentry) {
$hostinfo = [];
if (
!preg_match(
'/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
trim($hostentry),
$hostinfo
)
) {
$this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));
//Not a valid host entry
continue;
}
//$hostinfo[1]: optional ssl or tls prefix
//$hostinfo[2]: the hostname
//$hostinfo[3]: optional port number
//The host string prefix can temporarily override the current setting for SMTPSecure
//If it's not specified, the default value is used
//Check the host name is a valid name or IP address before trying to use it
if (!static::isValidHost($hostinfo[2])) {
$this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);
continue;
}
$prefix = '';
$secure = $this->SMTPSecure;
$tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
$prefix = 'ssl://';
$tls = false; //Can't have SSL and TLS at the same time
$secure = static::ENCRYPTION_SMTPS;
} elseif ('tls' === $hostinfo[1]) {
$tls = true;
//TLS doesn't use a prefix
$secure = static::ENCRYPTION_STARTTLS;
}
//Do we need the OpenSSL extension?
$sslext = defined('OPENSSL_ALGO_SHA256');
if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
//Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
if (!$sslext) {
throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
}
}
$host = $hostinfo[2];
$port = $this->Port;
if (
array_key_exists(3, $hostinfo) &&
is_numeric($hostinfo[3]) &&
$hostinfo[3] > 0 &&
$hostinfo[3] < 65536
) {
$port = (int) $hostinfo[3];
}
if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
try {
if ($this->Helo) {
$hello = $this->Helo;
} else {
$hello = $this->serverHostname();
}
$this->smtp->hello($hello);
//Automatically enable TLS encryption if:
//* it's not disabled
//* we have openssl extension
//* we are not already using SSL
//* the server offers STARTTLS
if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) {
$tls = true;
}
if ($tls) {
if (!$this->smtp->startTLS()) {
$message = $this->getSmtpErrorMessage('connect_host');
throw new Exception($message);
}
//We must resend EHLO after TLS negotiation
$this->smtp->hello($hello);
}
if (
$this->SMTPAuth && !$this->smtp->authenticate(
$this->Username,
$this->Password,
$this->AuthType,
$this->oauth
)
) {
throw new Exception($this->lang('authenticate'));
}
return true;
} catch (Exception $exc) {
$lastexception = $exc;
$this->edebug($exc->getMessage());
//We must have connected, but then failed TLS or Auth, so close connection nicely
$this->smtp->quit();
}
}
}
//If we get here, all connection attempts have failed, so close connection hard
$this->smtp->close();
//As we've caught all exceptions, just report whatever the last one was
if ($this->exceptions && null !== $lastexception) {
throw $lastexception;
}
if ($this->exceptions) {
// no exception was thrown, likely $this->smtp->connect() failed
$message = $this->getSmtpErrorMessage('connect_host');
throw new Exception($message);
}
return false;
}
/**
* Close the active SMTP session if one exists.
*/
public function smtpClose()
{
if ((null !== $this->smtp) && $this->smtp->connected()) {
$this->smtp->quit();
$this->smtp->close();
}
}
/**
* Set the language for error messages.
* The default language is English.
*
* @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr")
* Optionally, the language code can be enhanced with a 4-character
* script annotation and/or a 2-character country annotation.
* @param string $lang_path Path to the language file directory, with trailing separator (slash)
* Do not set this from user input!
*
* @return bool Returns true if the requested language was loaded, false otherwise.
*/
public function setLanguage($langcode = 'en', $lang_path = '')
{
//Backwards compatibility for renamed language codes
$renamed_langcodes = [
'br' => 'pt_br',
'cz' => 'cs',
'dk' => 'da',
'no' => 'nb',
'se' => 'sv',
'rs' => 'sr',
'tg' => 'tl',
'am' => 'hy',
];
if (array_key_exists($langcode, $renamed_langcodes)) {
$langcode = $renamed_langcodes[$langcode];
}
//Define full set of translatable strings in English
$PHPMAILER_LANG = [
'authenticate' => 'SMTP Error: Could not authenticate.',
'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' .
' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
'data_not_accepted' => 'SMTP Error: data not accepted.',
'empty_message' => 'Message body empty',
'encoding' => 'Unknown encoding: ',
'execute' => 'Could not execute: ',
'extension_missing' => 'Extension missing: ',
'file_access' => 'Could not access file: ',
'file_open' => 'File Error: Could not open file: ',
'from_failed' => 'The following From address failed: ',
'instantiate' => 'Could not instantiate mail function.',
'invalid_address' => 'Invalid address: ',
'invalid_header' => 'Invalid header name or value',
'invalid_hostentry' => 'Invalid hostentry: ',
'invalid_host' => 'Invalid host: ',
'mailer_not_supported' => ' mailer is not supported.',
'provide_address' => 'You must provide at least one recipient email address.',
'recipients_failed' => 'SMTP Error: The following recipients failed: ',
'signing' => 'Signing Error: ',
'smtp_code' => 'SMTP code: ',
'smtp_code_ex' => 'Additional SMTP info: ',
'smtp_connect_failed' => 'SMTP connect() failed.',
'smtp_detail' => 'Detail: ',
'smtp_error' => 'SMTP server error: ',
'variable_set' => 'Cannot set or reset variable: ',
];
if (empty($lang_path)) {
//Calculate an absolute path so it can work if CWD is not here
$lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
}
//Validate $langcode
$foundlang = true;
$langcode = strtolower($langcode);
if (
!preg_match('/^(?P<lang>[a-z]{2})(?P<script>_[a-z]{4})?(?P<country>_[a-z]{2})?$/', $langcode, $matches)
&& $langcode !== 'en'
) {
$foundlang = false;
$langcode = 'en';
}
//There is no English translation file
if ('en' !== $langcode) {
$langcodes = [];
if (!empty($matches['script']) && !empty($matches['country'])) {
$langcodes[] = $matches['lang'] . $matches['script'] . $matches['country'];
}
if (!empty($matches['country'])) {
$langcodes[] = $matches['lang'] . $matches['country'];
}
if (!empty($matches['script'])) {
$langcodes[] = $matches['lang'] . $matches['script'];
}
$langcodes[] = $matches['lang'];
//Try and find a readable language file for the requested language.
$foundFile = false;
foreach ($langcodes as $code) {
$lang_file = $lang_path . 'phpmailer.lang-' . $code . '.php';
if (static::fileIsAccessible($lang_file)) {
$foundFile = true;
break;
}
}
if ($foundFile === false) {
$foundlang = false;
} else {
$lines = file($lang_file);
foreach ($lines as $line) {
//Translation file lines look like this:
//$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.';
//These files are parsed as text and not PHP so as to avoid the possibility of code injection
//See https://blog.stevenlevithan.com/archives/match-quoted-string
$matches = [];
if (
preg_match(
'/^\$PHPMAILER_LANG\[\'([a-z\d_]+)\'\]\s*=\s*(["\'])(.+)*?\2;/',
$line,
$matches
) &&
//Ignore unknown translation keys
array_key_exists($matches[1], $PHPMAILER_LANG)
) {
//Overwrite language-specific strings so we'll never have missing translation keys.
$PHPMAILER_LANG[$matches[1]] = (string)$matches[3];
}
}
}
}
$this->language = $PHPMAILER_LANG;
return $foundlang; //Returns false if language not found
}
/**
* Get the array of strings for the current language.
*
* @return array
*/
public function getTranslations()
{
if (empty($this->language)) {
$this->setLanguage(); // Set the default language.
}
return $this->language;
}
/**
* Create recipient headers.
*
* @param string $type
* @param array $addr An array of recipients,
* where each recipient is a 2-element indexed array with element 0 containing an address
* and element 1 containing a name, like:
* [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']]
*
* @return string
*/
public function addrAppend($type, $addr)
{
$addresses = [];
foreach ($addr as $address) {
$addresses[] = $this->addrFormat($address);
}
return $type . ': ' . implode(', ', $addresses) . static::$LE;
}
/**
* Format an address for use in a message header.
*
* @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
* ['joe@example.com', 'Joe User']
*
* @return string
*/
public function addrFormat($addr)
{
if (empty($addr[1])) { //No name provided
return $this->secureHeader($addr[0]);
}
return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .
' <' . $this->secureHeader($addr[0]) . '>';
}
/**
* Word-wrap message.
* For use with mailers that do not automatically perform wrapping
* and for quoted-printable encoded messages.
* Original written by philippe.
*
* @param string $message The message to wrap
* @param int $length The line length to wrap to
* @param bool $qp_mode Whether to run in Quoted-Printable mode
*
* @return string
*/
public function wrapText($message, $length, $qp_mode = false)
{
if ($qp_mode) {
$soft_break = sprintf(' =%s', static::$LE);
} else {
$soft_break = static::$LE;
}
//If utf-8 encoding is used, we will need to make sure we don't
//split multibyte characters when we wrap
$is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);
$lelen = strlen(static::$LE);
$crlflen = strlen(static::$LE);
$message = static::normalizeBreaks($message);
//Remove a trailing line break
if (substr($message, -$lelen) === static::$LE) {
$message = substr($message, 0, -$lelen);
}
//Split message into lines
$lines = explode(static::$LE, $message);
//Message will be rebuilt in here
$message = '';
foreach ($lines as $line) {
$words = explode(' ', $line);
$buf = '';
$firstword = true;
foreach ($words as $word) {
if ($qp_mode && (strlen($word) > $length)) {
$space_left = $length - strlen($buf) - $crlflen;
if (!$firstword) {
if ($space_left > 20) {
$len = $space_left;
if ($is_utf8) {
$len = $this->utf8CharBoundary($word, $len);
} elseif ('=' === substr($word, $len - 1, 1)) {
--$len;
} elseif ('=' === substr($word, $len - 2, 1)) {
$len -= 2;
}
$part = substr($word, 0, $len);
$word = substr($word, $len);
$buf .= ' ' . $part;
$message .= $buf . sprintf('=%s', static::$LE);
} else {
$message .= $buf . $soft_break;
}
$buf = '';
}
while ($word !== '') {
if ($length <= 0) {
break;
}
$len = $length;
if ($is_utf8) {
$len = $this->utf8CharBoundary($word, $len);
} elseif ('=' === substr($word, $len - 1, 1)) {
--$len;
} elseif ('=' === substr($word, $len - 2, 1)) {
$len -= 2;
}
$part = substr($word, 0, $len);
$word = (string) substr($word, $len);
if ($word !== '') {
$message .= $part . sprintf('=%s', static::$LE);
} else {
$buf = $part;
}
}
} else {
$buf_o = $buf;
if (!$firstword) {
$buf .= ' ';
}
$buf .= $word;
if ('' !== $buf_o && strlen($buf) > $length) {
$message .= $buf_o . $soft_break;
$buf = $word;
}
}
$firstword = false;
}
$message .= $buf . static::$LE;
}
return $message;
}
/**
* Find the last character boundary prior to $maxLength in a utf-8
* quoted-printable encoded string.
* Original written by Colin Brown.
*
* @param string $encodedText utf-8 QP text
* @param int $maxLength Find the last character boundary prior to this length
*
* @return int
*/
public function utf8CharBoundary($encodedText, $maxLength)
{
$foundSplitPos = false;
$lookBack = 3;
while (!$foundSplitPos) {
$lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
$encodedCharPos = strpos($lastChunk, '=');
if (false !== $encodedCharPos) {
//Found start of encoded character byte within $lookBack block.
//Check the encoded byte value (the 2 chars after the '=')
$hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
$dec = hexdec($hex);
if ($dec < 128) {
//Single byte character.
//If the encoded char was found at pos 0, it will fit
//otherwise reduce maxLength to start of the encoded char
if ($encodedCharPos > 0) {
$maxLength -= $lookBack - $encodedCharPos;
}
$foundSplitPos = true;
} elseif ($dec >= 192) {
//First byte of a multi byte character
//Reduce maxLength to split at start of character
$maxLength -= $lookBack - $encodedCharPos;
$foundSplitPos = true;
} elseif ($dec < 192) {
//Middle byte of a multi byte character, look further back
$lookBack += 3;
}
} else {
//No encoded character found
$foundSplitPos = true;
}
}
return $maxLength;
}
/**
* Apply word wrapping to the message body.
* Wraps the message body to the number of chars set in the WordWrap property.
* You should only do this to plain-text bodies as wrapping HTML tags may break them.
* This is called automatically by createBody(), so you don't need to call it yourself.
*/
public function setWordWrap()
{
if ($this->WordWrap < 1) {
return;
}
switch ($this->message_type) {
case 'alt':
case 'alt_inline':
case 'alt_attach':
case 'alt_inline_attach':
$this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
break;
default:
$this->Body = $this->wrapText($this->Body, $this->WordWrap);
break;
}
}
/**
* Assemble message headers.
*
* @return string The assembled headers
*/
public function createHeader()
{
$result = '';
$result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);
//The To header is created automatically by mail(), so needs to be omitted here
if ('mail' !== $this->Mailer) {
if ($this->SingleTo) {
foreach ($this->to as $toaddr) {
$this->SingleToArray[] = $this->addrFormat($toaddr);
}
} elseif (count($this->to) > 0) {
$result .= $this->addrAppend('To', $this->to);
} elseif (count($this->cc) === 0) {
$result .= $this->headerLine('To', 'undisclosed-recipients:;');
}
}
$result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
//sendmail and mail() extract Cc from the header before sending
if (count($this->cc) > 0) {
$result .= $this->addrAppend('Cc', $this->cc);
}
//sendmail and mail() extract Bcc from the header before sending
if (
(
'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
)
&& count($this->bcc) > 0
) {
$result .= $this->addrAppend('Bcc', $this->bcc);
}
if (count($this->ReplyTo) > 0) {
$result .= $this->addrAppend('Reply-To', $this->ReplyTo);
}
//mail() sets the subject itself
if ('mail' !== $this->Mailer) {
$result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
}
//Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
//https://tools.ietf.org/html/rfc5322#section-3.6.4
if (
'' !== $this->MessageID &&
preg_match(
'/^<((([a-z\d!#$%&\'*+\/=?^_`{|}~-]+(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)' .
'|("(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]|[\x21\x23-\x5B\x5D-\x7E])' .
'|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*"))@(([a-z\d!#$%&\'*+\/=?^_`{|}~-]+' .
'(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)|(\[(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]' .
'|[\x21-\x5A\x5E-\x7E])|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*\])))>$/Di',
$this->MessageID
)
) {
$this->lastMessageID = $this->MessageID;
} else {
$this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
}
$result .= $this->headerLine('Message-ID', $this->lastMessageID);
if (null !== $this->Priority) {
$result .= $this->headerLine('X-Priority', $this->Priority);
}
if ('' === $this->XMailer) {
//Empty string for default X-Mailer header
$result .= $this->headerLine(
'X-Mailer',
'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
);
} elseif (is_string($this->XMailer) && trim($this->XMailer) !== '') {
//Some string
$result .= $this->headerLine('X-Mailer', trim($this->XMailer));
} //Other values result in no X-Mailer header
if ('' !== $this->ConfirmReadingTo) {
$result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
}
//Add custom headers
foreach ($this->CustomHeader as $header) {
$result .= $this->headerLine(
trim($header[0]),
$this->encodeHeader(trim($header[1]))
);
}
if (!$this->sign_key_file) {
$result .= $this->headerLine('MIME-Version', '1.0');
$result .= $this->getMailMIME();
}
return $result;
}
/**
* Get the message MIME type headers.
*
* @return string
*/
public function getMailMIME()
{
$result = '';
$ismultipart = true;
switch ($this->message_type) {
case 'inline':
$result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
$result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
break;
case 'attach':
case 'inline_attach':
case 'alt_attach':
case 'alt_inline_attach':
$result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';');
$result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
break;
case 'alt':
case 'alt_inline':
$result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
$result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
break;
default:
//Catches case 'plain': and case '':
$result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
$ismultipart = false;
break;
}
//RFC1341 part 5 says 7bit is assumed if not specified
if (static::ENCODING_7BIT !== $this->Encoding) {
//RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
if ($ismultipart) {
if (static::ENCODING_8BIT === $this->Encoding) {
$result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
}
//The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
} else {
$result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
}
}
return $result;
}
/**
* Returns the whole MIME message.
* Includes complete headers and body.
* Only valid post preSend().
*
* @see PHPMailer::preSend()
*
* @return string
*/
public function getSentMIMEMessage()
{
return static::stripTrailingWSP($this->MIMEHeader . $this->mailHeader) .
static::$LE . static::$LE . $this->MIMEBody;
}
/**
* Create a unique ID to use for boundaries.
*
* @return string
*/
protected function generateId()
{
$len = 32; //32 bytes = 256 bits
$bytes = '';
if (function_exists('random_bytes')) {
try {
$bytes = random_bytes($len);
} catch (\Exception $e) {
//Do nothing
}
} elseif (function_exists('openssl_random_pseudo_bytes')) {
/** @noinspection CryptographicallySecureRandomnessInspection */
$bytes = openssl_random_pseudo_bytes($len);
}
if ($bytes === '') {
//We failed to produce a proper random string, so make do.
//Use a hash to force the length to the same as the other methods
$bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
}
//We don't care about messing up base64 format here, just want a random string
return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
}
/**
* Assemble the message body.
* Returns an empty string on failure.
*
* @throws Exception
*
* @return string The assembled message body
*/
public function createBody()
{
$body = '';
//Create unique IDs and preset boundaries
$this->uniqueid = $this->generateId();
$this->boundary[1] = 'b1_' . $this->uniqueid;
$this->boundary[2] = 'b2_' . $this->uniqueid;
$this->boundary[3] = 'b3_' . $this->uniqueid;
if ($this->sign_key_file) {
$body .= $this->getMailMIME() . static::$LE;
}
$this->setWordWrap();
$bodyEncoding = $this->Encoding;
$bodyCharSet = $this->CharSet;
//Can we do a 7-bit downgrade?
if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) {
$bodyEncoding = static::ENCODING_7BIT;
//All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
$bodyCharSet = static::CHARSET_ASCII;
}
//If lines are too long, and we're not already using an encoding that will shorten them,
//change to quoted-printable transfer encoding for the body part only
if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) {
$bodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
}
$altBodyEncoding = $this->Encoding;
$altBodyCharSet = $this->CharSet;
//Can we do a 7-bit downgrade?
if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) {
$altBodyEncoding = static::ENCODING_7BIT;
//All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
$altBodyCharSet = static::CHARSET_ASCII;
}
//If lines are too long, and we're not already using an encoding that will shorten them,
//change to quoted-printable transfer encoding for the alt body part only
if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) {
$altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
}
//Use this as a preamble in all multipart message types
$mimepre = 'This is a multi-part message in MIME format.' . static::$LE . static::$LE;
switch ($this->message_type) {
case 'inline':
$body .= $mimepre;
$body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
$body .= $this->encodeString($this->Body, $bodyEncoding);
$body .= static::$LE;
$body .= $this->attachAll('inline', $this->boundary[1]);
break;
case 'attach':
$body .= $mimepre;
$body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
$body .= $this->encodeString($this->Body, $bodyEncoding);
$body .= static::$LE;
$body .= $this->attachAll('attachment', $this->boundary[1]);
break;
case 'inline_attach':
$body .= $mimepre;
$body .= $this->textLine('--' . $this->boundary[1]);
$body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
$body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
$body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
$body .= static::$LE;
$body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
$body .= $this->encodeString($this->Body, $bodyEncoding);
$body .= static::$LE;
$body .= $this->attachAll('inline', $this->boundary[2]);
$body .= static::$LE;
$body .= $this->attachAll('attachment', $this->boundary[1]);
break;
case 'alt':
$body .= $mimepre;
$body .= $this->getBoundary(
$this->boundary[1],
$altBodyCharSet,
static::CONTENT_TYPE_PLAINTEXT,
$altBodyEncoding
);
$body .= $this->encodeString($this->AltBody, $altBodyEncoding);
$body .= static::$LE;
$body .= $this->getBoundary(
$this->boundary[1],
$bodyCharSet,
static::CONTENT_TYPE_TEXT_HTML,
$bodyEncoding
);
$body .= $this->encodeString($this->Body, $bodyEncoding);
$body .= static::$LE;
if (!empty($this->Ical)) {
$method = static::ICAL_METHOD_REQUEST;
foreach (static::$IcalMethods as $imethod) {
if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
$method = $imethod;
break;
}
}
$body .= $this->getBoundary(
$this->boundary[1],
'',
static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
''
);
$body .= $this->encodeString($this->Ical, $this->Encoding);
$body .= static::$LE;
}
$body .= $this->endBoundary($this->boundary[1]);
break;
case 'alt_inline':
$body .= $mimepre;
$body .= $this->getBoundary(
$this->boundary[1],
$altBodyCharSet,
static::CONTENT_TYPE_PLAINTEXT,
$altBodyEncoding
);
$body .= $this->encodeString($this->AltBody, $altBodyEncoding);
$body .= static::$LE;
$body .= $this->textLine('--' . $this->boundary[1]);
$body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
$body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
$body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
$body .= static::$LE;
$body .= $this->getBoundary(
$this->boundary[2],
$bodyCharSet,
static::CONTENT_TYPE_TEXT_HTML,
$bodyEncoding
);
$body .= $this->encodeString($this->Body, $bodyEncoding);
$body .= static::$LE;
$body .= $this->attachAll('inline', $this->boundary[2]);
$body .= static::$LE;
$body .= $this->endBoundary($this->boundary[1]);
break;
case 'alt_attach':
$body .= $mimepre;
$body .= $this->textLine('--' . $this->boundary[1]);
$body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
$body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
$body .= static::$LE;
$body .= $this->getBoundary(
$this->boundary[2],
$altBodyCharSet,
static::CONTENT_TYPE_PLAINTEXT,
$altBodyEncoding
);
$body .= $this->encodeString($this->AltBody, $altBodyEncoding);
$body .= static::$LE;
$body .= $this->getBoundary(
$this->boundary[2],
$bodyCharSet,
static::CONTENT_TYPE_TEXT_HTML,
$bodyEncoding
);
$body .= $this->encodeString($this->Body, $bodyEncoding);
$body .= static::$LE;
if (!empty($this->Ical)) {
$method = static::ICAL_METHOD_REQUEST;
foreach (static::$IcalMethods as $imethod) {
if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
$method = $imethod;
break;
}
}
$body .= $this->getBoundary(
$this->boundary[2],
'',
static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
''
);
$body .= $this->encodeString($this->Ical, $this->Encoding);
}
$body .= $this->endBoundary($this->boundary[2]);
$body .= static::$LE;
$body .= $this->attachAll('attachment', $this->boundary[1]);
break;
case 'alt_inline_attach':
$body .= $mimepre;
$body .= $this->textLine('--' . $this->boundary[1]);
$body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
$body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
$body .= static::$LE;
$body .= $this->getBoundary(
$this->boundary[2],
$altBodyCharSet,
static::CONTENT_TYPE_PLAINTEXT,
$altBodyEncoding
);
$body .= $this->encodeString($this->AltBody, $altBodyEncoding);
$body .= static::$LE;
$body .= $this->textLine('--' . $this->boundary[2]);
$body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
$body .= $this->textLine(' boundary="' . $this->boundary[3] . '";');
$body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
$body .= static::$LE;
$body .= $this->getBoundary(
$this->boundary[3],
$bodyCharSet,
static::CONTENT_TYPE_TEXT_HTML,
$bodyEncoding
);
$body .= $this->encodeString($this->Body, $bodyEncoding);
$body .= static::$LE;
$body .= $this->attachAll('inline', $this->boundary[3]);
$body .= static::$LE;
$body .= $this->endBoundary($this->boundary[2]);
$body .= static::$LE;
$body .= $this->attachAll('attachment', $this->boundary[1]);
break;
default:
//Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
//Reset the `Encoding` property in case we changed it for line length reasons
$this->Encoding = $bodyEncoding;
$body .= $this->encodeString($this->Body, $this->Encoding);
break;
}
if ($this->isError()) {
$body = '';
if ($this->exceptions) {
throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
}
} elseif ($this->sign_key_file) {
try {
if (!defined('PKCS7_TEXT')) {
throw new Exception($this->lang('extension_missing') . 'openssl');
}
$file = tempnam(sys_get_temp_dir(), 'srcsign');
$signed = tempnam(sys_get_temp_dir(), 'mailsign');
file_put_contents($file, $body);
//Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
if (empty($this->sign_extracerts_file)) {
$sign = @openssl_pkcs7_sign(
$file,
$signed,
'file://' . realpath($this->sign_cert_file),
['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
[]
);
} else {
$sign = @openssl_pkcs7_sign(
$file,
$signed,
'file://' . realpath($this->sign_cert_file),
['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
[],
PKCS7_DETACHED,
$this->sign_extracerts_file
);
}
@unlink($file);
if ($sign) {
$body = file_get_contents($signed);
@unlink($signed);
//The message returned by openssl contains both headers and body, so need to split them up
$parts = explode("\n\n", $body, 2);
$this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
$body = $parts[1];
} else {
@unlink($signed);
throw new Exception($this->lang('signing') . openssl_error_string());
}
} catch (Exception $exc) {
$body = '';
if ($this->exceptions) {
throw $exc;
}
}
}
return $body;
}
/**
* Return the start of a message boundary.
*
* @param string $boundary
* @param string $charSet
* @param string $contentType
* @param string $encoding
*
* @return string
*/
protected function getBoundary($boundary, $charSet, $contentType, $encoding)
{
$result = '';
if ('' === $charSet) {
$charSet = $this->CharSet;
}
if ('' === $contentType) {
$contentType = $this->ContentType;
}
if ('' === $encoding) {
$encoding = $this->Encoding;
}
$result .= $this->textLine('--' . $boundary);
$result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
$result .= static::$LE;
//RFC1341 part 5 says 7bit is assumed if not specified
if (static::ENCODING_7BIT !== $encoding) {
$result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
}
$result .= static::$LE;
return $result;
}
/**
* Return the end of a message boundary.
*
* @param string $boundary
*
* @return string
*/
protected function endBoundary($boundary)
{
return static::$LE . '--' . $boundary . '--' . static::$LE;
}
/**
* Set the message type.
* PHPMailer only supports some preset message types, not arbitrary MIME structures.
*/
protected function setMessageType()
{
$type = [];
if ($this->alternativeExists()) {
$type[] = 'alt';
}
if ($this->inlineImageExists()) {
$type[] = 'inline';
}
if ($this->attachmentExists()) {
$type[] = 'attach';
}
$this->message_type = implode('_', $type);
if ('' === $this->message_type) {
//The 'plain' message_type refers to the message having a single body element, not that it is plain-text
$this->message_type = 'plain';
}
}
/**
* Format a header line.
*
* @param string $name
* @param string|int $value
*
* @return string
*/
public function headerLine($name, $value)
{
return $name . ': ' . $value . static::$LE;
}
/**
* Return a formatted mail line.
*
* @param string $value
*
* @return string
*/
public function textLine($value)
{
return $value . static::$LE;
}
/**
* Add an attachment from a path on the filesystem.
* Never use a user-supplied path to a file!
* Returns false if the file could not be found or read.
* Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client.
* If you need to do that, fetch the resource yourself and pass it in via a local file or string.
*
* @param string $path Path to the attachment
* @param string $name Overrides the attachment name
* @param string $encoding File encoding (see $Encoding)
* @param string $type MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified
* @param string $disposition Disposition to use
*
* @throws Exception
*
* @return bool
*/
public function addAttachment(
$path,
$name = '',
$encoding = self::ENCODING_BASE64,
$type = '',
$disposition = 'attachment'
) {
try {
if (!static::fileIsAccessible($path)) {
throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
}
//If a MIME type is not specified, try to work it out from the file name
if ('' === $type) {
$type = static::filenameToType($path);
}
$filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
if ('' === $name) {
$name = $filename;
}
if (!$this->validateEncoding($encoding)) {
throw new Exception($this->lang('encoding') . $encoding);
}
$this->attachment[] = [
0 => $path,
1 => $filename,
2 => $name,
3 => $encoding,
4 => $type,
5 => false, //isStringAttachment
6 => $disposition,
7 => $name,
];
} catch (Exception $exc) {
$this->setError($exc->getMessage());
$this->edebug($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return false;
}
return true;
}
/**
* Return the array of attachments.
*
* @return array
*/
public function getAttachments()
{
return $this->attachment;
}
/**
* Attach all file, string, and binary attachments to the message.
* Returns an empty string on failure.
*
* @param string $disposition_type
* @param string $boundary
*
* @throws Exception
*
* @return string
*/
protected function attachAll($disposition_type, $boundary)
{
//Return text of body
$mime = [];
$cidUniq = [];
$incl = [];
//Add all attachments
foreach ($this->attachment as $attachment) {
//Check if it is a valid disposition_filter
if ($attachment[6] === $disposition_type) {
//Check for string attachment
$string = '';
$path = '';
$bString = $attachment[5];
if ($bString) {
$string = $attachment[0];
} else {
$path = $attachment[0];
}
$inclhash = hash('sha256', serialize($attachment));
if (in_array($inclhash, $incl, true)) {
continue;
}
$incl[] = $inclhash;
$name = $attachment[2];
$encoding = $attachment[3];
$type = $attachment[4];
$disposition = $attachment[6];
$cid = $attachment[7];
if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) {
continue;
}
$cidUniq[$cid] = true;
$mime[] = sprintf('--%s%s', $boundary, static::$LE);
//Only include a filename property if we have one
if (!empty($name)) {
$mime[] = sprintf(
'Content-Type: %s; name=%s%s',
$type,
static::quotedString($this->encodeHeader($this->secureHeader($name))),
static::$LE
);
} else {
$mime[] = sprintf(
'Content-Type: %s%s',
$type,
static::$LE
);
}
//RFC1341 part 5 says 7bit is assumed if not specified
if (static::ENCODING_7BIT !== $encoding) {
$mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
}
//Only set Content-IDs on inline attachments
if ((string) $cid !== '' && $disposition === 'inline') {
$mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;
}
//Allow for bypassing the Content-Disposition header
if (!empty($disposition)) {
$encoded_name = $this->encodeHeader($this->secureHeader($name));
if (!empty($encoded_name)) {
$mime[] = sprintf(
'Content-Disposition: %s; filename=%s%s',
$disposition,
static::quotedString($encoded_name),
static::$LE . static::$LE
);
} else {
$mime[] = sprintf(
'Content-Disposition: %s%s',
$disposition,
static::$LE . static::$LE
);
}
} else {
$mime[] = static::$LE;
}
//Encode as string attachment
if ($bString) {
$mime[] = $this->encodeString($string, $encoding);
} else {
$mime[] = $this->encodeFile($path, $encoding);
}
if ($this->isError()) {
return '';
}
$mime[] = static::$LE;
}
}
$mime[] = sprintf('--%s--%s', $boundary, static::$LE);
return implode('', $mime);
}
/**
* Encode a file attachment in requested format.
* Returns an empty string on failure.
*
* @param string $path The full path to the file
* @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
*
* @return string
*/
protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
{
try {
if (!static::fileIsAccessible($path)) {
throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
}
$file_buffer = file_get_contents($path);
if (false === $file_buffer) {
throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
}
$file_buffer = $this->encodeString($file_buffer, $encoding);
return $file_buffer;
} catch (Exception $exc) {
$this->setError($exc->getMessage());
$this->edebug($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return '';
}
}
/**
* Encode a string in requested format.
* Returns an empty string on failure.
*
* @param string $str The text to encode
* @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
*
* @throws Exception
*
* @return string
*/
public function encodeString($str, $encoding = self::ENCODING_BASE64)
{
$encoded = '';
switch (strtolower($encoding)) {
case static::ENCODING_BASE64:
$encoded = chunk_split(
base64_encode($str),
static::STD_LINE_LENGTH,
static::$LE
);
break;
case static::ENCODING_7BIT:
case static::ENCODING_8BIT:
$encoded = static::normalizeBreaks($str);
//Make sure it ends with a line break
if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
$encoded .= static::$LE;
}
break;
case static::ENCODING_BINARY:
$encoded = $str;
break;
case static::ENCODING_QUOTED_PRINTABLE:
$encoded = $this->encodeQP($str);
break;
default:
$this->setError($this->lang('encoding') . $encoding);
if ($this->exceptions) {
throw new Exception($this->lang('encoding') . $encoding);
}
break;
}
return $encoded;
}
/**
* Encode a header value (not including its label) optimally.
* Picks shortest of Q, B, or none. Result includes folding if needed.
* See RFC822 definitions for phrase, comment and text positions.
*
* @param string $str The header value to encode
* @param string $position What context the string will be used in
*
* @return string
*/
public function encodeHeader($str, $position = 'text')
{
$matchcount = 0;
switch (strtolower($position)) {
case 'phrase':
if (!preg_match('/[\200-\377]/', $str)) {
//Can't use addslashes as we don't know the value of magic_quotes_sybase
$encoded = addcslashes($str, "\0..\37\177\\\"");
if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
return $encoded;
}
return "\"$encoded\"";
}
$matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
break;
/* @noinspection PhpMissingBreakStatementInspection */
case 'comment':
$matchcount = preg_match_all('/[()"]/', $str, $matches);
//fallthrough
case 'text':
default:
$matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
break;
}
if ($this->has8bitChars($str)) {
$charset = $this->CharSet;
} else {
$charset = static::CHARSET_ASCII;
}
//Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
$overhead = 8 + strlen($charset);
if ('mail' === $this->Mailer) {
$maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
} else {
$maxlen = static::MAX_LINE_LENGTH - $overhead;
}
//Select the encoding that produces the shortest output and/or prevents corruption.
if ($matchcount > strlen($str) / 3) {
//More than 1/3 of the content needs encoding, use B-encode.
$encoding = 'B';
} elseif ($matchcount > 0) {
//Less than 1/3 of the content needs encoding, use Q-encode.
$encoding = 'Q';
} elseif (strlen($str) > $maxlen) {
//No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
$encoding = 'Q';
} else {
//No reformatting needed
$encoding = false;
}
switch ($encoding) {
case 'B':
if ($this->hasMultiBytes($str)) {
//Use a custom function which correctly encodes and wraps long
//multibyte strings without breaking lines within a character
$encoded = $this->base64EncodeWrapMB($str, "\n");
} else {
$encoded = base64_encode($str);
$maxlen -= $maxlen % 4;
$encoded = trim(chunk_split($encoded, $maxlen, "\n"));
}
$encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
break;
case 'Q':
$encoded = $this->encodeQ($str, $position);
$encoded = $this->wrapText($encoded, $maxlen, true);
$encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
$encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
break;
default:
return $str;
}
return trim(static::normalizeBreaks($encoded));
}
/**
* Check if a string contains multi-byte characters.
*
* @param string $str multi-byte text to wrap encode
*
* @return bool
*/
public function hasMultiBytes($str)
{
if (function_exists('mb_strlen')) {
return strlen($str) > mb_strlen($str, $this->CharSet);
}
//Assume no multibytes (we can't handle without mbstring functions anyway)
return false;
}
/**
* Does a string contain any 8-bit chars (in any charset)?
*
* @param string $text
*
* @return bool
*/
public function has8bitChars($text)
{
return (bool) preg_match('/[\x80-\xFF]/', $text);
}
/**
* Encode and wrap long multibyte strings for mail headers
* without breaking lines within a character.
* Adapted from a function by paravoid.
*
* @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
*
* @param string $str multi-byte text to wrap encode
* @param string $linebreak string to use as linefeed/end-of-line
*
* @return string
*/
public function base64EncodeWrapMB($str, $linebreak = null)
{
$start = '=?' . $this->CharSet . '?B?';
$end = '?=';
$encoded = '';
if (null === $linebreak) {
$linebreak = static::$LE;
}
$mb_length = mb_strlen($str, $this->CharSet);
//Each line must have length <= 75, including $start and $end
$length = 75 - strlen($start) - strlen($end);
//Average multi-byte ratio
$ratio = $mb_length / strlen($str);
//Base64 has a 4:3 ratio
$avgLength = floor($length * $ratio * .75);
$offset = 0;
for ($i = 0; $i < $mb_length; $i += $offset) {
$lookBack = 0;
do {
$offset = $avgLength - $lookBack;
$chunk = mb_substr($str, $i, $offset, $this->CharSet);
$chunk = base64_encode($chunk);
++$lookBack;
} while (strlen($chunk) > $length);
$encoded .= $chunk . $linebreak;
}
//Chomp the last linefeed
return substr($encoded, 0, -strlen($linebreak));
}
/**
* Encode a string in quoted-printable format.
* According to RFC2045 section 6.7.
*
* @param string $string The text to encode
*
* @return string
*/
public function encodeQP($string)
{
return static::normalizeBreaks(quoted_printable_encode($string));
}
/**
* Encode a string using Q encoding.
*
* @see http://tools.ietf.org/html/rfc2047#section-4.2
*
* @param string $str the text to encode
* @param string $position Where the text is going to be used, see the RFC for what that means
*
* @return string
*/
public function encodeQ($str, $position = 'text')
{
//There should not be any EOL in the string
$pattern = '';
$encoded = str_replace(["\r", "\n"], '', $str);
switch (strtolower($position)) {
case 'phrase':
//RFC 2047 section 5.3
$pattern = '^A-Za-z0-9!*+\/ -';
break;
/*
* RFC 2047 section 5.2.
* Build $pattern without including delimiters and []
*/
/* @noinspection PhpMissingBreakStatementInspection */
case 'comment':
$pattern = '\(\)"';
/* Intentional fall through */
case 'text':
default:
//RFC 2047 section 5.1
//Replace every high ascii, control, =, ? and _ characters
$pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
break;
}
$matches = [];
if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
//If the string contains an '=', make sure it's the first thing we replace
//so as to avoid double-encoding
$eqkey = array_search('=', $matches[0], true);
if (false !== $eqkey) {
unset($matches[0][$eqkey]);
array_unshift($matches[0], '=');
}
foreach (array_unique($matches[0]) as $char) {
$encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
}
}
//Replace spaces with _ (more readable than =20)
//RFC 2047 section 4.2(2)
return str_replace(' ', '_', $encoded);
}
/**
* Add a string or binary attachment (non-filesystem).
* This method can be used to attach ascii or binary data,
* such as a BLOB record from a database.
*
* @param string $string String attachment data
* @param string $filename Name of the attachment
* @param string $encoding File encoding (see $Encoding)
* @param string $type File extension (MIME) type
* @param string $disposition Disposition to use
*
* @throws Exception
*
* @return bool True on successfully adding an attachment
*/
public function addStringAttachment(
$string,
$filename,
$encoding = self::ENCODING_BASE64,
$type = '',
$disposition = 'attachment'
) {
try {
//If a MIME type is not specified, try to work it out from the file name
if ('' === $type) {
$type = static::filenameToType($filename);
}
if (!$this->validateEncoding($encoding)) {
throw new Exception($this->lang('encoding') . $encoding);
}
//Append to $attachment array
$this->attachment[] = [
0 => $string,
1 => $filename,
2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),
3 => $encoding,
4 => $type,
5 => true, //isStringAttachment
6 => $disposition,
7 => 0,
];
} catch (Exception $exc) {
$this->setError($exc->getMessage());
$this->edebug($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return false;
}
return true;
}
/**
* Add an embedded (inline) attachment from a file.
* This can include images, sounds, and just about any other document type.
* These differ from 'regular' attachments in that they are intended to be
* displayed inline with the message, not just attached for download.
* This is used in HTML messages that embed the images
* the HTML refers to using the `$cid` value in `img` tags, for example `<img src="cid:mylogo">`.
* Never use a user-supplied path to a file!
*
* @param string $path Path to the attachment
* @param string $cid Content ID of the attachment; Use this to reference
* the content when using an embedded image in HTML
* @param string $name Overrides the attachment filename
* @param string $encoding File encoding (see $Encoding) defaults to `base64`
* @param string $type File MIME type (by default mapped from the `$path` filename's extension)
* @param string $disposition Disposition to use: `inline` (default) or `attachment`
* (unlikely you want this – {@see `addAttachment()`} instead)
*
* @return bool True on successfully adding an attachment
* @throws Exception
*
*/
public function addEmbeddedImage(
$path,
$cid,
$name = '',
$encoding = self::ENCODING_BASE64,
$type = '',
$disposition = 'inline'
) {
try {
if (!static::fileIsAccessible($path)) {
throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
}
//If a MIME type is not specified, try to work it out from the file name
if ('' === $type) {
$type = static::filenameToType($path);
}
if (!$this->validateEncoding($encoding)) {
throw new Exception($this->lang('encoding') . $encoding);
}
$filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
if ('' === $name) {
$name = $filename;
}
//Append to $attachment array
$this->attachment[] = [
0 => $path,
1 => $filename,
2 => $name,
3 => $encoding,
4 => $type,
5 => false, //isStringAttachment
6 => $disposition,
7 => $cid,
];
} catch (Exception $exc) {
$this->setError($exc->getMessage());
$this->edebug($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return false;
}
return true;
}
/**
* Add an embedded stringified attachment.
* This can include images, sounds, and just about any other document type.
* If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type.
*
* @param string $string The attachment binary data
* @param string $cid Content ID of the attachment; Use this to reference
* the content when using an embedded image in HTML
* @param string $name A filename for the attachment. If this contains an extension,
* PHPMailer will attempt to set a MIME type for the attachment.
* For example 'file.jpg' would get an 'image/jpeg' MIME type.
* @param string $encoding File encoding (see $Encoding), defaults to 'base64'
* @param string $type MIME type - will be used in preference to any automatically derived type
* @param string $disposition Disposition to use
*
* @throws Exception
*
* @return bool True on successfully adding an attachment
*/
public function addStringEmbeddedImage(
$string,
$cid,
$name = '',
$encoding = self::ENCODING_BASE64,
$type = '',
$disposition = 'inline'
) {
try {
//If a MIME type is not specified, try to work it out from the name
if ('' === $type && !empty($name)) {
$type = static::filenameToType($name);
}
if (!$this->validateEncoding($encoding)) {
throw new Exception($this->lang('encoding') . $encoding);
}
//Append to $attachment array
$this->attachment[] = [
0 => $string,
1 => $name,
2 => $name,
3 => $encoding,
4 => $type,
5 => true, //isStringAttachment
6 => $disposition,
7 => $cid,
];
} catch (Exception $exc) {
$this->setError($exc->getMessage());
$this->edebug($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return false;
}
return true;
}
/**
* Validate encodings.
*
* @param string $encoding
*
* @return bool
*/
protected function validateEncoding($encoding)
{
return in_array(
$encoding,
[
self::ENCODING_7BIT,
self::ENCODING_QUOTED_PRINTABLE,
self::ENCODING_BASE64,
self::ENCODING_8BIT,
self::ENCODING_BINARY,
],
true
);
}
/**
* Check if an embedded attachment is present with this cid.
*
* @param string $cid
*
* @return bool
*/
protected function cidExists($cid)
{
foreach ($this->attachment as $attachment) {
if ('inline' === $attachment[6] && $cid === $attachment[7]) {
return true;
}
}
return false;
}
/**
* Check if an inline attachment is present.
*
* @return bool
*/
public function inlineImageExists()
{
foreach ($this->attachment as $attachment) {
if ('inline' === $attachment[6]) {
return true;
}
}
return false;
}
/**
* Check if an attachment (non-inline) is present.
*
* @return bool
*/
public function attachmentExists()
{
foreach ($this->attachment as $attachment) {
if ('attachment' === $attachment[6]) {
return true;
}
}
return false;
}
/**
* Check if this message has an alternative body set.
*
* @return bool
*/
public function alternativeExists()
{
return !empty($this->AltBody);
}
/**
* Clear queued addresses of given kind.
*
* @param string $kind 'to', 'cc', or 'bcc'
*/
public function clearQueuedAddresses($kind)
{
$this->RecipientsQueue = array_filter(
$this->RecipientsQueue,
static function ($params) use ($kind) {
return $params[0] !== $kind;
}
);
}
/**
* Clear all To recipients.
*/
public function clearAddresses()
{
foreach ($this->to as $to) {
unset($this->all_recipients[strtolower($to[0])]);
}
$this->to = [];
$this->clearQueuedAddresses('to');
}
/**
* Clear all CC recipients.
*/
public function clearCCs()
{
foreach ($this->cc as $cc) {
unset($this->all_recipients[strtolower($cc[0])]);
}
$this->cc = [];
$this->clearQueuedAddresses('cc');
}
/**
* Clear all BCC recipients.
*/
public function clearBCCs()
{
foreach ($this->bcc as $bcc) {
unset($this->all_recipients[strtolower($bcc[0])]);
}
$this->bcc = [];
$this->clearQueuedAddresses('bcc');
}
/**
* Clear all ReplyTo recipients.
*/
public function clearReplyTos()
{
$this->ReplyTo = [];
$this->ReplyToQueue = [];
}
/**
* Clear all recipient types.
*/
public function clearAllRecipients()
{
$this->to = [];
$this->cc = [];
$this->bcc = [];
$this->all_recipients = [];
$this->RecipientsQueue = [];
}
/**
* Clear all filesystem, string, and binary attachments.
*/
public function clearAttachments()
{
$this->attachment = [];
}
/**
* Clear all custom headers.
*/
public function clearCustomHeaders()
{
$this->CustomHeader = [];
}
/**
* Add an error message to the error container.
*
* @param string $msg
*/
protected function setError($msg)
{
++$this->error_count;
if ('smtp' === $this->Mailer && null !== $this->smtp) {
$lasterror = $this->smtp->getError();
if (!empty($lasterror['error'])) {
$msg .= $this->lang('smtp_error') . $lasterror['error'];
if (!empty($lasterror['detail'])) {
$msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail'];
}
if (!empty($lasterror['smtp_code'])) {
$msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code'];
}
if (!empty($lasterror['smtp_code_ex'])) {
$msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex'];
}
}
}
$this->ErrorInfo = $msg;
}
/**
* Return an RFC 822 formatted date.
*
* @return string
*/
public static function rfcDate()
{
//Set the time zone to whatever the default is to avoid 500 errors
//Will default to UTC if it's not set properly in php.ini
date_default_timezone_set(@date_default_timezone_get());
return date('D, j M Y H:i:s O');
}
/**
* Get the server hostname.
* Returns 'localhost.localdomain' if unknown.
*
* @return string
*/
protected function serverHostname()
{
$result = '';
if (!empty($this->Hostname)) {
$result = $this->Hostname;
} elseif (isset($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) {
$result = $_SERVER['SERVER_NAME'];
} elseif (function_exists('gethostname') && gethostname() !== false) {
$result = gethostname();
} elseif (php_uname('n') !== false) {
$result = php_uname('n');
}
if (!static::isValidHost($result)) {
return 'localhost.localdomain';
}
return $result;
}
/**
* Validate whether a string contains a valid value to use as a hostname or IP address.
* IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
*
* @param string $host The host name or IP address to check
*
* @return bool
*/
public static function isValidHost($host)
{
//Simple syntax limits
if (
empty($host)
|| !is_string($host)
|| strlen($host) > 256
|| !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+\])$/', $host)
) {
return false;
}
//Looks like a bracketed IPv6 address
if (strlen($host) > 2 && substr($host, 0, 1) === '[' && substr($host, -1, 1) === ']') {
return filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
}
//If removing all the dots results in a numeric string, it must be an IPv4 address.
//Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
if (is_numeric(str_replace('.', '', $host))) {
//Is it a valid IPv4 address?
return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
}
//Is it a syntactically valid hostname (when embeded in a URL)?
return filter_var('http://' . $host, FILTER_VALIDATE_URL) !== false;
}
/**
* Get an error message in the current language.
*
* @param string $key
*
* @return string
*/
protected function lang($key)
{
if (count($this->language) < 1) {
$this->setLanguage(); //Set the default language
}
if (array_key_exists($key, $this->language)) {
if ('smtp_connect_failed' === $key) {
//Include a link to troubleshooting docs on SMTP connection failure.
//This is by far the biggest cause of support questions
//but it's usually not PHPMailer's fault.
return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
}
return $this->language[$key];
}
//Return the key as a fallback
return $key;
}
/**
* Build an error message starting with a generic one and adding details if possible.
*
* @param string $base_key
* @return string
*/
private function getSmtpErrorMessage($base_key)
{
$message = $this->lang($base_key);
$error = $this->smtp->getError();
if (!empty($error['error'])) {
$message .= ' ' . $error['error'];
if (!empty($error['detail'])) {
$message .= ' ' . $error['detail'];
}
}
return $message;
}
/**
* Check if an error occurred.
*
* @return bool True if an error did occur
*/
public function isError()
{
return $this->error_count > 0;
}
/**
* Add a custom header.
* $name value can be overloaded to contain
* both header name and value (name:value).
*
* @param string $name Custom header name
* @param string|null $value Header value
*
* @throws Exception
*/
public function addCustomHeader($name, $value = null)
{
if (null === $value && strpos($name, ':') !== false) {
//Value passed in as name:value
list($name, $value) = explode(':', $name, 2);
}
$name = trim($name);
$value = (null === $value) ? '' : trim($value);
//Ensure name is not empty, and that neither name nor value contain line breaks
if (empty($name) || strpbrk($name . $value, "\r\n") !== false) {
if ($this->exceptions) {
throw new Exception($this->lang('invalid_header'));
}
return false;
}
$this->CustomHeader[] = [$name, $value];
return true;
}
/**
* Returns all custom headers.
*
* @return array
*/
public function getCustomHeaders()
{
return $this->CustomHeader;
}
/**
* Create a message body from an HTML string.
* Automatically inlines images and creates a plain-text version by converting the HTML,
* overwriting any existing values in Body and AltBody.
* Do not source $message content from user input!
* $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
* will look for an image file in $basedir/images/a.png and convert it to inline.
* If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
* Converts data-uri images into embedded attachments.
* If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
*
* @param string $message HTML message string
* @param string $basedir Absolute path to a base directory to prepend to relative paths to images
* @param bool|callable $advanced Whether to use the internal HTML to text converter
* or your own custom converter
* @return string The transformed message body
*
* @throws Exception
*
* @see PHPMailer::html2text()
*/
public function msgHTML($message, $basedir = '', $advanced = false)
{
preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images);
if (array_key_exists(2, $images)) {
if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
//Ensure $basedir has a trailing /
$basedir .= '/';
}
foreach ($images[2] as $imgindex => $url) {
//Convert data URIs into embedded images
//e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
$match = [];
if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
$data = base64_decode($match[3]);
} elseif ('' === $match[2]) {
$data = rawurldecode($match[3]);
} else {
//Not recognised so leave it alone
continue;
}
//Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
//will only be embedded once, even if it used a different encoding
$cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; //RFC2392 S 2
if (!$this->cidExists($cid)) {
$this->addStringEmbeddedImage(
$data,
$cid,
'embed' . $imgindex,
static::ENCODING_BASE64,
$match[1]
);
}
$message = str_replace(
$images[0][$imgindex],
$images[1][$imgindex] . '="cid:' . $cid . '"',
$message
);
continue;
}
if (
//Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
!empty($basedir)
//Ignore URLs containing parent dir traversal (..)
&& (strpos($url, '..') === false)
//Do not change urls that are already inline images
&& 0 !== strpos($url, 'cid:')
//Do not change absolute URLs, including anonymous protocol
&& !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
) {
$filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
$directory = dirname($url);
if ('.' === $directory) {
$directory = '';
}
//RFC2392 S 2
$cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';
if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
$basedir .= '/';
}
if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
$directory .= '/';
}
if (
$this->addEmbeddedImage(
$basedir . $directory . $filename,
$cid,
$filename,
static::ENCODING_BASE64,
static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
)
) {
$message = preg_replace(
'/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
$images[1][$imgindex] . '="cid:' . $cid . '"',
$message
);
}
}
}
}
$this->isHTML();
//Convert all message body line breaks to LE, makes quoted-printable encoding work much better
$this->Body = static::normalizeBreaks($message);
$this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
if (!$this->alternativeExists()) {
$this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
. static::$LE;
}
return $this->Body;
}
/**
* Convert an HTML string into plain text.
* This is used by msgHTML().
* Note - older versions of this function used a bundled advanced converter
* which was removed for license reasons in #232.
* Example usage:
*
* ```php
* //Use default conversion
* $plain = $mail->html2text($html);
* //Use your own custom converter
* $plain = $mail->html2text($html, function($html) {
* $converter = new MyHtml2text($html);
* return $converter->get_text();
* });
* ```
*
* @param string $html The HTML text to convert
* @param bool|callable $advanced Any boolean value to use the internal converter,
* or provide your own callable for custom conversion.
* *Never* pass user-supplied data into this parameter
*
* @return string
*/
public function html2text($html, $advanced = false)
{
if (is_callable($advanced)) {
return call_user_func($advanced, $html);
}
return html_entity_decode(
trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
ENT_QUOTES,
$this->CharSet
);
}
/**
* Get the MIME type for a file extension.
*
* @param string $ext File extension
*
* @return string MIME type of file
*/
public static function _mime_types($ext = '')
{
$mimes = [
'xl' => 'application/excel',
'js' => 'application/javascript',
'hqx' => 'application/mac-binhex40',
'cpt' => 'application/mac-compactpro',
'bin' => 'application/macbinary',
'doc' => 'application/msword',
'word' => 'application/msword',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'class' => 'application/octet-stream',
'dll' => 'application/octet-stream',
'dms' => 'application/octet-stream',
'exe' => 'application/octet-stream',
'lha' => 'application/octet-stream',
'lzh' => 'application/octet-stream',
'psd' => 'application/octet-stream',
'sea' => 'application/octet-stream',
'so' => 'application/octet-stream',
'oda' => 'application/oda',
'pdf' => 'application/pdf',
'ai' => 'application/postscript',
'eps' => 'application/postscript',
'ps' => 'application/postscript',
'smi' => 'application/smil',
'smil' => 'application/smil',
'mif' => 'application/vnd.mif',
'xls' => 'application/vnd.ms-excel',
'ppt' => 'application/vnd.ms-powerpoint',
'wbxml' => 'application/vnd.wap.wbxml',
'wmlc' => 'application/vnd.wap.wmlc',
'dcr' => 'application/x-director',
'dir' => 'application/x-director',
'dxr' => 'application/x-director',
'dvi' => 'application/x-dvi',
'gtar' => 'application/x-gtar',
'php3' => 'application/x-httpd-php',
'php4' => 'application/x-httpd-php',
'php' => 'application/x-httpd-php',
'phtml' => 'application/x-httpd-php',
'phps' => 'application/x-httpd-php-source',
'swf' => 'application/x-shockwave-flash',
'sit' => 'application/x-stuffit',
'tar' => 'application/x-tar',
'tgz' => 'application/x-tar',
'xht' => 'application/xhtml+xml',
'xhtml' => 'application/xhtml+xml',
'zip' => 'application/zip',
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mp2' => 'audio/mpeg',
'mp3' => 'audio/mpeg',
'm4a' => 'audio/mp4',
'mpga' => 'audio/mpeg',
'aif' => 'audio/x-aiff',
'aifc' => 'audio/x-aiff',
'aiff' => 'audio/x-aiff',
'ram' => 'audio/x-pn-realaudio',
'rm' => 'audio/x-pn-realaudio',
'rpm' => 'audio/x-pn-realaudio-plugin',
'ra' => 'audio/x-realaudio',
'wav' => 'audio/x-wav',
'mka' => 'audio/x-matroska',
'bmp' => 'image/bmp',
'gif' => 'image/gif',
'jpeg' => 'image/jpeg',
'jpe' => 'image/jpeg',
'jpg' => 'image/jpeg',
'png' => 'image/png',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'webp' => 'image/webp',
'avif' => 'image/avif',
'heif' => 'image/heif',
'heifs' => 'image/heif-sequence',
'heic' => 'image/heic',
'heics' => 'image/heic-sequence',
'eml' => 'message/rfc822',
'css' => 'text/css',
'html' => 'text/html',
'htm' => 'text/html',
'shtml' => 'text/html',
'log' => 'text/plain',
'text' => 'text/plain',
'txt' => 'text/plain',
'rtx' => 'text/richtext',
'rtf' => 'text/rtf',
'vcf' => 'text/vcard',
'vcard' => 'text/vcard',
'ics' => 'text/calendar',
'xml' => 'text/xml',
'xsl' => 'text/xml',
'csv' => 'text/csv',
'wmv' => 'video/x-ms-wmv',
'mpeg' => 'video/mpeg',
'mpe' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mp4' => 'video/mp4',
'm4v' => 'video/mp4',
'mov' => 'video/quicktime',
'qt' => 'video/quicktime',
'rv' => 'video/vnd.rn-realvideo',
'avi' => 'video/x-msvideo',
'movie' => 'video/x-sgi-movie',
'webm' => 'video/webm',
'mkv' => 'video/x-matroska',
];
$ext = strtolower($ext);
if (array_key_exists($ext, $mimes)) {
return $mimes[$ext];
}
return 'application/octet-stream';
}
/**
* Map a file name to a MIME type.
* Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
*
* @param string $filename A file name or full path, does not need to exist as a file
*
* @return string
*/
public static function filenameToType($filename)
{
//In case the path is a URL, strip any query string before getting extension
$qpos = strpos($filename, '?');
if (false !== $qpos) {
$filename = substr($filename, 0, $qpos);
}
$ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
return static::_mime_types($ext);
}
/**
* Multi-byte-safe pathinfo replacement.
* Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
*
* @see http://www.php.net/manual/en/function.pathinfo.php#107461
*
* @param string $path A filename or path, does not need to exist as a file
* @param int|string $options Either a PATHINFO_* constant,
* or a string name to return only the specified piece
*
* @return string|array
*/
public static function mb_pathinfo($path, $options = null)
{
$ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
$pathinfo = [];
if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
if (array_key_exists(1, $pathinfo)) {
$ret['dirname'] = $pathinfo[1];
}
if (array_key_exists(2, $pathinfo)) {
$ret['basename'] = $pathinfo[2];
}
if (array_key_exists(5, $pathinfo)) {
$ret['extension'] = $pathinfo[5];
}
if (array_key_exists(3, $pathinfo)) {
$ret['filename'] = $pathinfo[3];
}
}
switch ($options) {
case PATHINFO_DIRNAME:
case 'dirname':
return $ret['dirname'];
case PATHINFO_BASENAME:
case 'basename':
return $ret['basename'];
case PATHINFO_EXTENSION:
case 'extension':
return $ret['extension'];
case PATHINFO_FILENAME:
case 'filename':
return $ret['filename'];
default:
return $ret;
}
}
/**
* Set or reset instance properties.
* You should avoid this function - it's more verbose, less efficient, more error-prone and
* harder to debug than setting properties directly.
* Usage Example:
* `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);`
* is the same as:
* `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`.
*
* @param string $name The property name to set
* @param mixed $value The value to set the property to
*
* @return bool
*/
public function set($name, $value = '')
{
if (property_exists($this, $name)) {
$this->{$name} = $value;
return true;
}
$this->setError($this->lang('variable_set') . $name);
return false;
}
/**
* Strip newlines to prevent header injection.
*
* @param string $str
*
* @return string
*/
public function secureHeader($str)
{
return trim(str_replace(["\r", "\n"], '', $str));
}
/**
* Normalize line breaks in a string.
* Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
* Defaults to CRLF (for message bodies) and preserves consecutive breaks.
*
* @param string $text
* @param string $breaktype What kind of line break to use; defaults to static::$LE
*
* @return string
*/
public static function normalizeBreaks($text, $breaktype = null)
{
if (null === $breaktype) {
$breaktype = static::$LE;
}
//Normalise to \n
$text = str_replace([self::CRLF, "\r"], "\n", $text);
//Now convert LE as needed
if ("\n" !== $breaktype) {
$text = str_replace("\n", $breaktype, $text);
}
return $text;
}
/**
* Remove trailing breaks from a string.
*
* @param string $text
*
* @return string The text to remove breaks from
*/
public static function stripTrailingWSP($text)
{
return rtrim($text, " \r\n\t");
}
/**
* Return the current line break format string.
*
* @return string
*/
public static function getLE()
{
return static::$LE;
}
/**
* Set the line break format string, e.g. "\r\n".
*
* @param string $le
*/
protected static function setLE($le)
{
static::$LE = $le;
}
/**
* Set the public and private key files and password for S/MIME signing.
*
* @param string $cert_filename
* @param string $key_filename
* @param string $key_pass Password for private key
* @param string $extracerts_filename Optional path to chain certificate
*/
public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
{
$this->sign_cert_file = $cert_filename;
$this->sign_key_file = $key_filename;
$this->sign_key_pass = $key_pass;
$this->sign_extracerts_file = $extracerts_filename;
}
/**
* Quoted-Printable-encode a DKIM header.
*
* @param string $txt
*
* @return string
*/
public function DKIM_QP($txt)
{
$line = '';
$len = strlen($txt);
for ($i = 0; $i < $len; ++$i) {
$ord = ord($txt[$i]);
if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {
$line .= $txt[$i];
} else {
$line .= '=' . sprintf('%02X', $ord);
}
}
return $line;
}
/**
* Generate a DKIM signature.
*
* @param string $signHeader
*
* @throws Exception
*
* @return string The DKIM signature value
*/
public function DKIM_Sign($signHeader)
{
if (!defined('PKCS7_TEXT')) {
if ($this->exceptions) {
throw new Exception($this->lang('extension_missing') . 'openssl');
}
return '';
}
$privKeyStr = !empty($this->DKIM_private_string) ?
$this->DKIM_private_string :
file_get_contents($this->DKIM_private);
if ('' !== $this->DKIM_passphrase) {
$privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
} else {
$privKey = openssl_pkey_get_private($privKeyStr);
}
if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
if (\PHP_MAJOR_VERSION < 8) {
openssl_pkey_free($privKey);
}
return base64_encode($signature);
}
if (\PHP_MAJOR_VERSION < 8) {
openssl_pkey_free($privKey);
}
return '';
}
/**
* Generate a DKIM canonicalization header.
* Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
* Canonicalized headers should *always* use CRLF, regardless of mailer setting.
*
* @see https://tools.ietf.org/html/rfc6376#section-3.4.2
*
* @param string $signHeader Header
*
* @return string
*/
public function DKIM_HeaderC($signHeader)
{
//Normalize breaks to CRLF (regardless of the mailer)
$signHeader = static::normalizeBreaks($signHeader, self::CRLF);
//Unfold header lines
//Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
//@see https://tools.ietf.org/html/rfc5322#section-2.2
//That means this may break if you do something daft like put vertical tabs in your headers.
$signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
//Break headers out into an array
$lines = explode(self::CRLF, $signHeader);
foreach ($lines as $key => $line) {
//If the header is missing a :, skip it as it's invalid
//This is likely to happen because the explode() above will also split
//on the trailing LE, leaving an empty line
if (strpos($line, ':') === false) {
continue;
}
list($heading, $value) = explode(':', $line, 2);
//Lower-case header name
$heading = strtolower($heading);
//Collapse white space within the value, also convert WSP to space
$value = preg_replace('/[ \t]+/', ' ', $value);
//RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
//But then says to delete space before and after the colon.
//Net result is the same as trimming both ends of the value.
//By elimination, the same applies to the field name
$lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t");
}
return implode(self::CRLF, $lines);
}
/**
* Generate a DKIM canonicalization body.
* Uses the 'simple' algorithm from RFC6376 section 3.4.3.
* Canonicalized bodies should *always* use CRLF, regardless of mailer setting.
*
* @see https://tools.ietf.org/html/rfc6376#section-3.4.3
*
* @param string $body Message Body
*
* @return string
*/
public function DKIM_BodyC($body)
{
if (empty($body)) {
return self::CRLF;
}
//Normalize line endings to CRLF
$body = static::normalizeBreaks($body, self::CRLF);
//Reduce multiple trailing line breaks to a single one
return static::stripTrailingWSP($body) . self::CRLF;
}
/**
* Create the DKIM header and body in a new message header.
*
* @param string $headers_line Header lines
* @param string $subject Subject
* @param string $body Body
*
* @throws Exception
*
* @return string
*/
public function DKIM_Add($headers_line, $subject, $body)
{
$DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms
$DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of header & body
$DKIMquery = 'dns/txt'; //Query method
$DKIMtime = time();
//Always sign these headers without being asked
//Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1
$autoSignHeaders = [
'from',
'to',
'cc',
'date',
'subject',
'reply-to',
'message-id',
'content-type',
'mime-version',
'x-mailer',
];
if (stripos($headers_line, 'Subject') === false) {
$headers_line .= 'Subject: ' . $subject . static::$LE;
}
$headerLines = explode(static::$LE, $headers_line);
$currentHeaderLabel = '';
$currentHeaderValue = '';
$parsedHeaders = [];
$headerLineIndex = 0;
$headerLineCount = count($headerLines);
foreach ($headerLines as $headerLine) {
$matches = [];
if (preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) {
if ($currentHeaderLabel !== '') {
//We were previously in another header; This is the start of a new header, so save the previous one
$parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
}
$currentHeaderLabel = $matches[1];
$currentHeaderValue = $matches[2];
} elseif (preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) {
//This is a folded continuation of the current header, so unfold it
$currentHeaderValue .= ' ' . $matches[1];
}
++$headerLineIndex;
if ($headerLineIndex >= $headerLineCount) {
//This was the last line, so finish off this header
$parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
}
}
$copiedHeaders = [];
$headersToSignKeys = [];
$headersToSign = [];
foreach ($parsedHeaders as $header) {
//Is this header one that must be included in the DKIM signature?
if (in_array(strtolower($header['label']), $autoSignHeaders, true)) {
$headersToSignKeys[] = $header['label'];
$headersToSign[] = $header['label'] . ': ' . $header['value'];
if ($this->DKIM_copyHeaderFields) {
$copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
str_replace('|', '=7C', $this->DKIM_QP($header['value']));
}
continue;
}
//Is this an extra custom header we've been asked to sign?
if (in_array($header['label'], $this->DKIM_extraHeaders, true)) {
//Find its value in custom headers
foreach ($this->CustomHeader as $customHeader) {
if ($customHeader[0] === $header['label']) {
$headersToSignKeys[] = $header['label'];
$headersToSign[] = $header['label'] . ': ' . $header['value'];
if ($this->DKIM_copyHeaderFields) {
$copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
str_replace('|', '=7C', $this->DKIM_QP($header['value']));
}
//Skip straight to the next header
continue 2;
}
}
}
}
$copiedHeaderFields = '';
if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) {
//Assemble a DKIM 'z' tag
$copiedHeaderFields = ' z=';
$first = true;
foreach ($copiedHeaders as $copiedHeader) {
if (!$first) {
$copiedHeaderFields .= static::$LE . ' |';
}
//Fold long values
if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) {
$copiedHeaderFields .= substr(
chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS),
0,
-strlen(static::$LE . self::FWS)
);
} else {
$copiedHeaderFields .= $copiedHeader;
}
$first = false;
}
$copiedHeaderFields .= ';' . static::$LE;
}
$headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;
$headerValues = implode(static::$LE, $headersToSign);
$body = $this->DKIM_BodyC($body);
//Base64 of packed binary SHA-256 hash of body
$DKIMb64 = base64_encode(pack('H*', hash('sha256', $body)));
$ident = '';
if ('' !== $this->DKIM_identity) {
$ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;
}
//The DKIM-Signature header is included in the signature *except for* the value of the `b` tag
//which is appended after calculating the signature
//https://tools.ietf.org/html/rfc6376#section-3.5
$dkimSignatureHeader = 'DKIM-Signature: v=1;' .
' d=' . $this->DKIM_domain . ';' .
' s=' . $this->DKIM_selector . ';' . static::$LE .
' a=' . $DKIMsignatureType . ';' .
' q=' . $DKIMquery . ';' .
' t=' . $DKIMtime . ';' .
' c=' . $DKIMcanonicalization . ';' . static::$LE .
$headerKeys .
$ident .
$copiedHeaderFields .
' bh=' . $DKIMb64 . ';' . static::$LE .
' b=';
//Canonicalize the set of headers
$canonicalizedHeaders = $this->DKIM_HeaderC(
$headerValues . static::$LE . $dkimSignatureHeader
);
$signature = $this->DKIM_Sign($canonicalizedHeaders);
$signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS));
return static::normalizeBreaks($dkimSignatureHeader . $signature);
}
/**
* Detect if a string contains a line longer than the maximum line length
* allowed by RFC 2822 section 2.1.1.
*
* @param string $str
*
* @return bool
*/
public static function hasLineLongerThanMax($str)
{
return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);
}
/**
* If a string contains any "special" characters, double-quote the name,
* and escape any double quotes with a backslash.
*
* @param string $str
*
* @return string
*
* @see RFC822 3.4.1
*/
public static function quotedString($str)
{
if (preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) {
//If the string contains any of these chars, it must be double-quoted
//and any double quotes must be escaped with a backslash
return '"' . str_replace('"', '\\"', $str) . '"';
}
//Return the string untouched, it doesn't need quoting
return $str;
}
/**
* Allows for public read access to 'to' property.
* Before the send() call, queued addresses (i.e. with IDN) are not yet included.
*
* @return array
*/
public function getToAddresses()
{
return $this->to;
}
/**
* Allows for public read access to 'cc' property.
* Before the send() call, queued addresses (i.e. with IDN) are not yet included.
*
* @return array
*/
public function getCcAddresses()
{
return $this->cc;
}
/**
* Allows for public read access to 'bcc' property.
* Before the send() call, queued addresses (i.e. with IDN) are not yet included.
*
* @return array
*/
public function getBccAddresses()
{
return $this->bcc;
}
/**
* Allows for public read access to 'ReplyTo' property.
* Before the send() call, queued addresses (i.e. with IDN) are not yet included.
*
* @return array
*/
public function getReplyToAddresses()
{
return $this->ReplyTo;
}
/**
* Allows for public read access to 'all_recipients' property.
* Before the send() call, queued addresses (i.e. with IDN) are not yet included.
*
* @return array
*/
public function getAllRecipientAddresses()
{
return $this->all_recipients;
}
/**
* Perform a callback.
*
* @param bool $isSent
* @param array $to
* @param array $cc
* @param array $bcc
* @param string $subject
* @param string $body
* @param string $from
* @param array $extra
*/
protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)
{
if (!empty($this->action_function) && is_callable($this->action_function)) {
call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra);
}
}
/**
* Get the OAuthTokenProvider instance.
*
* @return OAuthTokenProvider
*/
public function getOAuth()
{
return $this->oauth;
}
/**
* Set an OAuthTokenProvider instance.
*/
public function setOAuth(OAuthTokenProvider $oauth)
{
$this->oauth = $oauth;
}
}
<?php
/**
* PHPMailer RFC821 SMTP email transport class.
* PHP Version 5.5.
*
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
*
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
* @author Brent R. Matzelle (original founder)
* @copyright 2012 - 2020 Marcus Bointon
* @copyright 2010 - 2012 Jim Jagielski
* @copyright 2004 - 2009 Andy Prevost
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
* @note This program is distributed in the hope that it will be useful - WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE.
*/
namespace PHPMailer\PHPMailer;
/**
* PHPMailer RFC821 SMTP email transport class.
* Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
*
* @author Chris Ryan
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
*/
class SMTP
{
/**
* The PHPMailer SMTP version number.
*
* @var string
*/
const VERSION = '6.6.5';
/**
* SMTP line break constant.
*
* @var string
*/
const LE = "\r\n";
/**
* The SMTP port to use if one is not specified.
*
* @var int
*/
const DEFAULT_PORT = 25;
/**
* The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
* *excluding* a trailing CRLF break.
*
* @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6
*
* @var int
*/
const MAX_LINE_LENGTH = 998;
/**
* The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
* *including* a trailing CRLF line break.
*
* @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5
*
* @var int
*/
const MAX_REPLY_LENGTH = 512;
/**
* Debug level for no output.
*
* @var int
*/
const DEBUG_OFF = 0;
/**
* Debug level to show client -> server messages.
*
* @var int
*/
const DEBUG_CLIENT = 1;
/**
* Debug level to show client -> server and server -> client messages.
*
* @var int
*/
const DEBUG_SERVER = 2;
/**
* Debug level to show connection status, client -> server and server -> client messages.
*
* @var int
*/
const DEBUG_CONNECTION = 3;
/**
* Debug level to show all messages.
*
* @var int
*/
const DEBUG_LOWLEVEL = 4;
/**
* Debug output level.
* Options:
* * self::DEBUG_OFF (`0`) No debug output, default
* * self::DEBUG_CLIENT (`1`) Client commands
* * self::DEBUG_SERVER (`2`) Client commands and server responses
* * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
* * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
*
* @var int
*/
public $do_debug = self::DEBUG_OFF;
/**
* How to handle debug output.
* Options:
* * `echo` Output plain-text as-is, appropriate for CLI
* * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
* * `error_log` Output to error log as configured in php.ini
* Alternatively, you can provide a callable expecting two params: a message string and the debug level:
*
* ```php
* $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
* ```
*
* Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
* level output is used:
*
* ```php
* $mail->Debugoutput = new myPsr3Logger;
* ```
*
* @var string|callable|\Psr\Log\LoggerInterface
*/
public $Debugoutput = 'echo';
/**
* Whether to use VERP.
*
* @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
* @see http://www.postfix.org/VERP_README.html Info on VERP
*
* @var bool
*/
public $do_verp = false;
/**
* The timeout value for connection, in seconds.
* Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
* This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
*
* @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
*
* @var int
*/
public $Timeout = 300;
/**
* How long to wait for commands to complete, in seconds.
* Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
*
* @var int
*/
public $Timelimit = 300;
/**
* Patterns to extract an SMTP transaction id from reply to a DATA command.
* The first capture group in each regex will be used as the ID.
* MS ESMTP returns the message ID, which may not be correct for internal tracking.
*
* @var string[]
*/
protected $smtp_transaction_id_patterns = [
'exim' => '/[\d]{3} OK id=(.*)/',
'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/',
'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/',
'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/',
'Amazon_SES' => '/[\d]{3} Ok (.*)/',
'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
'Mailjet' => '/[\d]{3} OK queued as (.*)/',
];
/**
* The last transaction ID issued in response to a DATA command,
* if one was detected.
*
* @var string|bool|null
*/
protected $last_smtp_transaction_id;
/**
* The socket for the server connection.
*
* @var ?resource
*/
protected $smtp_conn;
/**
* Error information, if any, for the last SMTP command.
*
* @var array
*/
protected $error = [
'error' => '',
'detail' => '',
'smtp_code' => '',
'smtp_code_ex' => '',
];
/**
* The reply the server sent to us for HELO.
* If null, no HELO string has yet been received.
*
* @var string|null
*/
protected $helo_rply;
/**
* The set of SMTP extensions sent in reply to EHLO command.
* Indexes of the array are extension names.
* Value at index 'HELO' or 'EHLO' (according to command that was sent)
* represents the server name. In case of HELO it is the only element of the array.
* Other values can be boolean TRUE or an array containing extension options.
* If null, no HELO/EHLO string has yet been received.
*
* @var array|null
*/
protected $server_caps;
/**
* The most recent reply received from the server.
*
* @var string
*/
protected $last_reply = '';
/**
* Output debugging info via a user-selected method.
*
* @param string $str Debug string to output
* @param int $level The debug level of this message; see DEBUG_* constants
*
* @see SMTP::$Debugoutput
* @see SMTP::$do_debug
*/
protected function edebug($str, $level = 0)
{
if ($level > $this->do_debug) {
return;
}
//Is this a PSR-3 logger?
if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
$this->Debugoutput->debug($str);
return;
}
//Avoid clash with built-in function names
if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
call_user_func($this->Debugoutput, $str, $level);
return;
}
switch ($this->Debugoutput) {
case 'error_log':
//Don't output, just log
error_log($str);
break;
case 'html':
//Cleans up output a bit for a better looking, HTML-safe output
echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
preg_replace('/[\r\n]+/', '', $str),
ENT_QUOTES,
'UTF-8'
), "<br>\n";
break;
case 'echo':
default:
//Normalize line breaks
$str = preg_replace('/\r\n|\r/m', "\n", $str);
echo gmdate('Y-m-d H:i:s'),
"\t",
//Trim trailing space
trim(
//Indent for readability, except for trailing break
str_replace(
"\n",
"\n \t ",
trim($str)
)
),
"\n";
}
}
/**
* Connect to an SMTP server.
*
* @param string $host SMTP server IP or host name
* @param int $port The port number to connect to
* @param int $timeout How long to wait for the connection to open
* @param array $options An array of options for stream_context_create()
*
* @return bool
*/
public function connect($host, $port = null, $timeout = 30, $options = [])
{
//Clear errors to avoid confusion
$this->setError('');
//Make sure we are __not__ connected
if ($this->connected()) {
//Already connected, generate error
$this->setError('Already connected to a server');
return false;
}
if (empty($port)) {
$port = self::DEFAULT_PORT;
}
//Connect to the SMTP server
$this->edebug(
"Connection: opening to $host:$port, timeout=$timeout, options=" .
(count($options) > 0 ? var_export($options, true) : 'array()'),
self::DEBUG_CONNECTION
);
$this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
if ($this->smtp_conn === false) {
//Error info already set inside `getSMTPConnection()`
return false;
}
$this->edebug('Connection: opened', self::DEBUG_CONNECTION);
//Get any announcement
$this->last_reply = $this->get_lines();
$this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
$responseCode = (int)substr($this->last_reply, 0, 3);
if ($responseCode === 220) {
return true;
}
//Anything other than a 220 response means something went wrong
//RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
//https://tools.ietf.org/html/rfc5321#section-3.1
if ($responseCode === 554) {
$this->quit();
}
//This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
$this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
$this->close();
return false;
}
/**
* Create connection to the SMTP server.
*
* @param string $host SMTP server IP or host name
* @param int $port The port number to connect to
* @param int $timeout How long to wait for the connection to open
* @param array $options An array of options for stream_context_create()
*
* @return false|resource
*/
protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
{
static $streamok;
//This is enabled by default since 5.0.0 but some providers disable it
//Check this once and cache the result
if (null === $streamok) {
$streamok = function_exists('stream_socket_client');
}
$errno = 0;
$errstr = '';
if ($streamok) {
$socket_context = stream_context_create($options);
set_error_handler([$this, 'errorHandler']);
$connection = stream_socket_client(
$host . ':' . $port,
$errno,
$errstr,
$timeout,
STREAM_CLIENT_CONNECT,
$socket_context
);
} else {
//Fall back to fsockopen which should work in more places, but is missing some features
$this->edebug(
'Connection: stream_socket_client not available, falling back to fsockopen',
self::DEBUG_CONNECTION
);
set_error_handler([$this, 'errorHandler']);
$connection = fsockopen(
$host,
$port,
$errno,
$errstr,
$timeout
);
}
restore_error_handler();
//Verify we connected properly
if (!is_resource($connection)) {
$this->setError(
'Failed to connect to server',
'',
(string) $errno,
$errstr
);
$this->edebug(
'SMTP ERROR: ' . $this->error['error']
. ": $errstr ($errno)",
self::DEBUG_CLIENT
);
return false;
}
//SMTP server can take longer to respond, give longer timeout for first read
//Windows does not have support for this timeout function
if (strpos(PHP_OS, 'WIN') !== 0) {
$max = (int)ini_get('max_execution_time');
//Don't bother if unlimited, or if set_time_limit is disabled
if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
@set_time_limit($timeout);
}
stream_set_timeout($connection, $timeout, 0);
}
return $connection;
}
/**
* Initiate a TLS (encrypted) session.
*
* @return bool
*/
public function startTLS()
{
if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
return false;
}
//Allow the best TLS version(s) we can
$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
//PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
//so add them back in manually if we can
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}
//Begin encrypted connection
set_error_handler([$this, 'errorHandler']);
$crypto_ok = stream_socket_enable_crypto(
$this->smtp_conn,
true,
$crypto_method
);
restore_error_handler();
return (bool) $crypto_ok;
}
/**
* Perform SMTP authentication.
* Must be run after hello().
*
* @see hello()
*
* @param string $username The user name
* @param string $password The password
* @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
* @param OAuthTokenProvider $OAuth An optional OAuthTokenProvider instance for XOAUTH2 authentication
*
* @return bool True if successfully authenticated
*/
public function authenticate(
$username,
$password,
$authtype = null,
$OAuth = null
) {
if (!$this->server_caps) {
$this->setError('Authentication is not allowed before HELO/EHLO');
return false;
}
if (array_key_exists('EHLO', $this->server_caps)) {
//SMTP extensions are available; try to find a proper authentication method
if (!array_key_exists('AUTH', $this->server_caps)) {
$this->setError('Authentication is not allowed at this stage');
//'at this stage' means that auth may be allowed after the stage changes
//e.g. after STARTTLS
return false;
}
$this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
$this->edebug(
'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
self::DEBUG_LOWLEVEL
);
//If we have requested a specific auth type, check the server supports it before trying others
if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
$this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
$authtype = null;
}
if (empty($authtype)) {
//If no auth mechanism is specified, attempt to use these, in this order
//Try CRAM-MD5 first as it's more secure than the others
foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
if (in_array($method, $this->server_caps['AUTH'], true)) {
$authtype = $method;
break;
}
}
if (empty($authtype)) {
$this->setError('No supported authentication methods found');
return false;
}
$this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
}
if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
$this->setError("The requested authentication method \"$authtype\" is not supported by the server");
return false;
}
} elseif (empty($authtype)) {
$authtype = 'LOGIN';
}
switch ($authtype) {
case 'PLAIN':
//Start authentication
if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
return false;
}
//Send encoded username and password
if (
//Format from https://tools.ietf.org/html/rfc4616#section-2
//We skip the first field (it's forgery), so the string starts with a null byte
!$this->sendCommand(
'User & Password',
base64_encode("\0" . $username . "\0" . $password),
235
)
) {
return false;
}
break;
case 'LOGIN':
//Start authentication
if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
return false;
}
if (!$this->sendCommand('Username', base64_encode($username), 334)) {
return false;
}
if (!$this->sendCommand('Password', base64_encode($password), 235)) {
return false;
}
break;
case 'CRAM-MD5':
//Start authentication
if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
return false;
}
//Get the challenge
$challenge = base64_decode(substr($this->last_reply, 4));
//Build the response
$response = $username . ' ' . $this->hmac($challenge, $password);
//send encoded credentials
return $this->sendCommand('Username', base64_encode($response), 235);
case 'XOAUTH2':
//The OAuth instance must be set up prior to requesting auth.
if (null === $OAuth) {
return false;
}
$oauth = $OAuth->getOauth64();
//Start authentication
if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
return false;
}
break;
default:
$this->setError("Authentication method \"$authtype\" is not supported");
return false;
}
return true;
}
/**
* Calculate an MD5 HMAC hash.
* Works like hash_hmac('md5', $data, $key)
* in case that function is not available.
*
* @param string $data The data to hash
* @param string $key The key to hash with
*
* @return string
*/
protected function hmac($data, $key)
{
if (function_exists('hash_hmac')) {
return hash_hmac('md5', $data, $key);
}
//The following borrowed from
//http://php.net/manual/en/function.mhash.php#27225
//RFC 2104 HMAC implementation for php.
//Creates an md5 HMAC.
//Eliminates the need to install mhash to compute a HMAC
//by Lance Rushing
$bytelen = 64; //byte length for md5
if (strlen($key) > $bytelen) {
$key = pack('H*', md5($key));
}
$key = str_pad($key, $bytelen, chr(0x00));
$ipad = str_pad('', $bytelen, chr(0x36));
$opad = str_pad('', $bytelen, chr(0x5c));
$k_ipad = $key ^ $ipad;
$k_opad = $key ^ $opad;
return md5($k_opad . pack('H*', md5($k_ipad . $data)));
}
/**
* Check connection state.
*
* @return bool True if connected
*/
public function connected()
{
if (is_resource($this->smtp_conn)) {
$sock_status = stream_get_meta_data($this->smtp_conn);
if ($sock_status['eof']) {
//The socket is valid but we are not connected
$this->edebug(
'SMTP NOTICE: EOF caught while checking if connected',
self::DEBUG_CLIENT
);
$this->close();
return false;
}
return true; //everything looks good
}
return false;
}
/**
* Close the socket and clean up the state of the class.
* Don't use this function without first trying to use QUIT.
*
* @see quit()
*/
public function close()
{
$this->server_caps = null;
$this->helo_rply = null;
if (is_resource($this->smtp_conn)) {
//Close the connection and cleanup
fclose($this->smtp_conn);
$this->smtp_conn = null; //Makes for cleaner serialization
$this->edebug('Connection: closed', self::DEBUG_CONNECTION);
}
}
/**
* Send an SMTP DATA command.
* Issues a data command and sends the msg_data to the server,
* finalizing the mail transaction. $msg_data is the message
* that is to be send with the headers. Each header needs to be
* on a single line followed by a <CRLF> with the message headers
* and the message body being separated by an additional <CRLF>.
* Implements RFC 821: DATA <CRLF>.
*
* @param string $msg_data Message data to send
*
* @return bool
*/
public function data($msg_data)
{
//This will use the standard timelimit
if (!$this->sendCommand('DATA', 'DATA', 354)) {
return false;
}
/* The server is ready to accept data!
* According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
* so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
* smaller lines to fit within the limit.
* We will also look for lines that start with a '.' and prepend an additional '.'.
* NOTE: this does not count towards line-length limit.
*/
//Normalize line breaks before exploding
$lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
/* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
* of the first line (':' separated) does not contain a space then it _should_ be a header and we will
* process all lines before a blank line as headers.
*/
$field = substr($lines[0], 0, strpos($lines[0], ':'));
$in_headers = false;
if (!empty($field) && strpos($field, ' ') === false) {
$in_headers = true;
}
foreach ($lines as $line) {
$lines_out = [];
if ($in_headers && $line === '') {
$in_headers = false;
}
//Break this line up into several smaller lines if it's too long
//Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
while (isset($line[self::MAX_LINE_LENGTH])) {
//Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
//so as to avoid breaking in the middle of a word
$pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
//Deliberately matches both false and 0
if (!$pos) {
//No nice break found, add a hard break
$pos = self::MAX_LINE_LENGTH - 1;
$lines_out[] = substr($line, 0, $pos);
$line = substr($line, $pos);
} else {
//Break at the found point
$lines_out[] = substr($line, 0, $pos);
//Move along by the amount we dealt with
$line = substr($line, $pos + 1);
}
//If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
if ($in_headers) {
$line = "\t" . $line;
}
}
$lines_out[] = $line;
//Send the lines to the server
foreach ($lines_out as $line_out) {
//Dot-stuffing as per RFC5321 section 4.5.2
//https://tools.ietf.org/html/rfc5321#section-4.5.2
if (!empty($line_out) && $line_out[0] === '.') {
$line_out = '.' . $line_out;
}
$this->client_send($line_out . static::LE, 'DATA');
}
}
//Message data has been sent, complete the command
//Increase timelimit for end of DATA command
$savetimelimit = $this->Timelimit;
$this->Timelimit *= 2;
$result = $this->sendCommand('DATA END', '.', 250);
$this->recordLastTransactionID();
//Restore timelimit
$this->Timelimit = $savetimelimit;
return $result;
}
/**
* Send an SMTP HELO or EHLO command.
* Used to identify the sending server to the receiving server.
* This makes sure that client and server are in a known state.
* Implements RFC 821: HELO <SP> <domain> <CRLF>
* and RFC 2821 EHLO.
*
* @param string $host The host name or IP to connect to
*
* @return bool
*/
public function hello($host = '')
{
//Try extended hello first (RFC 2821)
if ($this->sendHello('EHLO', $host)) {
return true;
}
//Some servers shut down the SMTP service here (RFC 5321)
if (substr($this->helo_rply, 0, 3) == '421') {
return false;
}
return $this->sendHello('HELO', $host);
}
/**
* Send an SMTP HELO or EHLO command.
* Low-level implementation used by hello().
*
* @param string $hello The HELO string
* @param string $host The hostname to say we are
*
* @return bool
*
* @see hello()
*/
protected function sendHello($hello, $host)
{
$noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
$this->helo_rply = $this->last_reply;
if ($noerror) {
$this->parseHelloFields($hello);
} else {
$this->server_caps = null;
}
return $noerror;
}
/**
* Parse a reply to HELO/EHLO command to discover server extensions.
* In case of HELO, the only parameter that can be discovered is a server name.
*
* @param string $type `HELO` or `EHLO`
*/
protected function parseHelloFields($type)
{
$this->server_caps = [];
$lines = explode("\n", $this->helo_rply);
foreach ($lines as $n => $s) {
//First 4 chars contain response code followed by - or space
$s = trim(substr($s, 4));
if (empty($s)) {
continue;
}
$fields = explode(' ', $s);
if (!empty($fields)) {
if (!$n) {
$name = $type;
$fields = $fields[0];
} else {
$name = array_shift($fields);
switch ($name) {
case 'SIZE':
$fields = ($fields ? $fields[0] : 0);
break;
case 'AUTH':
if (!is_array($fields)) {
$fields = [];
}
break;
default:
$fields = true;
}
}
$this->server_caps[$name] = $fields;
}
}
}
/**
* Send an SMTP MAIL command.
* Starts a mail transaction from the email address specified in
* $from. Returns true if successful or false otherwise. If True
* the mail transaction is started and then one or more recipient
* commands may be called followed by a data command.
* Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
*
* @param string $from Source address of this message
*
* @return bool
*/
public function mail($from)
{
$useVerp = ($this->do_verp ? ' XVERP' : '');
return $this->sendCommand(
'MAIL FROM',
'MAIL FROM:<' . $from . '>' . $useVerp,
250
);
}
/**
* Send an SMTP QUIT command.
* Closes the socket if there is no error or the $close_on_error argument is true.
* Implements from RFC 821: QUIT <CRLF>.
*
* @param bool $close_on_error Should the connection close if an error occurs?
*
* @return bool
*/
public function quit($close_on_error = true)
{
$noerror = $this->sendCommand('QUIT', 'QUIT', 221);
$err = $this->error; //Save any error
if ($noerror || $close_on_error) {
$this->close();
$this->error = $err; //Restore any error from the quit command
}
return $noerror;
}
/**
* Send an SMTP RCPT command.
* Sets the TO argument to $toaddr.
* Returns true if the recipient was accepted false if it was rejected.
* Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
*
* @param string $address The address the message is being sent to
* @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
* or DELAY. If you specify NEVER all other notifications are ignored.
*
* @return bool
*/
public function recipient($address, $dsn = '')
{
if (empty($dsn)) {
$rcpt = 'RCPT TO:<' . $address . '>';
} else {
$dsn = strtoupper($dsn);
$notify = [];
if (strpos($dsn, 'NEVER') !== false) {
$notify[] = 'NEVER';
} else {
foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
if (strpos($dsn, $value) !== false) {
$notify[] = $value;
}
}
}
$rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
}
return $this->sendCommand(
'RCPT TO',
$rcpt,
[250, 251]
);
}
/**
* Send an SMTP RSET command.
* Abort any transaction that is currently in progress.
* Implements RFC 821: RSET <CRLF>.
*
* @return bool True on success
*/
public function reset()
{
return $this->sendCommand('RSET', 'RSET', 250);
}
/**
* Send a command to an SMTP server and check its return code.
*
* @param string $command The command name - not sent to the server
* @param string $commandstring The actual command to send
* @param int|array $expect One or more expected integer success codes
*
* @return bool True on success
*/
protected function sendCommand($command, $commandstring, $expect)
{
if (!$this->connected()) {
$this->setError("Called $command without being connected");
return false;
}
//Reject line breaks in all commands
if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
$this->setError("Command '$command' contained line breaks");
return false;
}
$this->client_send($commandstring . static::LE, $command);
$this->last_reply = $this->get_lines();
//Fetch SMTP code and possible error code explanation
$matches = [];
if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
$code = (int) $matches[1];
$code_ex = (count($matches) > 2 ? $matches[2] : null);
//Cut off error code from each response line
$detail = preg_replace(
"/{$code}[ -]" .
($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
'',
$this->last_reply
);
} else {
//Fall back to simple parsing if regex fails
$code = (int) substr($this->last_reply, 0, 3);
$code_ex = null;
$detail = substr($this->last_reply, 4);
}
$this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
if (!in_array($code, (array) $expect, true)) {
$this->setError(
"$command command failed",
$detail,
$code,
$code_ex
);
$this->edebug(
'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
self::DEBUG_CLIENT
);
return false;
}
//Don't clear the error store when using keepalive
if ($command !== 'RSET') {
$this->setError('');
}
return true;
}
/**
* Send an SMTP SAML command.
* Starts a mail transaction from the email address specified in $from.
* Returns true if successful or false otherwise. If True
* the mail transaction is started and then one or more recipient
* commands may be called followed by a data command. This command
* will send the message to the users terminal if they are logged
* in and send them an email.
* Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
*
* @param string $from The address the message is from
*
* @return bool
*/
public function sendAndMail($from)
{
return $this->sendCommand('SAML', "SAML FROM:$from", 250);
}
/**
* Send an SMTP VRFY command.
*
* @param string $name The name to verify
*
* @return bool
*/
public function verify($name)
{
return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
}
/**
* Send an SMTP NOOP command.
* Used to keep keep-alives alive, doesn't actually do anything.
*
* @return bool
*/
public function noop()
{
return $this->sendCommand('NOOP', 'NOOP', 250);
}
/**
* Send an SMTP TURN command.
* This is an optional command for SMTP that this class does not support.
* This method is here to make the RFC821 Definition complete for this class
* and _may_ be implemented in future.
* Implements from RFC 821: TURN <CRLF>.
*
* @return bool
*/
public function turn()
{
$this->setError('The SMTP TURN command is not implemented');
$this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
return false;
}
/**
* Send raw data to the server.
*
* @param string $data The data to send
* @param string $command Optionally, the command this is part of, used only for controlling debug output
*
* @return int|bool The number of bytes sent to the server or false on error
*/
public function client_send($data, $command = '')
{
//If SMTP transcripts are left enabled, or debug output is posted online
//it can leak credentials, so hide credentials in all but lowest level
if (
self::DEBUG_LOWLEVEL > $this->do_debug &&
in_array($command, ['User & Password', 'Username', 'Password'], true)
) {
$this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
} else {
$this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
}
set_error_handler([$this, 'errorHandler']);
$result = fwrite($this->smtp_conn, $data);
restore_error_handler();
return $result;
}
/**
* Get the latest error.
*
* @return array
*/
public function getError()
{
return $this->error;
}
/**
* Get SMTP extensions available on the server.
*
* @return array|null
*/
public function getServerExtList()
{
return $this->server_caps;
}
/**
* Get metadata about the SMTP server from its HELO/EHLO response.
* The method works in three ways, dependent on argument value and current state:
* 1. HELO/EHLO has not been sent - returns null and populates $this->error.
* 2. HELO has been sent -
* $name == 'HELO': returns server name
* $name == 'EHLO': returns boolean false
* $name == any other string: returns null and populates $this->error
* 3. EHLO has been sent -
* $name == 'HELO'|'EHLO': returns the server name
* $name == any other string: if extension $name exists, returns True
* or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
*
* @param string $name Name of SMTP extension or 'HELO'|'EHLO'
*
* @return string|bool|null
*/
public function getServerExt($name)
{
if (!$this->server_caps) {
$this->setError('No HELO/EHLO was sent');
return null;
}
if (!array_key_exists($name, $this->server_caps)) {
if ('HELO' === $name) {
return $this->server_caps['EHLO'];
}
if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
return false;
}
$this->setError('HELO handshake was used; No information about server extensions available');
return null;
}
return $this->server_caps[$name];
}
/**
* Get the last reply from the server.
*
* @return string
*/
public function getLastReply()
{
return $this->last_reply;
}
/**
* Read the SMTP server's response.
* Either before eof or socket timeout occurs on the operation.
* With SMTP we can tell if we have more lines to read if the
* 4th character is '-' symbol. If it is a space then we don't
* need to read anything else.
*
* @return string
*/
protected function get_lines()
{
//If the connection is bad, give up straight away
if (!is_resource($this->smtp_conn)) {
return '';
}
$data = '';
$endtime = 0;
stream_set_timeout($this->smtp_conn, $this->Timeout);
if ($this->Timelimit > 0) {
$endtime = time() + $this->Timelimit;
}
$selR = [$this->smtp_conn];
$selW = null;
while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
//Must pass vars in here as params are by reference
//solution for signals inspired by https://github.com/symfony/symfony/pull/6540
set_error_handler([$this, 'errorHandler']);
$n = stream_select($selR, $selW, $selW, $this->Timelimit);
restore_error_handler();
if ($n === false) {
$message = $this->getError()['detail'];
$this->edebug(
'SMTP -> get_lines(): select failed (' . $message . ')',
self::DEBUG_LOWLEVEL
);
//stream_select returns false when the `select` system call is interrupted
//by an incoming signal, try the select again
if (stripos($message, 'interrupted system call') !== false) {
$this->edebug(
'SMTP -> get_lines(): retrying stream_select',
self::DEBUG_LOWLEVEL
);
$this->setError('');
continue;
}
break;
}
if (!$n) {
$this->edebug(
'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
self::DEBUG_LOWLEVEL
);
break;
}
//Deliberate noise suppression - errors are handled afterwards
$str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
$this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
$data .= $str;
//If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
//or 4th character is a space or a line break char, we are done reading, break the loop.
//String array access is a significant micro-optimisation over strlen
if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
break;
}
//Timed-out? Log and break
$info = stream_get_meta_data($this->smtp_conn);
if ($info['timed_out']) {
$this->edebug(
'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
self::DEBUG_LOWLEVEL
);
break;
}
//Now check if reads took too long
if ($endtime && time() > $endtime) {
$this->edebug(
'SMTP -> get_lines(): timelimit reached (' .
$this->Timelimit . ' sec)',
self::DEBUG_LOWLEVEL
);
break;
}
}
return $data;
}
/**
* Enable or disable VERP address generation.
*
* @param bool $enabled
*/
public function setVerp($enabled = false)
{
$this->do_verp = $enabled;
}
/**
* Get VERP address generation mode.
*
* @return bool
*/
public function getVerp()
{
return $this->do_verp;
}
/**
* Set error messages and codes.
*
* @param string $message The error message
* @param string $detail Further detail on the error
* @param string $smtp_code An associated SMTP error code
* @param string $smtp_code_ex Extended SMTP code
*/
protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
{
$this->error = [
'error' => $message,
'detail' => $detail,
'smtp_code' => $smtp_code,
'smtp_code_ex' => $smtp_code_ex,
];
}
/**
* Set debug output method.
*
* @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
*/
public function setDebugOutput($method = 'echo')
{
$this->Debugoutput = $method;
}
/**
* Get debug output method.
*
* @return string
*/
public function getDebugOutput()
{
return $this->Debugoutput;
}
/**
* Set debug output level.
*
* @param int $level
*/
public function setDebugLevel($level = 0)
{
$this->do_debug = $level;
}
/**
* Get debug output level.
*
* @return int
*/
public function getDebugLevel()
{
return $this->do_debug;
}
/**
* Set SMTP timeout.
*
* @param int $timeout The timeout duration in seconds
*/
public function setTimeout($timeout = 0)
{
$this->Timeout = $timeout;
}
/**
* Get SMTP timeout.
*
* @return int
*/
public function getTimeout()
{
return $this->Timeout;
}
/**
* Reports an error number and string.
*
* @param int $errno The error number returned by PHP
* @param string $errmsg The error message returned by PHP
* @param string $errfile The file the error occurred in
* @param int $errline The line number the error occurred on
*/
protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
{
$notice = 'Connection failed.';
$this->setError(
$notice,
$errmsg,
(string) $errno
);
$this->edebug(
"$notice Error #$errno: $errmsg [$errfile line $errline]",
self::DEBUG_CONNECTION
);
}
/**
* Extract and return the ID of the last SMTP transaction based on
* a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
* Relies on the host providing the ID in response to a DATA command.
* If no reply has been received yet, it will return null.
* If no pattern was matched, it will return false.
*
* @return bool|string|null
*/
protected function recordLastTransactionID()
{
$reply = $this->getLastReply();
if (empty($reply)) {
$this->last_smtp_transaction_id = null;
} else {
$this->last_smtp_transaction_id = false;
foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
$matches = [];
if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
$this->last_smtp_transaction_id = trim($matches[1]);
break;
}
}
}
return $this->last_smtp_transaction_id;
}
/**
* Get the queue/transaction ID of the last SMTP transaction
* If no reply has been received yet, it will return null.
* If no pattern was matched, it will return false.
*
* @return bool|string|null
*
* @see recordLastTransactionID()
*/
public function getLastTransactionID()
{
return $this->last_smtp_transaction_id;
}
}
<?php
/**
* REST API: WP_REST_Post_Search_Handler class
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/
/**
* Core class representing a search handler for posts in the REST API.
*
* @since 5.0.0
*/
class WP_REST_Post_Search_Handler extends WP_REST_Search_Handler {
/**
* Constructor.
*
* @since 5.0.0
*/
public function __construct() {
$this->type = 'post';
// Support all public post types except attachments.
$this->subtypes = array_diff(
array_values(
get_post_types(
array(
'public' => true,
'show_in_rest' => true,
),
'names'
)
),
array( 'attachment' )
);
}
/**
* Searches the object type content for a given search request.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full REST request.
* @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing
* an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the
* total count for the matching search results.
*/
public function search_items( WP_REST_Request $request ) {
// Get the post types to search for the current request.
$post_types = $request[ WP_REST_Search_Controller::PROP_SUBTYPE ];
if ( in_array( WP_REST_Search_Controller::TYPE_ANY, $post_types, true ) ) {
$post_types = $this->subtypes;
}
$query_args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'paged' => (int) $request['page'],
'posts_per_page' => (int) $request['per_page'],
'ignore_sticky_posts' => true,
'fields' => 'ids',
);
if ( ! empty( $request['search'] ) ) {
$query_args['s'] = $request['search'];
}
/**
* Filters the query arguments for a search request.
*
* Enables adding extra arguments or setting defaults for a post search request.
*
* @since 5.1.0
*
* @param array $query_args Key value array of query var to query value.
* @param WP_REST_Request $request The request used.
*/
$query_args = apply_filters( 'rest_post_search_query', $query_args, $request );
$query = new WP_Query();
$found_ids = $query->query( $query_args );
$total = $query->found_posts;
return array(
self::RESULT_IDS => $found_ids,
self::RESULT_TOTAL => $total,
);
}
/**
* Prepares the search result for a given ID.
*
* @since 5.0.0
*
* @param int $id Item ID.
* @param array $fields Fields to include for the item.
* @return array Associative array containing all fields for the item.
*/
public function prepare_item( $id, array $fields ) {
$post = get_post( $id );
$data = array();
if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_ID ] = (int) $post->ID;
}
if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) {
if ( post_type_supports( $post->post_type, 'title' ) ) {
add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
$data[ WP_REST_Search_Controller::PROP_TITLE ] = get_the_title( $post->ID );
remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
} else {
$data[ WP_REST_Search_Controller::PROP_TITLE ] = '';
}
}
if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_URL ] = get_permalink( $post->ID );
}
if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type;
}
if ( in_array( WP_REST_Search_Controller::PROP_SUBTYPE, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_SUBTYPE ] = $post->post_type;
}
return $data;
}
/**
* Prepares links for the search result of a given ID.
*
* @since 5.0.0
*
* @param int $id Item ID.
* @return array Links for the given item.
*/
public function prepare_item_links( $id ) {
$post = get_post( $id );
$links = array();
$item_route = $this->detect_rest_item_route( $post );
if ( ! empty( $item_route ) ) {
$links['self'] = array(
'href' => rest_url( $item_route ),
'embeddable' => true,
);
}
$links['about'] = array(
'href' => rest_url( 'core/types/' . $post->post_type ),
);
return $links;
}
/**
* Overwrites the default protected title format.
*
* By default, WordPress will show password protected posts with a title of
* "Protected: %s". As the REST API communicates the protected status of a post
* in a machine readable format, we remove the "Protected: " prefix.
*
* @since 5.0.0
*
* @return string Protected title format.
*/
public function protected_title_format() {
return '%s';
}
/**
* Attempts to detect the route to access a single item.
*
* @since 5.0.0
*
* @param WP_Post $post Post object.
* @return string REST route relative to the REST base URI, or empty string if unknown.
*/
protected function detect_rest_item_route( $post ) {
$post_type = get_post_type_object( $post->post_type );
if ( ! $post_type ) {
return '';
}
// It's currently impossible to detect the REST URL from a custom controller.
if ( ! empty( $post_type->rest_controller_class ) && 'WP_REST_Posts_Controller' !== $post_type->rest_controller_class ) {
return '';
}
$namespace = 'core';
$rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
return sprintf( '%s/%s/%d', $namespace, $rest_base, $post->ID );
}
}
<?php
/**
* REST API: WP_REST_Search_Handler class
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/
/**
* Core base class representing a search handler for an object type in the REST API.
*
* @since 5.0.0
*/
abstract class WP_REST_Search_Handler {
/**
* Field containing the IDs in the search result.
*/
const RESULT_IDS = 'ids';
/**
* Field containing the total count in the search result.
*/
const RESULT_TOTAL = 'total';
/**
* Object type managed by this search handler.
*
* @since 5.0.0
* @var string
*/
protected $type = '';
/**
* Object subtypes managed by this search handler.
*
* @since 5.0.0
* @var array
*/
protected $subtypes = array();
/**
* Gets the object type managed by this search handler.
*
* @since 5.0.0
*
* @return string Object type identifier.
*/
public function get_type() {
return $this->type;
}
/**
* Gets the object subtypes managed by this search handler.
*
* @since 5.0.0
*
* @return array Array of object subtype identifiers.
*/
public function get_subtypes() {
return $this->subtypes;
}
/**
* Searches the object type content for a given search request.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full REST request.
* @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing
* an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the
* total count for the matching search results.
*/
abstract public function search_items( WP_REST_Request $request );
/**
* Prepares the search result for a given ID.
*
* @since 5.0.0
*
* @param int $id Item ID.
* @param array $fields Fields to include for the item.
* @return array Associative array containing all fields for the item.
*/
abstract public function prepare_item( $id, array $fields );
/**
* Prepares links for the search result of a given ID.
*
* @since 5.0.0
*
* @param int $id Item ID.
* @return array Links for the given item.
*/
abstract public function prepare_item_links( $id );
}
<?php
/**
* REST API: WP_REST_Response class
*
* @package WordPress
* @subpackage REST_API
* @since 4.4.0
*/
/**
* Core class used to implement a REST response object.
*
* @since 4.4.0
*
* @see WP_HTTP_Response
*/
class WP_REST_Response extends WP_HTTP_Response {
/**
* Links related to the response.
*
* @since 4.4.0
* @var array
*/
protected $links = array();
/**
* The route that was to create the response.
*
* @since 4.4.0
* @var string
*/
protected $matched_route = '';
/**
* The handler that was used to create the response.
*
* @since 4.4.0
* @var null|array
*/
protected $matched_handler = null;
/**
* Adds a link to the response.
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple.
*
* @since 4.4.0
*
* @link https://tools.ietf.org/html/rfc5988
* @link https://www.iana.org/assignments/link-relations/link-relations.xml
*
* @param string $rel Link relation. Either an IANA registered type,
* or an absolute URL.
* @param string $href Target URI for the link.
* @param array $attributes Optional. Link parameters to send along with the URL. Default empty array.
*/
public function add_link( $rel, $href, $attributes = array() ) {
if ( empty( $this->links[ $rel ] ) ) {
$this->links[ $rel ] = array();
}
if ( isset( $attributes['href'] ) ) {
// Remove the href attribute, as it's used for the main URL.
unset( $attributes['href'] );
}
$this->links[ $rel ][] = array(
'href' => $href,
'attributes' => $attributes,
);
}
/**
* Removes a link from the response.
*
* @since 4.4.0
*
* @param string $rel Link relation. Either an IANA registered type, or an absolute URL.
* @param string $href Optional. Only remove links for the relation matching the given href.
* Default null.
*/
public function remove_link( $rel, $href = null ) {
if ( ! isset( $this->links[ $rel ] ) ) {
return;
}
if ( $href ) {
$this->links[ $rel ] = wp_list_filter( $this->links[ $rel ], array( 'href' => $href ), 'NOT' );
} else {
$this->links[ $rel ] = array();
}
if ( ! $this->links[ $rel ] ) {
unset( $this->links[ $rel ] );
}
}
/**
* Adds multiple links to the response.
*
* Link data should be an associative array with link relation as the key.
* The value can either be an associative array of link attributes
* (including `href` with the URL for the response), or a list of these
* associative arrays.
*
* @since 4.4.0
*
* @param array $links Map of link relation to list of links.
*/
public function add_links( $links ) {
foreach ( $links as $rel => $set ) {
// If it's a single link, wrap with an array for consistent handling.
if ( isset( $set['href'] ) ) {
$set = array( $set );
}
foreach ( $set as $attributes ) {
$this->add_link( $rel, $attributes['href'], $attributes );
}
}
}
/**
* Retrieves links for the response.
*
* @since 4.4.0
*
* @return array List of links.
*/
public function get_links() {
return $this->links;
}
/**
* Sets a single link header.
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple.
*
* @since 4.4.0
*
* @link https://tools.ietf.org/html/rfc5988
* @link https://www.iana.org/assignments/link-relations/link-relations.xml
*
* @param string $rel Link relation. Either an IANA registered type, or an absolute URL.
* @param string $link Target IRI for the link.
* @param array $other Optional. Other parameters to send, as an assocative array.
* Default empty array.
*/
public function link_header( $rel, $link, $other = array() ) {
$header = '<' . $link . '>; rel="' . $rel . '"';
foreach ( $other as $key => $value ) {
if ( 'title' === $key ) {
$value = '"' . $value . '"';
}
$header .= '; ' . $key . '=' . $value;
}
$this->header( 'Link', $header, false );
}
/**
* Retrieves the route that was used.
*
* @since 4.4.0
*
* @return string The matched route.
*/
public function get_matched_route() {
return $this->matched_route;
}
/**
* Sets the route (regex for path) that caused the response.
*
* @since 4.4.0
*
* @param string $route Route name.
*/
public function set_matched_route( $route ) {
$this->matched_route = $route;
}
/**
* Retrieves the handler that was used to generate the response.
*
* @since 4.4.0
*
* @return null|array The handler that was used to create the response.
*/
public function get_matched_handler() {
return $this->matched_handler;
}
/**
* Sets the handler that was responsible for generating the response.
*
* @since 4.4.0
*
* @param array $handler The matched handler.
*/
public function set_matched_handler( $handler ) {
$this->matched_handler = $handler;
}
/**
* Checks if the response is an error, i.e. >= 400 response code.
*
* @since 4.4.0
*
* @return bool Whether the response is an error.
*/
public function is_error() {
return $this->get_status() >= 400;
}
/**
* Retrieves a WP_Error object from the response.
*
* @since 4.4.0
*
* @return WP_Error|null WP_Error or null on not an errored response.
*/
public function as_error() {
if ( ! $this->is_error() ) {
return null;
}
$error = new WP_Error;
if ( is_array( $this->get_data() ) ) {
$data = $this->get_data();
$error->add( $data['code'], $data['message'], $data['data'] );
if ( ! empty( $data['additional_errors'] ) ) {
foreach ( $data['additional_errors'] as $err ) {
$error->add( $err['code'], $err['message'], $err['data'] );
}
}
} else {
$error->add( $this->get_status(), '', array( 'status' => $this->get_status() ) );
}
return $error;
}
/**
* Retrieves the CURIEs (compact URIs) used for relations.
*
* @since 4.5.0
*
* @return array Compact URIs.
*/
public function get_curies() {
$curies = array(
array(
'name' => 'wp',
'href' => 'https://api.w.org/{rel}',
'templated' => true,
),
);
/**
* Filters extra CURIEs available on API responses.
*
* CURIEs allow a shortened version of URI relations. This allows a more
* usable form for custom relations than using the full URI. These work
* similarly to how XML namespaces work.
*
* Registered CURIES need to specify a name and URI template. This will
* automatically transform URI relations into their shortened version.
* The shortened relation follows the format `{name}:{rel}`. `{rel}` in
* the URI template will be replaced with the `{rel}` part of the
* shortened relation.
*
* For example, a CURIE with name `example` and URI template
* `http://w.org/{rel}` would transform a `http://w.org/term` relation
* into `example:term`.
*
* Well-behaved clients should expand and normalise these back to their
* full URI relation, however some naive clients may not resolve these
* correctly, so adding new CURIEs may break backward compatibility.
*
* @since 4.5.0
*
* @param array $additional Additional CURIEs to register with the API.
*/
$additional = apply_filters( 'rest_response_link_curies', array() );
return array_merge( $curies, $additional );
}
}
<?php
/**
* REST API: WP_REST_Server class
*
* @package WordPress
* @subpackage REST_API
* @since 4.4.0
*/
/**
* Core class used to implement the WordPress REST API server.
*
* @since 4.4.0
*/
class WP_REST_Server {
/**
* Alias for GET transport method.
*
* @since 4.4.0
* @var string
*/
const READABLE = 'GET';
/**
* Alias for POST transport method.
*
* @since 4.4.0
* @var string
*/
const CREATABLE = 'POST';
/**
* Alias for POST, PUT, PATCH transport methods together.
*
* @since 4.4.0
* @var string
*/
const EDITABLE = 'POST, PUT, PATCH';
/**
* Alias for DELETE transport method.
*
* @since 4.4.0
* @var string
*/
const DELETABLE = 'DELETE';
/**
* Alias for GET, POST, PUT, PATCH & DELETE transport methods together.
*
* @since 4.4.0
* @var string
*/
const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE';
/**
* Namespaces registered to the server.
*
* @since 4.4.0
* @var array
*/
protected $namespaces = array();
/**
* Endpoints registered to the server.
*
* @since 4.4.0
* @var array
*/
protected $endpoints = array();
/**
* Options defined for the routes.
*
* @since 4.4.0
* @var array
*/
protected $route_options = array();
/**
* Instantiates the REST server.
*
* @since 4.4.0
*/
public function __construct() {
$this->endpoints = array(
// Meta endpoints.
'/' => array(
'callback' => array( $this, 'get_index' ),
'methods' => 'GET',
'args' => array(
'context' => array(
'default' => 'view',
),
),
),
);
}
/**
* Checks the authentication headers if supplied.
*
* @since 4.4.0
*
* @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful
* or no authentication provided
*/
public function check_authentication() {
/**
* Filters REST authentication errors.
*
* This is used to pass a WP_Error from an authentication method back to
* the API.
*
* Authentication methods should check first if they're being used, as
* multiple authentication methods can be enabled on a site (cookies,
* HTTP basic auth, OAuth). If the authentication method hooked in is
* not actually being attempted, null should be returned to indicate
* another authentication method should check instead. Similarly,
* callbacks should ensure the value is `null` before checking for
* errors.
*
* A WP_Error instance can be returned if an error occurs, and this should
* match the format used by API methods internally (that is, the `status`
* data should be used). A callback can return `true` to indicate that
* the authentication method was used, and it succeeded.
*
* @since 4.4.0
*
* @param WP_Error|null|bool WP_Error if authentication error, null if authentication
* method wasn't used, true if authentication succeeded.
*/
return apply_filters( 'rest_authentication_errors', null );
}
/**
* Converts an error to a response object.
*
* This iterates over all error codes and messages to change it into a flat
* array. This enables simpler client behaviour, as it is represented as a
* list in JSON rather than an object/map.
*
* @since 4.4.0
*
* @param WP_Error $error WP_Error instance.
* @return WP_REST_Response List of associative arrays with code and message keys.
*/
protected function error_to_response( $error ) {
$error_data = $error->get_error_data();
if ( is_array( $error_data ) && isset( $error_data['status'] ) ) {
$status = $error_data['status'];
} else {
$status = 500;
}
$errors = array();
foreach ( (array) $error->errors as $code => $messages ) {
foreach ( (array) $messages as $message ) {
$errors[] = array(
'code' => $code,
'message' => $message,
'data' => $error->get_error_data( $code ),
);
}
}
$data = $errors[0];
if ( count( $errors ) > 1 ) {
// Remove the primary error.
array_shift( $errors );
$data['additional_errors'] = $errors;
}
$response = new WP_REST_Response( $data, $status );
return $response;
}
/**
* Retrieves an appropriate error representation in JSON.
*
* Note: This should only be used in WP_REST_Server::serve_request(), as it
* cannot handle WP_Error internally. All callbacks and other internal methods
* should instead return a WP_Error with the data set to an array that includes
* a 'status' key, with the value being the HTTP status to send.
*
* @since 4.4.0
*
* @param string $code WP_Error-style code.
* @param string $message Human-readable message.
* @param int $status Optional. HTTP status code to send. Default null.
* @return string JSON representation of the error
*/
protected function json_error( $code, $message, $status = null ) {
if ( $status ) {
$this->set_status( $status );
}
$error = compact( 'code', 'message' );
return wp_json_encode( $error );
}
/**
* Handles serving an API request.
*
* Matches the current server URI to a route and runs the first matching
* callback then outputs a JSON representation of the returned value.
*
* @since 4.4.0
*
* @see WP_REST_Server::dispatch()
*
* @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
* Default null.
* @return false|null Null if not served and a HEAD request, false otherwise.
*/
public function serve_request( $path = null ) {
$content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json';
$this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
$this->send_header( 'X-Robots-Tag', 'noindex' );
$api_root = get_rest_url();
if ( ! empty( $api_root ) ) {
$this->send_header( 'Link', '<' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"' );
}
/*
* Mitigate possible JSONP Flash attacks.
*
* https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
*/
$this->send_header( 'X-Content-Type-Options', 'nosniff' );
$this->send_header( 'Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages' );
$this->send_header( 'Access-Control-Allow-Headers', 'Authorization, Content-Type' );
/**
* Send nocache headers on authenticated requests.
*
* @since 4.4.0
*
* @param bool $rest_send_nocache_headers Whether to send no-cache headers.
*/
$send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
if ( $send_no_cache_headers ) {
foreach ( wp_get_nocache_headers() as $header => $header_value ) {
if ( empty( $header_value ) ) {
$this->remove_header( $header );
} else {
$this->send_header( $header, $header_value );
}
}
}
/**
* Filters whether the REST API is enabled.
*
* @since 4.4.0
* @deprecated 4.7.0 Use the rest_authentication_errors filter to restrict access to the API
*
* @param bool $rest_enabled Whether the REST API is enabled. Default true.
*/
apply_filters_deprecated(
'rest_enabled',
array( true ),
'4.7.0',
'rest_authentication_errors',
__( 'The REST API can no longer be completely disabled, the rest_authentication_errors filter can be used to restrict access to the API, instead.' )
);
/**
* Filters whether jsonp is enabled.
*
* @since 4.4.0
*
* @param bool $jsonp_enabled Whether jsonp is enabled. Default true.
*/
$jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
$jsonp_callback = null;
if ( isset( $_GET['_jsonp'] ) ) {
if ( ! $jsonp_enabled ) {
echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
return false;
}
$jsonp_callback = $_GET['_jsonp'];
if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 );
return false;
}
}
if ( empty( $path ) ) {
if ( isset( $_SERVER['PATH_INFO'] ) ) {
$path = $_SERVER['PATH_INFO'];
} else {
$path = '/';
}
}
$request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
$request->set_query_params( wp_unslash( $_GET ) );
$request->set_body_params( wp_unslash( $_POST ) );
$request->set_file_params( $_FILES );
$request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
$request->set_body( $this->get_raw_data() );
/*
* HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
* $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
* header.
*/
if ( isset( $_GET['_method'] ) ) {
$request->set_method( $_GET['_method'] );
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
$request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
}
$result = $this->check_authentication();
if ( ! is_wp_error( $result ) ) {
$result = $this->dispatch( $request );
}
// Normalize to either WP_Error or WP_REST_Response...
$result = rest_ensure_response( $result );
// ...then convert WP_Error across.
if ( is_wp_error( $result ) ) {
$result = $this->error_to_response( $result );
}
/**
* Filters the API response.
*
* Allows modification of the response before returning.
*
* @since 4.4.0
* @since 4.5.0 Applied to embedded responses.
*
* @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response.
* @param WP_REST_Server $this Server instance.
* @param WP_REST_Request $request Request used to generate the response.
*/
$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
// Wrap the response in an envelope if asked for.
if ( isset( $_GET['_envelope'] ) ) {
$result = $this->envelope_response( $result, isset( $_GET['_embed'] ) );
}
// Send extra data from response objects.
$headers = $result->get_headers();
$this->send_headers( $headers );
$code = $result->get_status();
$this->set_status( $code );
/**
* Filters whether the request has already been served.
*
* Allow sending the request manually - by returning true, the API result
* will not be sent to the client.
*
* @since 4.4.0
*
* @param bool $served Whether the request has already been served.
* Default false.
* @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response.
* @param WP_REST_Request $request Request used to generate the response.
* @param WP_REST_Server $this Server instance.
*/
$served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
if ( ! $served ) {
if ( 'HEAD' === $request->get_method() ) {
return null;
}
// Embed links inside the request.
$result = $this->response_to_data( $result, isset( $_GET['_embed'] ) );
/**
* Filters the API response.
*
* Allows modification of the response data after inserting
* embedded data (if any) and before echoing the response data.
*
* @since 4.8.1
*
* @param array $result Response data to send to the client.
* @param WP_REST_Server $this Server instance.
* @param WP_REST_Request $request Request used to generate the response.
*/
$result = apply_filters( 'rest_pre_echo_response', $result, $this, $request );
$result = wp_json_encode( $result );
$json_error_message = $this->get_json_last_error();
if ( $json_error_message ) {
$json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, array( 'status' => 500 ) );
$result = $this->error_to_response( $json_error_obj );
$result = wp_json_encode( $result->data[0] );
}
if ( $jsonp_callback ) {
// Prepend '/**/' to mitigate possible JSONP Flash attacks.
// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
echo '/**/' . $jsonp_callback . '(' . $result . ')';
} else {
echo $result;
}
}
return null;
}
/**
* Converts a response to data to send.
*
* @since 4.4.0
*
* @param WP_REST_Response $response Response object.
* @param bool $embed Whether links should be embedded.
* @return array {
* Data with sub-requests embedded.
*
* @type array [$_links] Links.
* @type array [$_embedded] Embeddeds.
* }
*/
public function response_to_data( $response, $embed ) {
$data = $response->get_data();
$links = $this->get_compact_response_links( $response );
if ( ! empty( $links ) ) {
// Convert links to part of the data.
$data['_links'] = $links;
}
if ( $embed ) {
// Determine if this is a numeric array.
if ( wp_is_numeric_array( $data ) ) {
$data = array_map( array( $this, 'embed_links' ), $data );
} else {
$data = $this->embed_links( $data );
}
}
return $data;
}
/**
* Retrieves links from a response.
*
* Extracts the links from a response into a structured hash, suitable for
* direct output.
*
* @since 4.4.0
*
* @param WP_REST_Response $response Response to extract links from.
* @return array Map of link relation to list of link hashes.
*/
public static function get_response_links( $response ) {
$links = $response->get_links();
if ( empty( $links ) ) {
return array();
}
// Convert links to part of the data.
$data = array();
foreach ( $links as $rel => $items ) {
$data[ $rel ] = array();
foreach ( $items as $item ) {
$attributes = $item['attributes'];
$attributes['href'] = $item['href'];
$data[ $rel ][] = $attributes;
}
}
return $data;
}
/**
* Retrieves the CURIEs (compact URIs) used for relations.
*
* Extracts the links from a response into a structured hash, suitable for
* direct output.
*
* @since 4.5.0
*
* @param WP_REST_Response $response Response to extract links from.
* @return array Map of link relation to list of link hashes.
*/
public static function get_compact_response_links( $response ) {
$links = self::get_response_links( $response );
if ( empty( $links ) ) {
return array();
}
$curies = $response->get_curies();
$used_curies = array();
foreach ( $links as $rel => $items ) {
// Convert $rel URIs to their compact versions if they exist.
foreach ( $curies as $curie ) {
$href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
if ( strpos( $rel, $href_prefix ) !== 0 ) {
continue;
}
// Relation now changes from '$uri' to '$curie:$relation'.
$rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
preg_match( '!' . $rel_regex . '!', $rel, $matches );
if ( $matches ) {
$new_rel = $curie['name'] . ':' . $matches[1];
$used_curies[ $curie['name'] ] = $curie;
$links[ $new_rel ] = $items;
unset( $links[ $rel ] );
break;
}
}
}
// Push the curies onto the start of the links array.
if ( $used_curies ) {
$links['curies'] = array_values( $used_curies );
}
return $links;
}
/**
* Embeds the links from the data into the request.
*
* @since 4.4.0
*
* @param array $data Data from the request.
* @return array {
* Data with sub-requests embedded.
*
* @type array [$_links] Links.
* @type array [$_embedded] Embeddeds.
* }
*/
protected function embed_links( $data ) {
if ( empty( $data['_links'] ) ) {
return $data;
}
$embedded = array();
foreach ( $data['_links'] as $rel => $links ) {
// Ignore links to self, for obvious reasons.
if ( 'self' === $rel ) {
continue;
}
$embeds = array();
foreach ( $links as $item ) {
// Determine if the link is embeddable.
if ( empty( $item['embeddable'] ) ) {
// Ensure we keep the same order.
$embeds[] = array();
continue;
}
// Run through our internal routing and serve.
$request = WP_REST_Request::from_url( $item['href'] );
if ( ! $request ) {
$embeds[] = array();
continue;
}
// Embedded resources get passed context=embed.
if ( empty( $request['context'] ) ) {
$request['context'] = 'embed';
}
$response = $this->dispatch( $request );
/** This filter is documented in site-inc/rest-api/class-wp-rest-server.php */
$response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
$embeds[] = $this->response_to_data( $response, false );
}
// Determine if any real links were found.
$has_links = count( array_filter( $embeds ) );
if ( $has_links ) {
$embedded[ $rel ] = $embeds;
}
}
if ( ! empty( $embedded ) ) {
$data['_embedded'] = $embedded;
}
return $data;
}
/**
* Wraps the response in an envelope.
*
* The enveloping technique is used to work around browser/client
* compatibility issues. Essentially, it converts the full HTTP response to
* data instead.
*
* @since 4.4.0
*
* @param WP_REST_Response $response Response object.
* @param bool $embed Whether links should be embedded.
* @return WP_REST_Response New response with wrapped data
*/
public function envelope_response( $response, $embed ) {
$envelope = array(
'body' => $this->response_to_data( $response, $embed ),
'status' => $response->get_status(),
'headers' => $response->get_headers(),
);
/**
* Filters the enveloped form of a response.
*
* @since 4.4.0
*
* @param array $envelope Envelope data.
* @param WP_REST_Response $response Original response data.
*/
$envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
// Ensure it's still a response and return.
return rest_ensure_response( $envelope );
}
/**
* Registers a route to the server.
*
* @since 4.4.0
*
* @param string $namespace Namespace.
* @param string $route The REST route.
* @param array $route_args Route arguments.
* @param bool $override Optional. Whether the route should be overridden if it already exists.
* Default false.
*/
public function register_route( $namespace, $route, $route_args, $override = false ) {
if ( ! isset( $this->namespaces[ $namespace ] ) ) {
$this->namespaces[ $namespace ] = array();
$this->register_route(
$namespace,
'/' . $namespace,
array(
array(
'methods' => self::READABLE,
'callback' => array( $this, 'get_namespace_index' ),
'args' => array(
'namespace' => array(
'default' => $namespace,
),
'context' => array(
'default' => 'view',
),
),
),
)
);
}
// Associative to avoid double-registration.
$this->namespaces[ $namespace ][ $route ] = true;
$route_args['namespace'] = $namespace;
if ( $override || empty( $this->endpoints[ $route ] ) ) {
$this->endpoints[ $route ] = $route_args;
} else {
$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
}
}
/**
* Retrieves the route map.
*
* The route map is an associative array with path regexes as the keys. The
* value is an indexed array with the callback function/method as the first
* item, and a bitmask of HTTP methods as the second item (see the class
* constants).
*
* Each route can be mapped to more than one callback by using an array of
* the indexed arrays. This allows mapping e.g. GET requests to one callback
* and POST requests to another.
*
* Note that the path regexes (array keys) must have @ escaped, as this is
* used as the delimiter with preg_match()
*
* @since 4.4.0
*
* @return array `'/path/regex' => array( $callback, $bitmask )` or
* `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
*/
public function get_routes() {
/**
* Filters the array of available endpoints.
*
* @since 4.4.0
*
* @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
* to an array of callbacks for the endpoint. These take the format
* `'/path/regex' => array( $callback, $bitmask )` or
* `'/path/regex' => array( array( $callback, $bitmask ).
*/
$endpoints = apply_filters( 'rest_endpoints', $this->endpoints );
// Normalise the endpoints.
$defaults = array(
'methods' => '',
'accept_json' => false,
'accept_raw' => false,
'show_in_index' => true,
'args' => array(),
);
foreach ( $endpoints as $route => &$handlers ) {
if ( isset( $handlers['callback'] ) ) {
// Single endpoint, add one deeper.
$handlers = array( $handlers );
}
if ( ! isset( $this->route_options[ $route ] ) ) {
$this->route_options[ $route ] = array();
}
foreach ( $handlers as $key => &$handler ) {
if ( ! is_numeric( $key ) ) {
// Route option, move it to the options.
$this->route_options[ $route ][ $key ] = $handler;
unset( $handlers[ $key ] );
continue;
}
$handler = wp_parse_args( $handler, $defaults );
// Allow comma-separated HTTP methods.
if ( is_string( $handler['methods'] ) ) {
$methods = explode( ',', $handler['methods'] );
} elseif ( is_array( $handler['methods'] ) ) {
$methods = $handler['methods'];
} else {
$methods = array();
}
$handler['methods'] = array();
foreach ( $methods as $method ) {
$method = strtoupper( trim( $method ) );
$handler['methods'][ $method ] = true;
}
}
}
return $endpoints;
}
/**
* Retrieves namespaces registered on the server.
*
* @since 4.4.0
*
* @return array List of registered namespaces.
*/
public function get_namespaces() {
return array_keys( $this->namespaces );
}
/**
* Retrieves specified options for a route.
*
* @since 4.4.0
*
* @param string $route Route pattern to fetch options for.
* @return array|null Data as an associative array if found, or null if not found.
*/
public function get_route_options( $route ) {
if ( ! isset( $this->route_options[ $route ] ) ) {
return null;
}
return $this->route_options[ $route ];
}
/**
* Matches the request to a callback and call it.
*
* @since 4.4.0
*
* @param WP_REST_Request $request Request to attempt dispatching.
* @return WP_REST_Response Response returned by the callback.
*/
public function dispatch( $request ) {
/**
* Filters the pre-calculated result of a REST dispatch request.
*
* Allow hijacking the request before dispatching by returning a non-empty. The returned value
* will be used to serve the request instead.
*
* @since 4.4.0
*
* @param mixed $result Response to replace the requested version with. Can be anything
* a normal endpoint can return, or null to not hijack the request.
* @param WP_REST_Server $this Server instance.
* @param WP_REST_Request $request Request used to generate the response.
*/
$result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
if ( ! empty( $result ) ) {
return $result;
}
$method = $request->get_method();
$path = $request->get_route();
foreach ( $this->get_routes() as $route => $handlers ) {
$match = preg_match( '@^' . $route . '$@i', $path, $matches );
if ( ! $match ) {
continue;
}
$args = array();
foreach ( $matches as $param => $value ) {
if ( ! is_int( $param ) ) {
$args[ $param ] = $value;
}
}
foreach ( $handlers as $handler ) {
$callback = $handler['callback'];
$response = null;
// Fallback to GET method if no HEAD method is registered.
$checked_method = $method;
if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
$checked_method = 'GET';
}
if ( empty( $handler['methods'][ $checked_method ] ) ) {
continue;
}
if ( ! is_callable( $callback ) ) {
$response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
}
if ( ! is_wp_error( $response ) ) {
// Remove the redundant preg_match argument.
unset( $args[0] );
$request->set_url_params( $args );
$request->set_attributes( $handler );
$defaults = array();
foreach ( $handler['args'] as $arg => $options ) {
if ( isset( $options['default'] ) ) {
$defaults[ $arg ] = $options['default'];
}
}
$request->set_default_params( $defaults );
$check_required = $request->has_valid_params();
if ( is_wp_error( $check_required ) ) {
$response = $check_required;
} else {
$check_sanitized = $request->sanitize_params();
if ( is_wp_error( $check_sanitized ) ) {
$response = $check_sanitized;
}
}
}
/**
* Filters the response before executing any REST API callbacks.
*
* Allows plugins to perform additional validation after a
* request is initialized and matched to a registered route,
* but before it is executed.
*
* Note that this filter will not be called for requests that
* fail to authenticate or match to a registered route.
*
* @since 4.7.0
*
* @param WP_HTTP_Response|WP_Error $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*/
$response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
if ( ! is_wp_error( $response ) ) {
// Check permission specified on the route.
if ( ! empty( $handler['permission_callback'] ) ) {
$permission = call_user_func( $handler['permission_callback'], $request );
if ( is_wp_error( $permission ) ) {
$response = $permission;
} elseif ( false === $permission || null === $permission ) {
$response = new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to do that.' ), array( 'status' => rest_authorization_required_code() ) );
}
}
}
if ( ! is_wp_error( $response ) ) {
/**
* Filters the REST dispatch request result.
*
* Allow plugins to override dispatching the request.
*
* @since 4.4.0
* @since 4.5.0 Added `$route` and `$handler` parameters.
*
* @param mixed $dispatch_result Dispatch result, will be used if not empty.
* @param WP_REST_Request $request Request used to generate the response.
* @param string $route Route matched for the request.
* @param array $handler Route handler used for the request.
*/
$dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
// Allow plugins to halt the request via this filter.
if ( null !== $dispatch_result ) {
$response = $dispatch_result;
} else {
$response = call_user_func( $callback, $request );
}
}
/**
* Filters the response immediately after executing any REST API
* callbacks.
*
* Allows plugins to perform any needed cleanup, for example,
* to undo changes made during the {@see 'rest_request_before_callbacks'}
* filter.
*
* Note that this filter will not be called for requests that
* fail to authenticate or match to a registered route.
*
* Note that an endpoint's `permission_callback` can still be
* called after this filter - see `rest_send_allow_header()`.
*
* @since 4.7.0
*
* @param WP_HTTP_Response|WP_Error $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*/
$response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
} else {
$response = rest_ensure_response( $response );
}
$response->set_matched_route( $route );
$response->set_matched_handler( $handler );
return $response;
}
}
return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) );
}
/**
* Returns if an error occurred during most recent JSON encode/decode.
*
* Strings to be translated will be in format like
* "Encoding error: Maximum stack depth exceeded".
*
* @since 4.4.0
*
* @return bool|string Boolean false or string error message.
*/
protected function get_json_last_error() {
// See https://core.trac.wordpress.org/ticket/27799.
if ( ! function_exists( 'json_last_error' ) ) {
return false;
}
$last_error_code = json_last_error();
if ( ( defined( 'JSON_ERROR_NONE' ) && JSON_ERROR_NONE === $last_error_code ) || empty( $last_error_code ) ) {
return false;
}
return json_last_error_msg();
}
/**
* Retrieves the site index.
*
* This endpoint describes the capabilities of the site.
*
* @since 4.4.0
*
* @param array $request {
* Request.
*
* @type string $context Context.
* }
* @return array Index entity
*/
public function get_index( $request ) {
// General site data.
$available = array(
'name' => get_option( 'blogname' ),
'description' => get_option( 'blogdescription' ),
'url' => get_option( 'siteurl' ),
'home' => home_url(),
'gmt_offset' => get_option( 'gmt_offset' ),
'timezone_string' => get_option( 'timezone_string' ),
'namespaces' => array_keys( $this->namespaces ),
'authentication' => array(),
'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
);
$response = new WP_REST_Response( $available );
$response->add_link( 'help', 'http://v2.wp-api.org/' );
/**
* Filters the API root index data.
*
* This contains the data describing the API. This includes information
* about supported authentication schemes, supported namespaces, routes
* available on the API, and a small amount of data about the site.
*
* @since 4.4.0
*
* @param WP_REST_Response $response Response data.
*/
return apply_filters( 'rest_index', $response );
}
/**
* Retrieves the index for a namespace.
*
* @since 4.4.0
*
* @param WP_REST_Request $request REST request instance.
* @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
* WP_Error if the namespace isn't set.
*/
public function get_namespace_index( $request ) {
$namespace = $request['namespace'];
if ( ! isset( $this->namespaces[ $namespace ] ) ) {
return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) );
}
$routes = $this->namespaces[ $namespace ];
$endpoints = array_intersect_key( $this->get_routes(), $routes );
$data = array(
'namespace' => $namespace,
'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ),
);
$response = rest_ensure_response( $data );
// Link to the root index.
$response->add_link( 'up', rest_url( '/' ) );
/**
* Filters the namespace index data.
*
* This typically is just the route data for the namespace, but you can
* add any data you'd like here.
*
* @since 4.4.0
*
* @param WP_REST_Response $response Response data.
* @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
*/
return apply_filters( 'rest_namespace_index', $response, $request );
}
/**
* Retrieves the publicly-visible data for routes.
*
* @since 4.4.0
*
* @param array $routes Routes to get data for.
* @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
* @return array Route data to expose in indexes.
*/
public function get_data_for_routes( $routes, $context = 'view' ) {
$available = array();
// Find the available routes.
foreach ( $routes as $route => $callbacks ) {
$data = $this->get_data_for_route( $route, $callbacks, $context );
if ( empty( $data ) ) {
continue;
}
/**
* Filters the REST endpoint data.
*
* @since 4.4.0
*
* @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
*/
$available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
}
/**
* Filters the publicly-visible data for routes.
*
* This data is exposed on indexes and can be used by clients or
* developers to investigate the site and find out how to use it. It
* acts as a form of self-documentation.
*
* @since 4.4.0
*
* @param array $available Map of route to route data.
* @param array $routes Internal route data as an associative array.
*/
return apply_filters( 'rest_route_data', $available, $routes );
}
/**
* Retrieves publicly-visible data for the route.
*
* @since 4.4.0
*
* @param string $route Route to get data for.
* @param array $callbacks Callbacks to convert to data.
* @param string $context Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
* @return array|null Data for the route, or null if no publicly-visible data.
*/
public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
$data = array(
'namespace' => '',
'methods' => array(),
'endpoints' => array(),
);
if ( isset( $this->route_options[ $route ] ) ) {
$options = $this->route_options[ $route ];
if ( isset( $options['namespace'] ) ) {
$data['namespace'] = $options['namespace'];
}
if ( isset( $options['schema'] ) && 'help' === $context ) {
$data['schema'] = call_user_func( $options['schema'] );
}
}
$route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
foreach ( $callbacks as $callback ) {
// Skip to the next route if any callback is hidden.
if ( empty( $callback['show_in_index'] ) ) {
continue;
}
$data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
$endpoint_data = array(
'methods' => array_keys( $callback['methods'] ),
);
if ( isset( $callback['args'] ) ) {
$endpoint_data['args'] = array();
foreach ( $callback['args'] as $key => $opts ) {
$arg_data = array(
'required' => ! empty( $opts['required'] ),
);
if ( isset( $opts['default'] ) ) {
$arg_data['default'] = $opts['default'];
}
if ( isset( $opts['enum'] ) ) {
$arg_data['enum'] = $opts['enum'];
}
if ( isset( $opts['description'] ) ) {
$arg_data['description'] = $opts['description'];
}
if ( isset( $opts['type'] ) ) {
$arg_data['type'] = $opts['type'];
}
if ( isset( $opts['items'] ) ) {
$arg_data['items'] = $opts['items'];
}
$endpoint_data['args'][ $key ] = $arg_data;
}
}
$data['endpoints'][] = $endpoint_data;
// For non-variable routes, generate links.
if ( strpos( $route, '{' ) === false ) {
$data['_links'] = array(
'self' => rest_url( $route ),
);
}
}
if ( empty( $data['methods'] ) ) {
// No methods supported, hide the route.
return null;
}
return $data;
}
/**
* Sends an HTTP status code.
*
* @since 4.4.0
*
* @param int $code HTTP status.
*/
protected function set_status( $code ) {
status_header( $code );
}
/**
* Sends an HTTP header.
*
* @since 4.4.0
*
* @param string $key Header key.
* @param string $value Header value.
*/
public function send_header( $key, $value ) {
/*
* Sanitize as per RFC2616 (Section 4.2):
*
* Any LWS that occurs between field-content MAY be replaced with a
* single SP before interpreting the field value or forwarding the
* message downstream.
*/
$value = preg_replace( '/\s+/', ' ', $value );
header( sprintf( '%s: %s', $key, $value ) );
}
/**
* Sends multiple HTTP headers.
*
* @since 4.4.0
*
* @param array $headers Map of header name to header value.
*/
public function send_headers( $headers ) {
foreach ( $headers as $key => $value ) {
$this->send_header( $key, $value );
}
}
/**
* Removes an HTTP header from the current response.
*
* @since 4.8.0
*
* @param string $key Header key.
*/
public function remove_header( $key ) {
if ( function_exists( 'header_remove' ) ) {
// In PHP 5.3+ there is a way to remove an already set header.
header_remove( $key );
} else {
// In PHP 5.2, send an empty header, but only as a last resort to
// override a header already sent.
foreach ( headers_list() as $header ) {
if ( 0 === stripos( $header, "$key:" ) ) {
$this->send_header( $key, '' );
break;
}
}
}
}
/**
* Retrieves the raw request entity (body).
*
* @since 4.4.0
*
* @global string $HTTP_RAW_POST_DATA Raw post data.
*
* @return string Raw request data.
*/
public static function get_raw_data() {
global $HTTP_RAW_POST_DATA;
/*
* A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
* but we can do it ourself.
*/
if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
}
return $HTTP_RAW_POST_DATA;
}
/**
* Extracts headers from a PHP-style $_SERVER array.
*
* @since 4.4.0
*
* @param array $server Associative array similar to `$_SERVER`.
* @return array Headers extracted from the input.
*/
public function get_headers( $server ) {
$headers = array();
// CONTENT_* headers are not prefixed with HTTP_.
$additional = array(
'CONTENT_LENGTH' => true,
'CONTENT_MD5' => true,
'CONTENT_TYPE' => true,
);
foreach ( $server as $key => $value ) {
if ( strpos( $key, 'HTTP_' ) === 0 ) {
$headers[ substr( $key, 5 ) ] = $value;
} elseif ( isset( $additional[ $key ] ) ) {
$headers[ $key ] = $value;
}
}
return $headers;
}
}
<?php
/**
* REST API: WP_REST_Request class
*
* @package WordPress
* @subpackage REST_API
* @since 4.4.0
*/
/**
* Core class used to implement a REST request object.
*
* Contains data from the request, to be passed to the callback.
*
* Note: This implements ArrayAccess, and acts as an array of parameters when
* used in that manner. It does not use ArrayObject (as we cannot rely on SPL),
* so be aware it may have non-array behaviour in some cases.
*
* Note: When using features provided by ArrayAccess, be aware that WordPress deliberately
* does not distinguish between arguments of the same name for different request methods.
* For instance, in a request with `GET id=1` and `POST id=2`, `$request['id']` will equal
* 2 (`POST`) not 1 (`GET`). For more precision between request methods, use
* WP_REST_Request::get_body_params(), WP_REST_Request::get_url_params(), etc.
*
* @since 4.4.0
*
* @see ArrayAccess
*/
class WP_REST_Request implements ArrayAccess {
/**
* HTTP method.
*
* @since 4.4.0
* @var string
*/
protected $method = '';
/**
* Parameters passed to the request.
*
* These typically come from the `$_GET`, `$_POST` and `$_FILES`
* superglobals when being created from the global scope.
*
* @since 4.4.0
* @var array Contains GET, POST and FILES keys mapping to arrays of data.
*/
protected $params;
/**
* HTTP headers for the request.
*
* @since 4.4.0
* @var array Map of key to value. Key is always lowercase, as per HTTP specification.
*/
protected $headers = array();
/**
* Body data.
*
* @since 4.4.0
* @var string Binary data from the request.
*/
protected $body = null;
/**
* Route matched for the request.
*
* @since 4.4.0
* @var string
*/
protected $route;
/**
* Attributes (options) for the route that was matched.
*
* This is the options array used when the route was registered, typically
* containing the callback as well as the valid methods for the route.
*
* @since 4.4.0
* @var array Attributes for the request.
*/
protected $attributes = array();
/**
* Used to determine if the JSON data has been parsed yet.
*
* Allows lazy-parsing of JSON data where possible.
*
* @since 4.4.0
* @var bool
*/
protected $parsed_json = false;
/**
* Used to determine if the body data has been parsed yet.
*
* @since 4.4.0
* @var bool
*/
protected $parsed_body = false;
/**
* Constructor.
*
* @since 4.4.0
*
* @param string $method Optional. Request method. Default empty.
* @param string $route Optional. Request route. Default empty.
* @param array $attributes Optional. Request attributes. Default empty array.
*/
public function __construct( $method = '', $route = '', $attributes = array() ) {
$this->params = array(
'URL' => array(),
'GET' => array(),
'POST' => array(),
'FILES' => array(),
// See parse_json_params.
'JSON' => null,
'defaults' => array(),
);
$this->set_method( $method );
$this->set_route( $route );
$this->set_attributes( $attributes );
}
/**
* Retrieves the HTTP method for the request.
*
* @since 4.4.0
*
* @return string HTTP method.
*/
public function get_method() {
return $this->method;
}
/**
* Sets HTTP method for the request.
*
* @since 4.4.0
*
* @param string $method HTTP method.
*/
public function set_method( $method ) {
$this->method = strtoupper( $method );
}
/**
* Retrieves all headers from the request.
*
* @since 4.4.0
*
* @return array Map of key to value. Key is always lowercase, as per HTTP specification.
*/
public function get_headers() {
return $this->headers;
}
/**
* Canonicalizes the header name.
*
* Ensures that header names are always treated the same regardless of
* source. Header names are always case insensitive.
*
* Note that we treat `-` (dashes) and `_` (underscores) as the same
* character, as per header parsing rules in both Apache and nginx.
*
* @link https://stackoverflow.com/q/18185366
* @link https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#missing-disappearing-http-headers
* @link https://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
*
* @since 4.4.0
*
* @param string $key Header name.
* @return string Canonicalized name.
*/
public static function canonicalize_header_name( $key ) {
$key = strtolower( $key );
$key = str_replace( '-', '_', $key );
return $key;
}
/**
* Retrieves the given header from the request.
*
* If the header has multiple values, they will be concatenated with a comma
* as per the HTTP specification. Be aware that some non-compliant headers
* (notably cookie headers) cannot be joined this way.
*
* @since 4.4.0
*
* @param string $key Header name, will be canonicalized to lowercase.
* @return string|null String value if set, null otherwise.
*/
public function get_header( $key ) {
$key = $this->canonicalize_header_name( $key );
if ( ! isset( $this->headers[ $key ] ) ) {
return null;
}
return implode( ',', $this->headers[ $key ] );
}
/**
* Retrieves header values from the request.
*
* @since 4.4.0
*
* @param string $key Header name, will be canonicalized to lowercase.
* @return array|null List of string values if set, null otherwise.
*/
public function get_header_as_array( $key ) {
$key = $this->canonicalize_header_name( $key );
if ( ! isset( $this->headers[ $key ] ) ) {
return null;
}
return $this->headers[ $key ];
}
/**
* Sets the header on request.
*
* @since 4.4.0
*
* @param string $key Header name.
* @param string $value Header value, or list of values.
*/
public function set_header( $key, $value ) {
$key = $this->canonicalize_header_name( $key );
$value = (array) $value;
$this->headers[ $key ] = $value;
}
/**
* Appends a header value for the given header.
*
* @since 4.4.0
*
* @param string $key Header name.
* @param string $value Header value, or list of values.
*/
public function add_header( $key, $value ) {
$key = $this->canonicalize_header_name( $key );
$value = (array) $value;
if ( ! isset( $this->headers[ $key ] ) ) {
$this->headers[ $key ] = array();
}
$this->headers[ $key ] = array_merge( $this->headers[ $key ], $value );
}
/**
* Removes all values for a header.
*
* @since 4.4.0
*
* @param string $key Header name.
*/
public function remove_header( $key ) {
$key = $this->canonicalize_header_name( $key );
unset( $this->headers[ $key ] );
}
/**
* Sets headers on the request.
*
* @since 4.4.0
*
* @param array $headers Map of header name to value.
* @param bool $override If true, replace the request's headers. Otherwise, merge with existing.
*/
public function set_headers( $headers, $override = true ) {
if ( true === $override ) {
$this->headers = array();
}
foreach ( $headers as $key => $value ) {
$this->set_header( $key, $value );
}
}
/**
* Retrieves the content-type of the request.
*
* @since 4.4.0
*
* @return array Map containing 'value' and 'parameters' keys.
*/
public function get_content_type() {
$value = $this->get_header( 'content-type' );
if ( empty( $value ) ) {
return null;
}
$parameters = '';
if ( strpos( $value, ';' ) ) {
list( $value, $parameters ) = explode( ';', $value, 2 );
}
$value = strtolower( $value );
if ( strpos( $value, '/' ) === false ) {
return null;
}
// Parse type and subtype out.
list( $type, $subtype ) = explode( '/', $value, 2 );
$data = compact( 'value', 'type', 'subtype', 'parameters' );
$data = array_map( 'trim', $data );
return $data;
}
/**
* Retrieves the parameter priority order.
*
* Used when checking parameters in get_param().
*
* @since 4.4.0
*
* @return array List of types to check, in order of priority.
*/
protected function get_parameter_order() {
$order = array();
$content_type = $this->get_content_type();
if ( isset( $content_type['value'] ) && $content_type['value'] === 'application/json' ) {
$order[] = 'JSON';
}
$this->parse_json_params();
// Ensure we parse the body data.
$body = $this->get_body();
if ( 'POST' !== $this->method && ! empty( $body ) ) {
$this->parse_body_params();
}
$accepts_body_data = array( 'POST', 'PUT', 'PATCH', 'DELETE' );
if ( in_array( $this->method, $accepts_body_data ) ) {
$order[] = 'POST';
}
$order[] = 'GET';
$order[] = 'URL';
$order[] = 'defaults';
/**
* Filters the parameter order.
*
* The order affects which parameters are checked when using get_param() and family.
* This acts similarly to PHP's `request_order` setting.
*
* @since 4.4.0
*
* @param array $order {
* An array of types to check, in order of priority.
*
* @param string $type The type to check.
* }
* @param WP_REST_Request $this The request object.
*/
return apply_filters( 'rest_request_parameter_order', $order, $this );
}
/**
* Retrieves a parameter from the request.
*
* @since 4.4.0
*
* @param string $key Parameter name.
* @return mixed|null Value if set, null otherwise.
*/
public function get_param( $key ) {
$order = $this->get_parameter_order();
foreach ( $order as $type ) {
// Determine if we have the parameter for this type.
if ( isset( $this->params[ $type ][ $key ] ) ) {
return $this->params[ $type ][ $key ];
}
}
return null;
}
/**
* Sets a parameter on the request.
*
* @since 4.4.0
*
* @param string $key Parameter name.
* @param mixed $value Parameter value.
*/
public function set_param( $key, $value ) {
$order = $this->get_parameter_order();
$this->params[ $order[0] ][ $key ] = $value;
}
/**
* Retrieves merged parameters from the request.
*
* The equivalent of get_param(), but returns all parameters for the request.
* Handles merging all the available values into a single array.
*
* @since 4.4.0
*
* @return array Map of key to value.
*/
public function get_params() {
$order = $this->get_parameter_order();
$order = array_reverse( $order, true );
$params = array();
foreach ( $order as $type ) {
// array_merge / the "+" operator will mess up
// numeric keys, so instead do a manual foreach.
foreach ( (array) $this->params[ $type ] as $key => $value ) {
$params[ $key ] = $value;
}
}
return $params;
}
/**
* Retrieves parameters from the route itself.
*
* These are parsed from the URL using the regex.
*
* @since 4.4.0
*
* @return array Parameter map of key to value.
*/
public function get_url_params() {
return $this->params['URL'];
}
/**
* Sets parameters from the route.
*
* Typically, this is set after parsing the URL.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_url_params( $params ) {
$this->params['URL'] = $params;
}
/**
* Retrieves parameters from the query string.
*
* These are the parameters you'd typically find in `$_GET`.
*
* @since 4.4.0
*
* @return array Parameter map of key to value
*/
public function get_query_params() {
return $this->params['GET'];
}
/**
* Sets parameters from the query string.
*
* Typically, this is set from `$_GET`.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_query_params( $params ) {
$this->params['GET'] = $params;
}
/**
* Retrieves parameters from the body.
*
* These are the parameters you'd typically find in `$_POST`.
*
* @since 4.4.0
*
* @return array Parameter map of key to value.
*/
public function get_body_params() {
return $this->params['POST'];
}
/**
* Sets parameters from the body.
*
* Typically, this is set from `$_POST`.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_body_params( $params ) {
$this->params['POST'] = $params;
}
/**
* Retrieves multipart file parameters from the body.
*
* These are the parameters you'd typically find in `$_FILES`.
*
* @since 4.4.0
*
* @return array Parameter map of key to value
*/
public function get_file_params() {
return $this->params['FILES'];
}
/**
* Sets multipart file parameters from the body.
*
* Typically, this is set from `$_FILES`.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_file_params( $params ) {
$this->params['FILES'] = $params;
}
/**
* Retrieves the default parameters.
*
* These are the parameters set in the route registration.
*
* @since 4.4.0
*
* @return array Parameter map of key to value
*/
public function get_default_params() {
return $this->params['defaults'];
}
/**
* Sets default parameters.
*
* These are the parameters set in the route registration.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_default_params( $params ) {
$this->params['defaults'] = $params;
}
/**
* Retrieves the request body content.
*
* @since 4.4.0
*
* @return string Binary data from the request body.
*/
public function get_body() {
return $this->body;
}
/**
* Sets body content.
*
* @since 4.4.0
*
* @param string $data Binary data from the request body.
*/
public function set_body( $data ) {
$this->body = $data;
// Enable lazy parsing.
$this->parsed_json = false;
$this->parsed_body = false;
$this->params['JSON'] = null;
}
/**
* Retrieves the parameters from a JSON-formatted body.
*
* @since 4.4.0
*
* @return array Parameter map of key to value.
*/
public function get_json_params() {
// Ensure the parameters have been parsed out.
$this->parse_json_params();
return $this->params['JSON'];
}
/**
* Parses the JSON parameters.
*
* Avoids parsing the JSON data until we need to access it.
*
* @since 4.4.0
* @since 4.7.0 Returns error instance if value cannot be decoded.
* @return true|WP_Error True if the JSON data was passed or no JSON data was provided, WP_Error if invalid JSON was passed.
*/
protected function parse_json_params() {
if ( $this->parsed_json ) {
return true;
}
$this->parsed_json = true;
// Check that we actually got JSON.
$content_type = $this->get_content_type();
if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) {
return true;
}
$body = $this->get_body();
if ( empty( $body ) ) {
return true;
}
$params = json_decode( $body, true );
/*
* Check for a parsing error.
*
* Note that due to WP's JSON compatibility functions, json_last_error
* might not be defined: https://core.trac.wordpress.org/ticket/27799
*/
if ( null === $params && ( ! function_exists( 'json_last_error' ) || JSON_ERROR_NONE !== json_last_error() ) ) {
// Ensure subsequent calls receive error instance.
$this->parsed_json = false;
$error_data = array(
'status' => WP_Http::BAD_REQUEST,
);
if ( function_exists( 'json_last_error' ) ) {
$error_data['json_error_code'] = json_last_error();
$error_data['json_error_message'] = json_last_error_msg();
}
return new WP_Error( 'rest_invalid_json', __( 'Invalid JSON body passed.' ), $error_data );
}
$this->params['JSON'] = $params;
return true;
}
/**
* Parses the request body parameters.
*
* Parses out URL-encoded bodies for request methods that aren't supported
* natively by PHP. In PHP 5.x, only POST has these parsed automatically.
*
* @since 4.4.0
*/
protected function parse_body_params() {
if ( $this->parsed_body ) {
return;
}
$this->parsed_body = true;
/*
* Check that we got URL-encoded. Treat a missing content-type as
* URL-encoded for maximum compatibility.
*/
$content_type = $this->get_content_type();
if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
return;
}
parse_str( $this->get_body(), $params );
/*
* Amazingly, parse_str follows magic quote rules. Sigh.
*
* NOTE: Do not refactor to use `wp_unslash`.
*/
//get_magic_quotes_gpc is depricated in php 7.4
if(version_compare(PHP_VERSION, '7.4', '<')){
if ( get_magic_quotes_gpc() ) {
$params = stripslashes_deep( $params );
}
}
/*
* Add to the POST parameters stored internally. If a user has already
* set these manually (via `set_body_params`), don't override them.
*/
$this->params['POST'] = array_merge( $params, $this->params['POST'] );
}
/**
* Retrieves the route that matched the request.
*
* @since 4.4.0
*
* @return string Route matching regex.
*/
public function get_route() {
return $this->route;
}
/**
* Sets the route that matched the request.
*
* @since 4.4.0
*
* @param string $route Route matching regex.
*/
public function set_route( $route ) {
$this->route = $route;
}
/**
* Retrieves the attributes for the request.
*
* These are the options for the route that was matched.
*
* @since 4.4.0
*
* @return array Attributes for the request.
*/
public function get_attributes() {
return $this->attributes;
}
/**
* Sets the attributes for the request.
*
* @since 4.4.0
*
* @param array $attributes Attributes for the request.
*/
public function set_attributes( $attributes ) {
$this->attributes = $attributes;
}
/**
* Sanitizes (where possible) the params on the request.
*
* This is primarily based off the sanitize_callback param on each registered
* argument.
*
* @since 4.4.0
*
* @return true|WP_Error True if parameters were sanitized, WP_Error if an error occurred during sanitization.
*/
public function sanitize_params() {
$attributes = $this->get_attributes();
// No arguments set, skip sanitizing.
if ( empty( $attributes['args'] ) ) {
return true;
}
$order = $this->get_parameter_order();
$invalid_params = array();
foreach ( $order as $type ) {
if ( empty( $this->params[ $type ] ) ) {
continue;
}
foreach ( $this->params[ $type ] as $key => $value ) {
if ( ! isset( $attributes['args'][ $key ] ) ) {
continue;
}
$param_args = $attributes['args'][ $key ];
// If the arg has a type but no sanitize_callback attribute, default to rest_parse_request_arg.
if ( ! array_key_exists( 'sanitize_callback', $param_args ) && ! empty( $param_args['type'] ) ) {
$param_args['sanitize_callback'] = 'rest_parse_request_arg';
}
// If there's still no sanitize_callback, nothing to do here.
if ( empty( $param_args['sanitize_callback'] ) ) {
continue;
}
$sanitized_value = call_user_func( $param_args['sanitize_callback'], $value, $this, $key );
if ( is_wp_error( $sanitized_value ) ) {
$invalid_params[ $key ] = $sanitized_value->get_error_message();
} else {
$this->params[ $type ][ $key ] = $sanitized_value;
}
}
}
if ( $invalid_params ) {
return new WP_Error(
'rest_invalid_param',
sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
array(
'status' => 400,
'params' => $invalid_params,
)
);
}
return true;
}
/**
* Checks whether this request is valid according to its attributes.
*
* @since 4.4.0
*
* @return bool|WP_Error True if there are no parameters to validate or if all pass validation,
* WP_Error if required parameters are missing.
*/
public function has_valid_params() {
// If JSON data was passed, check for errors.
$json_error = $this->parse_json_params();
if ( is_wp_error( $json_error ) ) {
return $json_error;
}
$attributes = $this->get_attributes();
$required = array();
// No arguments set, skip validation.
if ( empty( $attributes['args'] ) ) {
return true;
}
foreach ( $attributes['args'] as $key => $arg ) {
$param = $this->get_param( $key );
if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
$required[] = $key;
}
}
if ( ! empty( $required ) ) {
return new WP_Error(
'rest_missing_callback_param',
sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ),
array(
'status' => 400,
'params' => $required,
)
);
}
/*
* Check the validation callbacks for each registered arg.
*
* This is done after required checking as required checking is cheaper.
*/
$invalid_params = array();
foreach ( $attributes['args'] as $key => $arg ) {
$param = $this->get_param( $key );
if ( null !== $param && ! empty( $arg['validate_callback'] ) ) {
$valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );
if ( false === $valid_check ) {
$invalid_params[ $key ] = __( 'Invalid parameter.' );
}
if ( is_wp_error( $valid_check ) ) {
$invalid_params[ $key ] = $valid_check->get_error_message();
}
}
}
if ( $invalid_params ) {
return new WP_Error(
'rest_invalid_param',
sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
array(
'status' => 400,
'params' => $invalid_params,
)
);
}
return true;
}
/**
* Checks if a parameter is set.
*
* @since 4.4.0
*
* @param string $offset Parameter name.
* @return bool Whether the parameter is set.
*/
public function offsetExists( $offset ) {
$order = $this->get_parameter_order();
foreach ( $order as $type ) {
if ( isset( $this->params[ $type ][ $offset ] ) ) {
return true;
}
}
return false;
}
/**
* Retrieves a parameter from the request.
*
* @since 4.4.0
*
* @param string $offset Parameter name.
* @return mixed|null Value if set, null otherwise.
*/
public function offsetGet( $offset ) {
return $this->get_param( $offset );
}
/**
* Sets a parameter on the request.
*
* @since 4.4.0
*
* @param string $offset Parameter name.
* @param mixed $value Parameter value.
*/
public function offsetSet( $offset, $value ) {
$this->set_param( $offset, $value );
}
/**
* Removes a parameter from the request.
*
* @since 4.4.0
*
* @param string $offset Parameter name.
*/
public function offsetUnset( $offset ) {
$order = $this->get_parameter_order();
// Remove the offset from every group.
foreach ( $order as $type ) {
unset( $this->params[ $type ][ $offset ] );
}
}
/**
* Retrieves a WP_REST_Request object from a full URL.
*
* @since 4.5.0
*
* @param string $url URL with protocol, domain, path and query args.
* @return WP_REST_Request|false WP_REST_Request object on success, false on failure.
*/
public static function from_url( $url ) {
$bits = parse_url( $url );
$query_params = array();
if ( ! empty( $bits['query'] ) ) {
wp_parse_str( $bits['query'], $query_params );
}
$api_root = rest_url();
if ( get_option( 'permalink_structure' ) && 0 === strpos( $url, $api_root ) ) {
// Pretty permalinks on, and URL is under the API root.
$api_url_part = substr( $url, strlen( untrailingslashit( $api_root ) ) );
$route = parse_url( $api_url_part, PHP_URL_PATH );
} elseif ( ! empty( $query_params['rest_route'] ) ) {
// ?rest_route=... set directly
$route = $query_params['rest_route'];
unset( $query_params['rest_route'] );
}
$request = false;
if ( ! empty( $route ) ) {
$request = new WP_REST_Request( 'GET', $route );
$request->set_query_params( $query_params );
}
/**
* Filters the request generated from a URL.
*
* @since 4.5.0
*
* @param WP_REST_Request|false $request Generated request object, or false if URL
* could not be parsed.
* @param string $url URL the request was generated from.
*/
return apply_filters( 'rest_request_from_url', $request, $url );
}
}
<?php
/**
* REST API: WP_REST_User_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to manage meta values for users via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Meta_Fields
*/
class WP_REST_User_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Retrieves the object meta type.
*
* @since 4.7.0
*
* @return string The user meta type.
*/
protected function get_meta_type() {
return 'user';
}
/**
* Retrieves the object meta subtype.
*
* @since 4.9.8
*
* @return string 'user' There are no subtypes.
*/
protected function get_meta_subtype() {
return 'user';
}
/**
* Retrieves the type for register_rest_field().
*
* @since 4.7.0
*
* @return string The user REST field type.
*/
public function get_rest_field_type() {
return 'user';
}
}
<?php
/**
* REST API: WP_REST_Comment_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class to manage comment meta via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Meta_Fields
*/
class WP_REST_Comment_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Retrieves the object type for comment meta.
*
* @since 4.7.0
*
* @return string The meta type.
*/
protected function get_meta_type() {
return 'comment';
}
/**
* Retrieves the object meta subtype.
*
* @since 4.9.8
*
* @return string 'comment' There are no subtypes.
*/
protected function get_meta_subtype() {
return 'comment';
}
/**
* Retrieves the type for register_rest_field() in the context of comments.
*
* @since 4.7.0
*
* @return string The REST field type.
*/
public function get_rest_field_type() {
return 'comment';
}
}
<?php
/**
* REST API: WP_REST_Term_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to manage meta values for terms via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Meta_Fields
*/
class WP_REST_Term_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Taxonomy to register fields for.
*
* @since 4.7.0
* @var string
*/
protected $taxonomy;
/**
* Constructor.
*
* @since 4.7.0
*
* @param string $taxonomy Taxonomy to register fields for.
*/
public function __construct( $taxonomy ) {
$this->taxonomy = $taxonomy;
}
/**
* Retrieves the object meta type.
*
* @since 4.7.0
*
* @return string The meta type.
*/
protected function get_meta_type() {
return 'term';
}
/**
* Retrieves the object meta subtype.
*
* @since 4.9.8
*
* @return string Subtype for the meta type, or empty string if no specific subtype.
*/
protected function get_meta_subtype() {
return $this->taxonomy;
}
/**
* Retrieves the type for register_rest_field().
*
* @since 4.7.0
*
* @return string The REST field type.
*/
public function get_rest_field_type() {
return 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy;
}
}
<?php
/**
* REST API: WP_REST_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class to manage meta values for an object via the REST API.
*
* @since 4.7.0
*/
abstract class WP_REST_Meta_Fields {
/**
* Retrieves the object meta type.
*
* @since 4.7.0
*
* @return string One of 'post', 'comment', 'term', 'user', or anything
* else supported by `_get_meta_table()`.
*/
abstract protected function get_meta_type();
/**
* Retrieves the object meta subtype.
*
* @since 4.9.8
*
* @return string Subtype for the meta type, or empty string if no specific subtype.
*/
protected function get_meta_subtype() {
return '';
}
/**
* Retrieves the object type for register_rest_field().
*
* @since 4.7.0
*
* @return string The REST field type, such as post type name, taxonomy name, 'comment', or `user`.
*/
abstract protected function get_rest_field_type();
/**
* Registers the meta field.
*
* @since 4.7.0
*
* @see register_rest_field()
*/
public function register_field() {
register_rest_field(
$this->get_rest_field_type(),
'meta',
array(
'get_callback' => array( $this, 'get_value' ),
'update_callback' => array( $this, 'update_value' ),
'schema' => $this->get_field_schema(),
)
);
}
/**
* Retrieves the meta field value.
*
* @since 4.7.0
*
* @param int $object_id Object ID to fetch meta for.
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|object Object containing the meta values by name, otherwise WP_Error object.
*/
public function get_value( $object_id, $request ) {
$fields = $this->get_registered_fields();
$response = array();
foreach ( $fields as $meta_key => $args ) {
$name = $args['name'];
$all_values = get_metadata( $this->get_meta_type(), $object_id, $meta_key, false );
if ( $args['single'] ) {
if ( empty( $all_values ) ) {
$value = $args['schema']['default'];
} else {
$value = $all_values[0];
}
$value = $this->prepare_value_for_response( $value, $request, $args );
} else {
$value = array();
foreach ( $all_values as $row ) {
$value[] = $this->prepare_value_for_response( $row, $request, $args );
}
}
$response[ $name ] = $value;
}
return $response;
}
/**
* Prepares a meta value for a response.
*
* This is required because some native types cannot be stored correctly
* in the database, such as booleans. We need to cast back to the relevant
* type before passing back to JSON.
*
* @since 4.7.0
*
* @param mixed $value Meta value to prepare.
* @param WP_REST_Request $request Current request object.
* @param array $args Options for the field.
* @return mixed Prepared value.
*/
protected function prepare_value_for_response( $value, $request, $args ) {
if ( ! empty( $args['prepare_callback'] ) ) {
$value = call_user_func( $args['prepare_callback'], $value, $request, $args );
}
return $value;
}
/**
* Updates meta values.
*
* @since 4.7.0
*
* @param array $meta Array of meta parsed from the request.
* @param int $object_id Object ID to fetch meta for.
* @return WP_Error|null WP_Error if one occurs, null on success.
*/
public function update_value( $meta, $object_id ) {
$fields = $this->get_registered_fields();
foreach ( $fields as $meta_key => $args ) {
$name = $args['name'];
if ( ! array_key_exists( $name, $meta ) ) {
continue;
}
/*
* A null value means reset the field, which is essentially deleting it
* from the database and then relying on the default value.
*/
if ( is_null( $meta[ $name ] ) ) {
$result = $this->delete_meta_value( $object_id, $meta_key, $name );
if ( is_wp_error( $result ) ) {
return $result;
}
continue;
}
$is_valid = rest_validate_value_from_schema( $meta[ $name ], $args['schema'], 'meta.' . $name );
if ( is_wp_error( $is_valid ) ) {
$is_valid->add_data( array( 'status' => 400 ) );
return $is_valid;
}
$value = rest_sanitize_value_from_schema( $meta[ $name ], $args['schema'] );
if ( $args['single'] ) {
$result = $this->update_meta_value( $object_id, $meta_key, $name, $value );
} else {
$result = $this->update_multi_meta_value( $object_id, $meta_key, $name, $value );
}
if ( is_wp_error( $result ) ) {
return $result;
}
}
return null;
}
/**
* Deletes a meta value for an object.
*
* @since 4.7.0
*
* @param int $object_id Object ID the field belongs to.
* @param string $meta_key Key for the field.
* @param string $name Name for the field that is exposed in the REST API.
* @return bool|WP_Error True if meta field is deleted, WP_Error otherwise.
*/
protected function delete_meta_value( $object_id, $meta_key, $name ) {
$meta_type = $this->get_meta_type();
if ( ! current_user_can( "delete_{$meta_type}_meta", $object_id, $meta_key ) ) {
return new WP_Error(
'rest_cannot_delete',
/* translators: %s: custom field key */
sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ),
array(
'key' => $name,
'status' => rest_authorization_required_code(),
)
);
}
if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ) ) ) {
return new WP_Error(
'rest_meta_database_error',
__( 'Could not delete meta value from database.' ),
array(
'key' => $name,
'status' => WP_Http::INTERNAL_SERVER_ERROR,
)
);
}
return true;
}
/**
* Updates multiple meta values for an object.
*
* Alters the list of values in the database to match the list of provided values.
*
* @since 4.7.0
*
* @param int $object_id Object ID to update.
* @param string $meta_key Key for the custom field.
* @param string $name Name for the field that is exposed in the REST API.
* @param array $values List of values to update to.
* @return bool|WP_Error True if meta fields are updated, WP_Error otherwise.
*/
protected function update_multi_meta_value( $object_id, $meta_key, $name, $values ) {
$meta_type = $this->get_meta_type();
if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) {
return new WP_Error(
'rest_cannot_update',
/* translators: %s: custom field key */
sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ),
array(
'key' => $name,
'status' => rest_authorization_required_code(),
)
);
}
$current = get_metadata( $meta_type, $object_id, $meta_key, false );
$to_remove = $current;
$to_add = $values;
foreach ( $to_add as $add_key => $value ) {
$remove_keys = array_keys( $to_remove, $value, true );
if ( empty( $remove_keys ) ) {
continue;
}
if ( count( $remove_keys ) > 1 ) {
// To remove, we need to remove first, then add, so don't touch.
continue;
}
$remove_key = $remove_keys[0];
unset( $to_remove[ $remove_key ] );
unset( $to_add[ $add_key ] );
}
// `delete_metadata` removes _all_ instances of the value, so only call once.
$to_remove = array_unique( $to_remove );
foreach ( $to_remove as $value ) {
if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) {
return new WP_Error(
'rest_meta_database_error',
__( 'Could not update meta value in database.' ),
array(
'key' => $name,
'status' => WP_Http::INTERNAL_SERVER_ERROR,
)
);
}
}
foreach ( $to_add as $value ) {
if ( ! add_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) {
return new WP_Error(
'rest_meta_database_error',
__( 'Could not update meta value in database.' ),
array(
'key' => $name,
'status' => WP_Http::INTERNAL_SERVER_ERROR,
)
);
}
}
return true;
}
/**
* Updates a meta value for an object.
*
* @since 4.7.0
*
* @param int $object_id Object ID to update.
* @param string $meta_key Key for the custom field.
* @param string $name Name for the field that is exposed in the REST API.
* @param mixed $value Updated value.
* @return bool|WP_Error True if the meta field was updated, WP_Error otherwise.
*/
protected function update_meta_value( $object_id, $meta_key, $name, $value ) {
$meta_type = $this->get_meta_type();
if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) {
return new WP_Error(
'rest_cannot_update',
/* translators: %s: custom field key */
sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ),
array(
'key' => $name,
'status' => rest_authorization_required_code(),
)
);
}
// Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false.
$old_value = get_metadata( $meta_type, $object_id, $meta_key );
$subtype = get_object_subtype( $meta_type, $object_id );
if ( 1 === count( $old_value ) ) {
if ( (string) sanitize_meta( $meta_key, $value, $meta_type, $subtype ) === $old_value[0] ) {
return true;
}
}
if ( ! update_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) {
return new WP_Error(
'rest_meta_database_error',
__( 'Could not update meta value in database.' ),
array(
'key' => $name,
'status' => WP_Http::INTERNAL_SERVER_ERROR,
)
);
}
return true;
}
/**
* Retrieves all the registered meta fields.
*
* @since 4.7.0
*
* @return array Registered fields.
*/
protected function get_registered_fields() {
$registered = array();
$meta_type = $this->get_meta_type();
$meta_subtype = $this->get_meta_subtype();
$meta_keys = get_registered_meta_keys( $meta_type );
if ( ! empty( $meta_subtype ) ) {
$meta_keys = array_merge( $meta_keys, get_registered_meta_keys( $meta_type, $meta_subtype ) );
}
foreach ( $meta_keys as $name => $args ) {
if ( empty( $args['show_in_rest'] ) ) {
continue;
}
$rest_args = array();
if ( is_array( $args['show_in_rest'] ) ) {
$rest_args = $args['show_in_rest'];
}
$default_args = array(
'name' => $name,
'single' => $args['single'],
'type' => ! empty( $args['type'] ) ? $args['type'] : null,
'schema' => array(),
'prepare_callback' => array( $this, 'prepare_value' ),
);
$default_schema = array(
'type' => $default_args['type'],
'description' => empty( $args['description'] ) ? '' : $args['description'],
'default' => isset( $args['default'] ) ? $args['default'] : null,
);
$rest_args = array_merge( $default_args, $rest_args );
$rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
$type = ! empty( $rest_args['type'] ) ? $rest_args['type'] : null;
$type = ! empty( $rest_args['schema']['type'] ) ? $rest_args['schema']['type'] : $type;
if ( ! in_array( $type, array( 'string', 'boolean', 'integer', 'number' ) ) ) {
continue;
}
if ( empty( $rest_args['single'] ) ) {
$rest_args['schema']['items'] = array(
'type' => $rest_args['type'],
);
$rest_args['schema']['type'] = 'array';
}
$registered[ $name ] = $rest_args;
}
return $registered;
}
/**
* Retrieves the object's meta schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Field schema data.
*/
public function get_field_schema() {
$fields = $this->get_registered_fields();
$schema = array(
'description' => __( 'Meta fields.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(),
'arg_options' => array(
'sanitize_callback' => null,
'validate_callback' => array( $this, 'check_meta_is_array' ),
),
);
foreach ( $fields as $args ) {
$schema['properties'][ $args['name'] ] = $args['schema'];
}
return $schema;
}
/**
* Prepares a meta value for output.
*
* Default preparation for meta fields. Override by passing the
* `prepare_callback` in your `show_in_rest` options.
*
* @since 4.7.0
*
* @param mixed $value Meta value from the database.
* @param WP_REST_Request $request Request object.
* @param array $args REST-specific options for the meta key.
* @return mixed Value prepared for output. If a non-JsonSerializable object, null.
*/
public static function prepare_value( $value, $request, $args ) {
$type = $args['schema']['type'];
// For multi-value fields, check the item type instead.
if ( 'array' === $type && ! empty( $args['schema']['items']['type'] ) ) {
$type = $args['schema']['items']['type'];
}
switch ( $type ) {
case 'string':
$value = (string) $value;
break;
case 'integer':
$value = (int) $value;
break;
case 'number':
$value = (float) $value;
break;
case 'boolean':
$value = (bool) $value;
break;
}
// Don't allow objects to be output.
if ( is_object( $value ) && ! ( $value instanceof JsonSerializable ) ) {
return null;
}
return $value;
}
/**
* Check the 'meta' value of a request is an associative array.
*
* @since 4.7.0
*
* @param mixed $value The meta value submitted in the request.
* @param WP_REST_Request $request Full details about the request.
* @param string $param The parameter name.
* @return WP_Error|string The meta array, if valid, otherwise an error.
*/
public function check_meta_is_array( $value, $request, $param ) {
if ( ! is_array( $value ) ) {
return false;
}
return $value;
}
}
<?php
/**
* REST API: WP_REST_Post_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to manage meta values for posts via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Meta_Fields
*/
class WP_REST_Post_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Post type to register fields for.
*
* @since 4.7.0
* @var string
*/
protected $post_type;
/**
* Constructor.
*
* @since 4.7.0
*
* @param string $post_type Post type to register fields for.
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
}
/**
* Retrieves the object meta type.
*
* @since 4.7.0
*
* @return string The meta type.
*/
protected function get_meta_type() {
return 'post';
}
/**
* Retrieves the object meta subtype.
*
* @since 4.9.8
*
* @return string Subtype for the meta type, or empty string if no specific subtype.
*/
protected function get_meta_subtype() {
return $this->post_type;
}
/**
* Retrieves the type for register_rest_field().
*
* @since 4.7.0
*
* @see register_rest_field()
*
* @return string The REST field type.
*/
public function get_rest_field_type() {
return $this->post_type;
}
}
<?php
/**
* REST API: WP_REST_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core base controller for managing and interacting with REST API items.
*
* @since 4.7.0
*/
abstract class WP_REST_Controller {
/**
* The namespace of this controller's route.
*
* @since 4.7.0
* @var string
*/
protected $namespace;
/**
* The base of this controller's route.
*
* @since 4.7.0
* @var string
*/
protected $rest_base;
/**
* Registers the routes for the objects of the controller.
*
* @since 4.7.0
*/
public function register_routes() {
/* translators: %s: register_routes() */
_doing_it_wrong( 'WP_REST_Controller::register_routes', sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ), '4.7' );
}
/**
* Checks if a given request has access to get items.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Retrieves a collection of items.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Checks if a given request has access to get a specific item.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Retrieves one item from the collection.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Checks if a given request has access to create items.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Creates one item from the collection.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Checks if a given request has access to update a specific item.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool True if the request has access to update the item, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Updates one item from the collection.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Checks if a given request has access to delete a specific item.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Deletes one item from the collection.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Prepares one item for create or update operation.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|object The prepared item, or WP_Error object on failure.
*/
protected function prepare_item_for_database( $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Prepares the item for the REST response.
*
* @since 4.7.0
*
* @param mixed $item WordPress representation of the item.
* @param WP_REST_Request $request Request object.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
/* translators: %s: method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Prepares a response for insertion into a collection.
*
* @since 4.7.0
*
* @param WP_REST_Response $response Response object.
* @return array|mixed Response data, ready for insertion into collection data.
*/
public function prepare_response_for_collection( $response ) {
if ( ! ( $response instanceof WP_REST_Response ) ) {
return $response;
}
$data = (array) $response->get_data();
$server = rest_get_server();
$links = $server->get_compact_response_links( $response );
if ( ! empty( $links ) ) {
$data['_links'] = $links;
}
return $data;
}
/**
* Filters a response based on the context defined in the schema.
*
* @since 4.7.0
*
* @param array $data Response data to fiter.
* @param string $context Context defined in the schema.
* @return array Filtered response.
*/
public function filter_response_by_context( $data, $context ) {
$schema = $this->get_item_schema();
foreach ( $data as $key => $value ) {
if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) {
continue;
}
if ( ! in_array( $context, $schema['properties'][ $key ]['context'], true ) ) {
unset( $data[ $key ] );
continue;
}
if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) {
foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) {
if ( empty( $details['context'] ) ) {
continue;
}
if ( ! in_array( $context, $details['context'], true ) ) {
if ( isset( $data[ $key ][ $attribute ] ) ) {
unset( $data[ $key ][ $attribute ] );
}
}
}
}
}
return $data;
}
/**
* Retrieves the item's schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
return $this->add_additional_fields_schema( array() );
}
/**
* Retrieves the item's schema for display / public consumption purposes.
*
* @since 4.7.0
*
* @return array Public item schema data.
*/
public function get_public_item_schema() {
$schema = $this->get_item_schema();
foreach ( $schema['properties'] as &$property ) {
unset( $property['arg_options'] );
}
return $schema;
}
/**
* Retrieves the query params for the collections.
*
* @since 4.7.0
*
* @return array Query parameters for the collection.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param(),
'page' => array(
'description' => __( 'Current page of the collection.' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
),
'per_page' => array(
'description' => __( 'Maximum number of items to be returned in result set.' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
),
'search' => array(
'description' => __( 'Limit results to those matching a string.' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
);
}
/**
* Retrieves the magical context param.
*
* Ensures consistent descriptions between endpoints, and populates enum from schema.
*
* @since 4.7.0
*
* @param array $args Optional. Additional arguments for context parameter. Default empty array.
* @return array Context parameter details.
*/
public function get_context_param( $args = array() ) {
$param_details = array(
'description' => __( 'Scope under which the request is made; determines fields present in response.' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$schema = $this->get_item_schema();
if ( empty( $schema['properties'] ) ) {
return array_merge( $param_details, $args );
}
$contexts = array();
foreach ( $schema['properties'] as $attributes ) {
if ( ! empty( $attributes['context'] ) ) {
$contexts = array_merge( $contexts, $attributes['context'] );
}
}
if ( ! empty( $contexts ) ) {
$param_details['enum'] = array_unique( $contexts );
rsort( $param_details['enum'] );
}
return array_merge( $param_details, $args );
}
/**
* Adds the values from additional fields to a data object.
*
* @since 4.7.0
*
* @param array $object Data object.
* @param WP_REST_Request $request Full details about the request.
* @return array Modified data object with additional fields.
*/
protected function add_additional_fields_to_object( $object, $request ) {
$additional_fields = $this->get_additional_fields();
$requested_fields = $this->get_fields_for_response( $request );
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['get_callback'] ) {
continue;
}
if ( ! in_array( $field_name, $requested_fields, true ) ) {
continue;
}
$object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request, $this->get_object_type() );
}
return $object;
}
/**
* Updates the values of additional fields added to a data object.
*
* @since 4.7.0
*
* @param array $object Data Object.
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error True on success, WP_Error object if a field cannot be updated.
*/
protected function update_additional_fields_for_object( $object, $request ) {
$additional_fields = $this->get_additional_fields();
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['update_callback'] ) {
continue;
}
// Don't run the update callbacks if the data wasn't passed in the request.
if ( ! isset( $request[ $field_name ] ) ) {
continue;
}
$result = call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() );
if ( is_wp_error( $result ) ) {
return $result;
}
}
return true;
}
/**
* Adds the schema from additional fields to a schema array.
*
* The type of object is inferred from the passed schema.
*
* @since 4.7.0
*
* @param array $schema Schema array.
* @return array Modified Schema array.
*/
protected function add_additional_fields_schema( $schema ) {
if ( empty( $schema['title'] ) ) {
return $schema;
}
// Can't use $this->get_object_type otherwise we cause an inf loop.
$object_type = $schema['title'];
$additional_fields = $this->get_additional_fields( $object_type );
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['schema'] ) {
continue;
}
$schema['properties'][ $field_name ] = $field_options['schema'];
}
return $schema;
}
/**
* Retrieves all of the registered additional fields for a given object-type.
*
* @since 4.7.0
*
* @param string $object_type Optional. The object type.
* @return array Registered additional fields (if any), empty array if none or if the object type could
* not be inferred.
*/
protected function get_additional_fields( $object_type = null ) {
if ( ! $object_type ) {
$object_type = $this->get_object_type();
}
if ( ! $object_type ) {
return array();
}
global $wp_rest_additional_fields;
if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
return array();
}
return $wp_rest_additional_fields[ $object_type ];
}
/**
* Retrieves the object type this controller is responsible for managing.
*
* @since 4.7.0
*
* @return string Object type for the controller.
*/
protected function get_object_type() {
$schema = $this->get_item_schema();
if ( ! $schema || ! isset( $schema['title'] ) ) {
return null;
}
return $schema['title'];
}
/**
* Gets an array of fields to be included on the response.
*
* Included fields are based on item schema and `_fields=` request argument.
*
* @since 4.9.6
*
* @param WP_REST_Request $request Full details about the request.
* @return array Fields to be included in the response.
*/
public function get_fields_for_response( $request ) {
$schema = $this->get_item_schema();
$fields = isset( $schema['properties'] ) ? array_keys( $schema['properties'] ) : array();
$additional_fields = $this->get_additional_fields();
foreach ( $additional_fields as $field_name => $field_options ) {
// For back-compat, include any field with an empty schema
// because it won't be present in $this->get_item_schema().
if ( is_null( $field_options['schema'] ) ) {
$fields[] = $field_name;
}
}
if ( ! isset( $request['_fields'] ) ) {
return $fields;
}
$requested_fields = wp_parse_list( $request['_fields'] );
if ( 0 === count( $requested_fields ) ) {
return $fields;
}
// Trim off outside whitespace from the comma delimited list.
$requested_fields = array_map( 'trim', $requested_fields );
// Always persist 'id', because it can be needed for add_additional_fields_to_object().
if ( in_array( 'id', $fields, true ) ) {
$requested_fields[] = 'id';
}
return array_intersect( $fields, $requested_fields );
}
/**
* Retrieves an array of endpoint arguments from the item schema for the controller.
*
* @since 4.7.0
*
* @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are
* checked for required values and may fall-back to a given default, this is not done
* on `EDITABLE` requests. Default WP_REST_Server::CREATABLE.
* @return array Endpoint arguments.
*/
public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
$schema = $this->get_item_schema();
$schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
$endpoint_args = array();
foreach ( $schema_properties as $field_id => $params ) {
// Arguments specified as `readonly` are not allowed to be set.
if ( ! empty( $params['readonly'] ) ) {
continue;
}
$endpoint_args[ $field_id ] = array(
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'rest_sanitize_request_arg',
);
if ( isset( $params['description'] ) ) {
$endpoint_args[ $field_id ]['description'] = $params['description'];
}
if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
$endpoint_args[ $field_id ]['default'] = $params['default'];
}
if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
$endpoint_args[ $field_id ]['required'] = true;
}
foreach ( array( 'type', 'format', 'enum', 'items', 'properties', 'additionalProperties' ) as $schema_prop ) {
if ( isset( $params[ $schema_prop ] ) ) {
$endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
}
}
// Merge in any options provided by the schema property.
if ( isset( $params['arg_options'] ) ) {
// Only use required / default from arg_options on CREATABLE endpoints.
if ( WP_REST_Server::CREATABLE !== $method ) {
$params['arg_options'] = array_diff_key(
$params['arg_options'],
array(
'required' => '',
'default' => '',
)
);
}
$endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
}
}
return $endpoint_args;
}
/**
* Sanitizes the slug value.
*
* @since 4.7.0
*
* @internal We can't use sanitize_title() directly, as the second
* parameter is the fallback title, which would end up being set to the
* request object.
*
* @see https://github.com/WP-API/WP-API/issues/1585
*
* @todo Remove this in favour of https://core.trac.wordpress.org/ticket/34659
*
* @param string $slug Slug value passed in request.
* @return string Sanitized value for the slug.
*/
public function sanitize_slug( $slug ) {
return sanitize_title( $slug );
}
}
<?php
/**
* REST API: WP_REST_Posts_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class to access posts via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Posts_Controller extends WP_REST_Controller {
/**
* Post type.
*
* @since 4.7.0
* @var string
*/
protected $post_type;
/**
* Instance of a post meta fields object.
*
* @since 4.7.0
* @var WP_REST_Post_Meta_Fields
*/
protected $meta;
/**
* Constructor.
*
* @since 4.7.0
*
* @param string $post_type Post type.
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
$this->namespace = 'core';
$obj = get_post_type_object( $post_type );
$this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
$this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
}
/**
* Registers the routes for the objects of the controller.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
$schema = $this->get_item_schema();
$get_item_args = array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
if ( isset( $schema['properties']['password'] ) ) {
$get_item_args['password'] = array(
'description' => __( 'The password for the post if it is password protected.' ),
'type' => 'string',
);
}
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the object.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $get_item_args,
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Whether to bypass trash and force deletion.' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to read posts.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$post_type = get_post_type_object( $this->post_type );
if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Retrieves a collection of posts.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
// Ensure a search string is set in case the orderby is set to 'relevance'.
if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) {
return new WP_Error( 'rest_no_search_term_defined', __( 'You need to define a search term to order by relevance.' ), array( 'status' => 400 ) );
}
// Ensure an include parameter is set in case the orderby is set to 'include'.
if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) {
return new WP_Error( 'rest_orderby_include_missing_include', __( 'You need to define an include parameter to order by include.' ), array( 'status' => 400 ) );
}
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
$args = array();
/*
* This array defines mappings between public API query parameters whose
* values are accepted as-passed, and their internal WP_Query parameter
* name equivalents (some are the same). Only values which are also
* present in $registered will be set.
*/
$parameter_mappings = array(
'author' => 'author__in',
'author_exclude' => 'author__not_in',
'exclude' => 'post__not_in',
'include' => 'post__in',
'menu_order' => 'menu_order',
'offset' => 'offset',
'order' => 'order',
'orderby' => 'orderby',
'page' => 'paged',
'parent' => 'post_parent__in',
'parent_exclude' => 'post_parent__not_in',
'search' => 's',
'slug' => 'post_name__in',
'status' => 'post_status',
);
/*
* For each known parameter which is both registered and present in the request,
* set the parameter's value on the query $args.
*/
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$args[ $wp_param ] = $request[ $api_param ];
}
}
// Check for & assign any parameters which require special handling or setting.
$args['date_query'] = array();
// Set before into date query. Date query must be specified as an array of an array.
if ( isset( $registered['before'], $request['before'] ) ) {
$args['date_query'][0]['before'] = $request['before'];
}
// Set after into date query. Date query must be specified as an array of an array.
if ( isset( $registered['after'], $request['after'] ) ) {
$args['date_query'][0]['after'] = $request['after'];
}
// Ensure our per_page parameter overrides any provided posts_per_page filter.
if ( isset( $registered['per_page'] ) ) {
$args['posts_per_page'] = $request['per_page'];
}
if ( isset( $registered['sticky'], $request['sticky'] ) ) {
$sticky_posts = get_option( 'sticky_posts', array() );
if ( ! is_array( $sticky_posts ) ) {
$sticky_posts = array();
}
if ( $request['sticky'] ) {
/*
* As post__in will be used to only get sticky posts,
* we have to support the case where post__in was already
* specified.
*/
$args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;
/*
* If we intersected, but there are no post ids in common,
* WP_Query won't return "no posts" for post__in = array()
* so we have to fake it a bit.
*/
if ( ! $args['post__in'] ) {
$args['post__in'] = array( 0 );
}
} elseif ( $sticky_posts ) {
/*
* As post___not_in will be used to only get posts that
* are not sticky, we have to support the case where post__not_in
* was already specified.
*/
$args['post__not_in'] = array_merge( $args['post__not_in'],