├── .nvmrc ├── .eslintignore ├── .gitignore ├── .snyk ├── tests └── phpunit │ ├── includes │ └── images │ │ └── wordpress-logo.png │ ├── bootstrap.php │ └── domain-mapping │ ├── RestrictedDomainsTest.php │ ├── MediaDomainsTest.php │ ├── MappingDomainsTest.php │ ├── PrimaryDomainTest.php │ └── DomainsTest.php ├── .eslintrc.json ├── domain-mapping ├── ui │ ├── API │ │ ├── Enums.js │ │ └── Domains.js │ ├── index.js │ ├── Components │ │ ├── DomainDisplayMedia.js │ │ ├── Message.js │ │ ├── DomainDisplayPrimary.js │ │ ├── DomainDisplaySecondary.js │ │ ├── DomainRow.js │ │ ├── DomainAdd.js │ │ └── DomainMapping.js │ └── App.css ├── sunrise.php ├── inc │ ├── compat.php │ ├── sunrise.php │ └── redirect.php ├── classes │ ├── third-party │ │ └── class-dm-yoast.php │ ├── class-dm-domain.php │ ├── class-dm-database.php │ ├── class-dm-ui.php │ ├── class-dm-healthchecks.php │ └── class-dm-media.php ├── domain-mapping.php ├── cli │ ├── class-darkmatter-dropin-cli.php │ ├── class-darkmatter-restrict-cli.php │ └── class-darkmatter-domain-cli.php ├── rest │ ├── class-dm-rest-restricted-controller.php │ └── class-dm-rest-domains-controller.php └── api │ ├── class-darkmatter-primary.php │ └── class-darkmatter-restrict.php ├── .github └── workflows │ ├── dependency-review.yml │ ├── snyk.yml │ └── phpunit.yml ├── .editorconfig ├── phpunit.xml.dist ├── scripts ├── release.sh └── install-wp-tests.sh ├── phpcs.xml ├── package.json ├── composer.json ├── dark-matter.php ├── .phpcs.xml.dist ├── dark-matter └── class-dm-pluginupdate.php ├── readme.md └── license.txt /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.15 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | domain-mapping/build/* 2 | vendor/* 3 | scripts/* 4 | postcss.config.js 5 | webpack.config.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules 3 | 4 | /dist/ 5 | /vendor/ 6 | 7 | # PHPUnit 8 | /.phpunit.cache 9 | .phpunit.result.cache -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.22.1 3 | ignore: {} 4 | patch: {} 5 | -------------------------------------------------------------------------------- /tests/phpunit/includes/images/wordpress-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cameronterry/dark-matter/HEAD/tests/phpunit/includes/images/wordpress-logo.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@wordpress/eslint-plugin/recommended-with-formatting" 4 | ], 5 | "parserOptions": { 6 | "requireConfigFile": false 7 | } 8 | } -------------------------------------------------------------------------------- /domain-mapping/ui/API/Enums.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Domain types. 3 | * 4 | * @type {{MEDIA: number, MAIN: number}} 5 | */ 6 | export const DOMAIN_TYPES = { 7 | MAIN: 1, 8 | MEDIA: 2, 9 | }; 10 | -------------------------------------------------------------------------------- /domain-mapping/ui/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { createRoot } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import DomainMapping from './Components/DomainMapping'; 10 | 11 | const container = document.getElementById( 'root' ); 12 | const root = createRoot( container ); 13 | root.render( ); 14 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: 'GitHub Dependency Review' 2 | 3 | on: pull_request 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | dependency-review: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Review dependencies and licenses 15 | uses: actions/dependency-review-action@v3 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.js] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.php] 12 | charset = utf-8 13 | end_of_line = lf 14 | trim_trailing_whitespace = true 15 | indent_style = tab 16 | insert_final_newline = true 17 | 18 | [{package.json,*.yml}] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ./tests/phpunit/domain-mapping/ 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /domain-mapping/sunrise.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | . 18 | 19 | */dev-lib/* 20 | */node_modules/* 21 | */vendor/* 22 | */dist/* 23 | */tests/phpunit/includes/* 24 | -------------------------------------------------------------------------------- /domain-mapping/ui/Components/DomainDisplayMedia.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component } from '@wordpress/element'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | class DomainDisplayMedia extends Component { 8 | /** 9 | * Render component 10 | */ 11 | render() { 12 | const url = 'https://' + this.props.data.domain; 13 | 14 | return ( 15 | 16 |

17 | 18 | { this.props.data.domain } 19 | 20 |

21 | 24 | | 25 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | export default DomainDisplayMedia; 34 | -------------------------------------------------------------------------------- /domain-mapping/ui/App.css: -------------------------------------------------------------------------------- 1 | .notice { 2 | position:relative; 3 | } 4 | 5 | .wp-list-table { 6 | 7 | & .domain-options { 8 | 9 | & button { 10 | -webkit-appearance: button; 11 | background: transparent; 12 | border: none; 13 | color: #0073AA; 14 | cursor: pointer; 15 | font-family: inherit; 16 | font-size: 100%; 17 | line-height: 1.15; 18 | margin: 0; 19 | padding: 0; 20 | text-transform: none; 21 | 22 | &:hover, &:focus { 23 | color: #00A0D2; 24 | } 25 | } 26 | 27 | & span { 28 | color: #DDDDDD; 29 | padding: 0 4px; 30 | } 31 | 32 | & .submitdelete { 33 | color: #AA0000; 34 | 35 | &:hover { 36 | color: #dc3232; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /domain-mapping/ui/Components/Message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component } from '@wordpress/element'; 5 | import { __, sprintf } from '@wordpress/i18n'; 6 | 7 | class Message extends Component { 8 | handleDismiss = ( event ) => { 9 | event.preventDefault(); 10 | 11 | this.props.dismiss( this.props.id, this.props.index ); 12 | }; 13 | 14 | /** 15 | * Render. 16 | */ 17 | render() { 18 | const { notice } = this.props; 19 | const classes = [ 'notice', 'is-dismissable' ]; 20 | 21 | classes.push( 'notice-' + notice.type ); 22 | 23 | return ( 24 |
25 |

{ notice.text }

26 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | export default Message; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "darkmatterplugin", 3 | "version": "2.5.1", 4 | "description": "Domain mapping for WordPress, by Dark Matter Plugin.", 5 | "keywords": [ 6 | "wordpress", 7 | "wordpress", 8 | "plugin" 9 | ], 10 | "homepage": "https://github.com/cameronterry/dark-matter#readme", 11 | "bugs": { 12 | "url": "https://github.com/cameronterry/dark-matter/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/cameronterry/dark-matter.git" 17 | }, 18 | "license": "GPL-2.0-only", 19 | "author": "Cameron Terry", 20 | "scripts": { 21 | "build": "NODE_ENV=production askyr-webpack", 22 | "build-dev": "NODE_ENV=development askyr-webpack" 23 | }, 24 | "devDependencies": { 25 | "@cameronterry/tooling": "github:cameronterry/tooling", 26 | "@wordpress/babel-preset-default": "^8.28.0", 27 | "@wordpress/dependency-extraction-webpack-plugin": "^6.24.0" 28 | }, 29 | "overrides": { 30 | "@babel/runtime": "^7.27.6" 31 | }, 32 | "buildEntryPoints": { 33 | "app-script": "./domain-mapping/ui/index.js", 34 | "app-style": "./domain-mapping/ui/App.css" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cameronterry/dark-matter", 3 | "version": "2.5.1", 4 | "description": "A highly opinionated domain mapping plugin for WordPress.", 5 | "type": "wordpress-plugin", 6 | "license": "GPL-2.0+", 7 | "authors": [ 8 | { 9 | "name": "Cameron Terry", 10 | "email": "cameronterry@protonmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=8.0" 15 | }, 16 | "require-dev": { 17 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0", 18 | "phpcompatibility/phpcompatibility-wp": "^2.1", 19 | "automattic/vipwpcs": "^3.0", 20 | "phpunit/phpunit": "^9.6", 21 | "yoast/phpunit-polyfills": "^4.0" 22 | }, 23 | "scripts": { 24 | "lint": "phpcs . --extensions=php --warning-severity=8 -s --runtime-set testVersion 8.0", 25 | "lint-all": "phpcs .", 26 | "lint-fix": "phpcbf .", 27 | "test": "phpunit", 28 | "test-setup": "bash scripts/install-wp-tests.sh dark_matter_test root root" 29 | }, 30 | "config": { 31 | "allow-plugins": { 32 | "dealerdirect/phpcodesniffer-composer-installer": true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/snyk.yml: -------------------------------------------------------------------------------- 1 | name: Snyk Scan 2 | 3 | on: workflow_dispatch # Temporarily disabling due to a peculiar error. 4 | 5 | jobs: 6 | security: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - name: Run Snyk to check for vulnerabilities for Node 11 | uses: snyk/actions/node@master 12 | continue-on-error: true # To make sure that SARIF upload gets called 13 | env: 14 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 15 | with: 16 | args: --sarif-file-output=snyk-node.sarif 17 | - name: Upload result to GitHub Code Scanning 18 | uses: github/codeql-action/upload-sarif@v2 19 | with: 20 | category: snyk-node 21 | sarif_file: snyk-node.sarif 22 | 23 | - name: Run Snyk to check for vulnerabilities for PHP 24 | uses: snyk/actions/php@master 25 | env: 26 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 27 | with: 28 | args: --sarif-file-output=snyk-php.sarif 29 | - name: Upload result to GitHub Code Scanning 30 | uses: github/codeql-action/upload-sarif@v2 31 | with: 32 | category: snyk-php 33 | sarif_file: snyk-php.sarif -------------------------------------------------------------------------------- /domain-mapping/ui/Components/DomainDisplayPrimary.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component } from '@wordpress/element'; 5 | import { __, sprintf } from '@wordpress/i18n'; 6 | 7 | class DomainDisplayPrimary extends Component { 8 | /** 9 | * Render component. 10 | */ 11 | render() { 12 | const url = 13 | ( this.props.data.is_https ? 'https://' : 'http://' ) + 14 | this.props.data.domain; 15 | 16 | return ( 17 | 18 |

19 | 20 | { this.props.data.domain } 21 | 22 |

23 | { this.props.data.is_active ? ( 24 | 27 | ) : ( 28 | 31 | ) } 32 | | 33 | 42 | | 43 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | export default DomainDisplayPrimary; 52 | -------------------------------------------------------------------------------- /tests/phpunit/bootstrap.php: -------------------------------------------------------------------------------- 1 | permalink = $dm_url->unmap( $indexable->permalink ); 42 | } 43 | 44 | return $intend_to_save; 45 | } 46 | } 47 | 48 | /** 49 | * Only instantiate this class if Yoast SEO is in use. 50 | */ 51 | new DM_Yoast(); 52 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | env: 4 | COMPOSER_CACHE: "${{ github.workspace }}/.composer-cache" 5 | 6 | on: 7 | push: 8 | branches: 9 | - develop 10 | - release/* 11 | pull_request: 12 | branches: 13 | - release/* 14 | 15 | jobs: 16 | phpunit: 17 | name: PHP Unit 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Start MySQL 25 | run: sudo systemctl start mysql.service 26 | 27 | - name: Set standard cache directories 28 | run: | 29 | composer config -g cache-dir "${{ env.COMPOSER_CACHE }}" 30 | 31 | - name: Prepare composer cache 32 | uses: actions/cache@v4 33 | with: 34 | path: ${{ env.COMPOSER_CACHE }} 35 | key: composer-${{ env.COMPOSER_VERSION }}-${{ hashFiles('**/composer.lock') }} 36 | restore-keys: | 37 | composer-${{ env.COMPOSER_VERSION }}- 38 | 39 | - name: Set PHP version 40 | uses: shivammathur/setup-php@v2 41 | with: 42 | php-version: '8.1' 43 | coverage: none 44 | tools: composer:v2 45 | 46 | # For Ubuntu 24.04 image, SVN was removed. But, this is needed for WP/PHPUnit. 47 | # https://github.com/actions/runner-images/issues/10636 48 | - name: Install SVN 49 | run: sudo apt-get install -y subversion 50 | 51 | - name: Install dependencies 52 | run: composer install 53 | 54 | - name: Setup WP Tests 55 | run: | 56 | composer run test-setup 57 | sleep 10 58 | 59 | - name: PHPUnit 60 | run: composer run test -------------------------------------------------------------------------------- /domain-mapping/ui/Components/DomainDisplaySecondary.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component } from '@wordpress/element'; 5 | import { __, sprintf } from '@wordpress/i18n'; 6 | 7 | class DomainDisplaySecondary extends Component { 8 | /** 9 | * Render component. 10 | */ 11 | render() { 12 | const url = 13 | ( this.props.data.is_https ? 'https://' : 'http://' ) + 14 | this.props.data.domain; 15 | 16 | return ( 17 | 18 |

19 | { this.props.data.domain } 20 |

21 | { this.props.data.is_active ? ( 22 | 25 | ) : ( 26 | 29 | ) } 30 | | 31 | 34 | | 35 | 38 | | 39 | 48 | | 49 | 52 | 53 | ); 54 | } 55 | } 56 | 57 | export default DomainDisplaySecondary; 58 | -------------------------------------------------------------------------------- /domain-mapping/domain-mapping.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins. 4 | 5 | 6 | . 7 | /vendor/ 8 | /node_modules/ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /domain-mapping/classes/class-dm-domain.php: -------------------------------------------------------------------------------- 1 | $value ) { 90 | $this->$key = $value; 91 | } 92 | 93 | $this->id = (int) $this->id; 94 | $this->blog_id = (int) $this->blog_id; 95 | $this->is_primary = (bool) $this->is_primary; 96 | $this->active = (bool) $this->active; 97 | $this->is_https = (bool) $this->is_https; 98 | $this->type = (int) $this->type; 99 | } 100 | 101 | /** 102 | * Converts this object to an array. 103 | * 104 | * @since 2.0.0 105 | * 106 | * @return array Object as array. 107 | */ 108 | public function to_array() { 109 | return get_object_vars( $this ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /domain-mapping/classes/class-dm-database.php: -------------------------------------------------------------------------------- 1 | upgrade_domains(); 43 | $this->upgrade_restrict(); 44 | } 45 | } 46 | 47 | /** 48 | * Upgrade the domains table. 49 | * 50 | * @since 2.0.0 51 | * 52 | * @return void 53 | */ 54 | public function upgrade_domains() { 55 | global $wpdb; 56 | $charset_collate = $wpdb->get_charset_collate(); 57 | 58 | $sql = "CREATE TABLE `{$wpdb->base_prefix}domain_mapping` ( 59 | id BIGINT(20) NOT NULL AUTO_INCREMENT, 60 | blog_id BIGINT(20) NOT NULL, 61 | is_primary TINYINT(4) DEFAULT '0', 62 | domain VARCHAR(255) NOT NULL, 63 | active TINYINT(4) DEFAULT '1', 64 | is_https TINYINT(4) DEFAULT '0', 65 | type TINYINT(4) DEFAULT '1', 66 | PRIMARY KEY (id) 67 | ) $charset_collate;"; 68 | 69 | dbDelta( $sql ); 70 | } 71 | 72 | /** 73 | * Upgrade the Reserve domains table. 74 | * 75 | * @since 2.0.0 76 | * 77 | * @return void 78 | */ 79 | public function upgrade_restrict() { 80 | global $wpdb; 81 | $charset_collate = $wpdb->get_charset_collate(); 82 | 83 | $sql = "CREATE TABLE `{$wpdb->base_prefix}domain_restrict` ( 84 | id BIGINT(20) NOT NULL AUTO_INCREMENT, 85 | domain VARCHAR(255) NOT NULL, 86 | PRIMARY KEY (id) 87 | ) $charset_collate;"; 88 | 89 | dbDelta( $sql ); 90 | } 91 | 92 | /** 93 | * Return the Singleton Instance of the class. 94 | * 95 | * @since 2.0.0 96 | * 97 | * @return DM_Database 98 | */ 99 | public static function instance() { 100 | static $instance = false; 101 | 102 | if ( ! $instance ) { 103 | $instance = new self(); 104 | } 105 | 106 | return $instance; 107 | } 108 | } 109 | DM_Database::instance(); 110 | -------------------------------------------------------------------------------- /domain-mapping/classes/class-dm-ui.php: -------------------------------------------------------------------------------- 1 | get_permission(), 46 | 'domains', 47 | array( 48 | $this, 49 | 'page', 50 | ) 51 | ); 52 | 53 | add_action( 'load-' . $hook_suffix, array( $this, 'enqueue' ) ); 54 | } 55 | 56 | /** 57 | * Enqueue assets for the Admin Page. 58 | * 59 | * @since 2.0.0 60 | * 61 | * @return void 62 | */ 63 | public function enqueue() { 64 | $script_data = include DM_PATH . 'dist/app-script.asset.php'; 65 | 66 | wp_enqueue_script( 67 | 'darkmatterplugin-admin-script', 68 | DM_PLUGIN_URL . 'dist/app-script.js', 69 | $script_data['dependencies'], 70 | $script_data['version'], 71 | [ 72 | 'in_footer' => true, 73 | ] 74 | ); 75 | wp_enqueue_style( 76 | 'darkmatterplugin-admin-style', 77 | DM_PLUGIN_URL . 'dist/app-style.css', 78 | [], 79 | $script_data['version'], 80 | ); 81 | } 82 | 83 | /** 84 | * Retrieve the capability that is required for using the admin page. 85 | * 86 | * @since 2.1.2 87 | * 88 | * @return string Capability that must be met to use the Admin page. 89 | */ 90 | public function get_permission() { 91 | /** 92 | * Allows the override of the default permission for per site domain management. 93 | * 94 | * @since 2.1.2 95 | * 96 | * @param string $capability Capability required to manage domains (upgrade_network / Super Admin). 97 | * @param string $context The context the permission is checked. 98 | */ 99 | return apply_filters( 'dark_matter_domain_permission', 'upgrade_network', 'admin' ); 100 | } 101 | 102 | /** 103 | * Very basic HTML output for the 104 | * 105 | * @since 2.0.0 106 | * 107 | * @return void 108 | */ 109 | public function page() { 110 | if ( ! current_user_can( $this->get_permission() ) ) { 111 | wp_die( esc_html__( 'You do not have permission to manage domains.', 'dark-matter' ) ); 112 | } 113 | ?> 114 |
115 | blog_id = $this->factory()->blog->create_object( 33 | [ 34 | 'domain' => 'darkmatter.test', 35 | 'path' => '/siteone', 36 | ] 37 | ); 38 | } 39 | 40 | /** 41 | * Add a new restricted domain. 42 | * 43 | * @return void 44 | */ 45 | public function test_add_restrict_domain() { 46 | $domain = 'restricteddomain1.test'; 47 | $result = DarkMatter_Restrict::instance()->add( $domain ); 48 | 49 | $this->assertTrue( $result, 'Adding a restricted domain.' ); 50 | } 51 | 52 | /** 53 | * Remove a restricted domain. 54 | * 55 | * @return void 56 | */ 57 | public function test_remove_restrict_domain() { 58 | $domain = 'restricteddomain1.test'; 59 | 60 | /** 61 | * Add domain. 62 | */ 63 | $result = DarkMatter_Restrict::instance()->add( $domain ); 64 | $this->assertTrue( $result, 'Adding a restricted domain.' ); 65 | 66 | /** 67 | * Remove domain. 68 | */ 69 | $result = DarkMatter_Restrict::instance()->delete( $domain ); 70 | $this->assertTrue( $result, 'Removing a restricted domain.' ); 71 | } 72 | 73 | /** 74 | * Ensure restricted domain rules are adhered to. 75 | * 76 | * @return void 77 | */ 78 | public function test_restrict_domain_add_domain() { 79 | $domain = 'restricteddomain1.test'; 80 | 81 | /** 82 | * Add domain. 83 | */ 84 | $result = DarkMatter_Restrict::instance()->add( $domain ); 85 | $this->assertTrue( $result, 'Adding a restricted domain.' ); 86 | 87 | /** 88 | * Attempt to add a new domain to a site and restrict. 89 | */ 90 | switch_to_blog( $this->blog_id ); 91 | 92 | $result = DarkMatter_Domains::instance()->add( $domain ); 93 | $this->assertWPError( $result, 'WP_Error for adding a restricted domain.' ); 94 | $this->assertSame( 'reserved', $result->get_error_code(), 'Correct WP_Error for restricted domain.' ); 95 | } 96 | 97 | /** 98 | * Retrieve a number of restricted domains. 99 | * 100 | * @return void 101 | */ 102 | public function test_get_restricted_domains() { 103 | $domains = [ 'restricteddomain1.test', 'restricteddomain2.test' ]; 104 | 105 | foreach ( $domains as $domain ) { 106 | $result = DarkMatter_Restrict::instance()->add( $domain ); 107 | $this->assertTrue( $result, 'Adding a restricted domain.' ); 108 | } 109 | 110 | $this->assertSame( 111 | $domains, 112 | DarkMatter_Restrict::instance()->get(), 113 | 'Get returns all the restricted domains properly.' 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/phpunit/domain-mapping/MediaDomainsTest.php: -------------------------------------------------------------------------------- 1 | blog_id = $this->factory()->blog->create_object( 33 | [ 34 | 'domain' => 'darkmatter.test', 35 | 'path' => '/siteone', 36 | ] 37 | ); 38 | } 39 | 40 | /** 41 | * Media domains set by the `DM_NETWORK_MEDIA` constant. 42 | * 43 | * @return void 44 | */ 45 | public function test_get_media_domains_constant() { 46 | $constant_domains = [ 47 | 'cdn1.darkmatter.test', 48 | ]; 49 | 50 | DarkMatter_Domains::instance()->network_media = $constant_domains; 51 | 52 | $domains = DarkMatter_Domains::instance()->get_domains_by_type(); 53 | 54 | $expected = []; 55 | 56 | foreach ( $constant_domains as $media_domain ) { 57 | $expected[] = new DM_Domain( 58 | (object) [ 59 | 'active' => true, 60 | 'blog_id' => get_current_blog_id(), 61 | 'domain' => $media_domain, 62 | 'id' => -1, 63 | 'is_https' => true, 64 | 'is_primary' => false, 65 | 'type' => DM_DOMAIN_TYPE_MEDIA, 66 | ] 67 | ); 68 | } 69 | 70 | $this->assertEquals( $expected, $domains, 'Media domains set by constant.' ); 71 | } 72 | 73 | /** 74 | * Get media domains as set by an administrator. 75 | * 76 | * @return void 77 | */ 78 | public function test_get_media_domains_manual() { 79 | /** 80 | * Reset any hard-coded media domains. 81 | */ 82 | DarkMatter_Domains::instance()->network_media = []; 83 | 84 | $media_domains = [ 85 | 'cdn1.mappeddomain1.test' => -1, 86 | 'cdn2.mappeddomain1.test' => -1, 87 | 'cdn3.mappeddomain1.test' => -1, 88 | ]; 89 | 90 | switch_to_blog( $this->blog_id ); 91 | 92 | /** 93 | * Create domains. 94 | */ 95 | foreach ( $media_domains as $domain => $id ) { 96 | $result = DarkMatter_Domains::instance()->add( 97 | $domain, 98 | false, 99 | true, 100 | true, 101 | true, 102 | DM_DOMAIN_TYPE_MEDIA 103 | ); 104 | $this->assertNotWPError( $result, 'Media domain created.' ); 105 | 106 | $media_domains[ $domain ] = $result->id; 107 | } 108 | 109 | /** 110 | * Retrieve the domains. 111 | */ 112 | $domains = DarkMatter_Domains::instance()->get_domains_by_type(); 113 | 114 | $expected = []; 115 | 116 | foreach ( $media_domains as $media_domain => $id ) { 117 | $expected[] = new DM_Domain( 118 | (object) [ 119 | 'active' => true, 120 | 'blog_id' => get_current_blog_id(), 121 | 'domain' => $media_domain, 122 | 'id' => $id, 123 | 'is_https' => true, 124 | 'is_primary' => false, 125 | 'type' => DM_DOMAIN_TYPE_MEDIA, 126 | ] 127 | ); 128 | } 129 | 130 | $this->assertEquals( $expected, $domains, 'Media domains set manually.' ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /domain-mapping/cli/class-darkmatter-dropin-cli.php: -------------------------------------------------------------------------------- 1 | is_dropin_latest() ) { 36 | WP_CLI::success( __( 'Current Sunrise dropin matches the Sunrise within Dark Matter plugin.', 'dark-matter' ) ); 37 | return; 38 | } 39 | 40 | WP_CLI::error( __( 'Sunrise dropin does not match the Sunrise within Dark Matter plugin. Consider using the "update" command to correct this issue.', 'dark-matter' ) ); 41 | } 42 | 43 | /** 44 | * Upgrade the Sunrise dropin plugin to the latest version within the Dark 45 | * Matter plugin. 46 | * 47 | * ### OPTIONS 48 | * 49 | * [--force] 50 | * : Force Dark Matter to override and update Sunrise dropin if a file 51 | * already exists. 52 | * 53 | * ### EXAMPLES 54 | * Install the Sunrise dropin plugin for new installations. 55 | * 56 | * wp darkmatter dropin update 57 | * 58 | * Update the Sunrise dropin plugin, even if a file is already present. 59 | * 60 | * wp darkmatter dropin update --force 61 | * 62 | * @since 2.0.0 63 | * 64 | * @param array $args CLI args. 65 | * @param array $assoc_args CLI args maintaining the flag names from the terminal. 66 | */ 67 | public function update( $args, $assoc_args ) { 68 | $destination = WP_CONTENT_DIR . '/sunrise.php'; 69 | $source = DM_PATH . '/domain-mapping/sunrise.php'; 70 | 71 | $opts = wp_parse_args( 72 | $assoc_args, 73 | [ 74 | 'force' => false, 75 | ] 76 | ); 77 | 78 | // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_is_writable 79 | if ( false === is_writable( WP_CONTENT_DIR ) ) { 80 | WP_CLI::error( __( 'The /wp-content/ directory needs to be writable by the current user in order to update.', 'dark-matter' ) ); 81 | } 82 | 83 | if ( false === $opts['force'] && file_exists( $destination ) ) { 84 | WP_CLI::error( __( 'Sunrise is already present. Use the --force flag to override.', 'dark-matter' ) ); 85 | } 86 | 87 | if ( false === is_readable( $source ) ) { 88 | WP_CLI::error( __( 'Cannot read the Sunrise dropin within the Dark Matter plugin folder.', 'dark-matter' ) ); 89 | } 90 | 91 | if ( false === file_exists( $source ) ) { 92 | WP_CLI::error( __( 'Sunrise dropin within the Dark Matter plugin is missing.', 'dark-matter' ) ); 93 | } 94 | 95 | // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 96 | if ( @copy( $source, $destination ) ) { 97 | WP_CLI::success( __( 'Updated the Sunrise dropin to the latest version.', 'dark-matter' ) ); 98 | } else { 99 | WP_CLI::error( __( 'Unknown error occurred preventing the update of Sunrise dropin.', 'dark-matter' ) ); 100 | } 101 | } 102 | } 103 | WP_CLI::add_command( 'darkmatter dropin', 'DarkMatter_Dropin_CLI' ); 104 | -------------------------------------------------------------------------------- /domain-mapping/inc/sunrise.php: -------------------------------------------------------------------------------- 1 | get( $fqdn ); 39 | 40 | if ( $dm_domain && $dm_domain->active ) { 41 | /** 42 | * Prepare all the global variables. This is require irrespective of whether 43 | * it is a primary or secondary domain. 44 | */ 45 | global $current_blog, $original_blog; 46 | $current_blog = get_site( $dm_domain->blog_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 47 | 48 | global $current_site; 49 | $current_site = WP_Network::get_instance( $current_blog->site_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 50 | 51 | // @codingStandardsIgnoreStart 52 | global $blog_id; 53 | $blog_id = $current_blog->blog_id; 54 | global $site_id; 55 | $site_id = $current_blog->site_id; 56 | // @codingStandardsIgnoreEnd 57 | 58 | /** 59 | * Dark Matter will disengage if the website is no longer public or is 60 | * archived or deleted. 61 | */ 62 | if ( (int) $current_blog->public < 0 || '0' !== $current_blog->archived || '0' !== $current_blog->deleted ) { 63 | return; 64 | } 65 | 66 | /** 67 | * If the primary domain, then update the WP_Site properties to match the 68 | * mapped domain and not the admin domain. 69 | */ 70 | if ( $dm_domain->is_primary ) { 71 | $original_blog = clone $current_blog; 72 | 73 | $current_blog->domain = $dm_domain->domain; 74 | $current_blog->path = '/'; 75 | 76 | /** 77 | * Load and prepare the WordPress Network. 78 | */ 79 | global $current_site; // phpcs:ignore WordPressVIPMinimum.Variables.VariableAnalysis.VariableRedeclaration 80 | $current_site = WP_Network::get_instance( $current_blog->site_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 81 | 82 | if ( ! defined( 'COOKIE_DOMAIN' ) ) { 83 | define( 'COOKIE_DOMAIN', $dm_domain->domain ); 84 | } 85 | 86 | define( 'DOMAIN_MAPPING', true ); 87 | 88 | if ( empty( $current_site->blog_id ) ) { 89 | $current_site->blog_id = get_main_site_id( $current_site->id ); 90 | } 91 | 92 | /** 93 | * Set the other necessary globals to ensure WordPress functions correctly. 94 | */ 95 | // @codingStandardsIgnoreStart 96 | global $blog_id; 97 | $blog_id = $current_blog->blog_id; 98 | global $site_id; 99 | $site_id = $current_blog->site_id; 100 | // @codingStandardsIgnoreEnd 101 | } 102 | } 103 | 104 | /** 105 | * Determine if we should perform a redirect. 106 | */ 107 | require_once $dirname . '/inc/redirect.php'; 108 | -------------------------------------------------------------------------------- /domain-mapping/cli/class-darkmatter-restrict-cli.php: -------------------------------------------------------------------------------- 1 | 25 | * : The domain you wish to add to the restrict list. 26 | * 27 | * ### EXAMPLES 28 | * Add a domain to the restrict list. 29 | * 30 | * wp darkmatter restrict add www.example.com 31 | * 32 | * @since 2.0.0 33 | * 34 | * @param array $args CLI args. 35 | * @param array $assoc_args CLI args maintaining the flag names from the terminal. 36 | */ 37 | public function add( $args, $assoc_args ) { 38 | if ( empty( $args[0] ) ) { 39 | WP_CLI::error( __( 'Please include a fully qualified domain name to be added.', 'dark-matter' ) ); 40 | } 41 | 42 | $fqdn = $args[0]; 43 | 44 | $restricted = DarkMatter_Restrict::instance(); 45 | $result = $restricted->add( $fqdn ); 46 | 47 | if ( is_wp_error( $result ) ) { 48 | WP_CLI::error( $result->get_error_message() ); 49 | } 50 | 51 | WP_CLI::success( $fqdn . __( ': is now restricted.', 'dark-matter' ) ); 52 | } 53 | 54 | /** 55 | * Retrieve a list of all Restricted domains for the Network. 56 | * 57 | * ### OPTIONS 58 | * 59 | * * [--format] 60 | * : Determine which format that should be returned. Defaults to "table" and 61 | * accepts "ids", "json", "csv", "yaml", and "count". 62 | * 63 | * ### EXAMPLES 64 | * List all domains for the Network. 65 | * 66 | * wp darkmatter restrict list 67 | * 68 | * List all domains for the Network in JSON format. 69 | * 70 | * wp darkmatter restrict list --format=json 71 | * 72 | * Return all restricted domains for the Network as a string separated by 73 | * spaces. 74 | * 75 | * wp darkmatter restrict list --format=ids 76 | * 77 | * @since 2.0.0 78 | * 79 | * @param array $args CLI args. 80 | * @param array $assoc_args CLI args maintaining the flag names from the terminal. 81 | */ 82 | public function list( $args, $assoc_args ) { 83 | /** 84 | * Handle and validate the format flag if provided. 85 | */ 86 | $opts = wp_parse_args( 87 | $assoc_args, 88 | [ 89 | 'format' => 'table', 90 | ] 91 | ); 92 | 93 | if ( ! in_array( $opts['format'], array( 'ids', 'table', 'json', 'csv', 'yaml', 'count' ) ) ) { 94 | $opts['format'] = 'table'; 95 | } 96 | 97 | $db = DarkMatter_Restrict::instance(); 98 | 99 | $restricted = $db->get(); 100 | 101 | /** 102 | * Only format the return array if "ids" is not specified. 103 | */ 104 | if ( 'ids' !== $opts['format'] ) { 105 | $restricted = array_map( 106 | function ( $domain ) { 107 | return array( 108 | 'F.Q.D.N.' => $domain, 109 | ); 110 | }, 111 | $restricted 112 | ); 113 | } 114 | 115 | WP_CLI\Utils\format_items( 116 | $opts['format'], 117 | $restricted, 118 | [ 119 | 'F.Q.D.N.', 120 | ] 121 | ); 122 | } 123 | 124 | /** 125 | * Remove a domain to the restrict for the WordPress Network. 126 | * 127 | * ### OPTIONS 128 | * 129 | * 130 | * : The domain you wish to remove to the restrict list. 131 | * 132 | * ### EXAMPLES 133 | * Remove a domain to the restrict list. 134 | * 135 | * wp darkmatter restrict remove www.example.com 136 | * 137 | * @since 2.0.0 138 | * 139 | * @param array $args CLI args. 140 | * @param array $assoc_args CLI args maintaining the flag names from the terminal. 141 | */ 142 | public function remove( $args, $assoc_args ) { 143 | if ( empty( $args[0] ) ) { 144 | WP_CLI::error( __( 'Please include a fully qualified domain name to be added.', 'dark-matter' ) ); 145 | } 146 | 147 | $fqdn = $args[0]; 148 | 149 | $restricted = DarkMatter_Restrict::instance(); 150 | $result = $restricted->delete( $fqdn ); 151 | 152 | if ( is_wp_error( $result ) ) { 153 | WP_CLI::error( $result->get_error_message() ); 154 | } 155 | 156 | WP_CLI::success( $fqdn . __( ': is no longer restricted.', 'dark-matter' ) ); 157 | } 158 | } 159 | WP_CLI::add_command( 'darkmatter restrict', 'DarkMatter_Restrict_CLI' ); 160 | -------------------------------------------------------------------------------- /tests/phpunit/domain-mapping/MappingDomainsTest.php: -------------------------------------------------------------------------------- 1 | blog_id = $this->factory()->blog->create_object( 61 | [ 62 | 'domain' => 'darkmatter.test', 63 | 'path' => '/siteone', 64 | ] 65 | ); 66 | 67 | DarkMatter_Domains::instance()->network_media = [ 68 | $this->media_domain, 69 | ]; 70 | 71 | switch_to_blog( $this->blog_id ); 72 | 73 | /** 74 | * Add domains to the new site. 75 | */ 76 | DarkMatter_Domains::instance()->add( 77 | $this->primary_domain, 78 | true, 79 | true 80 | ); 81 | 82 | /** 83 | * Create a post to use. Maps to `wp_insert_post()`. 84 | * 85 | * @link https://developer.wordpress.org/reference/functions/wp_insert_post/ 86 | */ 87 | $this->post = $this->factory()->post->create_and_get(); 88 | $this->attachment = $this->factory()->attachment->create_upload_object( 89 | DARKMATTER_PHPUNIT_DIR . '/includes/images/wordpress-logo.png', 90 | $this->post->ID 91 | ); 92 | 93 | /** 94 | * Set the attachment to be the feature image. 95 | */ 96 | set_post_thumbnail( $this->post, $this->attachment ); 97 | } 98 | 99 | /** 100 | * Ensure admin URL uses the unmapped domain. 101 | * 102 | * @return void 103 | */ 104 | public function test_admin_url() { 105 | $this->assertEquals( 106 | get_admin_url( null, '/' ), 107 | sprintf( 'https://%1$s/siteone/wp-admin/', WP_TESTS_DOMAIN ) 108 | ); 109 | } 110 | 111 | /** 112 | * Ensure the generic get attachment URL returns a Media Domain. 113 | * 114 | * @return void 115 | */ 116 | public function test_attachment_src() { 117 | $url = wp_get_attachment_image_url( $this->attachment ); 118 | $pos = stripos( 119 | $url, 120 | sprintf( 'https://%1$s/', $this->media_domain ) 121 | ); 122 | 123 | $this->assertNotFalse( $pos, '' ); 124 | } 125 | 126 | /** 127 | * A site with a primary domain will modify the Home URL. 128 | * 129 | * @return void 130 | */ 131 | public function test_home_url() { 132 | $this->assertEquals( 133 | get_home_url( null, '/' ), 134 | sprintf( 'https://%1$s/', $this->primary_domain ) 135 | ); 136 | } 137 | 138 | /** 139 | * Ensure the feature image uses a Media Domain. 140 | * 141 | * @return void 142 | */ 143 | public function test_feature_image() { 144 | $html = get_the_post_thumbnail( $this->post->ID ); 145 | $pos = stripos( 146 | $html, 147 | sprintf( 'https://%1$s/', $this->media_domain ) 148 | ); 149 | 150 | $this->assertNotFalse( $pos, '' ); 151 | } 152 | 153 | /** 154 | * Ensure the login URL goes to the admin domain (unmapped). 155 | * 156 | * @return void 157 | */ 158 | public function test_login_url() { 159 | $this->assertEquals( 160 | wp_login_url(), 161 | sprintf( 'https://%1$s/siteone/wp-login.php', WP_TESTS_DOMAIN ), 162 | 'Login URL' 163 | ); 164 | } 165 | 166 | /** 167 | * Ensure the logout URL goes to the admin domain (unmapped). 168 | * 169 | * @return void 170 | */ 171 | public function test_logout_url() { 172 | $url = wp_logout_url(); 173 | 174 | /** 175 | * We use `stripos()` as the logout action contains a nonce in the query string. 176 | */ 177 | $pos = stripos( 178 | $url, 179 | sprintf( 'https://%1$s/siteone/wp-login.php?action=logout', WP_TESTS_DOMAIN ) 180 | ); 181 | 182 | $this->assertNotFalse( $pos, 'Logout URL.' ); 183 | } 184 | 185 | /** 186 | * Ensure the REST URL to ensure it is . 187 | * 188 | * @return void 189 | */ 190 | public function test_rest_url() { 191 | DM_URL::instance()->is_request_mapped = true; 192 | 193 | $this->assertEquals( 194 | /** 195 | * Ensure the REST URL is HTTPS (it gets confused because it checks a number of `$_SERVER` variables). 196 | */ 197 | set_url_scheme( get_rest_url(), 'https' ), 198 | sprintf( 'https://%1$s/wp-json/', $this->primary_domain ), 199 | 'REST API URL' 200 | ); 201 | 202 | DM_URL::instance()->is_request_mapped = false; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /dark-matter/class-dm-pluginupdate.php: -------------------------------------------------------------------------------- 1 | version, '<' ); 61 | } 62 | 63 | /** 64 | * Method for calling Dark Matter Cloud to get the plugin information. 65 | * 66 | * @since 2.2.0 67 | * 68 | * @return false|mixed Dark Matter plugin information. 69 | */ 70 | private function request() { 71 | $response = get_transient( $this->cache_key ); 72 | 73 | if ( ! empty( $response ) ) { 74 | return json_decode( wp_remote_retrieve_body( $response ) ); 75 | } 76 | 77 | $response = wp_remote_get( 78 | 'https://www.darkmatterplugin.com/wp-json/packagemanager/v1/plugins/info/dark-matter', 79 | [ 80 | 'timeout' => 3, 81 | 'headers' => [ 82 | 'Accept' => 'application/json', 83 | ], 84 | ] 85 | ); 86 | 87 | /** 88 | * Validate the response and ensure it is useful. 89 | */ 90 | if ( 91 | is_wp_error( $response ) 92 | || 200 === wp_remote_retrieve_response_code( $response ) 93 | ) { 94 | $body = wp_remote_retrieve_body( $response ); 95 | 96 | /** 97 | * Update the transient and return the encoded JSON. 98 | */ 99 | if ( ! empty( $body ) ) { 100 | set_transient( $this->cache_key, $response, DAY_IN_SECONDS ); 101 | 102 | return json_decode( wp_remote_retrieve_body( $response ) ); 103 | } 104 | } 105 | 106 | /** 107 | * Got here, then the validation failed. 108 | */ 109 | return false; 110 | } 111 | 112 | /** 113 | * Gets the plugin information. 114 | * 115 | * @since 2.2.0 116 | * 117 | * @param false|object|array $result The result object or array. Default false. 118 | * @param string $action The type of information being requested from the Plugin Installation API. 119 | * @param object $args Plugin API arguments. 120 | * @return false|object|array 121 | */ 122 | public function plugin_info( $result = false, $action = '', $args = null ) { 123 | /** 124 | * Ensure we are only requesting the API at the most appropriate points. 125 | */ 126 | if ( 'plugin_information' !== $action || 'dark-matter' !== $args->slug ) { 127 | return false; 128 | } 129 | 130 | /** 131 | * Retrieve the plugin data. 132 | */ 133 | $data = $this->request(); 134 | 135 | if ( empty( $data ) || empty( $data->slug ) ) { 136 | return false; 137 | } 138 | 139 | $result = new stdClass(); 140 | 141 | $result->name = $data->name ?? ''; 142 | $result->slug = $data->slug ?? ''; 143 | $result->version = $data->version ?? ''; 144 | $result->new_version = $data->version ?? ''; 145 | $result->tested = $data->tested ?? ''; 146 | $result->requires = $data->requires ?? ''; 147 | $result->author = $data->author ?? ''; 148 | $result->author_profile = $data->author_homepage ?? ''; 149 | $result->download_link = $data->download_url ?? ''; 150 | $result->trunk = $data->download_url ?? ''; 151 | $result->requires_php = $data->requires_php ?? ''; 152 | $result->last_updated = $data->last_updated ?? ''; 153 | 154 | $result->sections = [ 155 | 'description' => $data->sections->description ?? '', 156 | 'installation' => $data->sections->installation ?? '', 157 | 'changelog' => $data->sections->changelog ?? '', 158 | ]; 159 | 160 | if ( ! empty( $data->banners ) ) { 161 | $result->banners = [ 162 | 'low' => $data->banners->low ?? '', 163 | 'high' => $data->banners->high ?? '', 164 | ]; 165 | } 166 | 167 | return $result; 168 | } 169 | 170 | /** 171 | * Adds any updates to Dark Matter to the relevant transient in WordPress. 172 | * 173 | * @since 2.2.0 174 | * 175 | * @param mixed $value Value of site transient. 176 | * @return mixed New value of site transient. 177 | */ 178 | public function push_update( $value = null ) { 179 | $data = $this->request(); 180 | $data->new_version = $data->version; 181 | $data->package = $data->download_url; 182 | 183 | if ( $this->needs_update( $data ) ) { 184 | $value->response[ $this->plugin_slug ] = $data; 185 | } else { 186 | $value->no_update[ $this->plugin_slug ] = $data; 187 | } 188 | 189 | return $value; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/phpunit/domain-mapping/PrimaryDomainTest.php: -------------------------------------------------------------------------------- 1 | blog->create_object( 46 | [ 47 | 'domain' => 'darkmatter.test', 48 | 'path' => '/siteone', 49 | ] 50 | ); 51 | } 52 | 53 | /** 54 | * Tear down logic after the tests are finished. 55 | * 56 | * @return void 57 | */ 58 | public static function wpTearDownAfterClass() { 59 | wp_delete_site( self::$blog_id ); 60 | } 61 | 62 | /** 63 | * Setup needing for conducting the tests. 64 | * 65 | * @return void 66 | */ 67 | public function setUp(): void { 68 | parent::setUp(); 69 | 70 | $this->darkmatter_domains = DarkMatter_Domains::instance(); 71 | $this->darkmatter_primary = DarkMatter_Primary::instance(); 72 | 73 | switch_to_blog( self::$blog_id ); 74 | } 75 | 76 | /** 77 | * Add a new primary domain. 78 | * 79 | * @return void 80 | */ 81 | public function test_add_primary_domain() { 82 | $domain = 'mappeddomain1.test'; 83 | 84 | /** 85 | * Create primary domain. 86 | */ 87 | $result = $this->darkmatter_domains->add( $domain, true, true ); 88 | $this->assertNotWPError( $result ); 89 | } 90 | 91 | /** 92 | * Set a pre-existing domain to Primary. 93 | * 94 | * @return void 95 | */ 96 | public function test_set_primary_domain() { 97 | $domain = 'mappeddomain1.test'; 98 | 99 | /** 100 | * Create a new domain. 101 | */ 102 | $result = $this->darkmatter_domains->add( $domain, false, true ); 103 | $this->assertNotWPError( $result ); 104 | 105 | /** 106 | * Set the domain to primary, ensuring DB is updated. 107 | */ 108 | $this->darkmatter_primary->set( self::$blog_id, $domain, true ); 109 | 110 | global $wpdb; 111 | $data = $wpdb->get_row( 112 | $wpdb->prepare( 113 | "SELECT * FROM `{$wpdb->base_prefix}domain_mapping` WHERE domain = %s AND blog_id = %d LIMIT 0, 1", 114 | $domain, 115 | self::$blog_id 116 | ) 117 | ); 118 | $this->assertSame( '1', $data->is_primary, 'Database update to primary.' ); 119 | 120 | /** 121 | * Ensure the domain get - which is cached - is returning the correct value. 122 | */ 123 | $result = $this->darkmatter_domains->get( $domain ); 124 | $this->assertTrue( $result->is_primary, 'Cached update to primary.' ); 125 | } 126 | 127 | /** 128 | * Unset a primary domain. 129 | * 130 | * @return void 131 | */ 132 | public function test_unset_primary_domain() { 133 | $domain = 'mappeddomain1.test'; 134 | 135 | /** 136 | * Create a new domain. 137 | */ 138 | $result = $this->darkmatter_domains->add( $domain, true, true ); 139 | $this->assertNotWPError( $result ); 140 | 141 | /** 142 | * Set the domain to primary, ensuring DB is updated. 143 | */ 144 | $this->darkmatter_primary->unset( self::$blog_id, $domain, true ); 145 | 146 | global $wpdb; 147 | $data = $wpdb->get_row( 148 | $wpdb->prepare( 149 | "SELECT * FROM `{$wpdb->base_prefix}domain_mapping` WHERE domain = %s AND blog_id = %d LIMIT 0, 1", 150 | $domain, 151 | self::$blog_id 152 | ) 153 | ); 154 | $this->assertSame( '0', $data->is_primary, 'Database update to unset primary.' ); 155 | 156 | /** 157 | * Ensure the domain get - which is cached - is returning the correct value. 158 | */ 159 | $result = $this->darkmatter_domains->get( $domain ); 160 | $this->assertFalse( $result->is_primary, 'Cached update to unset primary.' ); 161 | } 162 | 163 | /** 164 | * Make sure that all domains work as expected. 165 | * 166 | * @return void 167 | */ 168 | public function test_get_all_primary_domains() { 169 | $second_blog = $this->factory()->blog->create_object( 170 | [ 171 | 'domain' => 'darkmatter.test', 172 | 'path' => '/sitetwo', 173 | ] 174 | ); 175 | 176 | $domain1 = 'mappeddomain1.test'; 177 | $domain2 = 'mappeddomain2.test'; 178 | 179 | $expected = []; 180 | 181 | /** 182 | * Create and set primary domains. 183 | */ 184 | $result = $this->darkmatter_domains->add( $domain1, true, true ); 185 | $this->assertNotWPError( $result ); 186 | 187 | $expected[] = $result; 188 | 189 | switch_to_blog( $second_blog ); 190 | 191 | /** 192 | * Create and set primary domains. 193 | */ 194 | $result = $this->darkmatter_domains->add( $domain2, true, true ); 195 | $this->assertNotWPError( $result ); 196 | 197 | $expected[] = $result; 198 | 199 | /** 200 | * Get all primary domains. 201 | */ 202 | switch_to_blog( self::$blog_id ); 203 | 204 | $primaries = $this->darkmatter_primary->get_all(); 205 | $this->assertEquals( 2, count( $primaries ), 'Two primary domains found.' ); 206 | $this->assertEqualSets( $expected, $primaries, 'Compare to created with domains.' ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /domain-mapping/ui/Components/DomainRow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component } from '@wordpress/element'; 5 | import { __, sprintf } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import DomainDisplayMedia from './DomainDisplayMedia'; 11 | import DomainDisplayPrimary from './DomainDisplayPrimary'; 12 | import DomainDisplaySecondary from './DomainDisplaySecondary'; 13 | 14 | /** 15 | * Constants. 16 | */ 17 | import { DOMAIN_TYPES } from '../API/Enums'; 18 | 19 | class DomainRow extends Component { 20 | /** 21 | * Handle the Activating / Deactivating of domains. 22 | * 23 | * @param {Object} event 24 | */ 25 | handleActivate = ( event ) => { 26 | event.preventDefault(); 27 | 28 | const data = { ...this.props.domain }; 29 | data.is_active = ! data.is_active; 30 | 31 | this.props.update( data ); 32 | }; 33 | 34 | /** 35 | * Handle converting domains between secondary to media domains and vice versa. 36 | * 37 | * @param {Object} event 38 | */ 39 | handleConvert = ( event ) => { 40 | event.preventDefault(); 41 | 42 | const data = { ...this.props.domain }; 43 | 44 | if ( DOMAIN_TYPES.MAIN === data.type ) { 45 | /** Convert media domain to secondary domain. */ 46 | data.type = 2; 47 | } else if ( DOMAIN_TYPES.MEDIA === data.type ) { 48 | /** Convert media domain to secondary domain. */ 49 | data.type = 1; 50 | } 51 | 52 | this.props.update( data ); 53 | }; 54 | 55 | /** 56 | * Handle the Deleting of the domain. 57 | * 58 | * @param {Object} event 59 | */ 60 | handleDelete = ( event ) => { 61 | event.preventDefault(); 62 | 63 | let message = ''; 64 | 65 | if ( this.props.domain.is_primary ) { 66 | message = __( 67 | 'Deleting the primary domain will stop any domain mapping for this Site. You will need to manually set another domain as primary. Do you wish to proceed?', 68 | 'dark-matter' 69 | ); 70 | } else { 71 | message = sprintf( 72 | /* translators: domain name */ 73 | __( 'Are you sure you wish to delete %s?', 'dark-matter' ), 74 | this.props.domain.domain 75 | ); 76 | } 77 | 78 | // eslint-disable-next-line no-alert 79 | const confirm = window.confirm( message ); 80 | 81 | if ( ! confirm ) { 82 | return; 83 | } 84 | 85 | this.props.delete( this.props.domain.domain ); 86 | }; 87 | 88 | /** 89 | * Handle the Setting the primary domain. 90 | * 91 | * @param {Object} event 92 | */ 93 | handlePrimary = ( event ) => { 94 | event.preventDefault(); 95 | 96 | // eslint-disable-next-line no-alert 97 | const confirm = window.confirm( 98 | sprintf( 99 | /* translators: domain name */ 100 | __( 101 | 'Are you sure you wish to change %s to be the primary domain? This will cause 301 redirects and may affect SEO.', 102 | 'dark-matter' 103 | ), 104 | this.props.domain.domain 105 | ) 106 | ); 107 | 108 | if ( ! confirm ) { 109 | return; 110 | } 111 | 112 | const data = { ...this.props.domain }; 113 | data.is_primary = true; 114 | 115 | this.props.update( data ); 116 | }; 117 | 118 | /** 119 | * Handle the change in Protocol. 120 | * 121 | * @param {Object} event 122 | */ 123 | handleProtocol = ( event ) => { 124 | event.preventDefault(); 125 | 126 | const data = { ...this.props.domain }; 127 | const value = ! data.is_https; 128 | 129 | /** 130 | * We only want to get confirmation from the user if we are changing to 131 | * HTTPS. 132 | */ 133 | if ( value ) { 134 | // eslint-disable-next-line no-alert 135 | const confirm = window.confirm( 136 | sprintf( 137 | /* translators: domain name */ 138 | __( 139 | 'Please ensure that your server configuration includes %s for HTTPS. Do you wish to proceed?', 140 | 'dark-matter' 141 | ), 142 | this.props.domain.domain 143 | ) 144 | ); 145 | 146 | if ( ! confirm ) { 147 | return; 148 | } 149 | } 150 | 151 | data.is_https = value; 152 | 153 | this.props.update( data ); 154 | }; 155 | 156 | /** 157 | * Render. 158 | */ 159 | render() { 160 | const { type } = this.props; 161 | 162 | if ( DOMAIN_TYPES.MAIN === type ) { 163 | return this.renderMainDomain(); 164 | } else if ( DOMAIN_TYPES.MEDIA === type ) { 165 | return this.renderMediaDomain(); 166 | } 167 | } 168 | 169 | renderMainDomain() { 170 | return ( 171 | 172 | { this.props.domain.is_primary ? ( 173 | 179 | ) : ( 180 | 188 | ) } 189 | 190 | { this.props.domain.is_primary 191 | ? __( 'Yes', 'dark-matter' ) 192 | : __( 'No', 'dark-matter' ) } 193 | 194 | 195 | { this.props.domain.is_active 196 | ? __( 'Yes', 'dark-matter' ) 197 | : __( 'No', 'dark-matter' ) } 198 | 199 | 200 | { this.props.domain.is_https 201 | ? __( 'HTTPS', 'dark-matter' ) 202 | : __( 'HTTP', 'dark-matter' ) } 203 | 204 | 205 | ); 206 | } 207 | 208 | renderMediaDomain() { 209 | return ( 210 | 211 | 216 | 217 | { __( 'This is a Media domain used for audio, images, video, etc.', 'dark-matter' ) } 218 | 219 | 220 | { __( 'HTTPS', 'dark-matter' ) } 221 | 222 | 223 | ); 224 | } 225 | } 226 | 227 | export default DomainRow; 228 | -------------------------------------------------------------------------------- /domain-mapping/rest/class-dm-rest-restricted-controller.php: -------------------------------------------------------------------------------- 1 | namespace = 'dm/v1'; 22 | $this->rest_base = 'restricted'; 23 | } 24 | 25 | /** 26 | * Add a domain to the Restricted domains list. 27 | * 28 | * @since 2.0.0 29 | * 30 | * @param WP_REST_Request $request Current request. 31 | * @return WP_REST_Response|mixed WP_REST_Response on success. WP_Error on failure. 32 | */ 33 | public function create_item( $request ) { 34 | $db = DarkMatter_Restrict::instance(); 35 | 36 | $domain = ( isset( $request['domain'] ) ? $request['domain'] : '' ); 37 | 38 | $result = $db->add( $domain ); 39 | 40 | /** 41 | * Return errors as-is. This is maintain consistency and parity with the 42 | * WP CLI commands. 43 | */ 44 | if ( is_wp_error( $result ) ) { 45 | return rest_ensure_response( $result ); 46 | } 47 | 48 | $response = rest_ensure_response( 49 | array( 50 | 'domain' => $domain, 51 | ) 52 | ); 53 | 54 | $response->set_status( '201' ); 55 | 56 | return $response; 57 | } 58 | 59 | /** 60 | * Checks if a given request has access to add a Restricted domain. 61 | * 62 | * @since 2.0.0 63 | * 64 | * @param WP_REST_Request $request Current request. 65 | * @return boolean True if the current user is a Super Admin. False otherwise. 66 | */ 67 | public function create_item_permissions_check( $request ) { 68 | /** 69 | * Allows the override of the default permission for the restricted domain management. 70 | * 71 | * @since 2.1.2 72 | * 73 | * @param string $capability Capability required to manage domains (upgrade_network / Super Admin). 74 | * @param string $context The context the permission is checked. 75 | */ 76 | return current_user_can( apply_filters( 'dark_matter_restricted_permission', 'upgrade_network', 'rest-create' ) ); 77 | } 78 | 79 | /** 80 | * Delete a domain to the Restricted domains list. 81 | * 82 | * @since 2.0.0 83 | * 84 | * @param WP_REST_Request $request Current request. 85 | * @return WP_REST_Response|mixed WP_REST_Response on success. WP_Error on failure. 86 | */ 87 | public function delete_item( $request ) { 88 | $db = DarkMatter_Restrict::instance(); 89 | 90 | $domain = ( isset( $request['domain'] ) ? $request['domain'] : '' ); 91 | 92 | $result = $db->delete( $domain ); 93 | 94 | /** 95 | * Return errors as-is. This is maintain consistency and parity with the 96 | * WP CLI commands. 97 | */ 98 | if ( is_wp_error( $result ) ) { 99 | return rest_ensure_response( $result ); 100 | } 101 | 102 | return rest_ensure_response( 103 | array( 104 | 'deleted' => true, 105 | 'domain' => $domain, 106 | ) 107 | ); 108 | } 109 | 110 | /** 111 | * Checks if a given request has access to delete Restricted domains. 112 | * 113 | * @since 2.0.0 114 | * 115 | * @param WP_REST_Request $request Current request. 116 | * @return boolean True if the current user is a Super Admin. False otherwise. 117 | */ 118 | public function delete_item_permissions_check( $request ) { 119 | /** This action is documented in domain-mapping/rest/class-dm-rest-restricted-controller.php */ 120 | return current_user_can( apply_filters( 'dark_matter_restricted_permission', 'upgrade_network', 'rest-delete' ) ); 121 | } 122 | 123 | /** 124 | * Return the Restricted domains as a list in REST response. 125 | * 126 | * @since 2.0.0 127 | * 128 | * @param WP_REST_Request $request Current request. 129 | * @return WP_REST_Response|mixed WP_REST_Response on success. WP_Error on failure. 130 | */ 131 | public function get_items( $request ) { 132 | $db = DarkMatter_Restrict::instance(); 133 | 134 | return rest_ensure_response( $db->get() ); 135 | } 136 | 137 | /** 138 | * Checks if a given request has access to retrieve a list Restricted 139 | * domains. 140 | * 141 | * @since 2.0.0 142 | * 143 | * @param WP_REST_Request $request Current request. 144 | * @return boolean True if the current user is a Super Admin. False otherwise. 145 | */ 146 | public function get_items_permissions_check( $request ) { 147 | /** This action is documented in domain-mapping/rest/class-dm-rest-restricted-controller.php */ 148 | return current_user_can( apply_filters( 'dark_matter_restricted_permission', 'upgrade_network', 'rest-get' ) ); 149 | } 150 | 151 | /** 152 | * Register REST API routes for Restricted domains. 153 | * 154 | * @since 2.0.0 155 | * 156 | * @return void 157 | */ 158 | public function register_routes() { 159 | register_rest_route( 160 | $this->namespace, 161 | $this->rest_base, 162 | [ 163 | 'methods' => WP_REST_Server::CREATABLE, 164 | 'callback' => array( $this, 'create_item' ), 165 | 'permission_callback' => array( $this, 'create_item_permissions_check' ), 166 | ] 167 | ); 168 | 169 | register_rest_route( 170 | $this->namespace, 171 | $this->rest_base, 172 | [ 173 | 'methods' => WP_REST_Server::DELETABLE, 174 | 'callback' => array( $this, 'delete_item' ), 175 | 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 176 | ] 177 | ); 178 | 179 | register_rest_route( 180 | $this->namespace, 181 | $this->rest_base, 182 | [ 183 | 'methods' => WP_REST_Server::READABLE, 184 | 'callback' => array( $this, 'get_items' ), 185 | 'permission_callback' => array( $this, 'get_items_permissions_check' ), 186 | ] 187 | ); 188 | } 189 | } 190 | 191 | /** 192 | * Setup the REST Controller for Domains for use. 193 | * 194 | * @since 2.0.0 195 | * 196 | * @return void 197 | */ 198 | function dark_matter_restricted_rest() { 199 | $controller = new DM_REST_Restricted_Controller(); 200 | $controller->register_routes(); 201 | } 202 | add_action( 'rest_api_init', 'dark_matter_restricted_rest' ); 203 | -------------------------------------------------------------------------------- /domain-mapping/ui/Components/DomainAdd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component } from '@wordpress/element'; 5 | import { __, sprintf } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import Domains from '../API/Domains'; 11 | 12 | class DomainAdd extends Component { 13 | /** 14 | * Constructor. 15 | * 16 | * @param {Object} props 17 | */ 18 | constructor( props ) { 19 | super( props ); 20 | 21 | this.api = new Domains(); 22 | 23 | this.state = { 24 | domain: { 25 | domain: '', 26 | is_primary: false, 27 | is_active: true, 28 | is_https: true, 29 | }, 30 | }; 31 | } 32 | 33 | /** 34 | * Helper method to make the AJAX call to add the domain to the database. 35 | */ 36 | async addDomain() { 37 | const result = await this.api.add( this.state.domain ); 38 | 39 | let message = ''; 40 | 41 | if ( result.code ) { 42 | if ( 'primary' === result.code ) { 43 | message = sprintf( 44 | __( 'Cannot add domain. Primary domain cannot be overridden by a new domain.', 'dark-matter' ), 45 | result.message 46 | ); 47 | } else { 48 | message = sprintf( 49 | /* translators: error message */ 50 | __( 'Cannot add domain. %s', 'dark-matter' ), 51 | result.message 52 | ); 53 | } 54 | } else { 55 | message = sprintf( 56 | /* translators: added domain */ 57 | __( '%s; has been added.', 'dark-matter' ), 58 | result.domain 59 | ); 60 | } 61 | 62 | this.props.addNoticeAndRefresh( 63 | this.state.domain.domain, 64 | message, 65 | result.code ? 'error' : 'success' 66 | ); 67 | 68 | if ( ! result.code ) { 69 | this.reset(); 70 | } 71 | } 72 | 73 | /** 74 | * Handle the change event for each of the form elements. 75 | * 76 | * @param {Object} event Event Information. 77 | */ 78 | handleChange = ( event ) => { 79 | const name = event.target.name; 80 | const value = event.target.value; 81 | 82 | const domain = { ...this.state.domain }; 83 | domain[ name ] = value; 84 | 85 | this.setState( { 86 | domain, 87 | } ); 88 | }; 89 | 90 | /** 91 | * Handle the checkbox change for the protocol. 92 | * 93 | * @param {Object} event Event Information. 94 | */ 95 | handleCheckboxChange = ( event ) => { 96 | const name = event.target.name; 97 | 98 | const domain = { ...this.state.domain }; 99 | domain[ name ] = event.target.checked; 100 | 101 | this.setState( { 102 | domain, 103 | } ); 104 | }; 105 | 106 | /** 107 | * Handle the radio option change for the protocol. 108 | * 109 | * @param {Object} event Event Information. 110 | */ 111 | handleProtocol = ( event ) => { 112 | const value = event.target.value; 113 | 114 | const domain = { ...this.state.domain }; 115 | domain.is_https = 'https' === value; 116 | 117 | this.setState( { 118 | domain, 119 | } ); 120 | }; 121 | 122 | /** 123 | * Handle the form submission. 124 | * 125 | * @param {Object} event Event Information. 126 | */ 127 | handleSubmit = ( event ) => { 128 | event.preventDefault(); 129 | 130 | this.addDomain(); 131 | }; 132 | 133 | /** 134 | * Render the component. 135 | */ 136 | render() { 137 | return ( 138 |
139 |

Add Domain

140 | 141 | 142 | 143 | 148 | 156 | 157 | 158 | 163 | 172 | 173 | 174 | 179 | 188 | 189 | 190 | 191 | 219 | 220 | 221 |
144 | 147 | 149 | 155 |
159 | 162 | 164 | 171 |
175 | 178 | 180 | 187 |
{ __( 'Protocol', 'dark-matter' ) } 192 |

193 | 201 | 204 |

205 |

206 | 214 | 217 |

218 |
222 |

223 | 226 |

227 |
228 | ); 229 | } 230 | 231 | /** 232 | * Reset the form back to the default. 233 | */ 234 | reset() { 235 | this.setState( { 236 | domain: { 237 | domain: '', 238 | is_primary: false, 239 | is_active: true, 240 | is_https: false, 241 | }, 242 | } ); 243 | } 244 | } 245 | 246 | export default DomainAdd; 247 | -------------------------------------------------------------------------------- /domain-mapping/inc/redirect.php: -------------------------------------------------------------------------------- 1 | over the unmapped domain. 55 | */ 56 | ! empty( $_GET['customize_changeset_uuid'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended 57 | || 58 | /** 59 | * Do not redirect Previews 60 | */ 61 | ( ! empty( $_GET['preview'] ) || ! empty( $_GET['page_id'] ) || ! empty( $_GET['p'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended 62 | ) { 63 | return; 64 | } 65 | 66 | /** 67 | * Helper function which is used to determine if we need to perform a redirect 68 | * to the primary domain whilst retaining the remaining URI structure. 69 | * 70 | * @since 2.0.0 71 | * 72 | * @return void 73 | */ 74 | function darkmatter_maybe_redirect() { 75 | /** 76 | * Do not perform redirects if it is the main site. 77 | */ 78 | if ( is_main_site() ) { 79 | return; 80 | } 81 | 82 | $request_uri = ( empty( $_SERVER['REQUEST_URI'] ) ? '' : wp_strip_all_tags( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); 83 | 84 | $request = ltrim( $request_uri, '/' ); 85 | 86 | /** 87 | * Determine if the request is one we shouldn't handle for redirects. 88 | */ 89 | $filename = basename( $request ); 90 | $filename = strtok( $filename, '?' ); 91 | 92 | $ajax_filenames = array( 93 | 'admin-post.php' => true, 94 | 'admin-ajax.php' => true, 95 | ); 96 | 97 | /** 98 | * Check to see if the current request is an Admin Post action or an AJAX action. These two requests in Dark Matter 99 | * can be on either the admin domain or the primary domain. 100 | */ 101 | if ( ! empty( $filename ) && array_key_exists( $filename, $ajax_filenames ) ) { 102 | return; 103 | } 104 | 105 | $original_blog = get_site(); 106 | 107 | $http_host = ( empty( $_SERVER['HTTP_HOST'] ) ? '' : wp_strip_all_tags( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ); 108 | 109 | $host = trim( $http_host, '/' ); 110 | $primary = DarkMatter_Primary::instance()->get(); 111 | 112 | $is_admin = false; 113 | 114 | $admin_filenames = array( 115 | 'wp-login.php' => true, 116 | 'wp-register.php' => true, 117 | ); 118 | 119 | if ( is_admin() || ( ! empty( $filename ) && array_key_exists( $filename, $admin_filenames ) ) ) { 120 | $is_admin = true; 121 | } 122 | 123 | /** 124 | * Dark Matter will disengage if the website is no longer public or is archived or deleted. 125 | */ 126 | if ( (int) $original_blog->public < 0 || '0' !== $original_blog->archived || '0' !== $original_blog->deleted ) { 127 | return; 128 | } 129 | 130 | /** 131 | * If Allow Logins is enabled, then the `wp-login.php` request is to be made 132 | * available on both the primary mapped domain and admin domain. 133 | */ 134 | if ( ! apply_filters( 'darkmatter_allow_logins', false ) && $is_admin && $host === $original_blog->domain ) { 135 | return; 136 | } 137 | 138 | /** 139 | * If there is no primary domain, there is nothing to do. Also make sure the 140 | * domain is active. 141 | */ 142 | if ( ! $primary || ! $original_blog || ! $primary->active || absint( $original_blog->public ) < 1 ) { 143 | return; 144 | } 145 | 146 | if ( $is_admin && $host !== $original_blog->domain ) { 147 | $is_ssl_admin = ( defined( 'FORCE_SSL_ADMIN' ) && FORCE_SSL_ADMIN ); 148 | 149 | $url = 'http' . ( $is_ssl_admin ? 's' : '' ) . '://' . $original_blog->domain . $original_blog->path . $request; 150 | } elseif ( $host !== $primary->domain || is_ssl() !== $primary->is_https ) { 151 | $url = 'http' . ( $primary->is_https ? 's' : '' ) . '://' . $primary->domain . '/' . $request; 152 | 153 | /** 154 | * Make sure the Path - if this is a sub-folder Network - is removed from 155 | * the URL. For sub-domain Networks, the path will be a single forward slash 156 | * (/). 157 | */ 158 | if ( '/' !== $original_blog->path ) { 159 | $path = '/' . trim( $original_blog->path, '/' ) . '/'; 160 | $url = str_ireplace( $path, '/', $url ); 161 | } 162 | } 163 | 164 | /** 165 | * If the URL is empty, then there is no redirect to perform. 166 | */ 167 | if ( empty( $url ) ) { 168 | return; 169 | } 170 | 171 | header( 'X-Redirect-By: Dark-Matter' ); 172 | header( 'Location:' . $url, true, 301 ); 173 | 174 | die; 175 | } 176 | 177 | /** 178 | * We use `muplugins_loaded` action (introduced in WordPress 2.8.0) rather than 179 | * the "ms_loaded" (introduced in WordPress 4.6.0). 180 | * 181 | * A hook on `muplugins_loaded` is used to ensure that WordPress has loaded the 182 | * Blog / Site globals. This is specifically useful when some one goes to the 183 | * Admin domain URL - http://my.sites.com/two/ - which is to redirect to the 184 | * primary domain - http://example.com. 185 | */ 186 | add_action( 'muplugins_loaded', 'darkmatter_maybe_redirect', 20 ); 187 | -------------------------------------------------------------------------------- /domain-mapping/ui/Components/DomainMapping.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies. 3 | */ 4 | import { Component } from '@wordpress/element'; 5 | import { __, sprintf } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies. 9 | */ 10 | import DomainAdd from './DomainAdd'; 11 | import Domains from '../API/Domains'; 12 | import DomainRow from './DomainRow'; 13 | import Message from './Message'; 14 | 15 | class DomainMapping extends Component { 16 | /** 17 | * Constructor. 18 | * 19 | * @param {Object} props 20 | */ 21 | constructor( props ) { 22 | super( props ); 23 | 24 | this.api = new Domains(); 25 | 26 | this.state = { 27 | domains: [], 28 | messages: [], 29 | }; 30 | } 31 | 32 | /** 33 | * Helper method to add a notice to Messages. 34 | * 35 | * @param {string} domain FQDN which the notice is applied to. 36 | * @param {string} text Message to be displayed in the notice. 37 | * @param {string} type Two types; "success" or "error". 38 | */ 39 | addNotice( domain, text, type ) { 40 | this.setState( { 41 | messages: [ 42 | ...this.state.messages, 43 | { 44 | id: new Date().getTime(), 45 | domain, 46 | text, 47 | type, 48 | }, 49 | ], 50 | } ); 51 | } 52 | 53 | /** 54 | * Helper method to add a notice to Messages and Refresh the table. 55 | * 56 | * @param {string} domain FQDN which the notice is applied to. 57 | * @param {string} text Message to be displayed in the notice. 58 | * @param {string} type Two types; "success" or "error". 59 | */ 60 | addNoticeAndRefresh = ( domain, text, type ) => { 61 | this.addNotice( domain, text, type ); 62 | 63 | this.getData(); 64 | }; 65 | 66 | /** 67 | * Retrieve the domains for the Site from the REST API. 68 | */ 69 | componentDidMount() { 70 | this.getData(); 71 | } 72 | 73 | /** 74 | * Performs the delete call to the REST API and handle the error message. 75 | * 76 | * @param {string} domain FQDN to be deleted. 77 | */ 78 | async delete( domain ) { 79 | const result = await this.api.delete( domain ); 80 | 81 | this.addNotice( 82 | domain, 83 | sprintf( 84 | /* translators: domain name */ 85 | __( '%s; has been deleted.', 'dark-matter' ), 86 | result.domain 87 | ), 88 | 'success' 89 | ); 90 | 91 | this.getData(); 92 | } 93 | 94 | /** 95 | * Handle the removal of the Notice from state. 96 | * 97 | * @param {string} id Element ID. 98 | * @param {number} index Message index to be removed from the array. 99 | */ 100 | dimissNotice = ( id, index ) => { 101 | this.setState( { 102 | messages: [ 103 | ...this.state.messages.slice( 0, index ), 104 | ...this.state.messages.slice( index + 1 ), 105 | ], 106 | } ); 107 | }; 108 | 109 | /** 110 | * Method for retrieve all the domains from the REST API. 111 | */ 112 | async getData() { 113 | const result = await this.api.getAll(); 114 | 115 | const primary = result.find( ( domainRecord ) => domainRecord.is_primary ); 116 | 117 | const adminDomain = document.getElementById( 'root' ).dataset?.adminDomain ?? ''; 118 | const currentDomain = document.querySelector( '#wp-admin-bar-view-site > a' ).getAttribute( 'href' ).split( 'wp-admin' )[0]; 119 | 120 | if ( adminDomain && currentDomain ) { 121 | document.querySelectorAll( `a[href*="${currentDomain}"][role="menuitem"]` ).forEach( ( menuItem ) => { 122 | if ( primary && primary?.is_active ) { 123 | menuItem.setAttribute( 'href', 124 | ( primary.is_https ? 'https' : 'http' ) 125 | + `://${primary.domain}/` 126 | ); 127 | } else { 128 | menuItem.setAttribute( 'href', adminDomain ); 129 | } 130 | } ); 131 | } 132 | 133 | this.setState( { 134 | domains: result, 135 | } ); 136 | } 137 | 138 | /** 139 | * Handle the Delete of a domain. 140 | * 141 | * @param {string} domain FQDN to be deleted. 142 | */ 143 | handleDelete = ( domain ) => { 144 | this.delete( domain ); 145 | }; 146 | 147 | /** 148 | * Handle the update for a domain. 149 | * 150 | * @param {Object} data Data set containing the updates for a domain record. 151 | */ 152 | handleUpdate = ( data ) => { 153 | this.update( data ); 154 | }; 155 | 156 | /** 157 | * Render the component. 158 | */ 159 | render() { 160 | const messages = []; 161 | const rows = []; 162 | 163 | this.state.domains.forEach( ( domain ) => { 164 | rows.push( 165 | 172 | ); 173 | } ); 174 | 175 | this.state.messages.forEach( ( message, index ) => { 176 | messages.push( 177 | 183 | ); 184 | } ); 185 | 186 | return ( 187 |
188 |

Domains

189 |
190 | { messages } 191 | 192 | 193 | 194 | 197 | 200 | 203 | 206 | 207 | 208 | { rows } 209 |
195 | { __( 'Domain', 'dark-matter' ) } 196 | 198 | { __( 'Is Primary?', 'dark-matter' ) } 199 | 201 | { __( 'Is Active?', 'dark-matter' ) } 202 | 204 | { __( 'Protocol', 'dark-matter' ) } 205 |
210 | 211 |
212 | ); 213 | } 214 | 215 | /** 216 | * Perform the update call to the REST API. 217 | * 218 | * @param {Object} data Data set containing updates to be sent to the REST API. 219 | */ 220 | async update( data ) { 221 | const result = await this.api.update( data ); 222 | 223 | this.addNotice( 224 | data.domain, 225 | result.code 226 | ? result.message 227 | : sprintf( 228 | /* translators: domain name */ 229 | __( 'Successfully updated %s.', 'dark-matter' ), 230 | data.domain 231 | ), 232 | result.code ? 'error' : 'success' 233 | ); 234 | 235 | this.getData(); 236 | } 237 | } 238 | 239 | export default DomainMapping; 240 | -------------------------------------------------------------------------------- /domain-mapping/api/class-darkmatter-primary.php: -------------------------------------------------------------------------------- 1 | dm_table = $wpdb->base_prefix . 'domain_mapping'; 51 | 52 | /** 53 | * Store a reference to $wpdb as it will be used a lot. 54 | */ 55 | $this->wpdb = $wpdb; 56 | } 57 | 58 | /** 59 | * Retrieve the Primary domain for a Site. 60 | * 61 | * @since 2.0.0 62 | * 63 | * @param integer $site_id Site ID to retrieve the primary domain for. 64 | * @return DM_Domain|boolean Returns the DM_Domain object on success. False otherwise. 65 | */ 66 | public function get( $site_id = 0 ) { 67 | $site_id = ( empty( $site_id ) ? get_current_blog_id() : $site_id ); 68 | 69 | /** 70 | * Attempt to retrieve the domain from cache. 71 | */ 72 | $cache_key = $site_id . '-primary'; 73 | $primary_domain = wp_cache_get( $cache_key, 'dark-matter' ); 74 | 75 | /** 76 | * If the Cache is unavailable, then attempt to load the domain from the 77 | * database and re-prime the cache. 78 | */ 79 | if ( ! $primary_domain ) { 80 | // phpcs:ignore 81 | $primary_domain = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT domain FROM {$this->dm_table} WHERE is_primary = 1 AND blog_id = %s", $site_id ) ); 82 | 83 | if ( empty( $primary_domain ) ) { 84 | /** 85 | * Set the cached value for Primary Domain to "none". This will 86 | * stop spurious database queries for some thing that has not 87 | * been setup up. 88 | */ 89 | wp_cache_set( $cache_key, 'none', 'dark-matter' ); 90 | 91 | /** 92 | * As the cache is modified, we update the `last_changed`. 93 | */ 94 | $this->update_last_changed(); 95 | 96 | return false; 97 | } 98 | } 99 | 100 | /** 101 | * Return false if the cache value is "none". 102 | */ 103 | if ( 'none' === $primary_domain ) { 104 | return false; 105 | } 106 | 107 | /** 108 | * Retrieve the entire Domain object. 109 | */ 110 | $db = DarkMatter_Domains::instance(); 111 | $_domain = $db->get( $primary_domain ); 112 | 113 | return $_domain; 114 | } 115 | 116 | /** 117 | * Retrieve all primary domains for the Network. 118 | * 119 | * @since 2.0.0 120 | * 121 | * @return array Array of DM_Domain objects of the Primary domains for each Site in the Network. 122 | */ 123 | public function get_all() { 124 | global $wpdb; 125 | 126 | // phpcs:ignore 127 | $_domains = $wpdb->get_col( "SELECT domain FROM {$this->dm_table} WHERE is_primary = 1 ORDER BY blog_id DESC, domain" ); 128 | 129 | if ( empty( $_domains ) ) { 130 | return array(); 131 | } 132 | 133 | $db = DarkMatter_Domains::instance(); 134 | 135 | /** 136 | * Retrieve the DM_Domain objects for each of the primary domains. 137 | */ 138 | $domains = array(); 139 | 140 | foreach ( $_domains as $_domain ) { 141 | $domains[] = $db->get( $_domain ); 142 | } 143 | 144 | return $domains; 145 | } 146 | 147 | /** 148 | * Helper function to the set the cache for the primary domain for a Site. 149 | * 150 | * @since 2.0.0 151 | * 152 | * @param integer $site_id Site ID to set the primary domain cache for. 153 | * @param string $domain Domain to be stored in the cache. 154 | * @return boolean True on success, false otherwise. 155 | */ 156 | public function set( $site_id = 0, $domain = '' ) { 157 | $new_primary_domain = DarkMatter_Domains::instance()->get( $domain ); 158 | 159 | if ( $new_primary_domain->blog_id !== $site_id ) { 160 | return false; 161 | } 162 | 163 | $result = DarkMatter_Domains::instance()->update( 164 | $new_primary_domain->domain, 165 | true, 166 | $new_primary_domain->is_https, 167 | true, 168 | $new_primary_domain->active, 169 | DM_DOMAIN_TYPE_MAIN 170 | ); 171 | 172 | if ( is_wp_error( $result ) ) { 173 | return false; 174 | } 175 | 176 | return true; 177 | } 178 | 179 | /** 180 | * Unset the primary domain for a given Site. By default, will change all 181 | * records with is_primary set to true. 182 | * 183 | * @since 2.0.0 184 | * 185 | * @param integer $site_id Site ID to unset the primary domain for. 186 | * @param string $domain Optional. If provided, will only affect that domain's record. 187 | * @param boolean $db Set to true to perform a database update. 188 | * @return boolean True on success. False otherwise. 189 | */ 190 | public function unset( $site_id = 0, $domain = '', $db = false ) { 191 | $new_primary_domain = DarkMatter_Domains::instance()->get( $domain ); 192 | 193 | if ( $new_primary_domain->blog_id !== $site_id ) { 194 | return false; 195 | } 196 | 197 | $result = DarkMatter_Domains::instance()->update( 198 | $new_primary_domain->domain, 199 | false, 200 | $new_primary_domain->is_https, 201 | true, 202 | $new_primary_domain->active, 203 | DM_DOMAIN_TYPE_MAIN 204 | ); 205 | 206 | if ( is_wp_error( $result ) ) { 207 | return false; 208 | } 209 | 210 | return true; 211 | } 212 | 213 | /** 214 | * Change the last changed cache note. 215 | * 216 | * @since 2.1.8 217 | * 218 | * @return void 219 | */ 220 | private function update_last_changed() { 221 | wp_cache_set( 'last_changed', microtime(), 'dark-matter' ); 222 | } 223 | 224 | /** 225 | * Return the Singleton Instance of the class. 226 | * 227 | * @since 2.0.0 228 | * 229 | * @return DarkMatter_Primary 230 | */ 231 | public static function instance() { 232 | static $instance = false; 233 | 234 | if ( ! $instance ) { 235 | $instance = new self(); 236 | } 237 | 238 | return $instance; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /scripts/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | WP_TESTS_DOMAIN="darkmatter.test" 15 | 16 | TMPDIR=${TMPDIR-/tmp} 17 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 18 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 19 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} 20 | 21 | download() { 22 | if [ `which curl` ]; then 23 | curl -s "$1" > "$2"; 24 | elif [ `which wget` ]; then 25 | wget -nv -O "$2" "$1" 26 | fi 27 | } 28 | 29 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 30 | WP_BRANCH=${WP_VERSION%\-*} 31 | WP_TESTS_TAG="branches/$WP_BRANCH" 32 | 33 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 34 | WP_TESTS_TAG="branches/$WP_VERSION" 35 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 36 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 37 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 38 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 39 | else 40 | WP_TESTS_TAG="tags/$WP_VERSION" 41 | fi 42 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 43 | WP_TESTS_TAG="trunk" 44 | else 45 | # http serves a single offer, whereas https serves multiple. we only want one 46 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 47 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 48 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 49 | if [[ -z "$LATEST_VERSION" ]]; then 50 | echo "Latest WordPress version could not be found" 51 | exit 1 52 | fi 53 | WP_TESTS_TAG="tags/$LATEST_VERSION" 54 | fi 55 | set -ex 56 | 57 | install_wp() { 58 | 59 | if [ -d $WP_CORE_DIR ]; then 60 | return; 61 | fi 62 | 63 | mkdir -p $WP_CORE_DIR 64 | 65 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 66 | mkdir -p $TMPDIR/wordpress-trunk 67 | rm -rf $TMPDIR/wordpress-trunk/* 68 | svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress 69 | mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR 70 | else 71 | if [ $WP_VERSION == 'latest' ]; then 72 | local ARCHIVE_NAME='latest' 73 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 74 | # https serves multiple offers, whereas http serves single. 75 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 76 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 77 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 78 | LATEST_VERSION=${WP_VERSION%??} 79 | else 80 | # otherwise, scan the releases and get the most up to date minor version of the major release 81 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 82 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 83 | fi 84 | if [[ -z "$LATEST_VERSION" ]]; then 85 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 86 | else 87 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 88 | fi 89 | else 90 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 91 | fi 92 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 93 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 94 | fi 95 | 96 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 97 | } 98 | 99 | install_test_suite() { 100 | # portable in-place argument for both GNU sed and Mac OSX sed 101 | if [[ $(uname -s) == 'Darwin' ]]; then 102 | local ioption='-i.bak' 103 | else 104 | local ioption='-i' 105 | fi 106 | 107 | # set up testing suite if it doesn't yet exist 108 | if [ ! -d $WP_TESTS_DIR ]; then 109 | # set up testing suite 110 | mkdir -p $WP_TESTS_DIR 111 | rm -rf $WP_TESTS_DIR/{includes,data} 112 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 113 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 114 | fi 115 | 116 | if [ ! -f wp-tests-config.php ]; then 117 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 118 | # remove all forward slashes in the end 119 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 120 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 123 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 124 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 125 | sed $ioption "s/example.org/$WP_TESTS_DOMAIN/" "$WP_TESTS_DIR"/wp-tests-config.php 126 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 127 | fi 128 | 129 | } 130 | 131 | recreate_db() { 132 | shopt -s nocasematch 133 | if [[ $1 =~ ^(y|yes)$ ]] 134 | then 135 | mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA 136 | create_db 137 | echo "Recreated the database ($DB_NAME)." 138 | else 139 | echo "Leaving the existing database ($DB_NAME) in place." 140 | fi 141 | shopt -u nocasematch 142 | } 143 | 144 | create_db() { 145 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 146 | } 147 | 148 | install_db() { 149 | 150 | if [ ${SKIP_DB_CREATE} = "true" ]; then 151 | return 0 152 | fi 153 | 154 | # parse DB_HOST for port or socket references 155 | local PARTS=(${DB_HOST//\:/ }) 156 | local DB_HOSTNAME=${PARTS[0]}; 157 | local DB_SOCK_OR_PORT=${PARTS[1]}; 158 | local EXTRA="" 159 | 160 | if ! [ -z $DB_HOSTNAME ] ; then 161 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 162 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 163 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 164 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 165 | elif ! [ -z $DB_HOSTNAME ] ; then 166 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 167 | fi 168 | fi 169 | 170 | # create database 171 | if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] 172 | then 173 | echo "Reinstalling will delete the existing test database ($DB_NAME)" 174 | read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB 175 | recreate_db $DELETE_EXISTING_DB 176 | else 177 | create_db 178 | fi 179 | } 180 | 181 | install_wp 182 | install_test_suite 183 | install_db 184 | -------------------------------------------------------------------------------- /tests/phpunit/domain-mapping/DomainsTest.php: -------------------------------------------------------------------------------- 1 | blog_id = $this->factory()->blog->create_object( 33 | [ 34 | 'domain' => 'darkmatter.test', 35 | 'path' => '/siteone', 36 | ] 37 | ); 38 | 39 | switch_to_blog( $this->blog_id ); 40 | } 41 | 42 | /** 43 | * Adding a new domain. 44 | * 45 | * @return void 46 | */ 47 | public function test_add_domain() { 48 | $domain = 'mappeddomain1.test'; 49 | DarkMatter_Domains::instance()->add( $domain ); 50 | 51 | global $wpdb; 52 | $data = $wpdb->get_row( 53 | $wpdb->prepare( 54 | "SELECT * FROM `{$wpdb->base_prefix}domain_mapping` WHERE domain = %s AND blog_id = %d LIMIT 0, 1", 55 | $domain, 56 | $this->blog_id 57 | ) 58 | ); 59 | 60 | $this->assertNotEmpty( $data ); 61 | } 62 | 63 | /** 64 | * Ensure that the same domain cannot be re-added. 65 | * 66 | * @return void 67 | */ 68 | public function test_add_add_again() { 69 | $domain = 'mappeddomain1.test'; 70 | 71 | /** 72 | * Add the domain. 73 | */ 74 | DarkMatter_Domains::instance()->add( $domain ); 75 | 76 | /** 77 | * Attempt to add the domain again. 78 | */ 79 | $error = DarkMatter_Domains::instance()->add( $domain ); 80 | 81 | $this->assertWPError( $error ); 82 | $this->assertSame( $error->get_error_code(), 'exists' ); 83 | } 84 | 85 | /** 86 | * Test removing a domain. 87 | * 88 | * @return void 89 | */ 90 | public function test_delete_domain() { 91 | $domain = 'mappeddomain1.test'; 92 | 93 | /** 94 | * Add the domain. 95 | */ 96 | DarkMatter_Domains::instance()->add( $domain ); 97 | 98 | /** 99 | * Attempt to add the domain again. 100 | */ 101 | $return = DarkMatter_Domains::instance()->delete( $domain ); 102 | 103 | $this->assertTrue( $return ); 104 | 105 | global $wpdb; 106 | $data = $wpdb->get_row( 107 | $wpdb->prepare( 108 | "SELECT * FROM `{$wpdb->base_prefix}domain_mapping` WHERE domain = %s AND blog_id = %d LIMIT 0, 1", 109 | $domain, 110 | $this->blog_id 111 | ) 112 | ); 113 | 114 | $this->assertEmpty( $data ); 115 | } 116 | 117 | /** 118 | * Test finding an existing domain. 119 | * 120 | * @return void 121 | */ 122 | public function test_find_domain() { 123 | $domain = 'mappeddomain1.test'; 124 | 125 | /** 126 | * Add the domain. 127 | */ 128 | DarkMatter_Domains::instance()->add( $domain ); 129 | 130 | /** 131 | * Attempt to add the domain again. 132 | */ 133 | $return = DarkMatter_Domains::instance()->find( $domain ); 134 | 135 | $this->assertNotFalse( $return ); 136 | $this->assertEquals( $return->domain, $domain ); 137 | } 138 | 139 | /** 140 | * Test updating an existing domain. 141 | * 142 | * @return void 143 | */ 144 | public function test_update_domain() { 145 | $domain = 'mappeddomain1.test'; 146 | 147 | /** 148 | * Add the domain. 149 | */ 150 | DarkMatter_Domains::instance()->add( $domain ); 151 | 152 | /** 153 | * Make sure the update did not return a WP_Error. 154 | */ 155 | $return = DarkMatter_Domains::instance()->update( $domain, true, true ); 156 | $this->assertNotWPError( $return ); 157 | 158 | /** 159 | * Assert the update did actually work. 160 | */ 161 | global $wpdb; 162 | $data = $wpdb->get_row( 163 | $wpdb->prepare( 164 | "SELECT * FROM `{$wpdb->base_prefix}domain_mapping` WHERE domain = %s AND blog_id = %d LIMIT 0, 1", 165 | $domain, 166 | $this->blog_id 167 | ) 168 | ); 169 | 170 | $this->assertEquals( '1', $data->is_primary ); 171 | $this->assertEquals( '1', $data->is_https ); 172 | } 173 | 174 | /** 175 | * Test international domains, such as Chinese, which have a system for handling non-ASCII characters. 176 | * 177 | * @return void 178 | */ 179 | public function test_validation_international_domains() { 180 | /** Chinese - Unicode - Invalid */ 181 | $return = DarkMatter_Domains::instance()->add( 'www.例如.中国' ); 182 | $this->assertWPError( $return, 'Chinese - Unicode - Invalid' ); 183 | $this->assertSame( $return->get_error_code(), 'domain' ); 184 | 185 | /** Chinese - ASCII - Valid */ 186 | $return = DarkMatter_Domains::instance()->add( 'www.xn--fsqu6v.xn--fiqs8s' ); 187 | $this->assertNotWPError( $return, 'Chinese - ASCII - Valid' ); 188 | } 189 | 190 | /** 191 | * Invalid domains and input that should not be allowed. 192 | * 193 | * @return void 194 | */ 195 | public function test_validation_invalid_domains() { 196 | /** Empty */ 197 | $return = DarkMatter_Domains::instance()->add( '' ); 198 | $this->assertWPError( $return ); 199 | $this->assertSame( $return->get_error_code(), 'empty' ); 200 | 201 | /** URI */ 202 | $return = DarkMatter_Domains::instance()->add( 'http://example.com/' ); 203 | $this->assertWPError( $return ); 204 | $this->assertSame( $return->get_error_code(), 'unsure' ); 205 | 206 | /** Domain + Path */ 207 | $return = DarkMatter_Domains::instance()->add( 'example.com/hello-world' ); 208 | $this->assertWPError( $return ); 209 | $this->assertSame( $return->get_error_code(), 'unsure' ); 210 | 211 | /** Domain + Port */ 212 | $return = DarkMatter_Domains::instance()->add( 'example.com:443' ); 213 | $this->assertWPError( $return ); 214 | $this->assertSame( $return->get_error_code(), 'unsure' ); 215 | 216 | /** DOMAIN_CURRENT_SITE */ 217 | $return = DarkMatter_Domains::instance()->add( 'darkmatter.test' ); 218 | $this->assertWPError( $return ); 219 | $this->assertSame( $return->get_error_code(), 'wp-config' ); 220 | 221 | /** Non-ASCII - i.e. emojis, etc. */ 222 | $return = DarkMatter_Domains::instance()->add( '🐲' ); 223 | $this->assertWPError( $return ); 224 | $this->assertSame( $return->get_error_code(), 'domain' ); 225 | 226 | /** Stored XSS (and curious input from an administrator one time ... ... ...) */ 227 | $return = DarkMatter_Domains::instance()->add( '' ); 228 | $this->assertWPError( $return ); 229 | $this->assertSame( $return->get_error_code(), 'unsure' ); 230 | } 231 | 232 | /** 233 | * Test valid domains. 234 | * 235 | * @return void 236 | */ 237 | public function test_validation_valid_domains() { 238 | /** 239 | * Valid domains 240 | * ============= 241 | */ 242 | 243 | /** Localhost */ 244 | $return = DarkMatter_Domains::instance()->add( 'localhost' ); 245 | $this->assertNotWPError( $return ); 246 | 247 | /** Example domain */ 248 | $return = DarkMatter_Domains::instance()->add( 'example.com' ); 249 | $this->assertNotWPError( $return ); 250 | 251 | /** Example sub-domain */ 252 | $return = DarkMatter_Domains::instance()->add( 'www.example.com' ); 253 | $this->assertNotWPError( $return ); 254 | 255 | /** Atypical test domain. */ 256 | $return = DarkMatter_Domains::instance()->add( 'development.test' ); 257 | $this->assertNotWPError( $return ); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /domain-mapping/api/class-darkmatter-restrict.php: -------------------------------------------------------------------------------- 1 | restrict_table = $wpdb->base_prefix . 'domain_restrict'; 34 | } 35 | 36 | /** 37 | * Perform basic checks before committing to a action performed by a method. 38 | * 39 | * @since 2.0.0 40 | * 41 | * @param string $fqdn Fully qualified domain name. 42 | * @return WP_Error|boolean True on pass. WP_Error on failure. 43 | */ 44 | private function _basic_checks( $fqdn ) { 45 | if ( empty( $fqdn ) ) { 46 | return new WP_Error( 'empty', __( 'Please include a fully qualified domain name to be added.', 'dark-matter' ) ); 47 | } 48 | 49 | /** 50 | * Ensure that the URL is purely a domain. In order for the parse_url() to work, the domain must be prefixed 51 | * with a double forward slash. 52 | */ 53 | if ( false === stripos( $fqdn, '//' ) ) { 54 | // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url 55 | $domain_parts = parse_url( '//' . ltrim( $fqdn, '/' ) ); 56 | } else { 57 | // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url 58 | $domain_parts = parse_url( $fqdn ); 59 | } 60 | 61 | if ( ! empty( $domain_parts['path'] ) || ! empty( $domain_parts['port'] ) || ! empty( $domain_parts['query'] ) ) { 62 | return new WP_Error( 'unsure', __( 'The domain provided contains path, port, or query string information. Please removed this before continuing.', 'dark-matter' ) ); 63 | } 64 | 65 | $fqdn = $domain_parts['host']; 66 | 67 | if ( defined( 'DOMAIN_CURRENT_SITE' ) && DOMAIN_CURRENT_SITE === $fqdn ) { 68 | return new WP_Error( 'wp-config', __( 'You cannot configure the WordPress Network primary domain.', 'dark-matter' ) ); 69 | } 70 | 71 | $domains = DarkMatter_Domains::instance(); 72 | if ( $domains->is_exist( $fqdn ) ) { 73 | return new WP_Error( 'used', __( 'This domain is in use.', 'dark-matter' ) ); 74 | } 75 | 76 | return $fqdn; 77 | } 78 | 79 | /** 80 | * Add a domain to the Restrict list. 81 | * 82 | * @since 2.0.0 83 | * 84 | * @param string $fqdn Domain to be added to the reserve list. 85 | * @return WP_Error|boolean True on success, WP_Error otherwise. 86 | */ 87 | public function add( $fqdn = '' ) { 88 | $fqdn = $this->_basic_checks( $fqdn ); 89 | 90 | if ( is_wp_error( $fqdn ) ) { 91 | return $fqdn; 92 | } 93 | 94 | if ( $this->is_exist( $fqdn ) ) { 95 | return new WP_Error( 'exists', __( 'The Domain is already Restricted.', 'dark-matter' ) ); 96 | } 97 | 98 | /** 99 | * Add the domain to the database. 100 | */ 101 | global $wpdb; 102 | 103 | // phpcs:ignore 104 | $result = $wpdb->insert( 105 | $this->restrict_table, 106 | array( 107 | 'domain' => $fqdn, 108 | ), 109 | array( '%s' ) 110 | ); 111 | 112 | if ( ! $result ) { 113 | return new WP_Error( 'unknown', __( 'An unknown error has occurred. The domain has not been removed from the Restrict list.', 'dark-matter' ) ); 114 | } 115 | 116 | $this->refresh_cache(); 117 | 118 | /** 119 | * Fires when a domain is added to the restricted list. 120 | * 121 | * @since 2.0.0 122 | * 123 | * @param string $fqdn Domain name that was restricted. 124 | */ 125 | do_action( 'darkmatter_restrict_add', $fqdn ); 126 | 127 | return true; 128 | } 129 | 130 | /** 131 | * Delete a domain to the Restrict list. 132 | * 133 | * @since 2.0.0 134 | * 135 | * @param string $fqdn Domain to be deleted to the restrict list. 136 | * @return WP_Error|boolean True on success, WP_Error otherwise. 137 | */ 138 | public function delete( $fqdn = '' ) { 139 | $fqdn = $this->_basic_checks( $fqdn ); 140 | 141 | if ( is_wp_error( $fqdn ) ) { 142 | return $fqdn; 143 | } 144 | 145 | if ( ! $this->is_exist( $fqdn ) ) { 146 | return new WP_Error( 'missing', __( 'The Domain is not found in the Restrict list.', 'dark-matter' ) ); 147 | } 148 | 149 | /** 150 | * Remove the domain to the database. 151 | */ 152 | global $wpdb; 153 | 154 | // phpcs:ignore 155 | $result = $wpdb->delete( 156 | $this->restrict_table, 157 | array( 158 | 'domain' => $fqdn, 159 | ), 160 | array( '%s' ) 161 | ); 162 | 163 | if ( ! $result ) { 164 | return new WP_Error( 'unknown', __( 'An unknown error has occurred. The domain has not been removed from the Restrict list.', 'dark-matter' ) ); 165 | } 166 | 167 | $this->refresh_cache(); 168 | 169 | /** 170 | * Fires when a domain is deleted from the restricted list. 171 | * 172 | * @since 2.0.0 173 | * 174 | * @param string $fqdn Domain name that was restricted. 175 | */ 176 | do_action( 'darkmatter_restrict_delete', $fqdn ); 177 | 178 | return true; 179 | } 180 | 181 | /** 182 | * Retrieve all restrict domains. 183 | * 184 | * @since 2.0.0 185 | * 186 | * @return array List of restrict domains. 187 | */ 188 | public function get() { 189 | /** 190 | * Attempt to retreive the domain from cache. 191 | */ 192 | $restrict_domains = wp_cache_get( 'restricted', 'dark-matter' ); 193 | 194 | /** 195 | * Fires after the domains have been retrieved from cache (if available) 196 | * and before the database is used to retrieve the Restricted domains. 197 | * 198 | * @since 2.0.0 199 | * 200 | * @param array $restricted_domains Restricted domains retrieved from Object Cache. 201 | */ 202 | $restrict_domains = apply_filters( 'darkmatter_restricted_get', $restrict_domains ); 203 | 204 | if ( $restrict_domains ) { 205 | return $restrict_domains; 206 | } 207 | 208 | /** 209 | * Then attempt to retrieve the domains from the database, assuming 210 | * there is any. 211 | */ 212 | global $wpdb; 213 | 214 | // phpcs:ignore 215 | $restricted_domains = $wpdb->get_col( "SELECT domain FROM {$this->restrict_table} ORDER BY domain" ); 216 | 217 | if ( empty( $restricted_domains ) ) { 218 | $restricted_domains = array(); 219 | } 220 | 221 | /** 222 | * May seem peculiar to cache an empty array here but as this will 223 | * likely be a slow changing data set, then it's pointless to keep 224 | * pounding the database unnecessarily. 225 | */ 226 | wp_cache_add( 'restricted', $restricted_domains, 'dark-matter' ); 227 | 228 | return $restricted_domains; 229 | } 230 | 231 | /** 232 | * Check if a domain has been restricted. 233 | * 234 | * @since 2.0.0 235 | * 236 | * @param string $fqdn Domain to check. 237 | * @return boolean True if found. False otherwise. 238 | */ 239 | public function is_exist( $fqdn = '' ) { 240 | if ( empty( $fqdn ) ) { 241 | return false; 242 | } 243 | 244 | $restricted_domains = $this->get(); 245 | 246 | return in_array( $fqdn, $restricted_domains ); 247 | } 248 | 249 | /** 250 | * Helper method to refresh the cache for Restricted domains. 251 | * 252 | * @since 2.0.0 253 | * 254 | * @return void 255 | */ 256 | public function refresh_cache() { 257 | wp_cache_delete( 'restricted', 'dark-matter' ); 258 | $this->get(); 259 | } 260 | 261 | /** 262 | * Return the Singleton Instance of the class. 263 | * 264 | * @since 2.0.0 265 | * 266 | * @return DarkMatter_Restrict 267 | */ 268 | public static function instance() { 269 | static $instance = false; 270 | 271 | if ( ! $instance ) { 272 | $instance = new self(); 273 | } 274 | 275 | return $instance; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Dark Matter 2 | 3 | Dark Matter is a highly opinionated domain mapping plugin for WordPress 4 | Networks, designed to work out of the box as-is with no setup. Unlike other 5 | plugins such as Donncha's "WordPress MU Domain Mapping" and WPMU Dev's premium 6 | domain mapping plugin, Dark Matter offers virtually no options beyond mapping 7 | individual domains. 8 | 9 | ## Constants 10 | 11 | ### Set Media Domains 12 | 13 | Media Domains can be setup through the CLI commands on a per site basis, or setup for all sites on a Multisite using a 14 | constant. The constant can support one or more domains. 15 | 16 | ```php 17 | define( 'DM_NETWORK_MEDIA', [ 18 | 'media1.example.com', 19 | /** 'media2.example.com', */ 20 | /** 'media3.example.com', */ 21 | ] ); 22 | ``` 23 | 24 | ## CLI Commands 25 | 26 | ### Add / Update / Remove Domains 27 | 28 | Examples of adding, removing and updating a domain for a Site. 29 | 30 | ``` 31 | wp --url="sites.my.com/siteone" darkmatter domain add www.example.com --primary --https 32 | wp --url="sites.my.com/siteone" darkmatter domain remove www.example.com 33 | wp --url="sites.my.com/siteone" darkmatter domain remove www.example.com --force 34 | wp --url="sites.my.com/siteone" darkmatter domain set www.example.com --primary 35 | wp --url="sites.my.com/siteone" darkmatter domain set www.example.com --secondary 36 | ``` 37 | 38 | ### Add / Update / Remove Media Domains 39 | 40 | Set a media domain for a site. 41 | ``` 42 | wp --url="sites.my.com/sitefifteen" darkmatter domain add example.mycdn.com --type=media 43 | ``` 44 | 45 | Convert a secondary domain into a media domain. Useful for when repurposing an old domain for use a CDN for media assets. 46 | ``` 47 | wp --url="sites.my.com/siteone" darkmatter domain set example.mycdn.com --type=media 48 | ``` 49 | 50 | Convert a Media domain to a main domain. This is useful in scenarios when a media domain is redundant and to ensure it redirects to the website. 51 | ``` 52 | wp --url="sites.my.com/siteone" darkmatter domain set secondarydomain.com --type=main --secondary 53 | ``` 54 | 55 | ### Listing Domains 56 | 57 | Examples of listing domains for a Site. 58 | 59 | ``` 60 | wp --url="sites.my.com/siteone" darkmatter domain list 61 | wp --url="sites.my.com/siteone" darkmatter domain list --format=json 62 | ``` 63 | 64 | Examples of listing domains for the entire Network. 65 | 66 | ``` 67 | wp darkmatter domain list 68 | wp darkmatter domain list --format=csv 69 | ``` 70 | 71 | Retrieve all the primary domains for the Network. 72 | 73 | ``` 74 | wp darkmatter domain list --primary 75 | ``` 76 | 77 | ### Reserving Domains 78 | 79 | Reserving a domain. This allows an administrator to setup the primary and / or secondary domains but stop Dark Matter performing redirects and rewrites. Please note; domains are enabled by default. 80 | 81 | ``` 82 | wp --url="sites.my.com/siteone" darkmatter domain add www.example.com --primary --https --disable 83 | wp --url="sites.my.com/siteone" darkmatter domain set www.example.com --enable 84 | wp --url="sites.my.com/siteone" darkmatter domain set www.example.com --disable 85 | ``` 86 | 87 | ### Restricting Domains 88 | 89 | Examples of adding and removing a restricted domains for a Network. This permits administrators to stop domains from being used for a WordPress website; useful for organisations which use multiple CMS. 90 | 91 | ``` 92 | wp darkmatter restrict add www.example.com 93 | wp darkmatter restrict remove www.example.com 94 | ``` 95 | 96 | Examples of retrieving a list of all restricted domains for a Network. 97 | 98 | ``` 99 | wp darkmatter restrict list 100 | wp darkmatter restrict list --format=json 101 | wp darkmatter restrict list --format=ids 102 | ``` 103 | 104 | ## Reporting problems 105 | 106 | You can use the Issues system here on Github to report problems with the Dark 107 | Matter plugin. To aid and speed-up diagnosing the problem, you are best to 108 | include as much as the following as you possibly can; 109 | 110 | * Check here to ensure the problem has not been reported by someone else. 111 | * WordPress; 112 | * Version of WordPress itself. 113 | * List of active plugins (installed but unused _should_ rarely cause problems) 114 | * Hosting information; 115 | * Either 116 | * Apache / Nginx 117 | * Operating System (Linux or Windows) 118 | * Or; 119 | * Host provider (Digital Ocean, Dreamhosts, GoDaddy, WP Engine, etc) 120 | * Browser (Chrome, IE, Firefox, Opera, etc with version) 121 | * Any additional information such as; 122 | * Using Cloudflare. 123 | 124 | All issues reported are taken seriously and are checked, but please bear in mind 125 | that responses are not always instant. 126 | 127 | ### Syntax 128 | 129 | Dark Matter does not have a coding style guide but there are several rules which 130 | should be observed; 131 | 132 | * Unix line breaks. 133 | * Tabs, not spaces. 134 | * [Yoda conditions](https://en.wikipedia.org/wiki/Yoda_conditions). 135 | * Code should be concise rather than terse. 136 | * Comments should not extend beyond the 80th character (default in Atom) unless; 137 | * Text for a bullet point. 138 | * Code example in comments. 139 | * PHPDoc conventions for @@link or @@param in describing a class or function. 140 | 141 | ### URLs to check 142 | 143 | The following is a list of example URLs which are worth checking (depending on 144 | the change) when developing with Dark Matter. 145 | 146 | Using www.wpnetwork.com as the "Admin domain" and www.example.com as the 147 | "Primary domain", Dark Matter should be tested with the following URLs; 148 | 149 | * http://www.wpnetwork.com/sitetwo/ (with trailing forward slash) => http://www.example.com/ 150 | * http://www.wpnetwork.com/sitetwo (without trailing forward slash) => http://www.example.com/ 151 | * http://www.wpnetwork.com/sitetwo/index.php (query string processing, without trailing forward slash) => http://www.example.com/ 152 | * http://www.wpnetwork.com/sitetwo/index.php/ (query string processing, with trailing forward slash) => http://www.example.com/ 153 | * http://www.wpnetwork.com/sitetwo/?utm_source=test (with query string) => http://www.example.com/?utm_source=test 154 | * http://www.wpnetwork.com/sitetwo?utm_source=test (with query string, without trailing forward slash) => http://www.example.com/?utm_source=test 155 | * http://www.wpnetwork.com/sitetwo/#test (hash URL test) => http://www.example.com/ 156 | * http://www.wpnetwork.com/sitetwo#test (hash URL test, without trailing forward slash) => http://www.example.com/ 157 | 158 | ## Development 159 | 160 | ### Prerequisites 161 | 162 | To develop with Dark Matter plugin requires the following to be installed. 163 | 164 | * Composer 2.0+ 165 | * Node 16.13+ 166 | * PHP 7.0+ 167 | * WordPress 5.9+ 168 | 169 | For a developer environment, Dark Matter plugin is most commonly built with VVV. However this is not a hard requirement 170 | and any WordPress development environment should work without any complications. 171 | 172 | ### Initial Setup 173 | 174 | If you have NVM installed, then you can ensure the most recent compatible version of Node is installed and ready for 175 | use. 176 | 177 | ```shell 178 | nvm use 179 | ``` 180 | 181 | And then you can run the following: 182 | 183 | ```shell 184 | npm run start 185 | ``` 186 | 187 | This NPM script will do the following: 188 | 189 | * Install all Composer dependencies, including dev dependencies. 190 | * Install all NPM dependencies, including dev dependencies. 191 | * Build both the production and developer scripts. 192 | 193 | After this point, the plugin is ready for use with your local WordPress installation. 194 | 195 | ### Unit Tests 196 | 197 | Use the following command to setup PHPUnit and the WordPress environment for it. 198 | 199 | ```shell 200 | composer run test-setup 201 | ``` 202 | 203 | After the setup has completed successfully, then the following command can be used to run the unit tests. 204 | 205 | ```shell 206 | composer run test 207 | ``` -------------------------------------------------------------------------------- /domain-mapping/classes/class-dm-healthchecks.php: -------------------------------------------------------------------------------- 1 | __( 'Dark Matter - Domain Mapping - Checking Sunrise dropin', 'dark-matter' ), 37 | 'test' => [ $this, 'test_dropin' ], 38 | ]; 39 | 40 | $tests['direct']['darkmatter_domain_mapping_ssl'] = [ 41 | 'label' => __( 'Dark Matter - Domain Mapping - Checking SSL configuration', 'dark-matter' ), 42 | 'test' => [ $this, 'test_ssl' ], 43 | ]; 44 | 45 | if ( ! is_main_site() ) { 46 | $tests['direct']['darkmatter_domain_mapping_primary_domain_set'] = [ 47 | 'label' => __( 'Dark Matter - Domain Mapping - Checking for primary domain', 'dark-matter' ), 48 | 'test' => [ $this, 'test_primary_domain_set' ], 49 | ]; 50 | } 51 | 52 | $tests['direct']['darkmatter_domain_mapping_cookie_domain'] = [ 53 | 'label' => __( 'Dark Matter - Domain Mapping - Checking for cookie domain settings', 'dark-matter' ), 54 | 'test' => [ $this, 'test_cookie_domain' ], 55 | ]; 56 | 57 | return $tests; 58 | } 59 | 60 | /** 61 | * Ensures that the COOKIE_DOMAIN constant is set by Dark Matter and not set elsewhere (such as wp-config.php). 62 | * 63 | * @since 2.1.0 64 | * 65 | * @return bool True if COOKIE_DOMAIN is set by Dark Matter. False otherwise. 66 | */ 67 | public function cookie_domain_dm_set() { 68 | return ( defined( 'DARKMATTER_COOKIE_SET' ) && DARKMATTER_COOKIE_SET ); 69 | } 70 | 71 | /** 72 | * Checks to ensure the dropin - sunrise.php - exists. 73 | * 74 | * @since 2.1.0 75 | * 76 | * @return bool True if sunrise.php exists. False otherwise. 77 | */ 78 | public function dropin_exists() { 79 | return file_exists( DM_PATH . '/domain-mapping/sunrise.php' ); 80 | } 81 | 82 | /** 83 | * Checks to ensure the constant `FORCE_SSL_ADMIN` is configured correctly for Dark Matter. 84 | * 85 | * @since 2.1.0 86 | * 87 | * @return bool True if `FORCE_SSL_ADMIN` is present and set. False otherwise. 88 | */ 89 | public function force_ssl_set() { 90 | return defined( 'FORCE_SSL_ADMIN' ) && FORCE_SSL_ADMIN; 91 | } 92 | 93 | /** 94 | * Checks the dropin - sunrise.php - to see if it is the correct version. 95 | * 96 | * @since 2.1.0 97 | * 98 | * @return bool True if the dropin is the correct version. False otherwise. 99 | */ 100 | public function is_dropin_latest() { 101 | $destination = WP_CONTENT_DIR . '/sunrise.php'; 102 | $source = DM_PATH . '/domain-mapping/sunrise.php'; 103 | 104 | return filesize( $destination ) === filesize( $source ) && md5_file( $destination ) === md5_file( $source ); 105 | } 106 | 107 | /** 108 | * Checks the COOKIE_DOMAIN constant to ensure it is compatible with Dark Matter. 109 | * 110 | * @since 2.1.0 111 | * 112 | * @return array Test result. 113 | */ 114 | public function test_cookie_domain() { 115 | $result = [ 116 | 'label' => __( 'Dark Matter single-sign on (bringing the admin bar to the public-facing side) is enabled.', 'dark-matter' ), 117 | 'status' => 'good', 118 | 'badge' => array( 119 | 'label' => __( 'Domain Mapping', 'dark-matter' ), 120 | 'color' => 'green', 121 | ), 122 | 'description' => sprintf( 123 | '

%s

', 124 | __( 'Dark Matter single-sign on is enabled and can load the admin bar when WordPress users are visiting the public-facing side of your site.', 'dark-matter' ) 125 | ), 126 | 'actions' => '', 127 | 'test' => 'darkmatter_domain_mapping_cookie_domain', 128 | ]; 129 | 130 | if ( ! $this->cookie_domain_dm_set() ) { 131 | $result['label'] = __( 'The cookie domain constant has been set and Dark Matter SSO has been disabled.', 'dark-matter' ); 132 | $result['badge']['color'] = 'red'; 133 | $result['status'] = 'critical'; 134 | $result['description'] = sprintf( 135 | '

%s

', 136 | sprintf( 137 | /* translators: COOKIE_DOMAIN constant */ 138 | __( 'The %1$s constant has been set, likely within your wp-config.php file. Dark Matter single-sign on (SSO) - which uses %1$s - has been disabled to prevent errors.', 'dark-matter' ), 139 | 'COOKIE_DOMAIN' 140 | ) 141 | ); 142 | } 143 | 144 | return $result; 145 | } 146 | 147 | /** 148 | * Checks the Sunrise dropin to ensure it is configured correctly and is up-to-date. 149 | * 150 | * @since 2.1.0 151 | * 152 | * @return array Test result. 153 | */ 154 | public function test_dropin() { 155 | $result = [ 156 | 'label' => __( 'Sunrise dropin is enabled and up-to-date.', 'dark-matter' ), 157 | 'status' => 'good', 158 | 'badge' => array( 159 | 'label' => __( 'Domain Mapping', 'dark-matter' ), 160 | 'color' => 'green', 161 | ), 162 | 'description' => sprintf( 163 | '

%s

', 164 | __( 'Sunrise is the name of the dropin file which maps custom domains to your WordPress sites.', 'dark-matter' ) 165 | ), 166 | 'actions' => '', 167 | 'test' => 'darkmatter_domain_mapping_dropin', 168 | ]; 169 | 170 | if ( ! $this->dropin_exists() ) { 171 | $result['label'] = __( 'Sunrise dropin cannot be found.', 'dark-matter' ); 172 | $result['badge']['color'] = 'red'; 173 | $result['status'] = 'critical'; 174 | $result['description'] = sprintf( 175 | '

%s

', 176 | __( 'Contact your system administrator to add sunrise.php to your wp-content/ folder.', 'dark-matter' ) 177 | ); 178 | 179 | return $result; 180 | } 181 | 182 | if ( ! defined( 'SUNRISE' ) ) { 183 | $result['label'] = __( 'SUNRISE constant is not setup.', 'dark-matter' ); 184 | $result['badge']['color'] = 'red'; 185 | $result['status'] = 'critical'; 186 | $result['description'] = sprintf( 187 | '

%s

', 188 | sprintf( 189 | /* translators: SUNRISE constant */ 190 | __( 'Please ensure the %1$s constant is present and set to "true" in your wp-config.php file.', 'dark-matter' ), 191 | 'SUNRISE' 192 | ) 193 | ); 194 | 195 | return $result; 196 | } 197 | 198 | if ( ! $this->is_dropin_latest() ) { 199 | $result['label'] = __( 'Your Sunrise dropin does not match the Dark Matter version.', 'dark-matter' ); 200 | $result['badge']['color'] = 'orange'; 201 | $result['status'] = 'recommended'; 202 | $result['description'] = sprintf( 203 | '

%s

', 204 | __( 'Sunrise dropin is different from the version recommended by Dark Matter. Please update sunrise.php to the version found in Dark Matter plugin folder.', 'dark-matter' ) 205 | ); 206 | } 207 | 208 | return $result; 209 | } 210 | 211 | /** 212 | * Checks the Sunrise dropin to ensure it is configured correctly and is up-to-date. 213 | * 214 | * @since 2.1.0 215 | * 216 | * @return array Test result. 217 | */ 218 | public function test_primary_domain_set() { 219 | $result = [ 220 | 'label' => __( 'You have a primary domain.', 'dark-matter' ), 221 | 'status' => 'good', 222 | 'badge' => array( 223 | 'label' => __( 'Domain Mapping', 'dark-matter' ), 224 | 'color' => 'green', 225 | ), 226 | 'description' => '', 227 | 'actions' => '', 228 | 'test' => 'darkmatter_domain_mapping_primary_domain_set', 229 | ]; 230 | 231 | $primary = DarkMatter_Primary::instance()->get(); 232 | 233 | if ( empty( $primary ) ) { 234 | $result['label'] = __( 'You have not a set a primary domain.', 'dark-matter' ); 235 | $result['badge']['color'] = 'orange'; 236 | $result['status'] = 'recommended'; 237 | $result['description'] = sprintf( 238 | '

%s

', 239 | sprintf( 240 | /* translators: link to unmapped homepage url */ 241 | __( 'No primary domain is set. Currently this site is can only be visited on the admin domain at; %1$s.', 'dark-matter' ), 242 | sprintf( 243 | '%1$s', 244 | home_url() 245 | ) 246 | ) 247 | ); 248 | } else { 249 | $result['description'] = sprintf( 250 | '

%1$s

', 251 | sprintf( 252 | /* translators: link to mapped homepage url */ 253 | __( 'People can now visit your website at; %1$s.', 'dark-matter' ), 254 | sprintf( 255 | '%1$s', 256 | home_url() 257 | ) 258 | ) 259 | ); 260 | } 261 | 262 | return $result; 263 | } 264 | 265 | /** 266 | * Checks SSL configuration for compatibility with Dark Matter domain mapping. 267 | * 268 | * @since 2.1.0 269 | * 270 | * @return array Test result. 271 | */ 272 | public function test_ssl() { 273 | $result = [ 274 | 'label' => __( 'Your SSL configuration is compatible with Dark Matter.', 'dark-matter' ), 275 | 'status' => 'good', 276 | 'badge' => array( 277 | 'label' => __( 'Domain Mapping', 'dark-matter' ), 278 | 'color' => 'green', 279 | ), 280 | 'description' => sprintf( 281 | '

%s

', 282 | __( 'Your admin area is secured by HTTPS and compatible with domain mapping.', 'dark-matter' ) 283 | ), 284 | 'actions' => sprintf( 285 | '

%s %s

', 286 | /* translators: Documentation explaining HTTPS and why it should be used. */ 287 | esc_url( __( 'https://wordpress.org/support/article/why-should-i-use-https/' ) ), 288 | __( 'Learn more about why you should use HTTPS' ), 289 | /* translators: Accessibility text. */ 290 | __( '(opens in a new tab)' ) 291 | ), 292 | 'test' => 'darkmatter_domain_mapping_ssl', 293 | ]; 294 | 295 | if ( ! $this->force_ssl_set() ) { 296 | $result['label'] = __( 'WordPress does not redirect admin requests to HTTPS.', 'dark-matter' ); 297 | $result['badge']['color'] = 'red'; 298 | $result['status'] = 'critical'; 299 | $result['description'] = sprintf( 300 | '

%s

', 301 | sprintf( 302 | /* translators: FORCE_SSL_ADMIN constant */ 303 | __( 'Please ensure the %1$s constant is present and set to "true" in your wp-config.php file.', 'dark-matter' ), 304 | 'FORCE_SSL_ADMIN' 305 | ) 306 | ); 307 | } 308 | 309 | return $result; 310 | } 311 | 312 | /** 313 | * Return the Singleton Instance of the class. 314 | * 315 | * @since 2.1.0 316 | * 317 | * @return DM_HealthChecks 318 | */ 319 | public static function instance() { 320 | static $instance = false; 321 | 322 | if ( ! $instance ) { 323 | $instance = new self(); 324 | } 325 | 326 | return $instance; 327 | } 328 | } 329 | DM_HealthChecks::instance(); 330 | -------------------------------------------------------------------------------- /domain-mapping/classes/class-dm-media.php: -------------------------------------------------------------------------------- 1 | sites[ $this->current_site_id ] ) && false !== $this->sites[ $this->current_site_id ]; 63 | } 64 | 65 | /** 66 | * Get the main domains for a particular site. 67 | * 68 | * @param integer $site_id Site ID to retrieve the main domains for. 69 | * @return array Main domains, essentially the unmapped (admin) domain and the primary (mapped) domain. 70 | */ 71 | private function get_main_domains( $site_id = 0 ) { 72 | /** 73 | * Ensure the site is actually a site. 74 | */ 75 | $blog = get_site( $site_id ); 76 | if ( ! is_a( $blog, 'WP_Site' ) ) { 77 | return []; 78 | } 79 | 80 | $unmapped = untrailingslashit( $blog->domain . $blog->path ); 81 | 82 | /** 83 | * Put together the main domains. 84 | */ 85 | $main_domains = [ 86 | $unmapped, 87 | ]; 88 | 89 | $primary = DarkMatter_Primary::instance()->get( $site_id ); 90 | if ( ! empty( $primary ) ) { 91 | $main_domains[] = $primary->domain; 92 | } 93 | 94 | return $main_domains; 95 | } 96 | 97 | /** 98 | * Convert WordPress Core's allowed mime types array, which has keys designed for regex, to straight-forward strings 99 | * for the individual extensions as keys on the array. 100 | * 101 | * For example: turn `image/jpeg` mime type key from `jpg|jpeg|jpe` into three separate key / values on the array. 102 | * 103 | * @return array All mime types and extensions. 104 | * 105 | * @since 2.2.0 106 | */ 107 | private function get_mime_types() { 108 | $mime_types = get_allowed_mime_types(); 109 | 110 | foreach ( $mime_types as $extension => $mime_type ) { 111 | /** 112 | * No divided - regex OR - then skip it. 113 | */ 114 | if ( false === stripos( $extension, '|' ) ) { 115 | continue; 116 | } 117 | 118 | /** 119 | * Get the separate extensions. 120 | */ 121 | $extensions = explode( '|', $extension ); 122 | 123 | /** 124 | * Add to the array. 125 | */ 126 | foreach ( $extensions as $ext ) { 127 | $mime_types[ $ext ] = $mime_type; 128 | } 129 | } 130 | 131 | return $mime_types; 132 | } 133 | 134 | /** 135 | * Initialise the Media setup. 136 | * 137 | * @return void 138 | * 139 | * @since 2.2.0 140 | */ 141 | public function init() { 142 | $this->current_site_id = get_current_blog_id(); 143 | $this->request_main_domains = $this->get_main_domains( $this->current_site_id ); 144 | 145 | $this->prime_site( $this->current_site_id ); 146 | } 147 | 148 | /** 149 | * Clean up the `post_content` to restore the data to the original state as if Dark Matter was not present. 150 | * 151 | * @param array $data An array of slashed, sanitized, and processed post data. 152 | * @return array Post data, with URLs unmapped. 153 | * 154 | * @since 2.2.0 155 | */ 156 | public function insert_post( $data = [] ) { 157 | if ( ! empty( $data['post_content'] ) ) { 158 | $data['post_content'] = $this->unmap( $data['post_content'] ); 159 | } 160 | 161 | return $data; 162 | } 163 | 164 | /** 165 | * Map Media domains where appropriate. 166 | * 167 | * @param string $content Content containing URLs - or a URL - to be adjusted. 168 | * @return string 169 | * 170 | * @since 2.2.0 171 | */ 172 | public function map( $content = '' ) { 173 | if ( ! $this->can_map() || empty( $content ) ) { 174 | return $content; 175 | } 176 | 177 | $domains_regex = implode( '|', $this->sites[ $this->current_site_id ]['main_domains'] ); 178 | 179 | /** 180 | * Find all URLs which are not mapped to a Media domain, but are either on the primary domain or admin domain 181 | * (to avoid confusion with a third party image, like GIPHY), and process them. 182 | * 183 | * Note on the regular expression: This regex looks a bit odd, and basically it's to find all URLs after the 184 | * protocol until it hits a character we do not want, like closing double-quote (") on a tag or whitespace. 185 | * Testing different expressions, some more concise, this was the most performant in a small generated sample of 186 | * `post_content` with a four-image gallery. 187 | * 188 | * * https?://(?:mappeddomain1\.test|helloworld\.com)/[^">\s]+ - 1,130 steps (https://regex101.com/r/mrEb9U/1) 189 | * * http?s:\/\/(?:.*?)\.(?:jpg|jpeg|gif|png|pdf) - 7,283 steps (https://regex101.com/r/XPkFIA/1) 190 | * * ([-a-z0-9_\/:.]+\.(jpg|jpeg|png)) - 11,593 steps (https://regex101.com/r/mGlUIH/1) 191 | * 192 | * There will no doubt be some pitfalls, but the performance profile of this approach seems to outweigh those 193 | * concerns (at the time of writing this). 194 | */ 195 | $urls = []; 196 | preg_match_all( "#https?://(?:{$domains_regex})/[^\"'>\s]+#", $content, $urls ); 197 | 198 | /** 199 | * Remove any duplicates. 200 | */ 201 | $urls = array_unique( $urls ); 202 | 203 | /** 204 | * No URLs, skip. 205 | */ 206 | if ( empty( $urls ) || empty( $urls[0] ) ) { 207 | return $content; 208 | } 209 | 210 | /** 211 | * Loop through all the URLs and replace as required. 212 | */ 213 | foreach ( $urls[0] as $url ) { 214 | $extension = pathinfo( $url, PATHINFO_EXTENSION ); 215 | 216 | /** 217 | * No extension, then we can ignore it. 218 | */ 219 | if ( empty( $extension ) ) { 220 | continue; 221 | } 222 | 223 | /** 224 | * Check for a valid extension. 225 | */ 226 | if ( array_key_exists( strtolower( $extension ), $this->sites[ $this->current_site_id ]['allowed_mimes'] ) ) { 227 | $content = str_ireplace( $url, $this->map_url( $url ), $content ); 228 | } 229 | } 230 | 231 | return $content; 232 | } 233 | 234 | /** 235 | * Used to map a URL to a Media domain. Any URL passed into this method **will** be mapped. 236 | * 237 | * @param string $url URL to be modified. 238 | * @return string URL with the domain changed to be from a Media domain. 239 | * 240 | * @since 2.2.0 241 | */ 242 | public function map_url( $url = '' ) { 243 | /** 244 | * No Media domains or the URL is blank, then bail. 245 | */ 246 | if ( ! $this->can_map() || empty( $url ) ) { 247 | return $url; 248 | } 249 | 250 | /** 251 | * Check to ensure the URL is on a domain we can map. This is also used to prevent double-mapping occurring. 252 | */ 253 | $do_map = false; 254 | 255 | foreach ( $this->sites[ $this->current_site_id ]['main_domains'] as $main_domain ) { 256 | if ( false !== stripos( $url, $main_domain ) ) { 257 | $do_map = true; 258 | break; 259 | } 260 | } 261 | 262 | if ( ! $do_map ) { 263 | return $url; 264 | } 265 | 266 | /** 267 | * Alternate through the Media domains if there is more than one. 268 | */ 269 | $index = 0; 270 | 271 | if ( $this->sites[ $this->current_site_id ]['media_domains_count'] > 1 ) { 272 | $index = wp_rand( 0, $this->sites[ $this->current_site_id ]['media_domains_count'] ); 273 | } 274 | 275 | $url = preg_replace( 276 | '#://(' . implode( '|', $this->sites[ $this->current_site_id ]['main_domains'] ) . ')#', 277 | '://' . untrailingslashit( $this->sites[ $this->current_site_id ]['media_domains'][ $index ]->domain ), 278 | $url 279 | ); 280 | 281 | /** 282 | * Ensure the URL is HTTPS. 283 | */ 284 | return set_url_scheme( $url, 'https' ); 285 | } 286 | 287 | /** 288 | * Apply filters to map / unmap asset domains on REST API. 289 | * 290 | * @return void 291 | * 292 | * @since 2.2.0 293 | */ 294 | public function prepare_rest() { 295 | /** 296 | * Loop all post types with REST endpoints to fix the mapping for content.raw property. 297 | */ 298 | $rest_post_types = get_post_types( array( 'show_in_rest' => true ) ); 299 | 300 | foreach ( $rest_post_types as $post_type ) { 301 | add_filter( "rest_prepare_{$post_type}", array( $this, 'prepare_rest_post_item' ), 10, 1 ); 302 | } 303 | } 304 | 305 | /** 306 | * Ensures the "raw" version of the content, typically used by Gutenberg through it's middleware pre-load / JS 307 | * hydrate process, gets handled the same as content (which runs through the `the_content` hook). 308 | * 309 | * @param WP_REST_Response $item Individual post / item in the response that is being processed. 310 | * @return WP_REST_Response Post / item with the content.raw, if present, mapped. 311 | * 312 | * @since 2.2.0 313 | */ 314 | public function prepare_rest_post_item( $item = null ) { 315 | if ( isset( $item->data['content']['raw'] ) ) { 316 | $item->data['content']['raw'] = $this->map( $item->data['content']['raw'] ); 317 | } 318 | 319 | return $item; 320 | } 321 | 322 | /** 323 | * Prime the settings needed for media domain mapping a particular website. 324 | * 325 | * @since 2.3.0 326 | * 327 | * @param integer $site_id Site ID to prime for media domains. 328 | * @return void 329 | */ 330 | private function prime_site( $site_id = 0 ) { 331 | /** 332 | * If we have seen this website before, skip doing it again. 333 | */ 334 | if ( isset( $this->sites[ $site_id ] ) ) { 335 | return; 336 | } 337 | 338 | $main_domains = $this->get_main_domains( $site_id ); 339 | if ( empty( $main_domains ) ) { 340 | $this->sites[ $site_id ] = false; 341 | return; 342 | } 343 | 344 | /** 345 | * Ensure we have media domains to use. 346 | */ 347 | $media_domains = DarkMatter_Domains::instance()->get_domains_by_type( DM_DOMAIN_TYPE_MEDIA, $site_id ); 348 | if ( empty( $media_domains ) ) { 349 | $this->sites[ $site_id ] = false; 350 | return; 351 | } 352 | 353 | /** 354 | * Seemingly WordPress' `wp_get_attachment_url()` doesn't seem to fully work as intended for `switch_to_blog()`. 355 | * Therefore we must add the requesters' main domains in order for the map / unmap to work, as the media assets 356 | * will be served on the requesters' domains rather than the domain of the site it belongs to. 357 | */ 358 | $main_domains = array_filter( 359 | array_merge( 360 | $main_domains, 361 | $this->request_main_domains 362 | ) 363 | ); 364 | 365 | $this->sites[ $site_id ] = [ 366 | 'allowed_mimes' => $this->get_mime_types(), 367 | 'main_domains' => $main_domains, 368 | 'media_domains' => $media_domains, 369 | 'media_domains_count' => count( $media_domains ), 370 | /** 371 | * The first entry is always the unmapped. 372 | */ 373 | 'unmapped' => $main_domains[0], 374 | ]; 375 | } 376 | 377 | /** 378 | * Handle the `switch_to_blog()` / `restore_current_blog()` functionality. 379 | * 380 | * @since 2.3.0 381 | * 382 | * @param int $site_id Site (Blog) ID, used to retrieve the site details and Primary Domain. 383 | * @return void 384 | */ 385 | public function switch_blog( $site_id = 0 ) { 386 | $this->current_site_id = ( ! empty( $site_id ) ? intval( $site_id ) : get_current_blog_id() ); 387 | $this->prime_site( $this->current_site_id ); 388 | } 389 | 390 | /** 391 | * Used to unmap Media domains. 392 | * 393 | * @param string $value Value that may contain Media domains. 394 | * @return string Value with the Media domains removed and replaced with the unmapped domain. 395 | * 396 | * @since 2.2.0 397 | */ 398 | public function unmap( $value = '' ) { 399 | if ( ! $this->can_map() || empty( $value ) ) { 400 | return $value; 401 | } 402 | 403 | /** 404 | * Create an array of strings of the Media domains. 405 | */ 406 | $media_domains = wp_list_pluck( $this->sites[ $this->current_site_id ]['media_domains'], 'domain' ); 407 | 408 | /** 409 | * Ensure we have domains that are to be unmapped. 410 | */ 411 | if ( empty( $media_domains ) ) { 412 | return $value; 413 | } 414 | 415 | /** 416 | * Replace the Media domains with the unmapped domain. 417 | */ 418 | return preg_replace( 419 | '#://(' . implode( '|', $media_domains ) . ')#', 420 | '://' . $this->sites[ $this->current_site_id ]['unmapped'], 421 | $value 422 | ); 423 | } 424 | 425 | /** 426 | * Return the Singleton Instance of the class. 427 | * 428 | * @return DM_Media 429 | * 430 | * @since 2.2.0 431 | */ 432 | public static function instance() { 433 | static $instance = false; 434 | 435 | if ( ! $instance ) { 436 | $instance = new self(); 437 | } 438 | 439 | return $instance; 440 | } 441 | } 442 | DM_Media::instance(); 443 | -------------------------------------------------------------------------------- /domain-mapping/cli/class-darkmatter-domain-cli.php: -------------------------------------------------------------------------------- 1 | 25 | * : The domain you wish to add. 26 | * 27 | * [--disable] 28 | * : Allows you to add a domain, primary or secondary, to the Site without 29 | * it being used immediately. 30 | * 31 | * [--force] 32 | * : Force Dark Matter to add the domain. This is required if you wish to 33 | * remove a Primary domain from a Site. 34 | * 35 | * [--https] 36 | * : Sets the protocol to be HTTPS. This is only needed when used with the --primary flag and is ignored otherwise. 37 | * 38 | * [--primary] 39 | * : Sets the domain to be the primary domain for the Site, the one which visitors will be redirected to. 40 | * 41 | * [--secondary] 42 | * : Sets the domain to be a secondary domain for the Site. Visitors will be redirected from this domain to the primary. 43 | * 44 | * [--type] 45 | * : Choose the type of domain. Useful for creating "media" domains. Defaults to "main". 46 | * 47 | * ### EXAMPLES 48 | * Set the primary domain and set the protocol to HTTPS. 49 | * 50 | * wp --url="sites.my.com/siteone" darkmatter domain add www.primarydomain.com --primary --https 51 | * 52 | * Set a media domain for a site. 53 | * 54 | * wp --url="sites.my.com/sitefifteen" darkmatter domain add fifteen.mycdn.com --type=media 55 | * 56 | * @since 2.0.0 57 | * 58 | * @param array $args CLI args. 59 | * @param array $assoc_args CLI args maintaining the flag names from the terminal. 60 | */ 61 | public function add( $args, $assoc_args ) { 62 | if ( empty( $args[0] ) ) { 63 | WP_CLI::error( __( 'Please include a fully qualified domain name to be added.', 'dark-matter' ) ); 64 | } 65 | 66 | $fqdn = $args[0]; 67 | 68 | $opts = wp_parse_args( 69 | $assoc_args, 70 | [ 71 | 'disable' => false, 72 | 'force' => false, 73 | 'https' => true, 74 | 'primary' => false, 75 | 'type' => 'main', 76 | ] 77 | ); 78 | 79 | $type = $this->check_type_opt( $opts['type'] ); 80 | 81 | /** 82 | * Add the domain. 83 | */ 84 | $db = DarkMatter_Domains::instance(); 85 | $result = $db->add( $fqdn, $opts['primary'], $opts['https'], $opts['force'], ! $opts['disable'], $type ); 86 | 87 | if ( is_wp_error( $result ) ) { 88 | $error_msg = $result->get_error_message(); 89 | 90 | if ( 'primary' === $result->get_error_code() ) { 91 | $error_msg = __( 'You cannot add this domain as the primary domain without using the --force flag.', 'dark-matter' ); 92 | } 93 | 94 | WP_CLI::error( $error_msg ); 95 | } 96 | 97 | WP_CLI::success( $fqdn . __( ': was added.', 'dark-matter' ) ); 98 | } 99 | 100 | /** 101 | * Checks to ensure the value of type is valid and useable. 102 | * 103 | * @param string $type Type value to be checked. 104 | * @return integer Domain type. 105 | * 106 | * @since 2.2.0 107 | */ 108 | private function check_type_opt( $type = '' ) { 109 | /** 110 | * Handle the Media flag. 111 | */ 112 | $domain_types = [ 113 | 'main' => DM_DOMAIN_TYPE_MAIN, 114 | 'media' => DM_DOMAIN_TYPE_MEDIA, 115 | ]; 116 | 117 | if ( array_key_exists( strtolower( $type ), $domain_types ) ) { 118 | return $domain_types[ $type ]; 119 | } 120 | 121 | return DM_DOMAIN_TYPE_MAIN; 122 | } 123 | 124 | /** 125 | * List a domain for the current Site. If the the URL is omitted and the 126 | * command is run on the root Site, it will list all domains available for 127 | * the whole network. 128 | * 129 | * ### OPTIONS 130 | * 131 | * [--format] 132 | * : Determine which format that should be returned. Defaults to "table" and 133 | * accepts "json", "csv", "yaml", and "count". 134 | * 135 | * [--primary] 136 | * : Filter the results to return only the Primary domains. This will ignore 137 | * the --url parameter. 138 | * 139 | * ### EXAMPLES 140 | * List all domains for a specific Site. 141 | * 142 | * wp --url="sites.my.com/siteone" darkmatter domain list 143 | * 144 | * Get all domains for a specific Site in JSON format. 145 | * 146 | * wp --url="sites.my.com/siteone" darkmatter domain list --format=json 147 | * 148 | * List all domains for all Sites. 149 | * 150 | * wp darkmatter domain list 151 | * 152 | * @since 2.0.0 153 | * 154 | * @param array $args CLI args. 155 | * @param array $assoc_args CLI args maintaining the flag names from the terminal. 156 | */ 157 | public function list( $args, $assoc_args ) { 158 | /** 159 | * Handle and validate the format flag if provided. 160 | */ 161 | $opts = wp_parse_args( 162 | $assoc_args, 163 | [ 164 | 'format' => 'table', 165 | 'primary' => false, 166 | ] 167 | ); 168 | 169 | if ( ! in_array( $opts['format'], array( 'table', 'json', 'csv', 'yaml', 'count' ) ) ) { 170 | $opts['format'] = 'table'; 171 | } 172 | 173 | if ( $opts['primary'] ) { 174 | $db = DarkMatter_Primary::instance(); 175 | $domains = $db->get_all(); 176 | } else { 177 | /** 178 | * Retrieve the current Blog ID. However this will be set to null if 179 | * this is the root Site to retrieve all domains. 180 | */ 181 | $site_id = get_current_blog_id(); 182 | 183 | if ( is_main_site() ) { 184 | $site_id = null; 185 | } 186 | 187 | $db = DarkMatter_Domains::instance(); 188 | $domains = $db->get_domains( $site_id ); 189 | } 190 | 191 | /** 192 | * Filter out and format the columns and values appropriately. 193 | */ 194 | $domains = array_map( 195 | function ( $domain ) { 196 | $no_val = __( 'No', 'dark-matter' ); 197 | $yes_val = __( 'Yes', 'dark-matter' ); 198 | 199 | $columns = array( 200 | 'F.Q.D.N.' => $domain->domain, 201 | 'Primary' => ( $domain->is_primary ? $yes_val : $no_val ), 202 | 'Protocol' => ( $domain->is_https ? 'HTTPS' : 'HTTP' ), 203 | 'Active' => ( $domain->active ? $yes_val : $no_val ), 204 | 'Type' => ( DM_DOMAIN_TYPE_MEDIA === $domain->type ? 'Media' : 'Main' ), 205 | ); 206 | 207 | /** 208 | * If the query is the root Site and we are displaying all domains, 209 | * then we retrieve and include the Site Name. 210 | */ 211 | $site = get_site( $domain->blog_id ); 212 | 213 | if ( empty( $site ) ) { 214 | $columns['Site'] = __( 'Unknown.', 'dark-matter' ); 215 | } else { 216 | $columns['Site'] = $site->blogname; 217 | } 218 | 219 | return $columns; 220 | }, 221 | $domains 222 | ); 223 | 224 | /** 225 | * Determine which headers to use for the Display. 226 | */ 227 | $display = [ 228 | 'F.Q.D.N.', 229 | 'Primary', 230 | 'Protocol', 231 | 'Active', 232 | 'Type', 233 | ]; 234 | 235 | if ( is_main_site() ) { 236 | $display = [ 237 | 'F.Q.D.N.', 238 | 'Site', 239 | 'Primary', 240 | 'Protocol', 241 | 'Active', 242 | 'Type', 243 | ]; 244 | } 245 | 246 | WP_CLI\Utils\format_items( $opts['format'], $domains, $display ); 247 | } 248 | 249 | /** 250 | * Remove a specific domain on a Site on the WordPress Network. 251 | * 252 | * ### OPTIONS 253 | * 254 | * 255 | * : The domain you wish to remove. 256 | * 257 | * [--force] 258 | * : Force Dark Matter to remove the domain. This is required if you wish to 259 | * remove a Primary domain from a Site. 260 | * 261 | * ### EXAMPLES 262 | * Remove a domain from a Site. 263 | * 264 | * wp --url="sites.my.com/siteone" darkmatter domain remove www.primarydomain.com 265 | * 266 | * Remove a primary domain from a Site. Please note; this ***WILL NOT*** set 267 | * another domain to replace the Primary. You must set this using either the 268 | * add or set commands. 269 | * 270 | * wp --url="sites.my.com/siteone" darkmatter domain remove www.primarydomain.com --force 271 | * 272 | * @since 2.0.0 273 | * 274 | * @param array $args CLI args. 275 | * @param array $assoc_args CLI args maintaining the flag names from the terminal. 276 | */ 277 | public function remove( $args, $assoc_args ) { 278 | if ( empty( $args[0] ) ) { 279 | WP_CLI::error( __( 'Please include a fully qualified domain name to be removed.', 'dark-matter' ) ); 280 | } 281 | 282 | $fqdn = $args[0]; 283 | 284 | $opts = wp_parse_args( 285 | $assoc_args, 286 | [ 287 | 'force' => false, 288 | ] 289 | ); 290 | 291 | $db = DarkMatter_Domains::instance(); 292 | 293 | /** 294 | * Remove the domain. 295 | */ 296 | $result = $db->delete( $fqdn, $opts['force'] ); 297 | 298 | if ( is_wp_error( $result ) ) { 299 | $error_msg = $result->get_error_message(); 300 | 301 | if ( 'primary' === $result->get_error_code() ) { 302 | $error_msg = __( 'You cannot delete a primary domain. Use --force flag if you really want to and know what you are doing.', 'dark-matter' ); 303 | } 304 | 305 | WP_CLI::error( $error_msg ); 306 | } 307 | 308 | WP_CLI::success( $fqdn . __( ': has been removed.', 'dark-matter' ) ); 309 | } 310 | 311 | /** 312 | * Update the flags for a specific domain on a Site on the WordPress Network. 313 | * 314 | * ### OPTIONS 315 | * 316 | * 317 | * : The domain you wish to update. 318 | * 319 | * [--enable] 320 | * : Enable the domain on the Site. 321 | * 322 | * [--disable] 323 | * : Disable the domain on the Site. 324 | * 325 | * [--force] 326 | * : Force Dark Matter to update the domain. 327 | * 328 | * [--use-http] 329 | * : Set the protocol to be HTTP. 330 | * 331 | * [--use-https] 332 | * : Set the protocol to be HTTPS. 333 | * 334 | * [--primary] 335 | * : Set the domain to be the primary domain for the Site, the one which 336 | * visitors will be redirected to. If a primary domain is already set, then 337 | * you must use the --force flag to perform the update. 338 | * 339 | * [--secondary] 340 | * : Set the domain to be a secondary domain for the Site. Visitors will be 341 | * redirected from this domain to the primary. 342 | * 343 | * [--type] 344 | * : Choose the type of domain. Useful for creating "media" domains. Defaults to "main". 345 | * 346 | * ### EXAMPLES 347 | * Set the primary domain and set the protocol to HTTPS. 348 | * 349 | * wp --url="sites.my.com/siteone" darkmatter domain set www.primarydomain.com --primary 350 | * wp --url="sites.my.com/siteone" darkmatter domain set www.secondarydomain.com --secondary 351 | * 352 | * Convert a secondary domain into a media domain. Useful for when repurposing an old domain for use a CDN for media 353 | * assets. 354 | * 355 | * wp --url="sites.my.com/siteone" darkmatter domain set www.secondarydomain.com --type=media 356 | * 357 | * Convert a Media domain to a main domain. This is useful in scenarios when a media domain is redundant and to 358 | * ensure it redirects to the website. 359 | * 360 | * wp --url="sites.my.com/siteone" darkmatter domain set one.mycdntest.com --type=main --secondary 361 | * 362 | * @since 2.0.0 363 | * 364 | * @param array $args CLI args. 365 | * @param array $assoc_args CLI args maintaining the flag names from the terminal. 366 | */ 367 | public function set( $args, $assoc_args ) { 368 | if ( empty( $args[0] ) ) { 369 | WP_CLI::error( __( 'Please include a fully qualified domain name to be removed.', 'dark-matter' ) ); 370 | } 371 | 372 | $fqdn = $args[0]; 373 | 374 | $db = DarkMatter_Domains::instance(); 375 | $domain_before = $db->get( $fqdn ); 376 | 377 | $opts = wp_parse_args( 378 | $assoc_args, 379 | [ 380 | 'disable' => false, 381 | 'enable' => false, 382 | 'force' => false, 383 | 'use-http' => null, 384 | 'use-https' => true, 385 | 'primary' => null, 386 | 'secondary' => null, 387 | ] 388 | ); 389 | 390 | /** 391 | * Ensure that contradicting options are not being supplied. 392 | */ 393 | if ( $opts['use-http'] && $opts['use-https'] ) { 394 | WP_CLI::error( __( 'A domain cannot be both HTTP and HTTPS.', 'dark-matter' ) ); 395 | } 396 | 397 | if ( $opts['primary'] && $opts['secondary'] ) { 398 | WP_CLI::error( __( 'A domain cannot be both primary and secondary.', 'dark-matter' ) ); 399 | } 400 | 401 | if ( $opts['enable'] && $opts['disable'] ) { 402 | WP_CLI::error( __( 'A domain cannot be both enabled and disabled.', 'dark-matter' ) ); 403 | } 404 | 405 | /** 406 | * Determine if we are switching between HTTP and HTTPS. 407 | */ 408 | $is_https = $opts['use-https']; 409 | 410 | if ( $opts['use-http'] ) { 411 | $is_https = false; 412 | } 413 | 414 | /** 415 | * Determine if we are switching between primary and secondary. 416 | */ 417 | $is_primary = $opts['primary']; 418 | 419 | if ( $opts['secondary'] ) { 420 | $is_primary = false; 421 | } 422 | 423 | /** 424 | * Determine if we are switching between enabled and disabled. 425 | */ 426 | $active = $domain_before->active; 427 | 428 | if ( $opts['enable'] ) { 429 | $active = true; 430 | } 431 | 432 | if ( $opts['disable'] ) { 433 | $active = false; 434 | } 435 | 436 | /** 437 | * If the type is specified, then validate it to ensure it is correct. 438 | */ 439 | $type = null; 440 | if ( ! empty( $opts['type'] ) ) { 441 | $type = $this->check_type_opt( $opts['type'] ); 442 | } 443 | 444 | /** 445 | * Update the records. 446 | */ 447 | $result = $db->update( $fqdn, $is_primary, $is_https, $opts['force'], $active, $type ); 448 | 449 | /** 450 | * Handle the output for errors and success. 451 | */ 452 | if ( is_wp_error( $result ) ) { 453 | $error_msg = $result->get_error_message(); 454 | 455 | if ( 'primary' === $result->get_error_code() ) { 456 | $error_msg = __( 'You cannot modify the primary domain. Use --force flag if you really want to and know what you are doing.', 'dark-matter' ); 457 | } 458 | 459 | WP_CLI::error( $error_msg ); 460 | } 461 | 462 | WP_CLI::success( $fqdn . __( ': successfully updated.', 'dark-matter' ) ); 463 | } 464 | } 465 | WP_CLI::add_command( 'darkmatter domain', 'DarkMatter_Domain_CLI' ); 466 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Dark Matter - WordPress domain mapping plugin 2 | 3 | Copyright 2022 by Cameron Terry 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 16 | 17 | GNU GENERAL PUBLIC LICENSE 18 | Version 2, June 1991 19 | 20 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 21 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 22 | Everyone is permitted to copy and distribute verbatim copies 23 | of this license document, but changing it is not allowed. 24 | 25 | Preamble 26 | 27 | The licenses for most software are designed to take away your 28 | freedom to share and change it. By contrast, the GNU General Public 29 | License is intended to guarantee your freedom to share and change free 30 | software--to make sure the software is free for all its users. This 31 | General Public License applies to most of the Free Software 32 | Foundation's software and to any other program whose authors commit to 33 | using it. (Some other Free Software Foundation software is covered by 34 | the GNU Lesser General Public License instead.) You can apply it to 35 | your programs, too. 36 | 37 | When we speak of free software, we are referring to freedom, not 38 | price. Our General Public Licenses are designed to make sure that you 39 | have the freedom to distribute copies of free software (and charge for 40 | this service if you wish), that you receive source code or can get it 41 | if you want it, that you can change the software or use pieces of it 42 | in new free programs; and that you know you can do these things. 43 | 44 | To protect your rights, we need to make restrictions that forbid 45 | anyone to deny you these rights or to ask you to surrender the rights. 46 | These restrictions translate to certain responsibilities for you if you 47 | distribute copies of the software, or if you modify it. 48 | 49 | For example, if you distribute copies of such a program, whether 50 | gratis or for a fee, you must give the recipients all the rights that 51 | you have. You must make sure that they, too, receive or can get the 52 | source code. And you must show them these terms so they know their 53 | rights. 54 | 55 | We protect your rights with two steps: (1) copyright the software, and 56 | (2) offer you this license which gives you legal permission to copy, 57 | distribute and/or modify the software. 58 | 59 | Also, for each author's protection and ours, we want to make certain 60 | that everyone understands that there is no warranty for this free 61 | software. If the software is modified by someone else and passed on, we 62 | want its recipients to know that what they have is not the original, so 63 | that any problems introduced by others will not reflect on the original 64 | authors' reputations. 65 | 66 | Finally, any free program is threatened constantly by software 67 | patents. We wish to avoid the danger that redistributors of a free 68 | program will individually obtain patent licenses, in effect making the 69 | program proprietary. To prevent this, we have made it clear that any 70 | patent must be licensed for everyone's free use or not licensed at all. 71 | 72 | The precise terms and conditions for copying, distribution and 73 | modification follow. 74 | 75 | GNU GENERAL PUBLIC LICENSE 76 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 77 | 78 | 0. This License applies to any program or other work which contains 79 | a notice placed by the copyright holder saying it may be distributed 80 | under the terms of this General Public License. The "Program", below, 81 | refers to any such program or work, and a "work based on the Program" 82 | means either the Program or any derivative work under copyright law: 83 | that is to say, a work containing the Program or a portion of it, 84 | either verbatim or with modifications and/or translated into another 85 | language. (Hereinafter, translation is included without limitation in 86 | the term "modification".) Each licensee is addressed as "you". 87 | 88 | Activities other than copying, distribution and modification are not 89 | covered by this License; they are outside its scope. The act of 90 | running the Program is not restricted, and the output from the Program 91 | is covered only if its contents constitute a work based on the 92 | Program (independent of having been made by running the Program). 93 | Whether that is true depends on what the Program does. 94 | 95 | 1. You may copy and distribute verbatim copies of the Program's 96 | source code as you receive it, in any medium, provided that you 97 | conspicuously and appropriately publish on each copy an appropriate 98 | copyright notice and disclaimer of warranty; keep intact all the 99 | notices that refer to this License and to the absence of any warranty; 100 | and give any other recipients of the Program a copy of this License 101 | along with the Program. 102 | 103 | You may charge a fee for the physical act of transferring a copy, and 104 | you may at your option offer warranty protection in exchange for a fee. 105 | 106 | 2. You may modify your copy or copies of the Program or any portion 107 | of it, thus forming a work based on the Program, and copy and 108 | distribute such modifications or work under the terms of Section 1 109 | above, provided that you also meet all of these conditions: 110 | 111 | a) You must cause the modified files to carry prominent notices 112 | stating that you changed the files and the date of any change. 113 | 114 | b) You must cause any work that you distribute or publish, that in 115 | whole or in part contains or is derived from the Program or any 116 | part thereof, to be licensed as a whole at no charge to all third 117 | parties under the terms of this License. 118 | 119 | c) If the modified program normally reads commands interactively 120 | when run, you must cause it, when started running for such 121 | interactive use in the most ordinary way, to print or display an 122 | announcement including an appropriate copyright notice and a 123 | notice that there is no warranty (or else, saying that you provide 124 | a warranty) and that users may redistribute the program under 125 | these conditions, and telling the user how to view a copy of this 126 | License. (Exception: if the Program itself is interactive but 127 | does not normally print such an announcement, your work based on 128 | the Program is not required to print an announcement.) 129 | 130 | These requirements apply to the modified work as a whole. If 131 | identifiable sections of that work are not derived from the Program, 132 | and can be reasonably considered independent and separate works in 133 | themselves, then this License, and its terms, do not apply to those 134 | sections when you distribute them as separate works. But when you 135 | distribute the same sections as part of a whole which is a work based 136 | on the Program, the distribution of the whole must be on the terms of 137 | this License, whose permissions for other licensees extend to the 138 | entire whole, and thus to each and every part regardless of who wrote it. 139 | 140 | Thus, it is not the intent of this section to claim rights or contest 141 | your rights to work written entirely by you; rather, the intent is to 142 | exercise the right to control the distribution of derivative or 143 | collective works based on the Program. 144 | 145 | In addition, mere aggregation of another work not based on the Program 146 | with the Program (or with a work based on the Program) on a volume of 147 | a storage or distribution medium does not bring the other work under 148 | the scope of this License. 149 | 150 | 3. You may copy and distribute the Program (or a work based on it, 151 | under Section 2) in object code or executable form under the terms of 152 | Sections 1 and 2 above provided that you also do one of the following: 153 | 154 | a) Accompany it with the complete corresponding machine-readable 155 | source code, which must be distributed under the terms of Sections 156 | 1 and 2 above on a medium customarily used for software interchange; or, 157 | 158 | b) Accompany it with a written offer, valid for at least three 159 | years, to give any third party, for a charge no more than your 160 | cost of physically performing source distribution, a complete 161 | machine-readable copy of the corresponding source code, to be 162 | distributed under the terms of Sections 1 and 2 above on a medium 163 | customarily used for software interchange; or, 164 | 165 | c) Accompany it with the information you received as to the offer 166 | to distribute corresponding source code. (This alternative is 167 | allowed only for noncommercial distribution and only if you 168 | received the program in object code or executable form with such 169 | an offer, in accord with Subsection b above.) 170 | 171 | The source code for a work means the preferred form of the work for 172 | making modifications to it. For an executable work, complete source 173 | code means all the source code for all modules it contains, plus any 174 | associated interface definition files, plus the scripts used to 175 | control compilation and installation of the executable. However, as a 176 | special exception, the source code distributed need not include 177 | anything that is normally distributed (in either source or binary 178 | form) with the major components (compiler, kernel, and so on) of the 179 | operating system on which the executable runs, unless that component 180 | itself accompanies the executable. 181 | 182 | If distribution of executable or object code is made by offering 183 | access to copy from a designated place, then offering equivalent 184 | access to copy the source code from the same place counts as 185 | distribution of the source code, even though third parties are not 186 | compelled to copy the source along with the object code. 187 | 188 | 4. You may not copy, modify, sublicense, or distribute the Program 189 | except as expressly provided under this License. Any attempt 190 | otherwise to copy, modify, sublicense or distribute the Program is 191 | void, and will automatically terminate your rights under this License. 192 | However, parties who have received copies, or rights, from you under 193 | this License will not have their licenses terminated so long as such 194 | parties remain in full compliance. 195 | 196 | 5. You are not required to accept this License, since you have not 197 | signed it. However, nothing else grants you permission to modify or 198 | distribute the Program or its derivative works. These actions are 199 | prohibited by law if you do not accept this License. Therefore, by 200 | modifying or distributing the Program (or any work based on the 201 | Program), you indicate your acceptance of this License to do so, and 202 | all its terms and conditions for copying, distributing or modifying 203 | the Program or works based on it. 204 | 205 | 6. Each time you redistribute the Program (or any work based on the 206 | Program), the recipient automatically receives a license from the 207 | original licensor to copy, distribute or modify the Program subject to 208 | these terms and conditions. You may not impose any further 209 | restrictions on the recipients' exercise of the rights granted herein. 210 | You are not responsible for enforcing compliance by third parties to 211 | this License. 212 | 213 | 7. If, as a consequence of a court judgment or allegation of patent 214 | infringement or for any other reason (not limited to patent issues), 215 | conditions are imposed on you (whether by court order, agreement or 216 | otherwise) that contradict the conditions of this License, they do not 217 | excuse you from the conditions of this License. If you cannot 218 | distribute so as to satisfy simultaneously your obligations under this 219 | License and any other pertinent obligations, then as a consequence you 220 | may not distribute the Program at all. For example, if a patent 221 | license would not permit royalty-free redistribution of the Program by 222 | all those who receive copies directly or indirectly through you, then 223 | the only way you could satisfy both it and this License would be to 224 | refrain entirely from distribution of the Program. 225 | 226 | If any portion of this section is held invalid or unenforceable under 227 | any particular circumstance, the balance of the section is intended to 228 | apply and the section as a whole is intended to apply in other 229 | circumstances. 230 | 231 | It is not the purpose of this section to induce you to infringe any 232 | patents or other property right claims or to contest validity of any 233 | such claims; this section has the sole purpose of protecting the 234 | integrity of the free software distribution system, which is 235 | implemented by public license practices. Many people have made 236 | generous contributions to the wide range of software distributed 237 | through that system in reliance on consistent application of that 238 | system; it is up to the author/donor to decide if he or she is willing 239 | to distribute software through any other system and a licensee cannot 240 | impose that choice. 241 | 242 | This section is intended to make thoroughly clear what is believed to 243 | be a consequence of the rest of this License. 244 | 245 | 8. If the distribution and/or use of the Program is restricted in 246 | certain countries either by patents or by copyrighted interfaces, the 247 | original copyright holder who places the Program under this License 248 | may add an explicit geographical distribution limitation excluding 249 | those countries, so that distribution is permitted only in or among 250 | countries not thus excluded. In such case, this License incorporates 251 | the limitation as if written in the body of this License. 252 | 253 | 9. The Free Software Foundation may publish revised and/or new versions 254 | of the General Public License from time to time. Such new versions will 255 | be similar in spirit to the present version, but may differ in detail to 256 | address new problems or concerns. 257 | 258 | Each version is given a distinguishing version number. If the Program 259 | specifies a version number of this License which applies to it and "any 260 | later version", you have the option of following the terms and conditions 261 | either of that version or of any later version published by the Free 262 | Software Foundation. If the Program does not specify a version number of 263 | this License, you may choose any version ever published by the Free Software 264 | Foundation. 265 | 266 | 10. If you wish to incorporate parts of the Program into other free 267 | programs whose distribution conditions are different, write to the author 268 | to ask for permission. For software which is copyrighted by the Free 269 | Software Foundation, write to the Free Software Foundation; we sometimes 270 | make exceptions for this. Our decision will be guided by the two goals 271 | of preserving the free status of all derivatives of our free software and 272 | of promoting the sharing and reuse of software generally. 273 | 274 | NO WARRANTY 275 | 276 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 277 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 278 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 279 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 280 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 281 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 282 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 283 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 284 | REPAIR OR CORRECTION. 285 | 286 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 287 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 288 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 289 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 290 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 291 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 292 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 293 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 294 | POSSIBILITY OF SUCH DAMAGES. 295 | 296 | END OF TERMS AND CONDITIONS 297 | 298 | How to Apply These Terms to Your New Programs 299 | 300 | If you develop a new program, and you want it to be of the greatest 301 | possible use to the public, the best way to achieve this is to make it 302 | free software which everyone can redistribute and change under these terms. 303 | 304 | To do so, attach the following notices to the program. It is safest 305 | to attach them to the start of each source file to most effectively 306 | convey the exclusion of warranty; and each file should have at least 307 | the "copyright" line and a pointer to where the full notice is found. 308 | 309 | {description} 310 | Copyright (C) {year} {fullname} 311 | 312 | This program is free software; you can redistribute it and/or modify 313 | it under the terms of the GNU General Public License as published by 314 | the Free Software Foundation; either version 2 of the License, or 315 | (at your option) any later version. 316 | 317 | This program is distributed in the hope that it will be useful, 318 | but WITHOUT ANY WARRANTY; without even the implied warranty of 319 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 320 | GNU General Public License for more details. 321 | 322 | You should have received a copy of the GNU General Public License along 323 | with this program; if not, write to the Free Software Foundation, Inc., 324 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 325 | 326 | Also add information on how to contact you by electronic and paper mail. 327 | 328 | If the program is interactive, make it output a short notice like this 329 | when it starts in an interactive mode: 330 | 331 | Gnomovision version 69, Copyright (C) year name of author 332 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 333 | This is free software, and you are welcome to redistribute it 334 | under certain conditions; type `show c' for details. 335 | 336 | The hypothetical commands `show w' and `show c' should show the appropriate 337 | parts of the General Public License. Of course, the commands you use may 338 | be called something other than `show w' and `show c'; they could even be 339 | mouse-clicks or menu items--whatever suits your program. 340 | 341 | You should also get your employer (if you work as a programmer) or your 342 | school, if any, to sign a "copyright disclaimer" for the program, if 343 | necessary. Here is a sample; alter the names: 344 | 345 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 346 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 347 | 348 | {signature of Ty Coon}, 1 April 1989 349 | Ty Coon, President of Vice 350 | 351 | This General Public License does not permit incorporating your program into 352 | proprietary programs. If your program is a subroutine library, you may 353 | consider it more useful to permit linking proprietary applications with the 354 | library. If this is what you want to do, use the GNU Lesser General 355 | Public License instead of this License. 356 | -------------------------------------------------------------------------------- /domain-mapping/rest/class-dm-rest-domains-controller.php: -------------------------------------------------------------------------------- 1 | namespace = 'dm/v1'; 22 | $this->rest_base = 'domain'; 23 | $this->rest_base_plural = 'domains'; 24 | } 25 | 26 | /** 27 | * Add a domain to the Site. 28 | * 29 | * @since 2.0.0 30 | * 31 | * @param WP_REST_Request $request Current request. 32 | * @return WP_REST_Response|mixed WP_REST_Response on success. WP_Error on failure. 33 | */ 34 | public function create_item( $request ) { 35 | $db = DarkMatter_Domains::instance(); 36 | 37 | $item = $this->prepare_item_for_database( $request ); 38 | 39 | $result = $db->add( $item['domain'], $item['is_primary'], $item['is_https'], $request['force'], $item['is_active'] ); 40 | 41 | /** 42 | * Return errors as-is. This is maintain consistency and parity with the 43 | * WP CLI commands. 44 | */ 45 | if ( is_wp_error( $result ) ) { 46 | return rest_ensure_response( $result ); 47 | } 48 | 49 | /** 50 | * Prepare response for successfully adding a domain. 51 | */ 52 | $response = rest_ensure_response( 53 | $this->prepare_item_for_response( $result, $request ) 54 | ); 55 | 56 | $response->set_status( 201 ); 57 | $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $result->domain ) ) ); 58 | 59 | return $response; 60 | } 61 | 62 | /** 63 | * Checks if a given request has access to add a domain. 64 | * 65 | * @since 2.0.0 66 | * 67 | * @param WP_REST_Request $request Current request. 68 | * @return boolean True if the current user is a Super Admin. False otherwise. 69 | */ 70 | public function create_item_permissions_check( $request ) { 71 | /** This action is documented in domain-mapping/classes/class-dm-ui.php */ 72 | return current_user_can( apply_filters( 'dark_matter_domain_permission', 'upgrade_network', 'rest-create' ) ); 73 | } 74 | 75 | /** 76 | * Delete a domain. 77 | * 78 | * @since 2.0.0 79 | * 80 | * @param WP_REST_Request $request Current request. 81 | * @return WP_REST_Response|mixed WP_REST_Response on success. WP_Error on failure. 82 | */ 83 | public function delete_item( $request ) { 84 | $db = DarkMatter_Domains::instance(); 85 | 86 | $result = $db->delete( $request['domain'], $request['force'] ); 87 | 88 | /** 89 | * Return errors as-is. This is maintain consistency and parity with the 90 | * WP CLI commands. 91 | */ 92 | if ( is_wp_error( $result ) ) { 93 | return rest_ensure_response( $result ); 94 | } 95 | 96 | /** 97 | * Handle the response for the REST endpoint. 98 | */ 99 | $response = rest_ensure_response( 100 | array( 101 | 'deleted' => true, 102 | 'domain' => $request['domain'], 103 | ) 104 | ); 105 | 106 | return $response; 107 | } 108 | 109 | /** 110 | * Checks if a given request has access to delete a domain. 111 | * 112 | * @since 2.0.0 113 | * 114 | * @param WP_REST_Request $request Current request. 115 | * @return boolean True if the current user is a Super Admin. False otherwise. 116 | */ 117 | public function delete_item_permissions_check( $request ) { 118 | /** This action is documented in domain-mapping/classes/class-dm-ui.php */ 119 | return current_user_can( apply_filters( 'dark_matter_domain_permission', 'upgrade_network', 'rest-delete' ) ); 120 | } 121 | 122 | /** 123 | * Return the Restricted domains as a list in REST response. 124 | * 125 | * @since 2.0.0 126 | * 127 | * @param WP_REST_Request $request Current request. 128 | * @return WP_REST_Response|mixed WP_REST_Response on success. WP_Error on failure. 129 | */ 130 | public function get_item( $request ) { 131 | $db = DarkMatter_Domains::instance(); 132 | 133 | $result = $db->get( $request['domain'] ); 134 | 135 | /** 136 | * Return errors as-is. This is maintain consistency and parity with the 137 | * WP CLI commands. 138 | */ 139 | if ( is_wp_error( $result ) ) { 140 | return rest_ensure_response( $result ); 141 | } 142 | 143 | /** 144 | * Handle the response for the REST endpoint. 145 | */ 146 | $response = $this->prepare_item_for_response( $result, $request ); 147 | 148 | return rest_ensure_response( $response ); 149 | } 150 | 151 | /** 152 | * JSON Schema definition for Domain. 153 | * 154 | * @since 2.0.0 155 | * 156 | * @return array JSON Schema definition. 157 | */ 158 | public function get_item_schema() { 159 | $schema = array( 160 | '$schema' => 'http://json-schema.org/draft-04/schema#', 161 | 'title' => 'Domain', 162 | 'type' => 'object', 163 | 'properties' => array( 164 | 'id' => array( 165 | 'context' => array( 'view', 'edit' ), 166 | 'description' => __( 'Unique identifier for the object.', 'dark-matter' ), 167 | 'readonly' => true, 168 | 'type' => 'integer', 169 | ), 170 | 'domain' => array( 171 | 'context' => array( 'view', 'edit' ), 172 | 'default' => '', 173 | 'description' => __( 'Domain name.', 'dark-matter' ), 174 | 'required' => true, 175 | 'type' => 'string', 176 | ), 177 | 'is_primary' => array( 178 | 'context' => array( 'view', 'edit' ), 179 | 'default' => null, 180 | 'description' => __( 'Domain is the primary domain for the Site.', 'dark-matter' ), 181 | 'required' => false, 182 | 'type' => 'boolean', 183 | ), 184 | 'is_active' => array( 185 | 'context' => array( 'view', 'edit' ), 186 | 'default' => null, 187 | 'description' => __( 'Domain is currently being used.', 'dark-matter' ), 188 | 'required' => false, 189 | 'type' => 'boolean', 190 | ), 191 | 'is_https' => array( 192 | 'context' => array( 'view', 'edit' ), 193 | 'default' => null, 194 | 'description' => __( 'Domain is to be available on the HTTPS protocol.', 'dark-matter' ), 195 | 'required' => false, 196 | 'type' => 'boolean', 197 | ), 198 | 'type' => array( 199 | 'context' => array( 'view', 'edit' ), 200 | 'default' => null, 201 | 'description' => __( 'Type of domain.', 'dark-matter' ), 202 | 'required' => false, 203 | 'type' => 'integer', 204 | ), 205 | 'site' => array( 206 | 'description' => __( 'Site ID the domain is assigned against.', 'dark-matter' ), 207 | 'type' => 'object', 208 | 'context' => array( 'view', 'edit' ), 209 | 'readonly' => true, 210 | 'properties' => array( 211 | 'blog_id' => array( 212 | 'context' => array( 'view', 'edit' ), 213 | 'description' => __( 'Site ID.', 'dark-matter' ), 214 | 'readonly' => true, 215 | 'required' => false, 216 | 'type' => 'integer', 217 | ), 218 | 'site_id' => array( 219 | 'context' => array( 'view', 'edit' ), 220 | 'description' => __( 'The ID of the site\'s parent network.', 'dark-matter' ), 221 | 'readonly' => true, 222 | 'required' => false, 223 | 'type' => 'integer', 224 | ), 225 | 'domain' => array( 226 | 'context' => array( 'view', 'edit' ), 227 | 'description' => __( 'Domain of the site.', 'dark-matter' ), 228 | 'readonly' => true, 229 | 'required' => false, 230 | 'type' => 'string', 231 | ), 232 | 'path' => array( 233 | 'context' => array( 'view', 'edit' ), 234 | 'description' => __( 'Path of the site.', 'dark-matter' ), 235 | 'readonly' => true, 236 | 'required' => false, 237 | 'type' => 'string', 238 | ), 239 | 'registered' => array( 240 | 'context' => array( 'view', 'edit' ), 241 | 'description' => __( 'The date on which the site was created or registered.', 'dark-matter' ), 242 | 'format' => 'date-time', 243 | 'readonly' => true, 244 | 'required' => false, 245 | 'type' => 'string', 246 | ), 247 | 'last_updated' => array( 248 | 'context' => array( 'view', 'edit' ), 249 | 'description' => __( 'The date and time on which site settings were last updated.', 'dark-matter' ), 250 | 'format' => 'date-time', 251 | 'readonly' => true, 252 | 'required' => false, 253 | 'type' => 'string', 254 | ), 255 | 'public' => array( 256 | 'context' => array( 'view', 'edit' ), 257 | 'description' => __( 'Whether the site should be treated as public.', 'dark-matter' ), 258 | 'readonly' => true, 259 | 'required' => false, 260 | 'type' => 'integer', 261 | ), 262 | 'archived' => array( 263 | 'context' => array( 'view', 'edit' ), 264 | 'description' => __( 'Whether the site should be treated as archived.', 'dark-matter' ), 265 | 'readonly' => true, 266 | 'required' => false, 267 | 'type' => 'boolean', 268 | ), 269 | 'mature' => array( 270 | 'context' => array( 'view', 'edit' ), 271 | 'description' => __( 'Whether the site should be treated as mature.', 'dark-matter' ), 272 | 'readonly' => true, 273 | 'required' => false, 274 | 'type' => 'boolean', 275 | ), 276 | 'spam' => array( 277 | 'context' => array( 'view', 'edit' ), 278 | 'description' => __( 'Whether the site should be treated as spam.', 'dark-matter' ), 279 | 'readonly' => true, 280 | 'required' => false, 281 | 'type' => 'boolean', 282 | ), 283 | 'deleted' => array( 284 | 'context' => array( 'view', 'edit' ), 285 | 'description' => __( 'Whether the site should be treated as deleted.', 'dark-matter' ), 286 | 'readonly' => true, 287 | 'required' => false, 288 | 'type' => 'boolean', 289 | ), 290 | ), 291 | ), 292 | ), 293 | ); 294 | 295 | return $schema; 296 | } 297 | 298 | /** 299 | * Return a list of Domains. 300 | * 301 | * @since 2.0.0 302 | * 303 | * @param WP_REST_Request $request Current request. 304 | * @return WP_REST_Response|mixed WP_REST_Response on success. WP_Error on failure. 305 | */ 306 | public function get_items( $request ) { 307 | $site_id = null; 308 | 309 | /** 310 | * Handle the processing of the Site ID parameter if it is provided. If 311 | * not, then set the $site_id to the Current Blog ID unless it is the 312 | * main site calling this endpoint. For the main site, we return all the 313 | * Domains for all Sites on the WordPress Network. 314 | */ 315 | if ( isset( $request['site_id'] ) ) { 316 | $site_id = $request['site_id']; 317 | } elseif ( ! is_main_site() ) { 318 | $site_id = get_current_blog_id(); 319 | } 320 | 321 | $db = DarkMatter_Domains::instance(); 322 | 323 | $response = array(); 324 | 325 | $result = $db->get_domains( $site_id ); 326 | 327 | /** 328 | * Return errors as-is. This is maintain consistency and parity with the 329 | * WP CLI commands. 330 | */ 331 | if ( is_wp_error( $result ) ) { 332 | return rest_ensure_response( $result ); 333 | } 334 | 335 | /** 336 | * Process the domains and prepare each for the JSON response. 337 | */ 338 | foreach ( $result as $dm_domain ) { 339 | $response[] = $this->prepare_item_for_response( $dm_domain, $request ); 340 | } 341 | 342 | return rest_ensure_response( $response ); 343 | } 344 | 345 | /** 346 | * Checks if a given request has access to get a domain or list of domains. 347 | * 348 | * @since 2.0.0 349 | * 350 | * @param WP_REST_Request $request Current request. 351 | * @return boolean True if the current user is a Super Admin. False otherwise. 352 | */ 353 | public function get_items_permissions_check( $request ) { 354 | /** This action is documented in domain-mapping/classes/class-dm-ui.php */ 355 | return current_user_can( apply_filters( 'dark_matter_domain_permission', 'upgrade_network', 'rest-get' ) ); 356 | } 357 | 358 | /** 359 | * Prepare item for adding to the database. 360 | * 361 | * @since 2.0.0 362 | * 363 | * @param WP_REST_Request $request Current request. 364 | * @return array Data provided by the call to the endpoint. 365 | */ 366 | protected function prepare_item_for_database( $request ) { 367 | $item = array( 368 | 'domain' => '', 369 | 'is_primary' => null, 370 | 'is_https' => null, 371 | 'is_active' => null, 372 | 'type' => null, 373 | ); 374 | 375 | $method = $request->get_method(); 376 | 377 | foreach ( $item as $key => $default ) { 378 | $value = $default; 379 | 380 | if ( isset( $request[ $key ] ) ) { 381 | $value = $request[ $key ]; 382 | } 383 | 384 | if ( WP_REST_Server::CREATABLE === $method && null === $value && 'is_primary' === $key ) { 385 | $value = false; 386 | } 387 | 388 | if ( WP_REST_Server::CREATABLE === $method && null === $value && 'is_https' === $key ) { 389 | $value = false; 390 | } 391 | 392 | if ( WP_REST_Server::CREATABLE === $method && null === $value && 'is_active' === $key ) { 393 | $value = true; 394 | } 395 | 396 | $item[ $key ] = $value; 397 | } 398 | 399 | return $item; 400 | } 401 | 402 | /** 403 | * Prepares a single domain output for response. 404 | * 405 | * @since 2.0.0 406 | * 407 | * @param DM_Domain $item Domain object to be prepared for response. 408 | * @param WP_REST_Request $request Current request. 409 | * @return array Prepared item for REST response. 410 | */ 411 | public function prepare_item_for_response( $item, $request ) { 412 | $fields = $this->get_fields_for_response( $request ); 413 | 414 | $data = array(); 415 | 416 | if ( in_array( 'id', $fields, true ) ) { 417 | $data['id'] = $item->id; 418 | } 419 | 420 | if ( in_array( 'domain', $fields, true ) ) { 421 | $data['domain'] = $item->domain; 422 | } 423 | 424 | if ( in_array( 'is_primary', $fields, true ) ) { 425 | $data['is_primary'] = $item->is_primary; 426 | } 427 | 428 | if ( in_array( 'is_active', $fields, true ) ) { 429 | $data['is_active'] = $item->active; 430 | } 431 | 432 | if ( in_array( 'is_https', $fields, true ) ) { 433 | $data['is_https'] = $item->is_https; 434 | } 435 | 436 | if ( in_array( 'type', $fields, true ) ) { 437 | $data['type'] = $item->type; 438 | } 439 | 440 | if ( in_array( 'site', $fields, true ) ) { 441 | $site_data = get_site( $item->blog_id ); 442 | 443 | if ( ! empty( $site_data ) ) { 444 | $data['site'] = $site_data->to_array(); 445 | 446 | $data['site']['blog_id'] = absint( $data['site']['blog_id'] ); 447 | $data['site']['site_id'] = absint( $data['site']['site_id'] ); 448 | $data['site']['public'] = absint( $data['site']['public'] ); 449 | $data['site']['archived'] = boolval( $data['site']['archived'] ); 450 | $data['site']['mature'] = boolval( $data['site']['mature'] ); 451 | $data['site']['spam'] = boolval( $data['site']['spam'] ); 452 | $data['site']['deleted'] = boolval( $data['site']['deleted'] ); 453 | 454 | if ( '0000-00-00 00:00:00' === $data['site']['registered'] ) { 455 | $data['site']['registered'] = null; 456 | } else { 457 | $data['site']['registered'] = mysql_to_rfc3339( $data['site']['registered'] ); 458 | } 459 | 460 | if ( '0000-00-00 00:00:00' === $data['site']['last_updated'] ) { 461 | $data['site']['last_updated'] = null; 462 | } else { 463 | $data['site']['last_updated'] = mysql_to_rfc3339( $data['site']['last_updated'] ); 464 | } 465 | } else { 466 | $data['site'] = null; 467 | } 468 | } 469 | 470 | return $data; 471 | } 472 | 473 | /** 474 | * Register the routes for the REST API. 475 | * 476 | * @since 2.0.0 477 | * 478 | * @return void 479 | */ 480 | public function register_routes() { 481 | register_rest_route( 482 | $this->namespace, 483 | $this->rest_base, 484 | array( 485 | 'methods' => WP_REST_Server::CREATABLE, 486 | 'callback' => array( $this, 'create_item' ), 487 | 'permission_callback' => array( $this, 'create_item_permissions_check' ), 488 | 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), 489 | ) 490 | ); 491 | 492 | register_rest_route( 493 | $this->namespace, 494 | $this->rest_base . '/(?P.+)', 495 | array( 496 | 'args' => array( 497 | 'domain' => array( 498 | 'description' => __( 'Site ID to retrieve a list of Domains.', 'dark-matter' ), 499 | 'required' => true, 500 | 'type' => 'string', 501 | ), 502 | ), 503 | array( 504 | 'methods' => WP_REST_Server::READABLE, 505 | 'callback' => array( $this, 'get_item' ), 506 | 'permission_callback' => array( $this, 'get_items_permissions_check' ), 507 | 'schema' => array( $this, 'get_item_schema' ), 508 | ), 509 | array( 510 | 'methods' => WP_REST_Server::DELETABLE, 511 | 'callback' => array( $this, 'delete_item' ), 512 | 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 513 | 'args' => array( 514 | 'force' => array( 515 | 'default' => false, 516 | 'description' => __( 'Force Dark Matter to remove the domain. This is required if you wish to remove a Primary domain from a Site.', 'dark-matter' ), 517 | 'type' => 'boolean', 518 | ), 519 | ), 520 | ), 521 | array( 522 | 'methods' => WP_REST_Server::EDITABLE, 523 | 'callback' => array( $this, 'update_item' ), 524 | 'permission_callback' => array( $this, 'update_item_permissions_check' ), 525 | 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), 526 | ), 527 | ) 528 | ); 529 | 530 | register_rest_route( 531 | $this->namespace, 532 | $this->rest_base_plural, 533 | array( 534 | 'methods' => WP_REST_Server::READABLE, 535 | 'callback' => array( $this, 'get_items' ), 536 | 'permission_callback' => array( $this, 'get_items_permissions_check' ), 537 | 'schema' => array( $this, 'get_item_schema' ), 538 | ) 539 | ); 540 | 541 | register_rest_route( 542 | $this->namespace, 543 | $this->rest_base_plural . '/(?P[\d]+)', 544 | array( 545 | 'args' => array( 546 | 'site_id' => array( 547 | 'description' => __( 'Site ID to retrieve a list of Domains.', 'dark-matter' ), 548 | 'required' => true, 549 | 'type' => 'integer', 550 | ), 551 | ), 552 | array( 553 | 'methods' => WP_REST_Server::READABLE, 554 | 'callback' => array( $this, 'get_items' ), 555 | 'permission_callback' => array( $this, 'get_items_permissions_check' ), 556 | 'schema' => array( $this, 'get_item_schema' ), 557 | ), 558 | ) 559 | ); 560 | } 561 | 562 | /** 563 | * Update a domain for a Site. 564 | * 565 | * @since 2.0.0 566 | * 567 | * @param WP_REST_Request $request Current request. 568 | * @return WP_REST_Response|mixed WP_REST_Response on success. WP_Error on failure. 569 | */ 570 | public function update_item( $request ) { 571 | $db = DarkMatter_Domains::instance(); 572 | 573 | $item = $this->prepare_item_for_database( $request ); 574 | 575 | $result = $db->update( 576 | $item['domain'], 577 | $item['is_primary'], 578 | $item['is_https'], 579 | $request['force'], 580 | $item['is_active'], 581 | $item['type'] 582 | ); 583 | 584 | /** 585 | * Return errors as-is. This is maintain consistency and parity with the 586 | * WP CLI commands. 587 | */ 588 | if ( is_wp_error( $result ) ) { 589 | return rest_ensure_response( $result ); 590 | } 591 | 592 | /** 593 | * Prepare response for successfully adding a domain. 594 | */ 595 | $response = $this->prepare_item_for_response( $result, $request ); 596 | $response = rest_ensure_response( $response ); 597 | 598 | return $response; 599 | } 600 | 601 | /** 602 | * Checks if a given request has access to update a domain. 603 | * 604 | * @since 2.0.0 605 | * 606 | * @param WP_REST_Request $request Current request. 607 | * @return boolean True if the current user is a Super Admin. False otherwise. 608 | */ 609 | public function update_item_permissions_check( $request ) { 610 | /** This action is documented in domain-mapping/classes/class-dm-ui.php */ 611 | return current_user_can( apply_filters( 'dark_matter_domain_permission', 'upgrade_network', 'rest-update' ) ); 612 | } 613 | } 614 | 615 | /** 616 | * Setup the REST Controller for Domains for use. 617 | * 618 | * @since 2.0.0 619 | * 620 | * @return void 621 | */ 622 | function dark_matter_domains_rest() { 623 | $controller = new DM_REST_Domains_Controller(); 624 | $controller->register_routes(); 625 | } 626 | add_action( 'rest_api_init', 'dark_matter_domains_rest' ); 627 | --------------------------------------------------------------------------------