├── .editorconfig ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── src ├── class-hercules.php ├── cli.php └── helpers.php └── sunrise.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.php] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [Makefile] 19 | indent_style = tab 20 | indent_size = 8 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Isotop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Pot config 2 | POT_NAME := hercules 3 | POT_FILE := languages/hercules.pot 4 | POT_SOURCE := $(shell find src -name '*.php' -type f) 5 | 6 | lint: 7 | make lint:php 8 | 9 | lint\:php: 10 | vendor/bin/phpcs -s --extensions=php --standard=phpcs.xml src/ 11 | 12 | pot: 13 | xgettext --language=php \ 14 | --add-comments=L10N \ 15 | --keyword=__ \ 16 | --keyword=_e \ 17 | --keyword=_n:1,2 \ 18 | --keyword=_x:1,2c \ 19 | --keyword=_ex:1,2c \ 20 | --keyword=_nx:4c,1,2 \ 21 | --keyword=esc_attr_ \ 22 | --keyword=esc_attr_e \ 23 | --keyword=esc_attr_x:1,2c \ 24 | --keyword=esc_html_ \ 25 | --keyword=esc_html_e \ 26 | --keyword=esc_html_x:1,2c \ 27 | --keyword=_n_noop:1,2 \ 28 | --keyword=_nx_noop:3c,1,2 \ 29 | --keyword=__ngettext_noop:1,2 \ 30 | --package-name=$(POT_NAME) \ 31 | --from-code=UTF-8 \ 32 | --output=$(POT_FILE) \ 33 | $(POT_SOURCE) 34 | 35 | # Add Poedit information to the template file. 36 | cat $(POT_FILE)|perl -pe 's/8bit\\n/8bit\\n\"\n\"X-Poedit-Basepath:\ 37 | ..\\n"\n"X-Poedit-SourceCharset: UTF-8\\n"\n\ 38 | "X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c;_n_noop:1,2;_nx_noop:3c,1,2;__ngettext_noop:1,2\\n"\n\ 39 | "X-Poedit-SearchPath-0: .\\n"\n\ 40 | "X-Poedit-SearchPathExcluded-0: *.js\\n"\n"Plural-Forms: nplurals=2; plural=(n != 1);\\n\\n/g'|tail -n +7|tee $(POT_FILE) >/dev/null 2>/dev/null 41 | 42 | test: 43 | vendor/bin/phpunit 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hercules [![Build Status](https://travis-ci.org/isotopsweden/wp-hercules.svg?branch=master)](https://travis-ci.org/isotopsweden/wp-hercules) 2 | 3 | > Requires PHP 5.5.9 and WordPress 4.5 4 | 5 | Simple domain mapping for top domains. 6 | 7 | ## Installation 8 | 9 | ``` 10 | composer require isotopsweden/wp-hercules 11 | ``` 12 | 13 | ## Usage 14 | 15 | Create `wp-content/sunrise.php` 16 | 17 | ```php 18 | sunrise.php.' ); 50 | } 51 | 52 | // Don't continue if not in multisite mode. 53 | if ( ! is_multisite() ) { 54 | wp_die( 'Hercules requires WordPress to be in multisite mode.' ); 55 | } 56 | 57 | // Don't continue if `WP_Site` don't exists. 58 | if ( ! class_exists( 'WP_Site' ) ) { 59 | wp_die( 'Hercules requires WordPress 4.5 or newer. Update now!' ); 60 | } 61 | 62 | $this->start(); 63 | } 64 | 65 | /** 66 | * Destroy global variables and set current site to null. 67 | */ 68 | public function destroy() { 69 | global $current_blog, $blog_id; 70 | 71 | $blog_id = 1; 72 | $this->site = $current_blog = null; 73 | } 74 | 75 | /** 76 | * Find site by domain. 77 | * 78 | * @param string $domain The domain to look for. 79 | * 80 | * @return \WP_Site|null 81 | */ 82 | protected function find_site( $domain ) { 83 | if ( empty( $domain ) || ! is_string( $domain ) ) { 84 | return; 85 | } 86 | 87 | // Get site by domain. 88 | if ( $site = get_site_by_path( $domain, '' ) ) { 89 | return $site instanceof WP_Site ? $site : new WP_Site( $site ); 90 | } 91 | 92 | if ( $site = get_site( 1 ) ) { 93 | $scheme = is_ssl() ? 'https' : 'http'; 94 | $uri = sprintf( '%s://%s', $scheme, $site->domain ); 95 | 96 | header( 'Location: ' . $uri ); 97 | die; 98 | } 99 | } 100 | 101 | /** 102 | * Get value by key from site. 103 | * 104 | * @param string $key The key to look for. 105 | * @param mixed $default Default null. 106 | * @param int $blog_id The blog id. Default zero. 107 | * 108 | * @return mixed 109 | */ 110 | public function get( $key, $default = null, $blog_id = 0 ) { 111 | if ( $site = $this->get_site( $blog_id ) ) { 112 | return isset( $site->$key ) ? $site->$key : $default; 113 | } 114 | 115 | return $default; 116 | } 117 | 118 | /** 119 | * Get domain from current site or from `HTTP_HOST`. 120 | * 121 | * @param int $blog_id Optional, default zero. 122 | * 123 | * @return string 124 | */ 125 | public function get_domain( $blog_id = 0 ) { 126 | if ( ! empty( $blog_id ) && $domain = $this->get( 'domain', null, $blog_id ) ) { 127 | return $domain; 128 | } 129 | 130 | if ( $this->site instanceof WP_Site ) { 131 | return $this->site->domain; 132 | } 133 | 134 | $domain = strtolower( stripslashes( $_SERVER['HTTP_HOST'] ) ); 135 | 136 | if ( substr( $domain, -3 ) === ':80' ) { 137 | $domain = substr( $domain, 0, -3 ); 138 | $_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -3 ); 139 | } else if ( substr( $domain, -4 ) === ':443' ) { 140 | $domain = substr( $domain, 0, -4 ); 141 | $_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -4 ); 142 | } 143 | 144 | return $domain; 145 | } 146 | 147 | /** 148 | * Get site by the current domain. 149 | * 150 | * @param int $blog_id Optional, default zero. 151 | * 152 | * @return \WP_Site|null 153 | */ 154 | public function get_site( $blog_id = 0 ) { 155 | if ( ! empty( $blog_id ) ) { 156 | // Check so the current site is the site we looking for or 157 | // try to get it from the database. 158 | if ( $this->site instanceof WP_Site && (int) $this->site->blog_id === $blog_id ) { 159 | return $this->site; 160 | } else if ( $blog_details = get_blog_details( $blog_id ) ) { 161 | $this->site = new WP_Site( $blog_details ); 162 | } 163 | } 164 | 165 | // If a site exists, return it. 166 | if ( $this->site instanceof WP_Site ) { 167 | return $this->site; 168 | } 169 | 170 | // Find site by current domain. 171 | return $this->site = $this->find_site( $this->get_domain() ); 172 | } 173 | 174 | /** 175 | * Get class instance. 176 | * 177 | * @return \Isotop\WordPress\Hercules\Hercules 178 | */ 179 | public static function instance() { 180 | if ( ! isset( static::$instance ) ) { 181 | static::$instance = new static; 182 | } 183 | 184 | return static::$instance; 185 | } 186 | 187 | /** 188 | * Mangle url to right domain, it will fetch the site 189 | * if `$blog_id` is different from the existing one. 190 | * 191 | * @param string $url The url. 192 | * @param string $path The path. Not used. 193 | * @param string $orig_scheme The scheme. Not used. 194 | * @param int $blog_id The blog id. 195 | * 196 | * @return string 197 | */ 198 | public function mangle_url( $url, $path = '', $orig_scheme = '', $blog_id = 0 ) { 199 | $domain = parse_url( $url, PHP_URL_HOST ); 200 | $regex = '#^(\w+://)' . preg_quote( $domain, '#' ) . '#i'; 201 | 202 | if ( empty( $blog_id ) ) { 203 | $blog_id = (int) $GLOBALS['blog_id']; 204 | } 205 | 206 | // Check if `/wp` should be added if `WP_SITEURL` contains `/wp` and url contains `/wp-`. Will fix issues with network pages. 207 | if ( defined( 'WP_SITEURL' ) && strpos( WP_SITEURL, '/wp' ) !== false && strpos( $url, '/wp/' ) === false && strpos( $url, '/wp-' ) !== false ) { 208 | $path = parse_url( $url, PHP_URL_PATH ); 209 | 210 | // Only add `/wp/` path don't include `/wp-json`. 211 | if ( ! is_null( $path ) && strpos( $path, '/wp-json' ) === false ) { 212 | $url = str_replace( $path, '/wp/' . ltrim( $path, '/' ), $url ); 213 | } 214 | } 215 | 216 | return preg_replace( $regex, '${1}' . $this->get_domain( $blog_id ), $url ); 217 | } 218 | 219 | /** 220 | * Dynamic build site url if possible 221 | * or return database url. 222 | * 223 | * @param mixed $url 224 | * 225 | * @return mixed $url 226 | */ 227 | public function pre_option_siteurl( $url ) { 228 | if ( defined( 'WP_SITEURL' ) ) { 229 | $url = WP_SITEURL; 230 | 231 | if ( defined( 'WP_CLI' ) ) { 232 | $url = str_replace( 'wp', '', WP_SITEURL ); 233 | } 234 | 235 | return $url; 236 | } 237 | 238 | return $url; 239 | } 240 | 241 | /** 242 | * Dynamic build home url if possible 243 | * or return database url. 244 | * 245 | * @param mixed $url 246 | * 247 | * @return mixed $url 248 | */ 249 | public function pre_option_home( $url ) { 250 | if ( defined( 'WP_HOME' ) ) { 251 | return WP_HOME; 252 | } 253 | 254 | return $url; 255 | } 256 | 257 | /** 258 | * Must use plugins loaded hook. 259 | */ 260 | public function muplugins_loaded() { 261 | // Replace current site url and home url with right domain. 262 | add_filter( 'site_url', [$this, 'mangle_url'], -10, 4 ); 263 | add_filter( 'home_url', [$this, 'mangle_url'], -10, 4 ); 264 | 265 | // Change database option for site url and home url to the current one. 266 | add_filter( 'pre_option_siteurl', [$this, 'pre_option_siteurl'] ); 267 | add_filter( 'pre_option_home', [$this, 'pre_option_home'] ); 268 | 269 | // Only change network site urls for main site. 270 | if ( is_main_site() ) { 271 | add_filter( 'network_site_url', [$this, 'mangle_url'], -10, 4 ); 272 | add_filter( 'network_home_url', [$this, 'mangle_url'], -10, 4 ); 273 | } 274 | 275 | $this->set_cookie_domain(); 276 | } 277 | 278 | /** 279 | * Set right cookie domain. 280 | */ 281 | protected function set_cookie_domain() { 282 | if ( defined( 'COOKIE_DOMAIN' ) ) { 283 | return; 284 | } 285 | 286 | $cookie_domain = $this->get_domain(); 287 | 288 | // Remove `www.` from cookie domain. 289 | if ( substr( $cookie_domain, 0, 4 ) === 'www.' ) { 290 | $cookie_domain = substr( $cookie_domain, 4 ); 291 | } 292 | 293 | /** 294 | * Modify cookie domain before definition. 295 | * 296 | * @param string $cookie_domain 297 | * 298 | * @return string 299 | */ 300 | $cookie_domain = apply_filters( 'hercules_cookie_domain', $cookie_domain ); 301 | 302 | if ( is_string( $cookie_domain ) ) { 303 | define( 'COOKIE_DOMAIN', $cookie_domain ); 304 | } 305 | } 306 | 307 | /** 308 | * Start domain mapping. 309 | * 310 | * @return bool 311 | */ 312 | public function start() { 313 | if ( ! ( $site = $this->get_site() ) ) { 314 | return false; 315 | } 316 | 317 | // Set global variables. 318 | global $current_blog, $blog_id; 319 | $current_blog = $site; 320 | $blog_id = $site->blog_id; 321 | 322 | /** 323 | * Hercules is loaded. 324 | */ 325 | do_action( 'hercules_loaded' ); 326 | 327 | // Started! 328 | return true; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/cli.php: -------------------------------------------------------------------------------- 1 | arguments ) !== 'site create' ) { 15 | return; 16 | } 17 | 18 | // Subdomains are required. 19 | if ( ! function_exists( 'is_subdomain_install' ) || ! is_subdomain_install() ) { 20 | WP_CLI::error( 'Hercules requires subdomains mode' ); 21 | } 22 | 23 | // Get domain. 24 | $domain = isset( WP_CLI::get_runner()->assoc_args['slug'] ) ? WP_CLI::get_runner()->assoc_args['slug'] : ''; 25 | 26 | // Not a valid host. 27 | if ( parse_url( 'http://' . $domain, PHP_URL_HOST ) !== $domain ) { 28 | WP_CLI::error( 'Hercules requires a valid top domain, e.g example.com' ); 29 | } 30 | 31 | // Remove `www.` from the domain if any. 32 | $domain = preg_replace( '|^www\.|', '', $domain ); 33 | 34 | // Split the domain. 35 | $domain = explode( '.', $domain ); 36 | 37 | // Set current site domain. 38 | $current_site->domain = 'dev'; 39 | 40 | // Set new slug. 41 | WP_CLI::get_runner()->run_command( ['site', 'create'], ['slug' => $domain[0]] ); 42 | 43 | exit; 44 | } ); 45 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | boot(); 7 | --------------------------------------------------------------------------------