`$domain` param, all other are optional. * * @param string $domain The domain. * @param string $base The base path. * @param string $lang The language. * @param string $protocol The protocol option. It can be `http`, `https` * or `auto`. * @return void * @since 1.0.0 */ public function addDomain($domain, $base = null, $lang = null, $protocol = 'auto') { $this->domains[$domain] = [ 'base' => $base, 'lang' => $lang, 'protocol' => $protocol, ]; } /** * Store the current list of domains in the WordPress options. * * This is can be used to persist changes made to the list of domains with * `resetDomains` and `addDomain` methods. * * @return void * @since 1.0.0 */ public function storeDomains() { update_option('multiple-domain-domains', $this->domains); } /** * When the current domain has a base URL restriction and the current * request URI doesn't match it, redirects the user. * * @return void */ public function redirect() { /* * Allow developers to create their own logic for redirection. */ do_action('multiple_domain_redirect', $this->domain); $base = $this->getDomainBase(); $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null; $base = ltrim($base, '/'); $uri = ltrim($uri, '/'); if (empty($base) || preg_match('/^wp-[a-z]+(\.php|\/|$)/i', $uri)) { return; } if (strpos($uri, $base) !== 0) { wp_redirect(home_url('/' . $base)); exit; } } /** * Replaces the domain in the given URL. * * The domain in the given URL is replaced with the current domain. If the * URL contains `/wp-admin/` it'll be ignored when replacing the domain and * returned as is. * * @param string $url The URL to fix. * @return string The domain replaced URL. * @since 0.10.0 */ public function fixUrl($url) { if (!preg_match('/\/wp-admin\/?/', $url)) { $domain = $this->getDomainFromUrl($url); $url = $this->replaceDomain($domain, $url); } return $url; } /** * Replaces the domain in `upload_dir` filter used by `wp_upload_dir()`. * * The domain in the given `url` and `baseurl` is replaced by the current * domain. * * @param array $uploads The array of `url`, `baseurl` and other * properties. * @return array The domain-replaced values. * @since 0.4 */ public function fixUploadDir($uploads) { $uploads['url'] = $this->fixUrl($uploads['url']); $uploads['baseurl'] = $this->fixUrl($uploads['baseurl']); return $uploads; } /** * Replaces the domain in post content. * * All occurrences of any of the available domains (i.e. all domains set in * the plugin config) will be replaced with the current domain. * * @param string $content The content to fix. * @return string The domain replaced content. * @since 0.8 */ public function fixContentUrls($content) { foreach (array_keys($this->domains) as $domain) { $content = $this->replaceDomain($domain, $content); } return $content; } /** * Add all available domains to allowed origins. * * This filter is used to prevent CORS issues. * * @param array $origins The default list of allowed origins. * @return array The updated list of allowed origins. * @since 0.8 */ public function addAllowedOrigins($origins) { foreach (array_keys($this->domains) as $domain) { $origins[] = 'https://' . $domain; $origins[] = 'http://' . $domain; } return array_values(array_unique($origins)); } /** * Add the current domain to the body class in a sanitized version. * * If the current domain is `example.com`, the class added to the page body * will be `multiple-domain-example-com`. Notice this filter only has effect * when the `body_class()` function is added to the page's ` tag`. * * @param array $classes The initial list of body class names. * @return array Updated list of body class names. * @since 0.9.0 */ public function addDomainBodyClass($classes) { $classes[] = 'multiple-domain-' . preg_replace('/[^a-z0-9]+/i', '-', $this->domain); return $classes; } /** * Add `hreflang` links to head for SEO purpose. * * @return void * @author Alexander Nosov * @since 0.4 */ public function addHrefLangTags() { /** * The WP class instance. * * @var WP */ global $wp; $uri = trailingslashit('/' . ltrim(add_query_arg([], $wp->request), '/')); $currentProtocol = $this->getCurrentProtocol(); foreach (array_keys($this->domains) as $domain) { $protocol = $this->getDomainProtocol($domain); if ($protocol === 'auto') { $protocol = $currentProtocol; } $protocol .= '://'; $lang = $this->getDomainLang($domain); if (!empty($lang)) { $this->outputHrefLangTag($protocol . $domain . $uri, $lang); } if ($domain === $this->originalDomain) { $this->outputHrefLangTag($protocol . $domain . $uri); } } } /** * Add `canonical` links to head for SEO purpose. * * @return void * @since 0.11.0 */ public function addCanonicalTag() { if (!$this->shouldAddCanonical()) { return; } /** * The WP class instance. * * @var WP */ global $wp; $uri = home_url(add_query_arg([], $wp->request), 'relative') . '/'; $currentProtocol = $this->getCurrentProtocol(); $protocol = $this->getDomainProtocol($this->originalDomain); if ($protocol === 'auto') { $protocol = $currentProtocol; } $protocol .= '://'; $this->outputCanonicalTag($protocol . $this->originalDomain . $uri); } /** * This shortcode simply returns the current domain. * * @return string The current domain. * @since 0.8.5 */ public function shortcode() { return $this->domain; } /** * Load text domain when plugin is loaded. * * @return void * @since 0.8.6 */ public function loaded() { $path = dirname(plugin_basename(MULTIPLE_DOMAIN_PLUGIN)) . '/languages/'; load_plugin_textdomain('multiple-domain', false, $path); } /** * Register vars to be used as text replacements in Yoast tags. * * @return void * @since 0.11.0 */ public function registerYoastVars() { wpseo_register_var_replacement( '%%multiple_domain%%', [ $this, 'getDomain' ], 'advanced', __('The current domain from Multiple Domain', 'multiple-domain') ); } /** * Get the current domain via request headers parsing. * * @return string|null The current domain. * @since 0.8.7 */ private function getDomainFromRequest() { $domain = $this->getHostHeader(); if (empty($domain)) { return null; } $matches = []; if (preg_match('/^(.*):(\d+)$/', $domain, $matches) && $this->isDefaultPort($matches[2])) { $domain = $matches[1]; } return $domain; } /** * Get the `Host` HTTP header value. * * To make it compatible with proxies, this function first tries to get the * value from `X-Host` header and, then, falls back to the regular `Host` * header. * * It returns `null` in case both headers are empty. * * @return string|null The HTTP `Host` header value. * @since 0.8.7 */ private function getHostHeader() { if (!empty($_SERVER['HTTP_X_HOST'])) { return $_SERVER['HTTP_X_HOST']; } if (!empty($_SERVER['HTTP_HOST'])) { return $_SERVER['HTTP_HOST']; } return null; } /** * Get the current URL protocol based on server settings. * * The possible returned values are `http` and `https`. * * @return string The protocol. */ private function getCurrentProtocol() { return empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off' ? 'http' : 'https'; } /** * Get an attribute by its name for the given domain. * * If no domain is passed to the function, it'll return the attribute value * for the current domain. * * Notice this function may return `null` when the attribute is not set in * the plugin config or doesn't exist. * * @param string $name The attribute name. * @param string|null $domain The domain. * @return string The attribute value. * @since 0.10.0 */ private function getDomainAttribute($name, $domain = null) { if (empty($domain)) { $domain = $this->domain; } $attribute = null; if (!empty($this->domains[$domain][$name])) { $attribute = $this->domains[$domain][$name]; } return $attribute; } /** * Replaces the domain. * * All occurrences of the given domain will be replaced with the current * domain in the content. * * The protocol may also be replaced following the protocol settings defined * in the plugin config for the current domain. * * @param string $domain The domain to replace. * @param string $content The content that will have the domain replaced. * @return string The domain-replaced content. */ private function replaceDomain($domain, $content) { if (MULTIPLE_DOMAIN_LOW_MEMORY) { return $this->replaceDomainUsingLessMemory($domain, $content); } if (array_key_exists($domain, $this->domains)) { $regex = '/(https?):\/\/' . preg_quote($domain, '/') . '(?![a-z0-9.\-:])/i'; $protocol = $this->getDomainProtocol($this->domain); $replace = ($protocol === 'auto' ? '${1}' : $protocol) . '://' . $this->domain; $content = preg_replace($regex, $replace, $content); } return $content; } /** * Replaces the domain using less memory. * * This function does the same as `replaceDoamin`, however it uses * `mb_eregi_replace` instead of `preg_replace` for less memory consumption. * On the other hand, it takes more time to execute. * * @param string $domain The domain to replace. * @param string $content The content that will have the domain replaced. * @return string The domain-replaced content. * @since 1.0.2 */ private function replaceDomainUsingLessMemory($domain, $content) { if (array_key_exists($domain, $this->domains)) { $regex = '(https?):\/\/' . preg_quote($domain, '/') . '(?![a-z0-9.\-:])'; $protocol = $this->getDomainProtocol($this->domain); $replace = ($protocol === 'auto' ? '\\1' : $protocol) . '://' . $this->domain; $content = mb_eregi_replace($regex, $replace, $content); } return $content; } /** * Parses the given URL to return only its domain. * * The server port may be included in the returning value depending on its * number and plugin settings. * * @param string $url The URL to parse. * @param bool $ignoreDefaultPorts If `true` is passed to this value, a * default HTTP or HTTPS port will be ignored even if it's present * in the URL. * @return string The domain. * @since 0.2 */ private function getDomainFromUrl($url, $ignoreDefaultPorts = false) { $parts = parse_url($url); $domain = $parts['host']; if (!empty($parts['port']) && !($ignoreDefaultPorts && $this->isDefaultPort($parts['port']))) { $domain .= ':' . $parts['port']; } return $domain; } /** * Checks if the given port is a default HTTP (`80`) or HTTPS (`443`) port. * * @param int $port The port to check. * @return bool Indicates if the port is a default one. * @since 0.2 */ private function isDefaultPort($port) { $port = (int) $port; return $port === self::PORT_HTTP || $port === self::PORT_HTTPS; } /** * Prints a `hreflang` link tag. * * @param string $url The URL to be set into `href` attribute. * @param string $lang The language code to be set into `hreflang` * attribute. Defaults to `x-default`. * @return void * @since 0.5 */ private function outputHrefLangTag($url, $lang = 'x-default') { $url = htmlentities($url); $lang = str_replace('_', '-', $lang); printf('', $url, $lang); } /** * Prints a `canonical` link tag. * * @param string $url The canonical URL to be set into `href` attribute. * @return void * @since 0.11.0 */ private function outputCanonicalTag($url) { $url = htmlentities($url); printf('', $url); } /** * Filter override WordPress built-in canonical tag generation if using the this plugin's canonical tag feature * * @param $url * @return string */ public function getCanonicalUrl($url) { // If *not* using the plugin's canonical tags, then return this URL. Otherwise, don't if (!$this->shouldAddCanonical()) { return $url; } return ''; } }