├── .distignore ├── .github ├── FUNDING.yml └── workflows │ ├── php-sniffer-ci.yml │ ├── phpstan-ci.yml │ ├── wporg-deploy.yml │ └── wporg-update-assets.yml ├── .wordpress-org ├── banner-1544x500.png ├── banner-772x250.png ├── icon-128x128.png ├── icon-256x256.png ├── screenshot-1.png ├── screenshot-2.png └── screenshot-3.png ├── license.txt ├── phpstan.neon.dist ├── readme.txt ├── wp-multi-network ├── assets │ ├── css │ │ └── wp-multi-network.css │ └── js │ │ └── wp-multi-network.js └── includes │ ├── classes │ ├── class-wp-ms-network-command.php │ ├── class-wp-ms-networks-admin-bar.php │ ├── class-wp-ms-networks-admin.php │ ├── class-wp-ms-networks-capabilities.php │ ├── class-wp-ms-networks-list-table.php │ └── class-wp-ms-rest-networks-controller.php │ ├── compat.php │ ├── deprecated.php │ ├── functions.php │ └── metaboxes │ ├── edit-network.php │ └── move-site.php └── wpmn-loader.php /.distignore: -------------------------------------------------------------------------------- 1 | # A set of files you probably don't want in your WordPress.org distribution 2 | .distignore 3 | .editorconfig 4 | .git 5 | .gitignore 6 | .gitattributes 7 | .travis.yml 8 | .DS_Store 9 | .wordpress-org 10 | .github 11 | tests 12 | composer.json 13 | composer.lock 14 | CONTRIBUTING.md 15 | deploy.sh 16 | LICENSE.md 17 | license.txt 18 | phpcs.xml.dist 19 | phpunit.xml.dist 20 | README.md 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | custom: ["https://buy.stripe.com/7sI3cd2tK1Cy2lydQR"] 3 | -------------------------------------------------------------------------------- /.github/workflows/php-sniffer-ci.yml: -------------------------------------------------------------------------------- 1 | name: PHP - Code Sniffer 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release/* 8 | pull_request: 9 | 10 | jobs: 11 | sniff-php: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: '7.2' 21 | coverage: none 22 | tools: composer, cs2pr 23 | 24 | - name: Get Composer cache directory 25 | id: composer-cache 26 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 27 | 28 | - name: Setup Composer cache 29 | uses: pat-s/always-upload-cache@v2.1.3 30 | with: 31 | path: ${{ steps.composer-cache.outputs.dir }} 32 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 33 | restore-keys: | 34 | ${{ runner.os }}-composer- 35 | ${{ runner.os }}- 36 | 37 | - name: Validate composer.json 38 | run: composer --no-interaction validate --no-check-all 39 | 40 | - name: Install dependencies 41 | run: composer install --prefer-dist --no-suggest --no-progress --no-interaction 42 | 43 | - name: Detect coding standard violations (PHPCS) 44 | run: vendor/bin/phpcs -q --report=checkstyle --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 | cs2pr --graceful-warnings 45 | -------------------------------------------------------------------------------- /.github/workflows/phpstan-ci.yml: -------------------------------------------------------------------------------- 1 | name: Run PHPStan 2 | 3 | on: [push] 4 | 5 | jobs: 6 | run-phpstan: 7 | name: Static Analysis 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | - name: Setup PHP 13 | uses: shivammathur/setup-php@v2 14 | with: 15 | php-version: 'latest' 16 | coverage: none 17 | tools: composer, cs2pr 18 | - name: Install PHP dependencies 19 | uses: ramsey/composer-install@v2 20 | with: 21 | composer-options: '--prefer-dist --no-scripts' 22 | - name: PHPStan 23 | run: composer phpstan 24 | -------------------------------------------------------------------------------- /.github/workflows/wporg-deploy.yml: -------------------------------------------------------------------------------- 1 | name: WordPress.org - Deploy 2 | on: 3 | push: 4 | tags: 5 | - "**" 6 | jobs: 7 | tag: 8 | name: New tag 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@main 12 | 13 | - name: WordPress Plugin Deploy 14 | uses: 10up/action-wordpress-plugin-deploy@stable 15 | env: 16 | SLUG: ${{ secrets.SLUG }} 17 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 18 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} -------------------------------------------------------------------------------- /.github/workflows/wporg-update-assets.yml: -------------------------------------------------------------------------------- 1 | name: WordPress.org - Update assets 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | master: 8 | name: Push to main 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@main 12 | - name: WordPress.org asset update 13 | uses: 10up/action-wordpress-plugin-asset-update@stable 14 | env: 15 | SLUG: ${{ secrets.SLUG }} 16 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 17 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} -------------------------------------------------------------------------------- /.wordpress-org/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuttter/wp-multi-network/542d9c11e54b1dd6d36a13b0b850a699344bf79f/.wordpress-org/banner-1544x500.png -------------------------------------------------------------------------------- /.wordpress-org/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuttter/wp-multi-network/542d9c11e54b1dd6d36a13b0b850a699344bf79f/.wordpress-org/banner-772x250.png -------------------------------------------------------------------------------- /.wordpress-org/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuttter/wp-multi-network/542d9c11e54b1dd6d36a13b0b850a699344bf79f/.wordpress-org/icon-128x128.png -------------------------------------------------------------------------------- /.wordpress-org/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuttter/wp-multi-network/542d9c11e54b1dd6d36a13b0b850a699344bf79f/.wordpress-org/icon-256x256.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuttter/wp-multi-network/542d9c11e54b1dd6d36a13b0b850a699344bf79f/.wordpress-org/screenshot-1.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuttter/wp-multi-network/542d9c11e54b1dd6d36a13b0b850a699344bf79f/.wordpress-org/screenshot-2.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuttter/wp-multi-network/542d9c11e54b1dd6d36a13b0b850a699344bf79f/.wordpress-org/screenshot-3.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - wp-multi-network/includes 5 | - wpmn-loader.php 6 | scanFiles: 7 | - %rootDir%/../../php-stubs/wp-cli-stubs/wp-cli-stubs.php 8 | - %rootDir%/../../php-stubs/wp-cli-stubs/wp-cli-commands-stubs.php 9 | #- %rootDir%/../../php-stubs/wp-cli-stubs/wp-cli-i18n-stubs.php 10 | #- %rootDir%/../../php-stubs/wp-cli-stubs/wp-cli-tools-stubs.php 11 | ignoreErrors: 12 | - '/^Call to static method encode\(\) on an unknown class Requests_IDNAEncoder\.$/' 13 | - '/^Constant WP_CONTENT_URL not found\.$/' 14 | # WP_Network::$blog_id is a private property that can be accessed via magic methods. 15 | - '/^Access to an undefined property WP_Network::\$blog_id\.$/' 16 | # WP_CLI\Fetchers\User::get() returns WP_User without root namespace. 17 | - '/^Access to property \$ID on an unknown class WP_CLI\\Fetchers\\WP_User\.$/' 18 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === WP Multi Network === 2 | Author: Triple J Software, Inc. 3 | Author URI: https://jjj.software 4 | Donate link: https://buy.stripe.com/7sI3cd2tK1Cy2lydQR 5 | Plugin URI: https://wordpress.org/plugins/wp-multi-network/ 6 | License URI: https://www.gnu.org/licenses/gpl-2.0.html 7 | License: GPLv2 or later 8 | Contributors: johnjamesjacoby, flixos90, rmccue, spacedmonkey 9 | Tags: network, sites, domains, global, admin 10 | Requires PHP: 5.2 11 | Requires at least: 4.9 12 | Tested up to: 6.1 13 | Stable tag: 2.5.2 14 | 15 | == Description == 16 | 17 | Turn your WordPress Multisite installation into many multisite networks, surrounding one global set of users. 18 | 19 | * Reveals hidden WordPress Multisite functionality. 20 | * Includes a "Networks" top-level Network-Admin menu. 21 | * Includes a List Table for viewing available networks. 22 | * Allows moving subsites between networks. 23 | * Allows global administrators to create new networks with their own sites and domain arrangements. 24 | * Group sites into logical networks using nearly any combination of domain (example.org) and path (/site/). 25 | 26 | == Installation == 27 | 28 | * Download and install using the built in WordPress plugin installer. 29 | * Activate in the "Plugins" network admin panel using the "Network Activate" link. 30 | * Comment out the `DOMAIN_CURRENT_SITE` line in your `wp-config.php` file. If you don't have this line, you probably need to enable multisite. 31 | * Start planning and creating your networks. 32 | 33 | == Frequently Asked Questions == 34 | 35 | = Can each network have a different domain? = 36 | 37 | Yes you can. That is what this plugin does best! 38 | 39 | Think of how WordPress.org works: 40 | 41 | * wordpress.org 42 | * make.wordpress.org/core 43 | * buddypress.org 44 | * codex.buddypress.org 45 | * bbpress.org 46 | * codex.bbpress.org 47 | * wordcamp.org 48 | * us.wordcamp.org/2021 49 | 50 | Users are global, and they can login to any of those domains with the same login and password. Each of those domains has their own subdomains and subdirectories, many of which are sites or (networks of them). 51 | 52 | = Will this work on standard WordPress? = 53 | 54 | You can activate it, but it won't do anything. You need to have the multisite functionality enabled and working first. 55 | 56 | = Where can I get support? = 57 | 58 | Create a GitHub issue: https://github.com/stuttter/wp-multi-network/issues/new 59 | 60 | = What about multisite constants? = 61 | 62 | For maximum flexibility, use something like... 63 | 64 | ` 65 | // Multisite 66 | define( 'MULTISITE', true ); 67 | define( 'SUBDOMAIN_INSTALL', false ); 68 | define( 'PATH_CURRENT_SITE', '/' ); 69 | define( 'DOMAIN_CURRENT_SITE', $_SERVER['HTTP_HOST'] ); 70 | 71 | // Likely not needed anymore (your config may vary) 72 | //define( 'SITE_ID_CURRENT_SITE', 1 ); 73 | //define( 'BLOG_ID_CURRENT_SITE', 1 ); 74 | 75 | // Un-comment and change to a URL to funnel no-site-found requests to 76 | //define( 'NOBLOGREDIRECT', '/404/' ); 77 | 78 | /** 79 | * These are purposely set for maximum compatibility with multisite and 80 | * multi-network. Your config may vary. 81 | */ 82 | define( 'WP_HOME', 'https://' . $_SERVER['HTTP_HOST'] ); 83 | define( 'WP_SITEURL', 'https://' . $_SERVER['HTTP_HOST'] ); 84 | ` 85 | 86 | = What about cookies? = 87 | 88 | Use something like this to allow cookies to work across networks... 89 | 90 | ` 91 | // Cookies 92 | define( 'COOKIEHASH', md5( 'yourrootdomain.com' ) ); 93 | define( 'COOKIE_DOMAIN', 'yourrootdomain.com' ); 94 | define( 'ADMIN_COOKIE_PATH', '/' ); 95 | define( 'COOKIEPATH', '/' ); 96 | define( 'SITECOOKIEPATH', '/' ); 97 | define( 'TEST_COOKIE', 'thing_test_cookie' ); 98 | define( 'AUTH_COOKIE', 'thing_' . COOKIEHASH ); 99 | define( 'USER_COOKIE', 'thing_user_' . COOKIEHASH ); 100 | define( 'PASS_COOKIE', 'thing_pass_' . COOKIEHASH ); 101 | define( 'SECURE_AUTH_COOKIE', 'thing_sec_' . COOKIEHASH ); 102 | define( 'LOGGED_IN_COOKIE', 'thing_logged_in' . COOKIEHASH ); 103 | ` 104 | 105 | = Uploads? = 106 | 107 | As of version 3.5, new WordPress multisite installs use a more efficient way to serve uploaded files. 108 | Unfortunately, this doesn't play well with multiple networks (yet). Installs that upgraded from 3.4 or below are not affected. 109 | 110 | WP Multi-Network needs to be running to help set the upload path for new sites, so all networks created with this plugin will have it network activated. 111 | If you disable it on one of your networks, any new site you create on that network will store its uploaded files under that network's main site's uploads folder. It's not pretty. 112 | 113 | Just leave this plugin network-activated (or in mu-plugins) and it will take care of everything. 114 | 115 | = Can I achieve a multi-level URL path structure (domain/network/site) with a subfolder network? = 116 | 117 | To achieve nested folder paths in this fashion network1/site1, network1/site2 etc, 118 | please follow the steps in https://paulund.co.uk/wordpress-multisite-nested-paths to construct a custom sunrise.php (Thanks to https://paulund.co.uk for providing these steps). 119 | 120 | = Where can I find documentation? = 121 | 122 | Not much to talk about really. Check the code for details! 123 | 124 | == Changelog == 125 | = 2.5.2 = 126 | * Use get_main_site_id function instead of get_main_site_for_network. 127 | * Tested against WordPress 6.1. 128 | 129 | = 2.5.1 = 130 | * Save main site on network as network option. 131 | 132 | = 2.5.0 = 133 | * Fix new networks sometimes not being created. 134 | * Fix moving sites sometimes not working. 135 | * Fix network name always being "New Network". 136 | * Fix several debug notices related to filter_input(). 137 | * Fix several redirection & admin-notice issues. 138 | * Allow networks to be created with empty network name & site name. 139 | * Update author link & plugin meta data. 140 | 141 | = 2.4.2 = 142 | * Update code for WordPress coding standards. 143 | * Other small bug fixes. 144 | 145 | = 2.4.1 = 146 | * Update required PHP / wordpress versions. 147 | 148 | = 2.4.0 = 149 | * Add networks REST API endpoint. 150 | 151 | = 2.3.0 = 152 | * Add network capability mapping. 153 | * Add WP CLI command. 154 | * Other improvements. 155 | 156 | = 2.2.1 = 157 | * Fix upload paths still using blogs.dir. 158 | 159 | = 2.2.0 = 160 | * WordPress 4.9 minimum version bump. 161 | * Fix bug preventing sites from being moved. 162 | * Tweak some styling. 163 | * Use more WordPress core functions for sites & networks. 164 | 165 | = 2.1.0 = 166 | * Add nonce checks to forms. 167 | * Add validation & output sanitization to form fields. 168 | 169 | = 2.0.0 = 170 | * WordPress 4.6 minimum version bump. 171 | * Caching improvements for WordPress 4.6. 172 | * Refactor list tables & admin method code. 173 | 174 | = 1.8.1 = 175 | * Fix site reassignment metabox from moving sites incorrectly. 176 | 177 | = 1.8.0 = 178 | * Support for core compat functions. 179 | * Fix bug causing site moves to break. 180 | * Fix bug allowing duplicate site URLs. 181 | * Remove _network_option() functions. 182 | * Remove network.zero placeholder. 183 | * WordPress 4.5 & 4.6 compatibility updates. 184 | 185 | = 1.7.0 = 186 | * WordPress 4.4 compatibility updates. 187 | * Metabox overhaul. 188 | * network.zero improvements. 189 | * Fix site assignments. 190 | * Various UI improvements. 191 | * Global, class, function, and method cleanup. 192 | 193 | = 1.6.1 = 194 | * WordPress 4.3 UI compatibility updates. 195 | * Remove site "Actions" column integration. 196 | 197 | = 1.6.0 = 198 | * Move inclusion to muplugins_loaded. 199 | * Introduce network switching API. 200 | * Introduce network options API. 201 | * Update action links to better match sites list. 202 | * Better support for domain mapping. 203 | * Refactor file names & locations. 204 | * Deprecate wpmn_fix_subsite_upload_path(). 205 | * Include basic WPCLI support. 206 | * Escaped gettext output. 207 | * Fix bulk network deletion. 208 | * Scrutinized code formatting. 209 | 210 | = 1.5.1 = 211 | * Fix debug notices when creating networks. 212 | * Fix incorrect variable usage causing weird output. 213 | * Adds default path when creating new networks. 214 | 215 | = 1.5 = 216 | * Support for WordPress 3.8. 217 | * Finally, a menu icon! 218 | * Improved output sanitization. 219 | 220 | = 1.4.1 = 221 | * Fix issue when changing network domain or path - contributed by mgburns. 222 | * Improve support for native uploaded file handling. 223 | 224 | = 1.4 = 225 | * Fix admin pages (let us know if you find something broken). 226 | * Add support for WP 3.5+ upload handling - thanks, RavanH (see notes: "What's up with uploads?"). 227 | 228 | = 1.3.1 = 229 | * Fix prepare() usages. 230 | * Fix some debug notices. 231 | 232 | = 1.3 = 233 | * Refactor into smaller pieces. 234 | * Add PHP docs. 235 | * Deprecate functions for friendlier core-style functions. 236 | * Code clean-up. 237 | * Remove inline JavaScript. 238 | 239 | = 1.2 = 240 | * Implemented 3.1 Network Admin Menu, backwards compatibility maintained. 241 | * Fix multiple minor issues. 242 | * Add Site Admin and Network Admin to Network lists. 243 | * Add various security and bullet proofing. 244 | 245 | = 1.1 = 246 | * Better WordPress 3.0 compatibility. 247 | 248 | = 1.0 = 249 | Getting started. 250 | -------------------------------------------------------------------------------- /wp-multi-network/assets/css/wp-multi-network.css: -------------------------------------------------------------------------------- 1 | th.column-title { 2 | width: 35%; 3 | } 4 | th.column-path { 5 | width: 15%; 6 | } 7 | th.column-blogs { 8 | width: 10%; 9 | } 10 | td.column-domain { 11 | white-space: nowrap; 12 | overflow: hidden; 13 | } 14 | 15 | .edit-network label span.scheme { 16 | display: inline-block; 17 | } 18 | 19 | .edit-network .form-field label input[type="text"] { 20 | width: 50%; 21 | display: inline-block; 22 | } 23 | 24 | #poststuff #wpmn-edit-network-assign-sites .inside { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | #wpmn-edit-network-publish .submitbox, 30 | #wpmn-move-site-publish .submitbox { 31 | margin: 10px -12px -12px; 32 | } 33 | 34 | #wpmn-edit-network-publish #major-publishing-actions, 35 | #wpmn-move-site-publish #major-publishing-actions { 36 | margin-top: 10px; 37 | } 38 | 39 | #misc-publishing-actions #network:before, 40 | #misc-publishing-actions #sites:before { 41 | font: normal 20px/1 dashicons; 42 | speak: none; 43 | display: inline-block; 44 | padding: 0 2px 0 0; 45 | top: 0; 46 | left: -1px; 47 | position: relative; 48 | vertical-align: top; 49 | -webkit-font-smoothing: antialiased; 50 | -moz-osx-font-smoothing: grayscale; 51 | text-decoration: none !important; 52 | } 53 | 54 | #misc-publishing-actions #network:before { 55 | content: '\f319'; 56 | } 57 | 58 | #misc-publishing-actions #sites:before { 59 | content: '\f325'; 60 | } 61 | 62 | table.widefat.move-site, 63 | table.widefat.assign-sites { 64 | border: none; 65 | box-shadow: none; 66 | } 67 | 68 | table.move-site thead th, 69 | table.assign-sites thead th { 70 | text-align: center; 71 | font-size: 13px; 72 | font-weight: 600; 73 | background: #f4f4f4; 74 | } 75 | 76 | .networks tbody td, 77 | .networks tbody th { 78 | -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1); 79 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1); 80 | padding: 10px 9px; 81 | } 82 | 83 | .networks .current th.check-column { 84 | border-left: 4px solid #00a0d2; 85 | } 86 | 87 | .networks .current td, 88 | .networks .current th { 89 | background-color: #f7fcfe; 90 | padding: 10px 9px; 91 | } 92 | 93 | table.assign-sites select[multiple] { 94 | width: 100%; 95 | height: 80px !important; 96 | margin: 0; 97 | } 98 | 99 | table.assign-sites .button { 100 | margin: 0 5px; 101 | width: 35%; 102 | height: 80px; 103 | } 104 | 105 | table.assign-sites td.column-actions { 106 | width: 16%; 107 | padding: 8px 0; 108 | } 109 | 110 | table.assign-sites td.column-actions .assign { 111 | float: left; 112 | content: "\f341"; 113 | } 114 | 115 | table.assign-sites td.column-actions .unassign { 116 | float: right; 117 | } 118 | 119 | table.assign-sites td.column-available { 120 | padding-right: 0; 121 | } 122 | 123 | table.assign-sites td.column-assigned { 124 | padding-left: 0; 125 | } 126 | table.assign-sites td.column-available, 127 | table.assign-sites td.column-assigned { 128 | width: 42%; 129 | } 130 | 131 | table.assign-sites td.column-assigned { 132 | text-align: right; 133 | } 134 | 135 | div.network-delete { 136 | background: #fff; 137 | border-left: 4px solid #dc3232; 138 | -webkit-box-shadow: 0 1px 1px 0 rgba( 0, 0, 0, 0.1 ); 139 | box-shadow: 0 1px 1px 0 rgba( 0, 0, 0, 0.1 ); 140 | margin: 5px 0 2px; 141 | padding: 1px 12px; 142 | } 143 | 144 | div.network-delete p { 145 | margin: 0.5em 0; 146 | padding: 2px; 147 | } 148 | 149 | ul.delete-sites { 150 | list-style: square; 151 | margin: 10px 20px; 152 | } 153 | 154 | ul.delete-sites li { 155 | margin: 0; 156 | } 157 | -------------------------------------------------------------------------------- /wp-multi-network/assets/js/wp-multi-network.js: -------------------------------------------------------------------------------- 1 | jQuery( document ).ready( 2 | function ( $ ) { 3 | 4 | $( '.if-js-closed' ) 5 | .removeClass( 'if-js-closed' ) 6 | .addClass( 'closed' ); 7 | 8 | $( '.postbox' ).children( 'h3' ).click( 9 | function () { 10 | if ( $( this.parentNode ).hasClass( 'closed' ) ) { 11 | $( this.parentNode ).removeClass( 'closed' ); 12 | } else { 13 | $( this.parentNode ).addClass( 'closed' ); 14 | } 15 | } 16 | ); 17 | 18 | /* Handle clicks to add/remove sites to/from selected list */ 19 | $( 'input[name=assign]' ).click( 20 | function () { 21 | move( 'from', 'to' ); 22 | } 23 | ); 24 | 25 | $( 'input[name=unassign]' ).click( 26 | function () { 27 | move( 'to', 'from' ); 28 | } 29 | ); 30 | 31 | /* Select all sites in "selected" box when submitting */ 32 | $( '#edit-network-form' ).submit( 33 | function () { 34 | $( '#to' ).children( 'option:enabled' ).attr( 'selected', true ); 35 | $( '#from' ).children( 'option:enabled' ).attr( 'selected', true ); 36 | } 37 | ); 38 | 39 | function move( from, to ) { 40 | jQuery( '#' + from ).children( 'option:selected' ).each( 41 | function () { 42 | jQuery( '#' + to ).append( jQuery( this ).clone() ); 43 | jQuery( this ).remove(); 44 | } 45 | ); 46 | } 47 | } 48 | ); 49 | -------------------------------------------------------------------------------- /wp-multi-network/includes/classes/class-wp-ms-network-command.php: -------------------------------------------------------------------------------- 1 | 32 | * : Domain for network 33 | * 34 | * 35 | * : Path for network 36 | * 37 | * --user= 38 | * : Set the WordPress user, this will be the administrator for the site and administrator for the network if network_admin is not provided. 39 | * 40 | * [--network_admin=] 41 | * : This will be the administrator for the network. 42 | * 43 | * [--site_name=] 44 | * : Name of the new network site 45 | * 46 | * [--network_name=] 47 | * : Name of the new network 48 | * 49 | * [--clone_network=] 50 | * : ID of network to clone 51 | * 52 | * [--options_to_clone=] 53 | * : Options to clone to new network 54 | * 55 | * @since 1.3.0 56 | * 57 | * @param string[] $args Positional CLI arguments. 58 | * @param array $assoc_args Associative CLI arguments. 59 | * @return void 60 | */ 61 | public function create( $args, $assoc_args ) { 62 | [ $domain, $path ] = $args; 63 | 64 | $assoc_args = wp_parse_args( 65 | $assoc_args, array( 66 | 'network_admin' => false, 67 | 'site_name' => false, 68 | 'network_name' => false, 69 | 'clone_network' => false, 70 | 'options_to_clone' => false, 71 | ) 72 | ); 73 | 74 | if ( $assoc_args['network_admin'] ) { 75 | $users = new \WP_CLI\Fetchers\User(); 76 | $user = $users->get( $assoc_args['network_admin'] ); 77 | if ( ! $user ) { 78 | WP_CLI::error( 'Super user does not exist.' ); 79 | } 80 | $network_admin_id = $user->ID; 81 | } else { 82 | $network_admin_id = get_current_user_id(); 83 | } 84 | 85 | $clone_network = $assoc_args['clone_network']; 86 | $options_to_clone = false; 87 | 88 | if ( ! empty( $clone_network ) && ! get_network( $clone_network ) ) { 89 | WP_CLI::error( sprintf( "Clone network %s doesn't exist.", $clone_network ) ); 90 | } 91 | 92 | $network_id = add_network( 93 | array( 94 | 'domain' => $domain, 95 | 'path' => $path, 96 | 'site_name' => $assoc_args['site_name'], 97 | 'network_name' => $assoc_args['network_name'], 98 | 'user_id' => get_current_user_id(), 99 | 'network_admin_id' => $network_admin_id, 100 | 'clone_network' => $clone_network, 101 | 'options_to_clone' => $options_to_clone, 102 | ) 103 | ); 104 | 105 | if ( is_wp_error( $network_id ) ) { 106 | WP_CLI::error( $network_id ); 107 | } 108 | 109 | WP_CLI::success( sprintf( 'Created network %d.', $network_id ) ); 110 | } 111 | 112 | /** 113 | * Update a network. 114 | * 115 | * 116 | * : ID for network 117 | * 118 | * 119 | * : Domain for network 120 | * 121 | * [--path=] 122 | * : Path for network 123 | * 124 | * @since 1.3.0 125 | * 126 | * @param string[] $args Positional CLI arguments. 127 | * @param array $assoc_args Associative CLI arguments. 128 | * @return void 129 | */ 130 | public function update( $args, $assoc_args ) { 131 | [ $id, $domain ] = $args; 132 | 133 | $defaults = array( 134 | 'path' => '', 135 | ); 136 | $assoc_args = wp_parse_args( $assoc_args, $defaults ); 137 | 138 | $network_id = update_network( (int) $id, $domain, $assoc_args['path'] ); 139 | 140 | if ( is_wp_error( $network_id ) ) { 141 | WP_CLI::error( $network_id ); 142 | } 143 | 144 | WP_CLI::success( sprintf( 'Updated network %d.', $id ) ); 145 | } 146 | 147 | /** 148 | * Delete a network. 149 | * 150 | * 151 | * : ID for network 152 | * 153 | * [--delete_blogs=] 154 | * : Delete blogs in this network 155 | * 156 | * @since 1.3.0 157 | * 158 | * @param string[] $args Positional CLI arguments. 159 | * @param array $assoc_args Associative CLI arguments. 160 | * @return void 161 | */ 162 | public function delete( $args, $assoc_args ) { 163 | [ $id ] = $args; 164 | 165 | $assoc_args = wp_parse_args( 166 | $assoc_args, array( 167 | 'delete_blogs' => false, 168 | ) 169 | ); 170 | 171 | $network_id = delete_network( (int) $id, $assoc_args['delete_blogs'] ); 172 | 173 | if ( is_wp_error( $network_id ) ) { 174 | WP_CLI::error( $network_id ); 175 | } 176 | 177 | WP_CLI::success( sprintf( 'Deleted network %d.', $id ) ); 178 | } 179 | 180 | /** 181 | * Move a site to another network. 182 | * 183 | * 184 | * : Site id to move 185 | * 186 | * 187 | * : New network id 188 | * 189 | * @subcommand move-site 190 | * 191 | * @since 1.3.0 192 | * 193 | * @param string[] $args Positional CLI arguments. 194 | * @param array $assoc_args Associative CLI arguments. 195 | * @return void 196 | */ 197 | public function move_site( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed 198 | [ $site_id, $new_network_id ] = $args; 199 | 200 | $network_id = move_site( (int) $site_id, (int) $new_network_id ); 201 | 202 | if ( is_wp_error( $network_id ) ) { 203 | WP_CLI::error( $network_id ); 204 | } 205 | 206 | WP_CLI::success( sprintf( 'Site %1$d has moved to network %2$d.', $site_id, $new_network_id ) ); 207 | } 208 | 209 | /** 210 | * List all networks. 211 | * 212 | * [--fields=] 213 | * : Limit the output to specific row fields. 214 | * 215 | * [--format=] 216 | * : Accepted values: table, csv, json, count. Default: table 217 | * 218 | * ## AVAILABLE FIELDS 219 | * 220 | * These fields will be displayed by default for each term: 221 | * 222 | * * id 223 | * * domain 224 | * * path 225 | * 226 | * @subcommand list 227 | * 228 | * @since 1.3.0 229 | * 230 | * @param string[] $args Positional CLI arguments. 231 | * @param array $assoc_args Associative CLI arguments. 232 | * @return void 233 | */ 234 | public function list_( $args, $assoc_args ) { 235 | $items = get_networks(); 236 | $formatter = $this->get_formatter( $assoc_args ); 237 | $formatter->display_items( $items ); 238 | } 239 | 240 | /** 241 | * Network activate or deactivate a plugin. 242 | * 243 | * 244 | * : Action to perform 245 | * 246 | * 247 | * : Plugin to activate for the network 248 | * 249 | * --network_id= 250 | * : Id of the network to activate on 251 | * 252 | * [--network] 253 | * : If set, the plugin will be activated for the entire multisite network. 254 | * 255 | * [--all] 256 | * : If set, all plugins will be activated. 257 | * 258 | * @since 1.3.0 259 | * 260 | * @param string[] $args Positional CLI arguments. 261 | * @param array $assoc_args Associative CLI arguments. 262 | * @return void 263 | */ 264 | public function plugin( $args, $assoc_args ) { 265 | $fetchers_plugin = new \WP_CLI\Fetchers\Plugin(); 266 | $action = array_shift( $args ); 267 | if ( ! in_array( $action, array( 'activate', 'deactivate' ), true ) ) { 268 | WP_CLI::error( sprintf( '%s is not a supported action.', $action ) ); 269 | } 270 | $network_wide = \WP_CLI\Utils\get_flag_value( $assoc_args, 'network' ); 271 | $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); 272 | 273 | $needing_activation = count( $args ); 274 | $assoc_args = wp_parse_args( 275 | $assoc_args, array( 276 | 'network_id' => false, 277 | ) 278 | ); 279 | $network_id = $assoc_args['network_id']; 280 | if ( get_network( $network_id ) ) { 281 | switch_to_network( $network_id ); 282 | if ( $all ) { 283 | $args = array_map( 284 | function ( $file ) { 285 | return \WP_CLI\Utils\get_plugin_name( $file ); 286 | }, array_keys( get_plugins() ) 287 | ); 288 | } 289 | foreach ( $fetchers_plugin->get_many( $args ) as $plugin ) { 290 | $status = $this->get_status( $plugin->file ); 291 | if ( $all && in_array( $status, array( 'active', 'active-network' ), true ) ) { 292 | --$needing_activation; 293 | continue; 294 | } 295 | 296 | // Network-active is the highest level of activation status. 297 | if ( 'active-network' === $status ) { 298 | WP_CLI::warning( "Plugin '{$plugin->name}' is already network active." ); 299 | continue; 300 | } 301 | 302 | // Don't reactivate active plugins, but do let them become network-active. 303 | if ( ! $network_wide && 'active' === $status ) { 304 | WP_CLI::warning( "Plugin '{$plugin->name}' is already active." ); 305 | continue; 306 | } 307 | 308 | // Plugins need to be deactivated before being network activated. 309 | if ( $network_wide && 'active' === $status ) { 310 | deactivate_plugins( $plugin->file, false, false ); 311 | } 312 | if ( 'activate' === $action ) { 313 | activate_plugins( $plugin->file, '', $network_wide ); 314 | } else { 315 | deactivate_plugins( $plugin->file, false, $network_wide ); 316 | } 317 | 318 | $this->active_output( $plugin->name, $plugin->file, $network_wide, 'activate' ); 319 | } 320 | restore_current_network(); 321 | } else { 322 | WP_CLI::error( sprintf( "Network %s doesn't exist.", $network_id ) ); 323 | } 324 | } 325 | 326 | /** 327 | * Gets the formatter object based on supplied parameters. 328 | * 329 | * @since 1.3.0 330 | * 331 | * @param array $assoc_args Associative CLI arguments. Passed by reference. 332 | * @return WP_CLI\Formatter WP-CLI formatter instance. 333 | */ 334 | protected function get_formatter( &$assoc_args ) { 335 | return new WP_CLI\Formatter( $assoc_args, $this->obj_fields, 'wp-multi-network' ); 336 | } 337 | 338 | /** 339 | * Checks whether a given plugin is active for the given context. 340 | * 341 | * @since 1.3.0 342 | * 343 | * @param string $file Plugin main file path relative to the plugins directory. 344 | * @param bool $network_wide Whether to check network-wide or not. 345 | * @return bool True if the plugin is active for the given context, false otherwise. 346 | */ 347 | private function check_active( $file, $network_wide ) { 348 | $required = $network_wide ? 'active-network' : 'active'; 349 | 350 | return $required === $this->get_status( $file ); 351 | } 352 | 353 | /** 354 | * Gets the activation status for a given plugin. 355 | * 356 | * @since 1.3.0 357 | * 358 | * @param string $file Plugin main file path relative to the plugins directory. 359 | * @return string Plugin activation status. Either 'active', 'active-network', or 'inactive'. 360 | */ 361 | protected function get_status( $file ) { 362 | if ( is_plugin_active_for_network( $file ) ) { 363 | return 'active-network'; 364 | } 365 | 366 | if ( is_plugin_active( $file ) ) { 367 | return 'active'; 368 | } 369 | 370 | return 'inactive'; 371 | } 372 | 373 | /** 374 | * Outputs the result of a plugin activation operation. 375 | * 376 | * @since 1.3.0 377 | * 378 | * @param string $name Plugin name. 379 | * @param string $file Plugin main file path relative to the plugins directory. 380 | * @param bool $network_wide Whether to check network-wide or not. 381 | * @param string $action Action performed. 382 | * @return void 383 | */ 384 | private function active_output( $name, $file, $network_wide, $action ) { 385 | $network_wide = $network_wide || ( is_multisite() && is_network_only_plugin( $file ) ); 386 | 387 | $check = $this->check_active( $file, $network_wide ); 388 | 389 | if ( ( 'activate' === $action ) ? $check : ! $check ) { 390 | if ( $network_wide ) { 391 | WP_CLI::success( "Plugin '{$name}' network {$action}d." ); 392 | } else { 393 | WP_CLI::success( "Plugin '{$name}' {$action}d." ); 394 | } 395 | } else { 396 | WP_CLI::warning( "Could not {$action} the '{$name}' plugin." ); 397 | } 398 | } 399 | } 400 | 401 | WP_CLI::add_command( 'wp-multi-network', 'WP_MS_Network_Command' ); 402 | -------------------------------------------------------------------------------- /wp-multi-network/includes/classes/class-wp-ms-networks-admin-bar.php: -------------------------------------------------------------------------------- 1 | 50 | 56 | add_menu( array( 82 | 'id' => 'my-networks', 83 | 'title' => __( 'My Networks', 'wp-multi-network' ), 84 | 'href' => network_admin_url( 'admin.php?page=networks' ), 85 | 'meta' => array( 86 | 'class' => 'networks-parent', 87 | ), 88 | ) ); 89 | 90 | foreach ( $networks as $network_id ) { 91 | $network = get_network( $network_id ); 92 | if ( ! $network ) { 93 | continue; 94 | } 95 | 96 | switch_to_network( $network_id ); 97 | 98 | if ( ! current_user_can( 'manage_network' ) ) { 99 | restore_current_network(); 100 | continue; 101 | } 102 | 103 | $wp_admin_bar->add_group( array( 104 | 'parent' => 'my-networks', 105 | 'id' => 'group-network-admin-' . $network_id, 106 | ) ); 107 | 108 | $wp_admin_bar->add_menu( array( 109 | 'parent' => 'group-network-admin-' . $network_id, 110 | 'id' => 'network-admin-' . $network_id, 111 | 'title' => $network->site_name, 112 | 'href' => network_admin_url(), 113 | ) ); 114 | 115 | $wp_admin_bar->add_menu( array( 116 | 'parent' => 'network-admin-' . $network_id, 117 | 'id' => 'network-admin-d', 118 | // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 119 | 'title' => __( 'Dashboard' ), 120 | 'href' => network_admin_url(), 121 | ) ); 122 | 123 | if ( current_user_can( 'manage_sites' ) ) { 124 | $wp_admin_bar->add_menu( array( 125 | 'parent' => 'network-admin-' . $network_id, 126 | 'id' => 'network-admin-s' . $network_id, 127 | // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 128 | 'title' => __( 'Sites' ), 129 | 'href' => network_admin_url( 'sites.php' ), 130 | ) ); 131 | } 132 | 133 | if ( current_user_can( 'manage_network_users' ) ) { 134 | $wp_admin_bar->add_menu( array( 135 | 'parent' => 'network-admin-' . $network_id, 136 | 'id' => 'network-admin-u' . $network_id, 137 | // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 138 | 'title' => __( 'Users' ), 139 | 'href' => network_admin_url( 'users.php' ), 140 | ) ); 141 | } 142 | 143 | if ( current_user_can( 'manage_network_themes' ) ) { 144 | $wp_admin_bar->add_menu( array( 145 | 'parent' => 'network-admin-' . $network_id, 146 | 'id' => 'network-admin-t' . $network_id, 147 | // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 148 | 'title' => __( 'Themes' ), 149 | 'href' => network_admin_url( 'themes.php' ), 150 | ) ); 151 | } 152 | 153 | if ( current_user_can( 'manage_network_plugins' ) ) { 154 | $wp_admin_bar->add_menu( array( 155 | 'parent' => 'network-admin-' . $network_id, 156 | 'id' => 'network-admin-p' . $network_id, 157 | // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 158 | 'title' => __( 'Plugins' ), 159 | 'href' => network_admin_url( 'plugins.php' ), 160 | ) ); 161 | } 162 | 163 | if ( current_user_can( 'manage_network_options' ) ) { 164 | $wp_admin_bar->add_menu( array( 165 | 'parent' => 'network-admin-' . $network_id, 166 | 'id' => 'network-admin-o' . $network_id, 167 | // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 168 | 'title' => __( 'Settings' ), 169 | 'href' => network_admin_url( 'settings.php' ), 170 | ) ); 171 | } 172 | 173 | restore_current_network(); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /wp-multi-network/includes/classes/class-wp-ms-networks-admin.php: -------------------------------------------------------------------------------- 1 | > 24 | */ 25 | private $feedback_strings = array(); 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * Hooks in the necessary methods. 31 | * 32 | * @since 1.3.0 33 | */ 34 | public function __construct() { 35 | add_action( 'admin_menu', array( $this, 'admin_menu' ) ); 36 | add_action( 'network_admin_menu', array( $this, 'network_admin_menu' ) ); 37 | add_action( 'network_admin_menu', array( $this, 'network_admin_menu_separator' ) ); 38 | 39 | add_action( 'admin_init', array( $this, 'route_save_handlers' ) ); 40 | 41 | add_action( 'admin_init', array( $this, 'set_feedback_strings' ) ); 42 | add_action( 'network_admin_notices', array( $this, 'network_admin_notices' ) ); 43 | 44 | add_filter( 'manage_sites_action_links', array( $this, 'add_move_blog_link' ), 10, 2 ); 45 | 46 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 47 | } 48 | 49 | /** 50 | * Add the Move action to the Sites page on WP >= 3.1. 51 | * 52 | * @since 1.3.0 53 | * 54 | * @param array $actions Array of action links. 55 | * @param int $blog_id Current site ID. 56 | * @return array Adjusted action links. 57 | */ 58 | public function add_move_blog_link( $actions = array(), $blog_id = 0 ) { 59 | 60 | // Bail if main site for network. 61 | if ( (int) get_current_site()->blog_id === (int) $blog_id ) { 62 | return $actions; 63 | } 64 | 65 | $url = $this->admin_url( 66 | array( 67 | 'action' => 'move', 68 | 'blog_id' => (int) $blog_id, 69 | ) 70 | ); 71 | 72 | if ( current_user_can( 'manage_networks' ) ) { 73 | $actions['move'] = '' . esc_html__( 'Move', 'wp-multi-network' ) . ''; 74 | } 75 | 76 | return $actions; 77 | } 78 | 79 | /** 80 | * Adds the My Networks page to the site-level dashboard. 81 | * 82 | * If the user is super admin on another network, don't require elevated 83 | * permissions on the current site. 84 | * 85 | * @since 1.3.0 86 | * @return void 87 | */ 88 | public function admin_menu() { 89 | 90 | // Bail if user has no networks. 91 | if ( ! user_has_networks() ) { 92 | return; 93 | } 94 | 95 | add_dashboard_page( esc_html__( 'My Networks', 'wp-multi-network' ), esc_html__( 'My Networks', 'wp-multi-network' ), 'read', 'my-networks', array( $this, 'page_my_networks' ) ); 96 | } 97 | 98 | /** 99 | * Add Networks menu and entries to the network-level dashboard 100 | * 101 | * This method puts the cart before the horse, and could maybe live in the 102 | * WP_MS_Networks_List_Table class also. 103 | * 104 | * @since 1.3.0 105 | * @return void 106 | */ 107 | public function network_admin_menu() { 108 | $page = add_menu_page( esc_html__( 'Networks', 'wp-multi-network' ), esc_html__( 'Networks', 'wp-multi-network' ), 'manage_networks', 'networks', array( $this, 'route_pages' ), 'dashicons-networking', -1 ); 109 | 110 | add_submenu_page( 'networks', esc_html__( 'All Networks', 'wp-multi-network' ), esc_html__( 'All Networks', 'wp-multi-network' ), 'list_networks', 'networks', array( $this, 'route_pages' ) ); 111 | add_submenu_page( 'networks', esc_html__( 'Add New', 'wp-multi-network' ), esc_html__( 'Add New', 'wp-multi-network' ), 'create_networks', 'add-new-network', array( $this, 'page_edit_network' ) ); 112 | 113 | add_action( "admin_head-{$page}", array( $this, 'fix_menu_highlight_for_move_page' ) ); 114 | 115 | require_once wpmn()->plugin_dir . '/includes/classes/class-wp-ms-networks-list-table.php'; 116 | } 117 | 118 | /** 119 | * Adds a separator between the 'Networks' and 'Dashboard' menu items on the 120 | * network dashboard. 121 | * 122 | * @since 1.5.2 123 | * @return void 124 | */ 125 | public function network_admin_menu_separator() { 126 | $GLOBALS['menu']['-2'] = array( '', 'read', 'separator', '', 'wp-menu-separator' ); // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited 127 | } 128 | 129 | /** 130 | * Fixes the menu highlight for the "Move" page, which is technically 131 | * under the "Sites" menu. 132 | * 133 | * @since 2.2.0 134 | * 135 | * @global string $plugin_page 136 | * @global string $submenu_file 137 | * @return void 138 | */ 139 | public function fix_menu_highlight_for_move_page() { 140 | global $plugin_page, $submenu_file; 141 | 142 | if ( 'networks' === $plugin_page ) { 143 | $action = filter_input( INPUT_GET, 'action' ); 144 | if ( 'move' === $action ) { 145 | $submenu_file = 'sites.php'; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited 146 | } 147 | } 148 | } 149 | 150 | /** 151 | * Registers and enqueues JavaScript on networks admin pages. 152 | * 153 | * @since 2.0.0 154 | * 155 | * @param string $page Optional. Current page hook. Default empty string. 156 | * @return void 157 | */ 158 | public function enqueue_scripts( $page = '' ) { 159 | 160 | // Bail if not a network page. 161 | if ( ! in_array( $page, array( 'toplevel_page_networks', 'networks_page_add-new-network' ), true ) ) { 162 | return; 163 | } 164 | 165 | wp_register_style( 'wp-multi-network', wpmn()->plugin_url . 'assets/css/wp-multi-network.css', array(), wpmn()->asset_version ); 166 | wp_register_script( 'wp-multi-network', wpmn()->plugin_url . 'assets/js/wp-multi-network.js', array( 'jquery', 'post' ), wpmn()->asset_version, true ); 167 | 168 | wp_enqueue_style( 'wp-multi-network' ); 169 | wp_enqueue_script( 'wp-multi-network' ); 170 | } 171 | 172 | /** 173 | * Sets feedback strings for network admin actions. 174 | * 175 | * @since 2.1.0 176 | * @return void 177 | */ 178 | public function set_feedback_strings() { 179 | $this->feedback_strings = array( 180 | 'network_updated' => array( 181 | '1' => esc_html__( 'Network updated.', 'wp-multi-network' ), 182 | '0' => esc_html__( 'Network not updated.', 'wp-multi-network' ), 183 | ), 184 | 'network_created' => array( 185 | '1' => esc_html__( 'Network created.', 'wp-multi-network' ), 186 | '0' => esc_html__( 'Network not created.', 'wp-multi-network' ), 187 | ), 188 | 'network_deleted' => array( 189 | '1' => esc_html__( 'Network deleted.', 'wp-multi-network' ), 190 | '0' => esc_html__( 'Network not deleted.', 'wp-multi-network' ), 191 | ), 192 | 'site_moved' => array( 193 | '1' => esc_html__( 'Site moved.', 'wp-multi-network' ), 194 | '0' => esc_html__( 'Site not moved.', 'wp-multi-network' ), 195 | ), 196 | ); 197 | } 198 | 199 | /** 200 | * Prints feedback notices for network admin actions as necessary. 201 | * 202 | * @since 1.3.0 203 | * @return void 204 | */ 205 | public function network_admin_notices() { 206 | $message = ''; 207 | $type = ''; 208 | foreach ( $this->feedback_strings as $slug => $messages ) { 209 | $passed = filter_input( INPUT_GET, $slug ); 210 | 211 | if ( is_string( $passed ) ) { 212 | if ( '1' === $passed ) { 213 | $message = $messages['1']; 214 | $type = 'updated'; 215 | } else { 216 | $message = $messages['0']; 217 | $type = 'error'; 218 | } 219 | 220 | break; 221 | } 222 | } 223 | 224 | if ( empty( $message ) || empty( $type ) ) { 225 | return; 226 | } 227 | ?> 228 | 229 |
230 |

231 | 232 | 233 |

234 |
235 | 236 | page_move_site(); 260 | break; 261 | 262 | // Delete a network. 263 | case 'delete_network': 264 | $this->page_delete_network(); 265 | break; 266 | 267 | // Edit a network. 268 | case 'edit_network': 269 | $this->page_edit_network(); 270 | break; 271 | 272 | // View the list of networks, with bulk action handling. 273 | case 'all_networks': 274 | $doaction = filter_input( INPUT_POST, 'action', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); 275 | if ( empty( $doaction ) || '-1' === $doaction ) { 276 | $doaction = filter_input( INPUT_POST, 'action2', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); 277 | } 278 | $doaction = sanitize_key( $doaction ); 279 | 280 | switch ( $doaction ) { 281 | case 'delete': 282 | $this->page_delete_networks(); 283 | break; 284 | default: 285 | $this->page_all_networks(); 286 | break; 287 | } 288 | break; 289 | 290 | // View the list of networks. 291 | default: 292 | $this->page_all_networks(); 293 | break; 294 | } 295 | } 296 | 297 | /** 298 | * Handles network management form submissions. 299 | * 300 | * @since 2.0.0 301 | * @return void 302 | */ 303 | public function route_save_handlers() { 304 | 305 | // Bail -- our fields aren't being edited 306 | // Otherwise we'll do an unnecessary nonce check. 307 | if ( empty( $_POST['network_edit'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing 308 | return; 309 | } 310 | 311 | $action = filter_input( INPUT_POST, 'action', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); 312 | if ( empty( $action ) ) { 313 | $alternative_actions = array( 'delete', 'delete_multiple', 'move' ); 314 | foreach ( $alternative_actions as $alternative_action ) { 315 | if ( filter_input( INPUT_POST, $alternative_action ) ) { 316 | $action = $alternative_action; 317 | break; 318 | } 319 | } 320 | } 321 | 322 | switch ( $action ) { 323 | 324 | // Create network. 325 | case 'create': 326 | $this->check_nonce(); 327 | $this->handle_add_network(); 328 | break; 329 | 330 | // Update network. 331 | case 'update': 332 | $this->check_nonce(); 333 | $this->handle_reassign_sites(); 334 | $this->handle_update_network(); 335 | break; 336 | 337 | // Delete network. 338 | case 'delete': 339 | $this->check_nonce(); 340 | $this->handle_delete_network(); 341 | break; 342 | 343 | // Delete multiple networks. 344 | case 'delete_multiple': 345 | $this->check_nonce(); 346 | $this->handle_delete_networks(); 347 | break; 348 | 349 | // Move site to different network. 350 | case 'move': 351 | $this->check_nonce(); 352 | $this->handle_move_site(); 353 | break; 354 | } 355 | } 356 | 357 | /** 358 | * Renders the new network creation dashboard page. 359 | * 360 | * @since 2.0.0 361 | * @return void 362 | */ 363 | public function page_edit_network() { 364 | $network_id = (int) filter_input( INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT ); 365 | $network = $network_id ? get_network( $network_id ) : null; 366 | 367 | add_meta_box( 'wpmn-edit-network-details', esc_html__( 'Details', 'wp-multi-network' ), 'wpmn_edit_network_details_metabox', get_current_screen()->id, 'normal', 'high', array( $network ) ); 368 | add_meta_box( 'wpmn-edit-network-publish', esc_html__( 'Network', 'wp-multi-network' ), 'wpmn_edit_network_publish_metabox', get_current_screen()->id, 'side', 'high', array( $network ) ); 369 | 370 | // Differentiate between a new network and an existing network. 371 | if ( empty( $network ) ) { 372 | $network_title = ''; 373 | 374 | add_meta_box( 'wpmn-edit-network-new-site', esc_html__( 'Root Site', 'wp-multi-network' ), 'wpmn_edit_network_new_site_metabox', get_current_screen()->id, 'advanced', 'high', array( $network ) ); 375 | } else { 376 | $network_title = get_network_option( $network->id, 'site_name', '' ); 377 | 378 | add_meta_box( 'wpmn-edit-network-assign-sites', esc_html__( 'Site Assignment', 'wp-multi-network' ), 'wpmn_edit_network_assign_sites_metabox', get_current_screen()->id, 'advanced', 'high', array( $network ) ); 379 | } 380 | 381 | $add_network_url = $this->admin_url( array( 'page' => 'add-new-network' ) ); 382 | ?> 383 | 384 |
385 |

386 | 392 | 393 | 399 |

400 | 401 |
402 | 403 |
404 |
405 |
406 |
407 |
408 |
409 | 410 | 411 |
412 |
413 |
414 | 415 |
416 | id, 'side', $network ); ?> 417 |
418 | 419 |
420 | id, 'normal', $network ); ?> 421 | id, 'advanced', $network ); ?> 422 |
423 |
424 |
425 |
426 |
427 | 428 | prepare_items(); 442 | 443 | $add_network_url = $this->admin_url( array( 'page' => 'add-new-network' ) ); 444 | $all_networks_url = $this->admin_url( array( 'action' => 'all_networks' ) ); 445 | $search_url = $this->admin_url( array( 'action' => 'domains' ) ); 446 | 447 | $search_text = filter_input( INPUT_POST, 's', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); 448 | ?> 449 | 450 |
451 |

452 | 457 | 458 | ' . esc_html__( 'Search results for “%s”', 'wp-multi-network' ) . '', esc_html( $search_text ) ); 464 | } 465 | ?> 466 |

467 | 468 |
469 | 470 | 474 | 475 |
476 | display(); ?> 477 |
478 |
479 | 480 | id, 504 | 'normal', 505 | 'high', 506 | array( $site ) 507 | ); 508 | 509 | // Move. 510 | add_meta_box( 511 | 'wpmn-move-site-publish', 512 | esc_html__( 'Site', 'wp-multi-network' ), 513 | 'wpmn_move_site_assign_metabox', 514 | get_current_screen()->id, 515 | 'side', 516 | 'high', 517 | array( $site ) 518 | ); 519 | 520 | $add_network_url = $this->admin_url( array( 'page' => 'add-new-network' ) ); 521 | ?> 522 | 523 |
524 |

525 | 530 | 531 | 534 |

535 | 536 |
537 | 538 |
539 |
540 |
541 |
542 | id, 'side', $site ); ?> 543 |
544 | 545 |
546 | id, 'normal', $site ); ?> 547 | id, 'advanced', $site ); ?> 548 |
549 |
550 |
551 |
552 |
553 | 554 | $network->id ) ); 573 | 574 | $add_network_url = $this->admin_url( array( 'page' => 'add-new-network' ) ); 575 | ?> 576 | 577 |
578 |

579 | 584 | 585 | 588 |

589 | 590 |
591 | 592 |
593 | 597 | 598 |
599 |

600 |
    601 | 605 |
  • domain . $site->path ); ?>
  • 606 | 610 |
611 |

612 | 613 | 622 |

623 |
624 |

625 | domain . $network->path ) 630 | ); 631 | ?> 632 |

633 | 634 | 641 | 642 | 643 |
644 |
645 | 646 | $all_networks, 664 | ) 665 | ); 666 | 667 | // Bail if no networks found. 668 | if ( empty( $networks ) ) { 669 | wp_die( esc_html__( 'You have selected an invalid network or networks for deletion', 'wp-multi-network' ) ); 670 | } 671 | 672 | foreach ( $networks as $network ) { 673 | if ( ! get_network( $network ) ) { 674 | wp_die( esc_html__( 'You have selected an invalid network for deletion.', 'wp-multi-network' ) ); 675 | } 676 | } 677 | 678 | $sites = get_sites( 679 | array( 680 | 'network__in' => $all_networks, 681 | ) 682 | ); 683 | ?> 684 | 685 |
686 |

687 |

688 | 689 |
690 | 691 |
692 | 696 | 697 |
698 |

:

699 |
    700 | 704 |
  • domain . $deleted_network->path ); ?>
  • 705 | 709 |
710 |

711 | 720 |

721 |

722 | 723 | 724 |

725 |
726 | 727 | 730 | 731 |
732 |

:

733 |
    734 | 739 |
  • domain . $deleted_network->path ); ?>
  • 740 | 745 |
746 |
747 | 748 | 752 |

753 | 759 | 760 | 761 |
762 |
763 | 764 | 778 | 779 |
780 |

781 | 782 |
783 | 784 | $network_id ) { 788 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery,WordPress.VIP.DirectDatabaseQuery.NoCaching 789 | $my_networks[ $key ] = $wpdb->get_row( 790 | $wpdb->prepare( 791 | 'SELECT s.*, sm.meta_value as site_name, b.blog_id FROM ' . $wpdb->site . ' s LEFT JOIN ' . $wpdb->sitemeta . ' as sm ON sm.site_id = s.id AND sm.meta_key = %s LEFT JOIN ' . $wpdb->blogs . ' b ON s.id = b.site_id AND b.path = s.path WHERE s.id = %d', 792 | 'site_name', 793 | $network_id 794 | ) 795 | ); 796 | } 797 | 798 | ?> 799 | 800 | = 20 ) { 804 | $cols = 4; 805 | } elseif ( $num >= 10 ) { 806 | $cols = 2; 807 | } 808 | $num_rows = ceil( $num / $cols ); 809 | $split = 0; 810 | $rows = array(); 811 | for ( $i = 1; $i <= $num_rows; $i++ ) { 812 | $rows[] = array_slice( $my_networks, $split, $cols ); 813 | $split = $split + $cols; 814 | } 815 | 816 | $c = ''; 817 | foreach ( $rows as $row ) { 818 | $c = ( 'alternate' === $c ) ? '' : 'alternate'; 819 | echo ""; 820 | $i = 0; 821 | foreach ( $row as $network ) { 822 | $s = ( 3 === $i ) ? '' : 'border-right: 1px solid #ccc;'; 823 | switch_to_network( $network->id ); 824 | ?> 825 | 826 | 848 | 849 | '; 854 | } 855 | ?> 856 |
827 |

site_name ); ?>

828 |

829 | " . esc_html__( 'Visit', 'wp-multi-network' ) . '', 832 | "" . esc_html__( 'Dashboard', 'wp-multi-network' ) . '', 833 | ); 834 | $network_actions = implode( ' | ', $network_actions ); 835 | 836 | /** 837 | * Filters the action links printed on the My Networks page. 838 | * 839 | * @since 2.0.0 840 | * 841 | * @param string $network_actions Network action links, separated by pipe ( | ) characters. 842 | * @param WP_Network $network Current network object. 843 | */ 844 | echo apply_filters( 'mynetworks_network_actions', $network_actions, $network ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 845 | ?> 846 |

847 |
857 |
858 | 859 | id; 880 | } 881 | 882 | // Sanitize values. 883 | $network_title = wp_unslash( filter_input( INPUT_POST, 'title', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); 884 | $network_domain = wp_unslash( filter_input( INPUT_POST, 'domain', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); 885 | $network_path = wp_unslash( filter_input( INPUT_POST, 'path', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); 886 | $site_name = wp_unslash( filter_input( INPUT_POST, 'new_site', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); 887 | 888 | // Additional formatting. 889 | $network_title = wp_strip_all_tags( $network_title ); 890 | $network_domain = str_replace( ' ', '', strtolower( sanitize_text_field( $network_domain ) ) ); 891 | $network_path = str_replace( ' ', '', strtolower( sanitize_text_field( $network_path ) ) ); 892 | 893 | // Fallback to network title if not explicitly set. 894 | $site_name = ! empty( $site_name ) 895 | ? wp_strip_all_tags( $site_name ) 896 | : $network_title; 897 | 898 | // Bail if missing fields. 899 | if ( empty( $network_domain ) || empty( $network_path ) ) { 900 | $this->handle_redirect( 901 | array( 902 | 'page' => 'add-new-network', 903 | 'network_created' => '0', 904 | ) 905 | ); 906 | } 907 | 908 | // Arguments for add_network(). 909 | $args = array( 910 | 'domain' => $network_domain, 911 | 'path' => $network_path, 912 | 'site_name' => $site_name, 913 | 'user_id' => get_current_user_id(), 914 | 'clone_network' => $clone, 915 | 'options_to_clone' => $options_to_clone, 916 | ); 917 | 918 | // Add network. 919 | $result = add_network( $args ); 920 | 921 | // Success! 922 | if ( ! empty( $result ) && ! is_wp_error( $result ) ) { 923 | 924 | // Update network name. 925 | if ( ! empty( $network_title ) ) { 926 | update_network_option( $result, 'site_name', $network_title ); 927 | } 928 | 929 | // Self-activate on new network. 930 | update_network_option( 931 | $result, 932 | 'active_sitewide_plugins', 933 | array( 934 | 'wp-multi-network/wpmn-loader.php' => time(), 935 | ) 936 | ); 937 | 938 | // Redirect. 939 | $this->handle_redirect( 940 | array( 941 | 'network_created' => '1', 942 | ) 943 | ); 944 | } 945 | 946 | // Failure. 947 | $this->handle_redirect( 948 | array( 949 | 'page' => 'add-new-network', 950 | 'network_created' => '0', 951 | ) 952 | ); 953 | } 954 | 955 | /** 956 | * Handles the request to update a network. 957 | * 958 | * @since 2.0.0 959 | * @return void 960 | */ 961 | private function handle_update_network() { 962 | 963 | // Sanitize network ID. 964 | $network_id = (int) filter_input( INPUT_POST, 'network_id', FILTER_SANITIZE_NUMBER_INT ); 965 | 966 | // Bail if invalid network. 967 | if ( ! get_network( $network_id ) ) { 968 | wp_die( esc_html__( 'Invalid network id.', 'wp-multi-network' ) ); 969 | } 970 | 971 | // Sanitize values. 972 | $network_title = wp_unslash( filter_input( INPUT_POST, 'title', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); 973 | $network_domain = wp_unslash( filter_input( INPUT_POST, 'domain', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); 974 | $network_path = wp_unslash( filter_input( INPUT_POST, 'path', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); 975 | 976 | // Additional formatting. 977 | $network_title = sanitize_text_field( $network_title ); 978 | $network_domain = Requests_IDNAEncoder::encode( str_replace( ' ', '', strtolower( sanitize_text_field( $network_domain ) ) ) ); 979 | $network_path = str_replace( ' ', '', strtolower( sanitize_text_field( $network_path ) ) ); 980 | 981 | // Bail if missing fields. 982 | if ( empty( $network_title ) || empty( $network_domain ) || empty( $network_path ) ) { 983 | $this->handle_redirect( 984 | array( 985 | 'id' => $network_id, 986 | 'action' => 'edit_network', 987 | 'network_updated' => '0', 988 | ) 989 | ); 990 | } 991 | 992 | // Update the network. 993 | $updated = update_network( $network_id, $network_domain, $network_path ); 994 | 995 | // Default success value. 996 | $success = '0'; 997 | 998 | // No failure. 999 | if ( ! is_wp_error( $updated ) ) { 1000 | 1001 | // Update network name. 1002 | update_network_option( $network_id, 'site_name', $network_title ); 1003 | 1004 | // Success! 1005 | $success = '1'; 1006 | } 1007 | 1008 | // Redirect. 1009 | $this->handle_redirect( 1010 | array( 1011 | 'id' => $network_id, 1012 | 'action' => 'edit_network', 1013 | 'network_updated' => $success, 1014 | ) 1015 | ); 1016 | } 1017 | 1018 | /** 1019 | * Handles the request to move a site to another network. 1020 | * 1021 | * @since 2.0.0 1022 | * @return void 1023 | */ 1024 | private function handle_move_site() { 1025 | 1026 | // Sanitize values. 1027 | $site_id = (int) filter_input( INPUT_GET, 'blog_id', FILTER_SANITIZE_NUMBER_INT ); 1028 | $new_network = (int) filter_input( INPUT_POST, 'to', FILTER_SANITIZE_NUMBER_INT ); 1029 | 1030 | // Bail if no site ID. 1031 | if ( empty( $site_id ) ) { 1032 | wp_safe_redirect( 1033 | add_query_arg( 1034 | array( 1035 | 'site_moved' => 0, 1036 | ), 1037 | network_admin_url( 'sites.php' ) 1038 | ) 1039 | ); 1040 | exit; 1041 | } 1042 | 1043 | // Query for site. 1044 | $site = get_site( $site_id ); 1045 | 1046 | // Bail if site cannot be found or new network is the same as existing. 1047 | if ( empty( $site ) || ( (int) $site->network_id === (int) $new_network ) ) { 1048 | wp_safe_redirect( 1049 | add_query_arg( 1050 | array( 1051 | 'site_moved' => 0, 1052 | ), 1053 | network_admin_url( 'sites.php' ) 1054 | ) 1055 | ); 1056 | exit; 1057 | } 1058 | 1059 | // Attempt to move site. 1060 | $moved = move_site( $site_id, $new_network ); 1061 | 1062 | // Success? 1063 | $success = is_wp_error( $moved ) 1064 | ? '0' 1065 | : '1'; 1066 | 1067 | // Redirect. 1068 | wp_safe_redirect( 1069 | add_query_arg( 1070 | array( 1071 | 'site_moved' => $success, 1072 | ), 1073 | network_admin_url( 'sites.php' ) 1074 | ) 1075 | ); 1076 | exit; 1077 | } 1078 | 1079 | /** 1080 | * Handles the request to reassign sites to another network. 1081 | * 1082 | * @since 2.0.0 1083 | * @return void 1084 | */ 1085 | private function handle_reassign_sites() { 1086 | 1087 | // Sanitize values. 1088 | $to = array_map( 'absint', (array) filter_input( INPUT_POST, 'to', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY ) ); 1089 | $from = array_map( 'absint', (array) filter_input( INPUT_POST, 'from', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY ) ); 1090 | 1091 | // Bail early if no movement. 1092 | if ( empty( $to ) && empty( $from ) ) { 1093 | return; 1094 | } 1095 | 1096 | // Sanitize network ID. 1097 | $network_id = (int) filter_input( INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT ); 1098 | 1099 | // Default to/from arrays. 1100 | $moving_to = array(); 1101 | $moving_from = array(); 1102 | 1103 | // Get sites for network. 1104 | $sites_list = get_sites( 1105 | array( 1106 | 'network_id' => $network_id, 1107 | 'fields' => 'ids', 1108 | ) 1109 | ); 1110 | 1111 | // Move sites out of current network. 1112 | foreach ( $from as $site_id ) { 1113 | if ( in_array( $site_id, $sites_list, true ) ) { 1114 | $moving_from[] = $site_id; 1115 | } 1116 | } 1117 | 1118 | // Move sites into current network. 1119 | foreach ( $to as $site_id ) { 1120 | if ( ! in_array( $site_id, $sites_list, true ) ) { 1121 | $moving_to[] = $site_id; 1122 | } 1123 | } 1124 | 1125 | $moving = array_filter( array_merge( $moving_to, $moving_from ) ); 1126 | 1127 | // Loop through and move sites. 1128 | foreach ( $moving as $site_id ) { 1129 | $site = get_site( $site_id ); 1130 | 1131 | // Skip if missing or main site. 1132 | if ( empty( $site ) || is_main_site( $site->id, $site->network_id ) ) { 1133 | continue; 1134 | } 1135 | 1136 | if ( in_array( $site_id, $to, true ) && ! in_array( $site_id, $sites_list, true ) ) { 1137 | move_site( $site_id, $network_id ); 1138 | } elseif ( in_array( $site_id, $from, true ) ) { 1139 | move_site( $site_id, 0 ); 1140 | } 1141 | } 1142 | } 1143 | 1144 | /** 1145 | * Handles the request to delete a network. 1146 | * 1147 | * @since 2.0.0 1148 | * @return void 1149 | */ 1150 | private function handle_delete_network() { 1151 | 1152 | // Sanitize values. 1153 | $network_id = (int) filter_input( INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT ); 1154 | $override = (bool) filter_input( INPUT_POST, 'override' ); 1155 | 1156 | // Attempt to delete network. 1157 | $result = delete_network( $network_id, $override ); 1158 | 1159 | // Failure. 1160 | if ( is_wp_error( $result ) ) { 1161 | wp_die( esc_html( $result->get_error_message() ) ); 1162 | } 1163 | 1164 | // Redirect. 1165 | $this->handle_redirect( 1166 | array( 1167 | 'network_deleted' => '1', 1168 | ) 1169 | ); 1170 | } 1171 | 1172 | /** 1173 | * Handles the request to delete multiple networks. 1174 | * 1175 | * @since 2.0.0 1176 | * @return void 1177 | */ 1178 | private function handle_delete_networks() { 1179 | 1180 | // Sanitize values. 1181 | $deleted_networks = array_map( 'absint', filter_input( INPUT_POST, 'deleted_networks', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY ) ); 1182 | $override = (bool) filter_input( INPUT_POST, 'override' ); 1183 | 1184 | // Loop through deleted networks. 1185 | if ( ! empty( $deleted_networks ) ) { 1186 | foreach ( $deleted_networks as $deleted_network ) { 1187 | 1188 | // Attempt to delete network. 1189 | $result = delete_network( $deleted_network, $override ); 1190 | 1191 | // Failure. 1192 | if ( is_wp_error( $result ) ) { 1193 | wp_die( esc_html( $result->get_error_message() ) ); 1194 | } 1195 | } 1196 | } 1197 | 1198 | // Redirect. 1199 | $this->handle_redirect( 1200 | array( 1201 | 'networks_deleted' => '1', 1202 | ) 1203 | ); 1204 | } 1205 | 1206 | /** 1207 | * Handles redirect after a page submit action. 1208 | * 1209 | * @since 2.0.0 1210 | * 1211 | * @param array $args Optional. URL query arguments. Default empty array. 1212 | * @return void 1213 | */ 1214 | private function handle_redirect( $args = array() ) { 1215 | wp_safe_redirect( $this->admin_url( $args ) ); 1216 | exit; 1217 | } 1218 | 1219 | /** 1220 | * Gets the URL of the networks page. 1221 | * 1222 | * @since 1.3.0 1223 | * 1224 | * @param array $args Optional. URL query arguments. Default empty array. 1225 | * @return string Absolute URL to the networks page. 1226 | */ 1227 | private function admin_url( $args = array() ) { 1228 | 1229 | // Parse arguments. 1230 | $r = wp_parse_args( 1231 | $args, 1232 | array( 1233 | 'page' => 'networks', 1234 | ) 1235 | ); 1236 | 1237 | // Where to? 1238 | $page = ! empty( $r['action'] ) && ( 'move' === $r['action'] ) 1239 | ? 'sites.php' 1240 | : 'admin.php'; 1241 | 1242 | // Add query arguments. 1243 | $result = add_query_arg( $r, network_admin_url( $page ) ); 1244 | 1245 | /** 1246 | * Filters the URL to the networks admin screen. 1247 | * 1248 | * @since 2.0.0 1249 | * 1250 | * @param string $result URL including query arguments. 1251 | * @param array $r Parsed query arguments to include. 1252 | * @param array $args Original query arguments to include. 1253 | */ 1254 | return apply_filters( 'edit_networks_screen_url', $result, $r, $args ); 1255 | } 1256 | 1257 | /** 1258 | * Checks the nonce for a network management form submission. 1259 | * 1260 | * @since 2.1.0 1261 | * @return void 1262 | */ 1263 | private function check_nonce() { 1264 | check_admin_referer( 'edit_network', 'network_edit' ); 1265 | } 1266 | } 1267 | -------------------------------------------------------------------------------- /wp-multi-network/includes/classes/class-wp-ms-networks-capabilities.php: -------------------------------------------------------------------------------- 1 | get_global_capabilities() ); 54 | if ( empty( $required_global_caps ) ) { 55 | return $caps; 56 | } 57 | 58 | // Check if the user has access to global capabilities. 59 | if ( ! $this->has_user_global_access( $user_id ) ) { 60 | $caps[] = 'do_not_allow'; 61 | } 62 | 63 | return $caps; 64 | } 65 | 66 | /** 67 | * Gets the primitive capabilities that should only be granted to global administrators. 68 | * 69 | * @since 2.3.0 70 | * 71 | * @return string[] List of primitive global capabilities. 72 | */ 73 | private function get_global_capabilities() { 74 | $global_capabilities = array( 75 | 'manage_networks', 76 | 'list_networks', 77 | 'create_networks', 78 | 'delete_networks', 79 | ); 80 | 81 | /** 82 | * Filters the primitive capabilities that should only be available to global administrators. 83 | * 84 | * @since 2.3.0 85 | * 86 | * @param array $global_capabilities List of primitive global capabilities. 87 | */ 88 | return apply_filters( 'wpms_global_capabilities', $global_capabilities ); 89 | } 90 | 91 | /** 92 | * Checks whether a given user has global access. 93 | * 94 | * @since 2.3.0 95 | * 96 | * @param int $user_id User ID to check for global administrator permissions. 97 | * @return bool True if the user has global access, false otherwise. 98 | */ 99 | private function has_user_global_access( $user_id ) { 100 | 101 | /** 102 | * Filters whether a given user should be granted global administrator capabilities. 103 | * 104 | * By default, global access is available to all network administrators. 105 | * 106 | * @since 2.3.0 107 | * 108 | * @param bool $has_global_access Whether the user should have global access. 109 | * @param int $user_id ID of the user checked. 110 | */ 111 | return apply_filters( 'wpms_has_user_global_access', is_super_admin( $user_id ), $user_id ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /wp-multi-network/includes/classes/class-wp-ms-networks-list-table.php: -------------------------------------------------------------------------------- 1 | false, 28 | 'plural' => 'networks', 29 | 'singular' => 'network', 30 | 'screen' => 'wpmn', 31 | ) 32 | ); 33 | } 34 | 35 | /** 36 | * Checks whether the current user can manage list table items during an AJAX request. 37 | * 38 | * @since 1.3.0 39 | * 40 | * @return bool True if the user can manage list table items, false otherwise. 41 | */ 42 | public function ajax_user_can() { 43 | return current_user_can( 'manage_networks' ); 44 | } 45 | 46 | /** 47 | * Prepares the list table items. 48 | * 49 | * @since 1.3.0 50 | * @return void 51 | */ 52 | public function prepare_items() { 53 | $per_page = $this->get_items_per_page( 'networks_per_page' ); 54 | $pagenum = $this->get_pagenum(); 55 | 56 | $order_by = filter_input( INPUT_GET, 'orderby', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); 57 | $order_by = ! empty( $order_by ) ? sanitize_key( $order_by ) : ''; 58 | $order = filter_input( INPUT_GET, 'order', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); 59 | $order = ! empty( $order ) ? strtoupper( $order ) : 'ASC'; 60 | $search = filter_input( INPUT_GET, 's', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); 61 | if ( ! $search ) { 62 | $search = filter_input( INPUT_POST, 's', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); 63 | } 64 | 65 | $search = stripslashes( trim( $search ) ); 66 | if ( false !== strpos( $search, '*' ) ) { 67 | $search = trim( $search, '*' ); 68 | } 69 | 70 | if ( ! in_array( $order, array( 'DESC', 'ASC' ), true ) ) { 71 | $order = 'ASC'; 72 | } 73 | 74 | $args = array( 75 | 'number' => intval( $per_page ), 76 | 'offset' => intval( ( $pagenum - 1 ) * $per_page ), 77 | 'orderby' => $order_by, 78 | 'order' => $order, 79 | 'search' => $search, 80 | 'no_found_rows' => false, 81 | ); 82 | 83 | $query = new WP_Network_Query(); 84 | 85 | $this->items = $query->query( $args ); 86 | $count = $query->found_networks; 87 | 88 | $this->set_pagination_args( 89 | array( 90 | 'total_items' => $count, 91 | 'per_page' => $per_page, 92 | ) 93 | ); 94 | } 95 | 96 | /** 97 | * Outputs the message to show when no list items are found. 98 | * 99 | * @since 1.3.0 100 | * @return void 101 | */ 102 | public function no_items() { 103 | esc_html_e( 'No networks found.', 'wp-multi-network' ); 104 | } 105 | 106 | /** 107 | * Gets the array of supported bulk actions. 108 | * 109 | * @since 1.3.0 110 | * 111 | * @return array Bulk actions as $slug => $label pairs. 112 | */ 113 | public function get_bulk_actions() { 114 | $actions = array(); 115 | 116 | if ( current_user_can( 'delete_networks' ) ) { 117 | $actions['delete'] = __( 'Delete', 'wp-multi-network' ); 118 | } 119 | 120 | return $actions; 121 | } 122 | 123 | /** 124 | * Outputs list table pagination. 125 | * 126 | * @since 1.3.0 127 | * 128 | * @param string $which Where to display the pagination. Either 'top' or 'bottom'. 129 | */ 130 | public function pagination( $which ) { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found 131 | parent::pagination( $which ); 132 | } 133 | 134 | /** 135 | * Gets the name of the default primary column. 136 | * 137 | * @since 1.3.0 138 | * 139 | * @return string Name of the default primary column, in this case, 'title'. 140 | */ 141 | protected function get_default_primary_column_name() { 142 | return 'title'; 143 | } 144 | 145 | /** 146 | * Gets the list table columns. 147 | * 148 | * @since 1.3.0 149 | * 150 | * @return array Columns as $slug => $label pairs. 151 | */ 152 | public function get_columns() { 153 | $columns = array( 154 | 'cb' => '', 155 | 'title' => __( 'Network Title', 'wp-multi-network' ), 156 | 'domain' => __( 'Domain', 'wp-multi-network' ), 157 | 'path' => __( 'Path', 'wp-multi-network' ), 158 | 'blogs' => __( 'Sites', 'wp-multi-network' ), 159 | 'admins' => __( 'Network Admins', 'wp-multi-network' ), 160 | ); 161 | 162 | /** 163 | * Filters the networks list table column. 164 | * 165 | * @since 1.3.0 166 | * 167 | * @param array $columns Columns as $slug => $label pairs. 168 | */ 169 | return apply_filters( 'wpmn_networks_columns', $columns ); 170 | } 171 | 172 | /** 173 | * Gets the list table columns that are sortable. 174 | * 175 | * @since 1.3.0 176 | * 177 | * @return array Columns as $slug => $orderby_field pairs. 178 | */ 179 | public function get_sortable_columns() { 180 | return array( 181 | 'title' => 'id', 182 | 'domain' => 'domain', 183 | 'path' => 'path', 184 | ); 185 | } 186 | 187 | /** 188 | * Generates content for a single row of the table. 189 | * 190 | * @since 2.3.0 191 | * 192 | * @param object $network The current network item. 193 | * @return void 194 | */ 195 | public function single_row( $network ) { 196 | $class = (int) get_current_site()->id === (int) $network->id ? 'current' : 'not-current'; 197 | 198 | echo ''; 199 | $this->single_row_columns( $network ); 200 | echo ''; 201 | } 202 | 203 | /** 204 | * Checks whether the current user can delete a given network. 205 | * 206 | * @since 2.0.0 207 | * 208 | * @param WP_Network $network Network to check delete capabilities for. 209 | * 210 | * @return bool True if the user can delete the network, false otherwise. 211 | */ 212 | private function can_delete( $network ) { 213 | 214 | // Bail if main network. 215 | if ( is_main_network( $network->id ) ) { 216 | return false; 217 | } 218 | 219 | // Bail if current network. 220 | if ( get_current_network_id() === $network->id ) { 221 | return false; 222 | } 223 | 224 | return current_user_can( 'delete_network', $network->id ); 225 | } 226 | 227 | /** 228 | * Gets the network states for a given network. 229 | * 230 | * @since 2.2.0 231 | * 232 | * @param WP_Network $network Network to get states for. 233 | * @return string HTML output containing states for the network. 234 | */ 235 | private function get_states( $network ) { 236 | $network_states = array(); 237 | $network_state = ''; 238 | 239 | if ( is_main_network( $network->id ) ) { 240 | $network_states['primary'] = esc_html__( 'Primary', 'wp-multi-network' ); 241 | } 242 | 243 | /** 244 | * Filters the default network display states used in the network list table. 245 | * 246 | * @since 2.2.0 247 | * 248 | * @param array $network_states An array of network display states. 249 | * @param WP_Network $network The current network object. 250 | */ 251 | $network_states = apply_filters( 'display_network_states', $network_states, $network ); 252 | 253 | // Setup states markup. 254 | if ( ! empty( $network_states ) ) { 255 | $state_count = count( $network_states ); 256 | $i = 0; 257 | $network_state = ' — '; 258 | 259 | foreach ( $network_states as $state ) { 260 | ++$i; 261 | 262 | $sep = $i === $state_count ? '' : ', '; 263 | $network_state .= "{$state}{$sep}"; 264 | } 265 | } 266 | 267 | return $network_state; 268 | } 269 | 270 | /** 271 | * Handles the checkbox column output. 272 | * 273 | * @since 2.0.0 274 | * 275 | * @param WP_Network $network The current network object. 276 | * @return void 277 | */ 278 | public function column_cb( $network ) { 279 | 280 | // Bail if user cannot delete the network. 281 | if ( ! $this->can_delete( $network ) ) { 282 | return; 283 | } 284 | 285 | ?> 286 | 295 | 296 | get_states( $network ); 309 | 310 | // Setup the title, with edit link if available. 311 | $link = esc_html( $network->site_name ); 312 | if ( current_user_can( 'edit_network', $network->id ) ) { 313 | $link = sprintf( 314 | '%3$s', 315 | esc_url( 316 | add_query_arg( 317 | array( 318 | 'page' => 'networks', 319 | 'action' => 'edit_network', 320 | 'id' => $network->id, 321 | ) 322 | ) 323 | ), 324 | /* translators: %s: network title */ 325 | esc_attr( sprintf( __( '“%s” (Edit)', 'wp-multi-network' ), $link ) ), 326 | $link 327 | ); 328 | } 329 | 330 | ?> 331 | 332 | 333 | 337 | 338 | 339 | domain ); 352 | } 353 | 354 | /** 355 | * Handles the network path column output. 356 | * 357 | * @since 2.0.0 358 | * 359 | * @param WP_Network $network The current network object. 360 | * @return void 361 | */ 362 | public function column_path( $network ) { 363 | echo esc_html( $network->path ); 364 | } 365 | 366 | /** 367 | * Handles the network sites column output. 368 | * 369 | * @since 2.0.0 370 | * 371 | * @param WP_Network $network The current network object. 372 | * @return void 373 | */ 374 | public function column_blogs( $network ) { 375 | $sites = get_network_option( $network->id, 'blog_count' ); 376 | 377 | switch_to_network( $network->id ); 378 | $url = network_admin_url( 'sites.php' ); 379 | restore_current_network(); 380 | 381 | echo '' . esc_html( $sites ) . ''; 382 | } 383 | 384 | /** 385 | * Handles the network administrators column output. 386 | * 387 | * @since 2.0.0 388 | * 389 | * @param WP_Network $network The current network object. 390 | * @return void 391 | */ 392 | public function column_admins( $network ) { 393 | $network_admins = (array) get_network_option( $network->id, 'site_admins', array() ); 394 | $network_admins = ! empty( $network_admins ) ? array_filter( $network_admins ) : array(); 395 | 396 | // Concatenate for markup. 397 | echo ! empty( $network_admins ) ? esc_html( join( ', ', $network_admins ) ) : esc_html( '—' ); 398 | } 399 | 400 | /** 401 | * Handles the ID column output. 402 | * 403 | * @since 2.0.0 404 | * 405 | * @param WP_Network $network The current network object. 406 | * @return void 407 | */ 408 | public function column_id( $network ) { 409 | echo esc_html( strval( $network->id ) ); 410 | } 411 | 412 | /** 413 | * Generates row action links markup. 414 | * 415 | * @since 2.0.0 416 | * 417 | * @param WP_Network $network Site being acted upon. 418 | * @param string $column_name Current column name. 419 | * @param string $primary Primary column name. 420 | * 421 | * @return string Row actions output. 422 | */ 423 | protected function handle_row_actions( $network, $column_name, $primary ) { 424 | 425 | // Bail if not primary column. 426 | if ( $primary !== $column_name ) { 427 | return ''; 428 | } 429 | 430 | switch_to_network( $network->id ); 431 | $network_admin_url = network_admin_url(); 432 | $network_home_url = network_home_url(); 433 | restore_current_network(); 434 | 435 | // Setup the base URL. 436 | $base_url = add_query_arg( 437 | array( 438 | 'page' => 'networks', 439 | 'id' => $network->id, 440 | ), 441 | remove_query_arg( 442 | array( 443 | 'action', 444 | 'network_created', 445 | 'page', 446 | 'site_moved', 447 | 'success', 448 | ) 449 | ) 450 | ); 451 | 452 | $actions = array(); 453 | 454 | // Edit the network. 455 | if ( current_user_can( 'edit_network', $network->id ) ) { 456 | $edit_network_url = add_query_arg( 457 | array( 458 | 'action' => 'edit_network', 459 | ), 460 | $base_url 461 | ); 462 | 463 | $actions['edit'] = '' . esc_html__( 'Edit', 'wp-multi-network' ) . ''; 464 | } 465 | 466 | // Visit the network dashboard. 467 | if ( current_user_can( 'manage_networks' ) ) { 468 | $actions['network_admin'] = '' . esc_html__( 'Dashboard', 'wp-multi-network' ) . ''; 469 | } 470 | 471 | // Visit the network main site. 472 | $actions['visit'] = '' . esc_html__( 'Visit', 'wp-multi-network' ) . ''; 473 | 474 | // Delete the network. 475 | if ( $this->can_delete( $network ) ) { 476 | $delete_network_url = wp_nonce_url( 477 | add_query_arg( 478 | array( 479 | 'action' => 'delete_network', 480 | ), $base_url 481 | ) 482 | ); 483 | 484 | $actions['delete'] = '' . esc_html__( 'Delete', 'wp-multi-network' ) . ''; 485 | } 486 | 487 | /** 488 | * Filters the networks list table row action links. 489 | * 490 | * @since 2.0.0 491 | * 492 | * @param array $filtered_acions Action links as $slug => $link_markup pairs. 493 | * @param int $network_id The current network ID. 494 | * @param string $network_sitename The current network name. 495 | */ 496 | $actions = apply_filters( 'manage_networks_action_links', array_filter( $actions ), $network->id, $network->site_name ); 497 | 498 | // Return all row actions. 499 | return $this->row_actions( $actions ); 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /wp-multi-network/includes/classes/class-wp-ms-rest-networks-controller.php: -------------------------------------------------------------------------------- 1 | namespace = 'wpmn/v1'; 26 | $this->rest_base = 'networks'; 27 | } 28 | 29 | /** 30 | * Registers the routes for the objects of the controller. 31 | * 32 | * @since 2.4.0 33 | * @return void 34 | */ 35 | public function register_routes() { 36 | 37 | register_rest_route( 38 | $this->namespace, '/' . $this->rest_base, array( 39 | array( 40 | 'methods' => WP_REST_Server::READABLE, 41 | 'callback' => array( $this, 'get_items' ), 42 | 'permission_callback' => array( $this, 'get_items_permissions_check' ), 43 | 'args' => $this->get_collection_params(), 44 | ), 45 | array( 46 | 'methods' => WP_REST_Server::CREATABLE, 47 | 'callback' => array( $this, 'create_item' ), 48 | 'permission_callback' => array( $this, 'create_item_permissions_check' ), 49 | 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), 50 | ), 51 | 'schema' => array( $this, 'get_public_item_schema' ), 52 | ) 53 | ); 54 | 55 | register_rest_route( 56 | $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( 57 | 'args' => array( 58 | 'id' => array( 59 | 'description' => __( 'Unique identifier for the object.', 'wp-multi-network' ), 60 | 'type' => 'integer', 61 | ), 62 | ), 63 | array( 64 | 'methods' => WP_REST_Server::READABLE, 65 | 'callback' => array( $this, 'get_item' ), 66 | 'permission_callback' => array( $this, 'get_item_permissions_check' ), 67 | 'args' => array( 68 | 'context' => $this->get_context_param( 69 | array( 70 | 'default' => 'view', 71 | ) 72 | ), 73 | ), 74 | ), 75 | array( 76 | 'methods' => WP_REST_Server::EDITABLE, 77 | 'callback' => array( $this, 'update_item' ), 78 | 'permission_callback' => array( $this, 'update_item_permissions_check' ), 79 | 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), 80 | ), 81 | array( 82 | 'methods' => WP_REST_Server::DELETABLE, 83 | 'callback' => array( $this, 'delete_item' ), 84 | 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 85 | 'args' => array( 86 | 'force' => array( 87 | 'type' => 'boolean', 88 | 'default' => false, 89 | 'description' => __( ' Flag to permit blog deletion - default setting of false will prevent deletion of occupied networks', 'wp-multi-network' ), 90 | ), 91 | ), 92 | ), 93 | 'schema' => array( $this, 'get_public_item_schema' ), 94 | ) 95 | ); 96 | } 97 | 98 | /** 99 | * Checks if a given request has access to read networks. 100 | * 101 | * @since 2.4.0 102 | * 103 | * @template T of WP_REST_Request 104 | * @param T $request Full details about the request. 105 | * 106 | * @return WP_Error|bool True if the request has read access, error object otherwise. 107 | */ 108 | public function get_items_permissions_check( $request ) { 109 | return current_user_can( 'list_networks' ); 110 | } 111 | 112 | /** 113 | * Retrieves a list of network items. 114 | * 115 | * @since 2.4.0 116 | * 117 | * @template T of WP_REST_Request 118 | * @param T $request Full details about the request. 119 | * 120 | * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. 121 | */ 122 | public function get_items( $request ) { 123 | 124 | // Retrieve the list of registered collection query parameters. 125 | $registered = $this->get_collection_params(); 126 | 127 | /* 128 | * This array defines mappings between public API query parameters whose 129 | * values are accepted as-passed, and their internal WP_Query parameter 130 | * name equivalents (some are the same). Only values which are also 131 | * present in $registered will be set. 132 | */ 133 | $parameter_mappings = array( 134 | 'domain' => 'domain__in', 135 | 'domain_exclude' => 'domain__not_in', 136 | 'exclude' => 'network__not_in', 137 | 'include' => 'network__in', 138 | 'offset' => 'offset', 139 | 'order' => 'order', 140 | 'path' => 'path__in', 141 | 'path_exclude' => 'path__not_in', 142 | 'per_page' => 'number', 143 | 'search' => 'search', 144 | ); 145 | 146 | $prepared_args = array(); 147 | 148 | /* 149 | * For each known parameter which is both registered and present in the request, 150 | * set the parameter's value on the query $prepared_args. 151 | */ 152 | foreach ( $parameter_mappings as $api_param => $wp_param ) { 153 | if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { 154 | $prepared_args[ $wp_param ] = $request[ $api_param ]; 155 | } 156 | } 157 | 158 | // Ensure certain parameter values default to empty strings. 159 | foreach ( array( 'search' ) as $param ) { 160 | if ( ! isset( $prepared_args[ $param ] ) ) { 161 | $prepared_args[ $param ] = ''; 162 | } 163 | } 164 | 165 | if ( isset( $registered['orderby'] ) ) { 166 | $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] ); 167 | } 168 | 169 | $prepared_args['no_found_rows'] = false; 170 | 171 | if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) { 172 | $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); 173 | } 174 | 175 | /** 176 | * Filters arguments, before passing to WP_Network_Query, when querying networks via the REST API. 177 | * 178 | * @since 2.4.0 179 | * 180 | * @link https://developer.wordpress.org/reference/classes/wp_network_query/ 181 | * 182 | * @param array $prepared_args Array of arguments for WP_Network_Query. 183 | * @param WP_REST_Request $request The current request. 184 | */ 185 | $prepared_args = apply_filters( 'rest_network_query', $prepared_args, $request ); 186 | 187 | $query = new WP_Network_Query(); 188 | $query_result = $query->query( $prepared_args ); 189 | $networks = array(); 190 | foreach ( $query_result as $network ) { 191 | if ( ! $this->check_read_permission( $network, $request ) ) { 192 | continue; 193 | } 194 | 195 | $data = $this->prepare_item_for_response( $network, $request ); 196 | $networks[] = $this->prepare_response_for_collection( $data ); 197 | } 198 | 199 | $total_networks = $query->found_networks; 200 | $max_pages = $query->max_num_pages; 201 | 202 | if ( $total_networks < 1 ) { 203 | // Out-of-bounds, run the query again without LIMIT for total count. 204 | unset( $prepared_args['number'], $prepared_args['offset'] ); 205 | 206 | $query = new WP_Network_Query(); 207 | $prepared_args['count'] = true; 208 | 209 | $total_networks = (int) $query->query( $prepared_args ); 210 | $max_pages = (int) ceil( $total_networks / $request['per_page'] ); 211 | } 212 | 213 | $response = rest_ensure_response( $networks ); 214 | $response->header( 'X-WP-Total', strval( $total_networks ) ); 215 | $response->header( 'X-WP-TotalPages', strval( $max_pages ) ); 216 | 217 | $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); 218 | 219 | if ( $request['page'] > 1 ) { 220 | $prev_page = $request['page'] - 1; 221 | 222 | if ( $prev_page > $max_pages ) { 223 | $prev_page = $max_pages; 224 | } 225 | 226 | $prev_link = add_query_arg( 'page', $prev_page, $base ); 227 | $response->link_header( 'prev', $prev_link ); 228 | } 229 | 230 | if ( $max_pages > $request['page'] ) { 231 | $next_page = $request['page'] + 1; 232 | $next_link = add_query_arg( 'page', $next_page, $base ); 233 | 234 | $response->link_header( 'next', $next_link ); 235 | } 236 | 237 | return $response; 238 | } 239 | 240 | /** 241 | * Get the network, if the ID is valid. 242 | * 243 | * @since 2.4.0 244 | * 245 | * @param int $id Supplied ID. 246 | * 247 | * @return WP_Network|WP_Error Network object if ID is valid, WP_Error otherwise. 248 | */ 249 | protected function get_network( $id ) { 250 | $error = new WP_Error( 251 | 'rest_network_invalid_id', __( 'Invalid network ID.', 'wp-multi-network' ), array( 252 | 'status' => 404, 253 | ) 254 | ); 255 | if ( (int) $id <= 0 ) { 256 | return $error; 257 | } 258 | 259 | $id = (int) $id; 260 | $network = get_network( $id ); 261 | if ( empty( $network ) ) { 262 | return $error; 263 | } 264 | 265 | return $network; 266 | } 267 | 268 | /** 269 | * Checks if a given request has access to read the network. 270 | * 271 | * @since 2.4.0 272 | * 273 | * @template T of WP_REST_Request 274 | * @param T $request Full details about the request. 275 | * 276 | * @return WP_Error|bool True if the request has read access for the item, error object otherwise. 277 | */ 278 | public function get_item_permissions_check( $request ) { 279 | $network = $this->get_network( $request['id'] ); 280 | if ( is_wp_error( $network ) ) { 281 | return $network; 282 | } 283 | 284 | return $this->check_read_permission( $network, $request ); 285 | } 286 | 287 | /** 288 | * Retrieves a network. 289 | * 290 | * @since 2.4.0 291 | * 292 | * @template T of WP_REST_Request 293 | * @param T $request Full details about the request. 294 | * 295 | * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. 296 | */ 297 | public function get_item( $request ) { 298 | $network = $this->get_network( $request['id'] ); 299 | if ( is_wp_error( $network ) ) { 300 | return $network; 301 | } 302 | 303 | $data = $this->prepare_item_for_response( $network, $request ); 304 | $response = rest_ensure_response( $data ); 305 | 306 | return $response; 307 | } 308 | 309 | /** 310 | * Checks if a given request has access to create a network. 311 | * 312 | * @since 2.4.0 313 | * 314 | * @template T of WP_REST_Request 315 | * @param T $request Full details about the request. 316 | * 317 | * @return WP_Error|bool True if the request has access to create items, error object otherwise. 318 | */ 319 | public function create_item_permissions_check( $request ) { 320 | return current_user_can( 'create_networks' ); 321 | } 322 | 323 | /** 324 | * Creates a network. 325 | * 326 | * @since 2.4.0 327 | * 328 | * @template T of WP_REST_Request 329 | * @param T $request Full details about the request. 330 | * 331 | * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. 332 | */ 333 | public function create_item( $request ) { 334 | if ( ! empty( $request['id'] ) ) { 335 | return new WP_Error( 336 | 'rest_network_exists', __( 'Cannot create existing network.', 'wp-multi-network' ), array( 337 | 'status' => 400, 338 | ) 339 | ); 340 | } 341 | 342 | $prepared_network = $this->prepare_item_for_database( $request ); 343 | if ( is_wp_error( $prepared_network ) ) { 344 | return $prepared_network; 345 | } 346 | 347 | /** 348 | * Filters a network before it is inserted via the REST API. 349 | * 350 | * Allows modification of the network right before it is inserted via insert_network(). 351 | * Returning a WP_Error value from the filter will shortcircuit insertion and allow 352 | * skipping further processing. 353 | * 354 | * @since 2.4.0 355 | * 356 | * @param array|WP_Error $prepared_network The prepared network data for insert_network(). 357 | * @param WP_REST_Request $request Request used to insert the network. 358 | */ 359 | $prepared_network = apply_filters( 'rest_pre_insert_network', $prepared_network, $request ); 360 | if ( is_wp_error( $prepared_network ) ) { 361 | return $prepared_network; 362 | } 363 | 364 | $network_id = add_network( wp_slash( (array) $prepared_network ) ); 365 | 366 | if ( is_wp_error( $network_id ) ) { 367 | return $this->get_wp_error( $network_id ); 368 | } elseif ( ! $network_id ) { 369 | return new WP_Error( 370 | 'rest_network_failed_create', __( 'Creating network failed.', 'wp-multi-network' ), array( 371 | 'status' => 500, 372 | ) 373 | ); 374 | } 375 | 376 | $network = $this->get_network( $network_id ); 377 | 378 | /** 379 | * Fires after a network is created or updated via the REST API. 380 | * 381 | * @since 2.4.0 382 | * 383 | * @param WP_Network $network Inserted or updated network object. 384 | * @param WP_REST_Request $request Request object. 385 | * @param bool $creating True when creating a network, false 386 | * when updating. 387 | */ 388 | do_action( 'rest_insert_network', $network, $request, true ); 389 | 390 | $fields_update = $this->update_additional_fields_for_object( $network, $request ); 391 | 392 | if ( is_wp_error( $fields_update ) ) { 393 | return $fields_update; 394 | } 395 | 396 | $context = current_user_can( 'manage_network' ) ? 'edit' : 'view'; 397 | 398 | $request->set_param( 'context', $context ); 399 | 400 | $response = $this->prepare_item_for_response( $network, $request ); 401 | $response = rest_ensure_response( $response ); 402 | 403 | $response->set_status( 201 ); 404 | $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $network_id ) ) ); 405 | 406 | return $response; 407 | } 408 | 409 | /** 410 | * Checks if a given REST request has access to update a network. 411 | * 412 | * @since 2.4.0 413 | * 414 | * @template T of WP_REST_Request 415 | * @param T $request Full details about the request. 416 | * 417 | * @return WP_Error|bool True if the request has access to update the item, error object otherwise. 418 | */ 419 | public function update_item_permissions_check( $request ) { 420 | $network = $this->get_network( $request['id'] ); 421 | if ( is_wp_error( $network ) ) { 422 | return $network; 423 | } 424 | 425 | if ( ! $this->check_edit_permission( $network ) ) { 426 | return new WP_Error( 427 | 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this network.', 'wp-multi-network' ), array( 428 | 'status' => rest_authorization_required_code(), 429 | ) 430 | ); 431 | } 432 | 433 | return true; 434 | } 435 | 436 | /** 437 | * Updates a network. 438 | * 439 | * @since 2.4.0 440 | * 441 | * @template T of WP_REST_Request 442 | * @param T $request Full details about the request. 443 | * 444 | * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. 445 | */ 446 | public function update_item( $request ) { 447 | $network = $this->get_network( $request['id'] ); 448 | if ( is_wp_error( $network ) ) { 449 | return $network; 450 | } 451 | 452 | $id = $network->id; 453 | 454 | $prepared_args = $this->prepare_item_for_database( $request ); 455 | 456 | if ( is_wp_error( $prepared_args ) ) { 457 | return $prepared_args; 458 | } 459 | 460 | if ( ! empty( $prepared_args ) ) { 461 | $domain = $prepared_args['domain']; 462 | $path = $prepared_args['path']; 463 | 464 | $updated = update_network( $id, $domain, $path ); 465 | 466 | if ( is_wp_error( $updated ) ) { 467 | return $this->get_wp_error( $updated ); 468 | } elseif ( false === $updated ) { 469 | return new WP_Error( 470 | 'rest_network_failed_edit', __( 'Updating network failed.', 'wp-multi-network' ), array( 471 | 'status' => 500, 472 | ) 473 | ); 474 | } 475 | } 476 | 477 | $network = $this->get_network( $id ); 478 | 479 | /** This action is documented in class-wp-rest-networks-api.php */ 480 | do_action( 'rest_insert_network', $network, $request, false ); 481 | 482 | $fields_update = $this->update_additional_fields_for_object( $network, $request ); 483 | 484 | if ( is_wp_error( $fields_update ) ) { 485 | return $fields_update; 486 | } 487 | 488 | $request->set_param( 'context', 'edit' ); 489 | 490 | $response = $this->prepare_item_for_response( $network, $request ); 491 | 492 | return rest_ensure_response( $response ); 493 | } 494 | 495 | /** 496 | * Checks if a given request has access to delete a network. 497 | * 498 | * @since 2.4.0 499 | * 500 | * @template T of WP_REST_Request 501 | * @param T $request Full details about the request. 502 | * 503 | * @return WP_Error|bool True if the request has access to delete the item, error object otherwise. 504 | */ 505 | public function delete_item_permissions_check( $request ) { 506 | $network = $this->get_network( $request['id'] ); 507 | if ( is_wp_error( $network ) ) { 508 | return $network; 509 | } 510 | 511 | return current_user_can( 'delete_network', $network->id ); 512 | } 513 | 514 | /** 515 | * Deletes a network. 516 | * 517 | * @since 2.4.0 518 | * 519 | * @template T of WP_REST_Request 520 | * @param T $request Full details about the request. 521 | * 522 | * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. 523 | */ 524 | public function delete_item( $request ) { 525 | $network = $this->get_network( $request['id'] ); 526 | if ( is_wp_error( $network ) ) { 527 | return $network; 528 | } 529 | 530 | $force = isset( $request['force'] ) ? (bool) $request['force'] : false; 531 | 532 | $request->set_param( 'context', 'edit' ); 533 | 534 | $previous = $this->prepare_item_for_response( $network, $request ); 535 | $result = delete_network( $network->id, $force ); 536 | $response = new WP_REST_Response(); 537 | $response->set_data( 538 | array( 539 | 'deleted' => true, 540 | 'previous' => $previous->get_data(), 541 | ) 542 | ); 543 | 544 | if ( is_wp_error( $result ) ) { 545 | return $this->get_wp_error( $result ); 546 | } elseif ( ! $result ) { 547 | return new WP_Error( 548 | 'rest_cannot_delete', __( 'The network cannot be deleted.', 'wp-multi-network' ), array( 549 | 'status' => 500, 550 | ) 551 | ); 552 | } 553 | 554 | /** 555 | * Fires after a network is deleted via the REST API. 556 | * 557 | * @since 2.4.0 558 | * 559 | * @param WP_Network $network The deleted network data. 560 | * @param WP_REST_Response $response The response returned from the API. 561 | * @param WP_REST_Request $request The request sent to the API. 562 | */ 563 | do_action( 'rest_delete_network', $network, $response, $request ); 564 | 565 | return $response; 566 | } 567 | 568 | /** 569 | * Prepares a single network output for response. 570 | * 571 | * @since 2.4.0 572 | * 573 | * @template T of WP_REST_Request 574 | * @param WP_Network $network Network object. 575 | * @param T $request Request object. 576 | * 577 | * @return WP_REST_Response Response object. 578 | */ 579 | public function prepare_item_for_response( $network, $request ) { 580 | $data = array( 581 | 'id' => (int) $network->id, 582 | 'path' => $network->path, 583 | 'domain' => $network->domain, 584 | 'cookie_domain' => $network->cookie_domain, 585 | 'site_name' => $network->site_name, 586 | 'site_id' => (int) $network->site_id, 587 | ); 588 | 589 | $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 590 | $data = $this->add_additional_fields_to_object( $data, $request ); 591 | $data = $this->filter_response_by_context( $data, $context ); 592 | 593 | // Wrap the data in a response object. 594 | $response = rest_ensure_response( $data ); 595 | 596 | $response->add_links( $this->prepare_links( $network ) ); 597 | 598 | /** 599 | * Filters a network returned from the API. 600 | * 601 | * Allows modification of the network right before it is returned. 602 | * 603 | * @since 2.4.0 604 | * 605 | * @param WP_REST_Response $response The response object. 606 | * @param WP_Network $network The original network object. 607 | * @param WP_REST_Request $request Request used to generate the response. 608 | */ 609 | return apply_filters( 'rest_prepare_network', $response, $network, $request ); 610 | } 611 | 612 | /** 613 | * Prepares links for the request. 614 | * 615 | * @since 2.4.0 616 | * 617 | * @param WP_Network $network Network object. 618 | * 619 | * @return array> Links for the given network. 620 | */ 621 | protected function prepare_links( $network ) { 622 | $links = array( 623 | 'self' => array( 624 | 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $network->id ) ), 625 | ), 626 | 'collection' => array( 627 | 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), 628 | ), 629 | ); 630 | 631 | return $links; 632 | } 633 | 634 | /** 635 | * Helper function to normalisze query params. 636 | * 637 | * @since 2.4.0 638 | * 639 | * @param string $query_param Query parameter. 640 | * 641 | * @return string The normalized query parameter. 642 | */ 643 | protected function normalize_query_param( $query_param ) { 644 | switch ( $query_param ) { 645 | case 'include': 646 | $normalized = 'network__in'; 647 | break; 648 | default: 649 | $normalized = $query_param; 650 | break; 651 | } 652 | 653 | return $normalized; 654 | } 655 | 656 | /** 657 | * Prepares a single network to be inserted into the database. 658 | * 659 | * @since 2.4.0 660 | * 661 | * @template T of WP_REST_Request 662 | * @param T $request Request object. 663 | * 664 | * @return array|WP_Error Prepared network, otherwise WP_Error object. 665 | */ 666 | protected function prepare_item_for_database( $request ) { 667 | $prepared_network = array(); 668 | 669 | if ( isset( $request['path'] ) ) { 670 | $prepared_network['path'] = $request['path']; 671 | } 672 | 673 | if ( isset( $request['domain'] ) ) { 674 | $prepared_network['domain'] = $request['domain']; 675 | } 676 | 677 | if ( isset( $request['site_name'] ) ) { 678 | $prepared_network['network_name'] = $request['site_name']; 679 | } 680 | 681 | /** 682 | * Filters a network after it is prepared for the database. 683 | * 684 | * Allows modification of the network right after it is prepared for the database. 685 | * 686 | * @since 2.4.0 687 | * 688 | * @param array $prepared_network The prepared network data for `insert_network`. 689 | * @param WP_REST_Request $request The current request. 690 | */ 691 | return apply_filters( 'rest_preprocess_network', $prepared_network, $request ); 692 | } 693 | 694 | /** 695 | * Retrieves the network's schema, conforming to JSON Schema. 696 | * 697 | * @since 2.4.0 698 | * 699 | * @return array 700 | */ 701 | public function get_item_schema() { 702 | $schema = array( 703 | '$schema' => 'http://json-schema.org/draft-04/schema#', 704 | 'title' => 'network', 705 | 'type' => 'object', 706 | 'properties' => array( 707 | 'id' => array( 708 | 'description' => __( 'Unique identifier for the object.', 'wp-multi-network' ), 709 | 'type' => 'integer', 710 | 'context' => array( 'view', 'edit', 'embed' ), 711 | 'readonly' => true, 712 | ), 713 | 'domain' => array( 714 | 'description' => __( 'The domain of the network object.', 'wp-multi-network' ), 715 | 'type' => 'string', 716 | 'context' => array( 'view', 'edit', 'embed' ), 717 | ), 718 | 'path' => array( 719 | 'description' => __( 'The path of the network object', 'wp-multi-network' ), 720 | 'type' => 'string', 721 | 'context' => array( 'view', 'edit', 'embed' ), 722 | ), 723 | 'site_id' => array( 724 | 'description' => __( 'The id of the main site on the network object.', 'wp-multi-network' ), 725 | 'type' => 'integer', 726 | 'context' => array( 'view', 'edit', 'embed' ), 727 | 'readonly' => true, 728 | ), 729 | 'link' => array( 730 | 'description' => __( 'URL to the object.', 'wp-multi-network' ), 731 | 'type' => 'string', 732 | 'format' => 'uri', 733 | 'context' => array( 'view', 'edit', 'embed' ), 734 | 'readonly' => true, 735 | ), 736 | ), 737 | ); 738 | 739 | return $this->add_additional_fields_schema( $schema ); 740 | } 741 | 742 | /** 743 | * Retrieves the query params for collections. 744 | * 745 | * @since 2.4.0 746 | * 747 | * @return array Networks collection parameters. 748 | */ 749 | public function get_collection_params() { 750 | $query_params = parent::get_collection_params(); 751 | 752 | $query_params['context']['default'] = 'view'; 753 | 754 | $query_params['domain'] = array( 755 | 'description' => __( 'Limit result set to networks assigned to specific domains.', 'wp-multi-network' ), 756 | 'type' => 'array', 757 | 'items' => array( 758 | 'type' => 'string', 759 | ), 760 | ); 761 | 762 | $query_params['domain_exclude'] = array( 763 | 'description' => __( 'Ensure result set excludes networks assigned to specific domain.', 'wp-multi-network' ), 764 | 'type' => 'array', 765 | 'items' => array( 766 | 'type' => 'string', 767 | ), 768 | ); 769 | 770 | $query_params['exclude'] = array( 771 | 'description' => __( 'Ensure result set excludes specific IDs.', 'wp-multi-network' ), 772 | 'type' => 'array', 773 | 'items' => array( 774 | 'type' => 'integer', 775 | ), 776 | 'default' => array(), 777 | ); 778 | 779 | $query_params['include'] = array( 780 | 'description' => __( 'Limit result set to specific IDs.', 'wp-multi-network' ), 781 | 'type' => 'array', 782 | 'items' => array( 783 | 'type' => 'integer', 784 | ), 785 | 'default' => array(), 786 | ); 787 | 788 | $query_params['offset'] = array( 789 | 'description' => __( 'Offset the result set by a specific number of items.', 'wp-multi-network' ), 790 | 'type' => 'integer', 791 | ); 792 | 793 | $query_params['order'] = array( 794 | 'description' => __( 'Order sort attribute ascending or descending.', 'wp-multi-network' ), 795 | 'type' => 'string', 796 | 'default' => 'desc', 797 | 'enum' => array( 798 | 'asc', 799 | 'desc', 800 | ), 801 | ); 802 | 803 | $query_params['orderby'] = array( 804 | 'description' => __( 'Sort collection by object attribute.', 'wp-multi-network' ), 805 | 'type' => 'string', 806 | 'default' => 'id', 807 | 'enum' => array( 808 | 'id', 809 | 'domain', 810 | 'path', 811 | 'domain_length', 812 | 'path_length', 813 | 'include', 814 | ), 815 | ); 816 | 817 | $query_params['path'] = array( 818 | 'description' => __( 'Limit result set to networks of specific path.', 'wp-multi-network' ), 819 | 'type' => 'array', 820 | 'items' => array( 821 | 'type' => 'string', 822 | ), 823 | ); 824 | 825 | $query_params['path_exclude'] = array( 826 | 'description' => __( 'Ensure result set excludes specific path.', 'wp-multi-network' ), 827 | 'type' => 'array', 828 | 'items' => array( 829 | 'type' => 'string', 830 | ), 831 | ); 832 | 833 | /** 834 | * Filter collection parameters for the networks controller. 835 | * 836 | * This filter registers the collection parameter, but does not map the 837 | * collection parameter to an internal WP_Network_Query parameter. Use the 838 | * `rest_network_query` filter to set WP_Network_Query parameters. 839 | * 840 | * @since 2.4.0 841 | * 842 | * @param array $query_params JSON Schema-formatted collection parameters. 843 | */ 844 | return apply_filters( 'rest_network_collection_params', $query_params ); 845 | } 846 | 847 | 848 | /** 849 | * Checks if the network can be read. 850 | * 851 | * @since 2.4.0 852 | * 853 | * @template T of WP_REST_Request 854 | * @param WP_Network $network Network object. 855 | * @param T $request Request data to check. 856 | * 857 | * @return bool Whether the network can be read. 858 | */ 859 | protected function check_read_permission( $network, $request ) { 860 | switch_to_network( $network->id ); 861 | $allowed = current_user_can( 'manage_network' ); 862 | restore_current_network(); 863 | 864 | return $allowed; 865 | } 866 | 867 | /** 868 | * Checks if a network can be edited or deleted. 869 | * 870 | * @since 2.4.0 871 | * 872 | * @param object $network Network object. 873 | * 874 | * @return bool Whether the network can be edited or deleted. 875 | */ 876 | protected function check_edit_permission( $network ) { 877 | return current_user_can( 'edit_network', $network->id ); 878 | } 879 | 880 | /** 881 | * Helper method to add status code to returned WP_error objects. 882 | * 883 | * @param WP_Error $error Input WP_Error object. 884 | * 885 | * @return WP_Error $error 886 | */ 887 | protected function get_wp_error( $error ) { 888 | $code = $error->get_error_code(); 889 | switch ( $code ) { 890 | case 'network_not_exist': 891 | $status = 404; 892 | break; 893 | case 'network_exists': 894 | $status = 500; 895 | break; 896 | case 'network_not_empty': 897 | case 'network_empty_domain': 898 | case 'network_not_exist': 899 | case 'network_not_updated': 900 | case 'blog_bad': 901 | $status = 400; 902 | break; 903 | case 'network_super_admin': 904 | case 'network_user': 905 | default: 906 | $status = 403; 907 | break; 908 | } 909 | $error->add_data( array( 'status' => $status ), $code ); 910 | 911 | return $error; 912 | } 913 | } 914 | -------------------------------------------------------------------------------- /wp-multi-network/includes/compat.php: -------------------------------------------------------------------------------- 1 | id ); 67 | if ( (int) $exists === (int) $site_id ) { 68 | return true; 69 | } 70 | 71 | if ( true === (bool) $exists ) { 72 | return false; 73 | } 74 | 75 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery,WordPress.VIP.DirectDatabaseQuery.NoCaching 76 | $signup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->signups} WHERE domain = %s AND path = %s", $domain, $path ) ); 77 | if ( ! empty( $signup ) ) { 78 | return false; 79 | } 80 | 81 | // Skip further validation if user has access to managing network sites. 82 | if ( current_user_can( 'manage_sites' ) ) { 83 | return true; 84 | } 85 | 86 | // Validate individual domain and path parts. 87 | $paths = explode( '/', $path ); 88 | $domains = substr_count( $domain, '.' ) > 1 ? (array) substr( $domain, 0, strpos( $domain, '.' ) ) : array(); 89 | $pieces = array_filter( array_merge( $domains, $paths ) ); 90 | foreach ( $pieces as $slug ) { 91 | // Bail if not lowercase or numbers. 92 | if ( preg_match( '/[^a-z0-9]+/', $slug ) ) { 93 | return false; 94 | } 95 | 96 | // Bail if all numeric. 97 | if ( preg_match( '/^[0-9]*$/', $slug ) ) { 98 | return false; 99 | } 100 | 101 | // Bail if less than 4 characters. 102 | if ( strlen( $slug ) < 3 ) { 103 | return false; 104 | } 105 | 106 | // Bail if contains illegal names. 107 | $illegal_names = get_site_option( 'illegal_names' ); 108 | if ( ! is_subdomain_install() ) { 109 | $illegal_names = array_merge( $illegal_names, get_subdirectory_reserved_names() ); 110 | } 111 | if ( in_array( $slug, $illegal_names, true ) ) { 112 | return false; 113 | } 114 | 115 | // Bail if username exists. 116 | if ( username_exists( $slug ) ) { 117 | return false; 118 | } 119 | 120 | // Bail if subdirectory install and page exists on primary site of network. 121 | if ( ! is_subdomain_install() ) { 122 | switch_to_blog( get_current_site()->blog_id ); 123 | $page = get_page_by_path( $slug ); 124 | restore_current_blog(); 125 | if ( ! empty( $page ) ) { 126 | return false; 127 | } 128 | } 129 | } 130 | 131 | return true; 132 | } 133 | endif; 134 | 135 | if ( ! function_exists( 'wp_get_main_network' ) ) : 136 | /** 137 | * Gets the main network. 138 | * 139 | * Uses the same logic as {@see is_main_network}, but returns the network object 140 | * instead. 141 | * 142 | * @return WP_Network|null Main network object, or null if not found. 143 | */ 144 | function wp_get_main_network() { 145 | if ( ! is_multisite() ) { 146 | return null; 147 | } 148 | 149 | return get_network( get_main_network_id() ); 150 | } 151 | endif; 152 | -------------------------------------------------------------------------------- /wp-multi-network/includes/deprecated.php: -------------------------------------------------------------------------------- 1 | blog_id ) { 49 | if ( ! get_option( 'WPLANG' ) ) { 50 | return ''; 51 | } 52 | } 53 | 54 | return $value; 55 | } 56 | add_filter( 'blog_option_upload_path', 'wpmn_fix_subsite_upload_path', 10, 2 ); 57 | } 58 | -------------------------------------------------------------------------------- /wp-multi-network/includes/functions.php: -------------------------------------------------------------------------------- 1 | ID; 65 | $user_login = $user_info->user_login; 66 | } else { 67 | $user_id = (int) $user_id; 68 | $user_info = get_userdata( $user_id ); 69 | $user_login = $user_info->user_login; 70 | } 71 | 72 | /** 73 | * Filters the networks a user is the administrator of, to short-circuit the process. 74 | * 75 | * @since 2.0.0 76 | * 77 | * @param array|bool|null $my_networks List of network IDs or false. Anything but null will short-circuit 78 | * the process. 79 | * @param int $user_id User ID for which the networks should be returned. 80 | */ 81 | $my_networks = apply_filters( 'networks_pre_user_is_network_admin', null, $user_id ); 82 | if ( null !== $my_networks ) { 83 | if ( empty( $my_networks ) ) { 84 | $my_networks = false; 85 | } 86 | 87 | /** 88 | * Filters the networks a user is the administrator of. 89 | * 90 | * @since 2.0.0 91 | * 92 | * @param array|bool $my_networks List of network IDs or false if no networks for the user. 93 | * @param int $user_id User ID for which the networks should be returned. 94 | */ 95 | return apply_filters( 'networks_user_is_network_admin', $my_networks, $user_id ); 96 | } 97 | 98 | $my_networks = array(); 99 | 100 | if ( is_multisite() ) { 101 | 102 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery,WordPress.VIP.DirectDatabaseQuery.NoCaching 103 | $my_networks = array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT site_id FROM {$wpdb->sitemeta} WHERE meta_key = %s AND meta_value LIKE %s", 'site_admins', '%"' . $user_login . '"%' ) ) ); 104 | } 105 | 106 | // If there are no networks, return false. 107 | if ( empty( $my_networks ) ) { 108 | $my_networks = false; 109 | } 110 | 111 | /** This filter is documented in wp-multi-network/includes/functions.php */ 112 | return apply_filters( 'networks_user_is_network_admin', $my_networks, $user_id ); 113 | } 114 | endif; 115 | 116 | if ( ! function_exists( 'get_main_site_for_network' ) ) : 117 | /** 118 | * Gets the main site for a network. 119 | * 120 | * @since 1.3.0 121 | * 122 | * @param int|WP_Network $network Optional. Network ID or object. Default is the current network. 123 | * @return int|bool Main site ID for the network or false if network not found. 124 | */ 125 | function get_main_site_for_network( $network = null ) { 126 | $network = get_network( $network ); 127 | 128 | // Bail if network not found. 129 | if ( empty( $network ) ) { 130 | return false; 131 | } 132 | 133 | if ( ! empty( $network->blog_id ) ) { 134 | $primary_id = $network->blog_id; 135 | } else { 136 | $primary_id = get_network_option( $network->id, 'main_site' ); 137 | 138 | if ( false === $primary_id ) { 139 | $sites = get_sites( array( 140 | 'network_id' => $network->id, 141 | 'domain' => $network->domain, 142 | 'path' => $network->path, 143 | 'fields' => 'ids', 144 | 'number' => 1, 145 | ) ); 146 | 147 | $primary_id = ! empty( $sites ) ? reset( $sites ) : 0; 148 | 149 | if ( ! empty( $primary_id ) ) { 150 | update_network_option( $network->id, 'main_site', $primary_id ); 151 | } 152 | } 153 | } 154 | 155 | return (int) $primary_id; 156 | } 157 | endif; 158 | 159 | if ( ! function_exists( 'is_main_site_for_network' ) ) : 160 | /** 161 | * Checks whether a main site is a given site for a network. 162 | * 163 | * @since 1.7.0 164 | * 165 | * @param int $site_id Site ID to check if it's the main site. 166 | * @return bool True if it is the main site, false otherwise. 167 | */ 168 | function is_main_site_for_network( $site_id ) { 169 | $site = get_site( $site_id ); 170 | $main = get_main_site_id( $site->network_id ); 171 | 172 | // Bail if no site or network was found. 173 | if ( empty( $main ) ) { 174 | return false; 175 | } 176 | 177 | return (int) $main === (int) $site_id; 178 | } 179 | endif; 180 | 181 | if ( ! function_exists( 'get_network_name' ) ) : 182 | /** 183 | * Gets the name of the current network. 184 | * 185 | * @since 1.7.0 186 | * 187 | * @global WP_Network $current_site Current network object. 188 | * 189 | * @return string Name of the current network. 190 | */ 191 | function get_network_name() { 192 | global $current_site; 193 | 194 | $site_name = get_site_option( 'site_name' ); 195 | if ( ! $site_name ) { 196 | $site_name = ucfirst( $current_site->domain ); 197 | } 198 | 199 | return $site_name; 200 | } 201 | endif; 202 | 203 | if ( ! function_exists( 'switch_to_network' ) ) : 204 | /** 205 | * Switches the current context to the given network. 206 | * 207 | * @since 1.3.0 208 | * 209 | * @global wpdb $wpdb WordPress database abstraction object. 210 | * @global bool $switched_network Whether the network context is switched. 211 | * @global array $switched_network_stack Stack of switched network objects. 212 | * @global WP_Network $current_site Current network object. 213 | * 214 | * @param int $new_network Optional. ID of the network to switch to. Default is the current network ID. 215 | * @param bool $validate Optional. Whether to validate that the given network exists. Default false. 216 | * @return bool True on successful switch, false on failure. 217 | */ 218 | function switch_to_network( $new_network = 0, $validate = false ) { 219 | global $wpdb, $switched_network, $switched_network_stack, $current_site; 220 | 221 | if ( empty( $new_network ) ) { 222 | $new_network = $current_site->id; 223 | } 224 | 225 | // Bail if network does not exist. 226 | if ( ( true === (bool) $validate ) && ! get_network( $new_network ) ) { 227 | return false; 228 | } 229 | 230 | if ( empty( $switched_network_stack ) ) { 231 | $switched_network_stack = array(); 232 | } 233 | 234 | array_push( $switched_network_stack, $current_site ); 235 | 236 | // If the same network, fire the hook and bail. 237 | if ( $current_site->id === $new_network ) { 238 | 239 | /** 240 | * Fires when the current network context is switched. 241 | * 242 | * @since 1.3.0 243 | * 244 | * @param int $new_network_id ID of the network that is being switched to. 245 | * @param int $old_network_id ID of the previously current network. 246 | */ 247 | do_action( 'switch_network', $current_site->id, $current_site->id ); 248 | $switched_network = true; 249 | return true; 250 | } 251 | 252 | $prev_site_id = $current_site->id; 253 | $current_site = get_network( $new_network ); // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited 254 | 255 | // Populate extra properties if not set already. 256 | if ( ! isset( $current_site->blog_id ) ) { 257 | $current_site->blog_id = get_main_site_id( $current_site->id ); 258 | } 259 | if ( ! isset( $current_site->site_name ) ) { 260 | $current_site->site_name = get_network_name(); 261 | } 262 | 263 | // Update network globals. 264 | $wpdb->siteid = $current_site->id; 265 | $GLOBALS['site_id'] = $current_site->id; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited 266 | $GLOBALS['domain'] = $current_site->domain; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited 267 | 268 | /** This action is documented in wp-multi-network/includes/functions.php */ 269 | do_action( 'switch_network', $current_site->id, $prev_site_id ); 270 | 271 | $switched_network = true; 272 | 273 | return true; 274 | } 275 | endif; 276 | 277 | if ( ! function_exists( 'restore_current_network' ) ) : 278 | /** 279 | * Restores the current context to the previous network. 280 | * 281 | * @since 1.3.0 282 | * 283 | * @global wpdb $wpdb WordPress database abstraction object. 284 | * @global bool $switched_network Whether the network context is switched. 285 | * @global array $switched_network_stack Stack of switched network objects. 286 | * @global WP_Network $current_site Current network object. 287 | * 288 | * @return bool True on successful restore, false on failure. 289 | */ 290 | function restore_current_network() { 291 | global $wpdb, $switched_network, $switched_network_stack, $current_site; 292 | 293 | // Bail if not switched. 294 | if ( true !== $switched_network ) { 295 | return false; 296 | } 297 | 298 | // Bail if no stack. 299 | if ( ! is_array( $switched_network_stack ) ) { 300 | return false; 301 | } 302 | 303 | $new_network = array_pop( $switched_network_stack ); 304 | 305 | // If the same network, fire the hook and bail. 306 | if ( (int) $new_network->id === (int) $current_site->id ) { 307 | 308 | /** This action is documented in wp-multi-network/includes/functions.php */ 309 | do_action( 'switch_network', $current_site->id, $current_site->id ); 310 | $switched_network = ( ! empty( $switched_network_stack ) ); 311 | return true; 312 | } 313 | 314 | $prev_network_id = $current_site->id; 315 | 316 | // Update network globals. 317 | $current_site = $new_network; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited 318 | $wpdb->siteid = $new_network->id; 319 | $GLOBALS['site_id'] = $new_network->id; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited 320 | $GLOBALS['domain'] = $new_network->domain; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited 321 | 322 | /** This action is documented in wp-multi-network/includes/functions.php */ 323 | do_action( 'switch_network', $new_network->id, $prev_network_id ); 324 | 325 | $switched_network = ! empty( $switched_network_stack ); 326 | 327 | return true; 328 | } 329 | endif; 330 | 331 | if ( ! function_exists( 'insert_network' ) ) : 332 | /** 333 | * Stores basic network info in the sites table. 334 | * 335 | * This function creates a row in the wp_site table and returns 336 | * the new network ID. It is the first step in creating a new network. 337 | * 338 | * @since 2.2.0 339 | * 340 | * @global wpdb $wpdb WordPress database abstraction object. 341 | * 342 | * @param string $domain The domain of the new network. 343 | * @param string $path The path of the new network. 344 | * @return int|bool|WP_Error The ID of the new network, or false on failure. 345 | */ 346 | function insert_network( $domain = '', $path = '/' ) { 347 | global $wpdb; 348 | 349 | // Bail if no domain or path. 350 | if ( empty( $domain ) ) { 351 | return new WP_Error( 352 | 'network_empty', 353 | esc_html__( 'Domain and path cannot be empty.', 'wp-multi-network' ) 354 | ); 355 | } 356 | 357 | // Always end path with a slash. 358 | $path = trailingslashit( $path ); 359 | 360 | // Query for networks. 361 | $networks = get_networks( 362 | array( 363 | 'domain' => $domain, 364 | 'path' => $path, 365 | 'number' => '1', 366 | ) 367 | ); 368 | 369 | // Bail if network already exists. 370 | if ( ! empty( $networks ) ) { 371 | return new WP_Error( 372 | 'network_exists', 373 | esc_html__( 'Network already exists.', 'wp-multi-network' ) 374 | ); 375 | } 376 | 377 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery 378 | $result = $wpdb->insert( 379 | $wpdb->site, 380 | array( 381 | 'domain' => $domain, 382 | 'path' => $path, 383 | ) 384 | ); 385 | 386 | // Bail if database error. 387 | if ( is_wp_error( $result ) ) { 388 | return $result; 389 | } 390 | 391 | // Bail if no result. 392 | if ( empty( $result ) ) { 393 | return false; 394 | } 395 | 396 | // Cast return value as int. 397 | $network_id = (int) $wpdb->insert_id; 398 | 399 | // Clean the network cache. 400 | clean_network_cache( $network_id ); 401 | 402 | // Return network ID. 403 | return $network_id; 404 | } 405 | endif; 406 | 407 | if ( ! function_exists( 'add_network' ) ) : 408 | /** 409 | * Adds a new network. 410 | * 411 | * @since 1.3.0 412 | * 413 | * @global wpdb $wpdb WordPress database abstraction object. 414 | * 415 | * @param array $args { 416 | * Array of network arguments. 417 | * 418 | * @type string $domain Domain name for new network - for VHOST=no, 419 | * this should be FQDN, otherwise domain only. 420 | * @type string $path Path to root of network hierarchy - should 421 | * be '/' unless WP is cohabiting with another 422 | * product on a domain. 423 | * @type string $site_name Name of the root blog to be created on 424 | * the new network. 425 | * @type string $network_name Name of the new network. 426 | * @type integer $user_id ID of the user to add as the site owner. 427 | * Defaults to current user ID. 428 | * @type integer $network_admin_id ID of the user to add as the network administrator. 429 | * Defaults to current user ID. 430 | * @type array $meta Array of metadata to save to this network. 431 | * Defaults to array( 'public' => false ). 432 | * @type integer $clone_network ID of network whose networkmeta values are 433 | * to be copied - default NULL. 434 | * @type array $options_to_clone Override default network meta options to copy 435 | * when cloning - default NULL. 436 | * } 437 | * @return int|WP_Error ID of newly created network, or WP_Error on failure. 438 | */ 439 | function add_network( $args = array() ) { 440 | global $wpdb, $wp_version, $wp_db_version; 441 | 442 | $func_args = func_get_args(); 443 | 444 | // Backward compatibility with old method of passing arguments. 445 | if ( ! is_array( $args ) || count( $func_args ) > 1 ) { 446 | 447 | // Log the deprecated arguments. 448 | _deprecated_argument( 449 | __METHOD__, 450 | '1.7.0', 451 | sprintf( 452 | /* translators: 1: method name, 2: file name */ 453 | esc_html__( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'wp-multi-network' ), 454 | __METHOD__, 455 | __FILE__ 456 | ) 457 | ); 458 | 459 | // Juggle function parameters. 460 | $old_args_keys = array( 461 | 0 => 'domain', 462 | 1 => 'path', 463 | 2 => 'site_name', 464 | 3 => 'clone_network', 465 | 4 => 'options_to_clone', 466 | ); 467 | 468 | // Reset arguments to empty array. 469 | $args = array(); 470 | 471 | // Loop through deprecated keys and add items to array. 472 | foreach ( $old_args_keys as $arg_num => $arg_key ) { 473 | if ( isset( $func_args[ $arg_num ] ) ) { 474 | $args[ $arg_key ] = $func_args[ $arg_num ]; 475 | } 476 | } 477 | } 478 | 479 | // Get the current user ID. 480 | $current_user_id = get_current_user_id(); 481 | 482 | // Default site meta. 483 | $default_site_meta = array( 484 | 'public' => get_option( 'blog_public', false ), 485 | ); 486 | 487 | // Default network meta. 488 | $default_network_meta = array( 489 | 'wpmu_upgrade_site' => $wp_db_version, 490 | 'initial_db_version' => $wp_db_version, 491 | ); 492 | 493 | // Parse all of the arguments. 494 | $r = wp_parse_args( $args, array( 495 | 496 | // Site & network arguments. 497 | 'domain' => '', 498 | 'path' => '/', 499 | 500 | // Site arguments. 501 | 'site_name' => esc_attr__( 'New Site', 'wp-multi-network' ), 502 | 'user_id' => $current_user_id, 503 | 'meta' => $default_site_meta, 504 | 505 | // Network arguments. 506 | 'network_name' => esc_attr__( 'New Network', 'wp-multi-network' ), 507 | 'network_admin_id' => $current_user_id, 508 | 'network_meta' => $default_network_meta, 509 | 'clone_network' => false, 510 | 'options_to_clone' => array_keys( network_options_to_copy() ), 511 | ) ); 512 | 513 | // Bail if no user with the given ID for the site exists. 514 | if ( empty( $r['user_id'] ) || ! get_userdata( $r['user_id'] ) ) { 515 | return new WP_Error( 516 | 'network_user', 517 | esc_html__( 'User does not exist.', 'wp-multi-network' ), 518 | array( 519 | 'status' => 403, 520 | ) 521 | ); 522 | } 523 | 524 | // Bail if no user with the given ID for the network exists. 525 | if ( empty( $r['network_admin_id'] ) || ! get_userdata( $r['network_admin_id'] ) ) { 526 | return new WP_Error( 527 | 'network_super_admin', 528 | esc_html__( 'User does not exist.', 'wp-multi-network' ), 529 | array( 530 | 'status' => 403, 531 | ) 532 | ); 533 | } 534 | 535 | // Strip spaces out of domain & path. 536 | $r['domain'] = str_replace( ' ', '', strtolower( $r['domain'] ) ); 537 | $r['path'] = str_replace( ' ', '', strtolower( $r['path'] ) ); 538 | 539 | // Insert the new network. 540 | $new_network_id = insert_network( $r['domain'], $r['path'] ); 541 | 542 | // Bail if insert returned an error. 543 | if ( empty( $new_network_id ) || is_wp_error( $new_network_id ) ) { 544 | return $new_network_id; 545 | } 546 | 547 | // Set the installation constant to true. 548 | if ( ! defined( 'WP_INSTALLING' ) ) { 549 | define( 'WP_INSTALLING', true ); 550 | } 551 | 552 | // Switch to new network. 553 | switch_to_network( $new_network_id ); 554 | 555 | // Make sure upload constants are defined. 556 | ms_upload_constants(); 557 | 558 | // Attempt to create the site. 559 | $new_blog_id = wpmu_create_blog( 560 | $r['domain'], 561 | $r['path'], 562 | $r['site_name'], 563 | $r['user_id'], 564 | $r['meta'], 565 | $new_network_id 566 | ); 567 | 568 | // Grant super admin priviledges. 569 | grant_super_admin( $r['network_admin_id'] ); 570 | 571 | // Restore current network. 572 | restore_current_network(); 573 | 574 | // Bail if main site could not be created. 575 | if ( is_wp_error( $new_blog_id ) ) { 576 | return $new_blog_id; 577 | } 578 | 579 | /** 580 | * Fires after a new network blog has been added. 581 | * 582 | * @param int $new_blog_id ID of the added network blog. 583 | * @param int $new_network_id ID of the added network. 584 | * @param array $r Full associative array of network arguments. 585 | * 586 | * @since 2.5.3 587 | */ 588 | do_action( 'added_network_blog', $new_blog_id, $new_network_id, $r ); 589 | 590 | // add new blog id as network meta data against the new network. 591 | $r['network_meta']['main_site'] = $new_blog_id; 592 | 593 | if ( empty( $r['network_meta']['site_name'] ) ) { 594 | $r['network_meta']['site_name'] = ! empty( $r['network_name'] ) 595 | ? $r['network_name'] 596 | : $r['site_name']; 597 | } 598 | 599 | foreach ( $r['network_meta'] as $key => $value ) { 600 | update_network_option( $new_network_id, $key, $value ); 601 | } 602 | 603 | // Fix upload path and URLs in WP < 3.7. 604 | $use_files_rewriting = defined( 'SITE_ID_CURRENT_SITE' ) && get_network( SITE_ID_CURRENT_SITE ) 605 | ? get_network_option( SITE_ID_CURRENT_SITE, 'ms_files_rewriting' ) 606 | : get_site_option( 'ms_files_rewriting' ); 607 | 608 | // Not using rewriting, and using a newer version of WordPress than 3.7. 609 | if ( empty( $use_files_rewriting ) && version_compare( $wp_version, '3.7', '>' ) ) { 610 | 611 | // WP_CONTENT_URL is locked to the current site and can't be overridden, 612 | // so we have to replace the hostname the hard way. 613 | $current_siteurl = get_option( 'siteurl' ); 614 | $new_siteurl = untrailingslashit( get_blogaddress_by_id( $new_blog_id ) ); 615 | $upload_url = str_replace( $current_siteurl, $new_siteurl, WP_CONTENT_URL ); 616 | $upload_url = $upload_url . '/uploads'; 617 | 618 | $upload_dir = WP_CONTENT_DIR; 619 | $needle = strval( ABSPATH ); 620 | if ( 0 === strpos( $upload_dir, $needle ) ) { 621 | $upload_dir = substr( $upload_dir, strlen( $needle ) ); 622 | } 623 | $upload_dir .= '/uploads'; 624 | 625 | if ( defined( 'MULTISITE' ) ) { 626 | $ms_dir = '/sites/' . $new_blog_id; 627 | } else { 628 | $ms_dir = '/' . $new_blog_id; 629 | } 630 | 631 | $upload_dir .= $ms_dir; 632 | $upload_url .= $ms_dir; 633 | 634 | update_blog_option( $new_blog_id, 'upload_path', $upload_dir ); 635 | update_blog_option( $new_blog_id, 'upload_url_path', $upload_url ); 636 | } 637 | 638 | // Clone network meta from existing network. 639 | if ( ! empty( $r['clone_network'] ) && get_network( $r['clone_network'] ) ) { 640 | 641 | // Define empty options cache array. 642 | $options_cache = array(); 643 | 644 | // Get the values of options to clone. 645 | foreach ( $r['options_to_clone'] as $option ) { 646 | $options_cache[ $option ] = get_network_option( $r['clone_network'], $option ); 647 | } 648 | 649 | // Clone options. 650 | foreach ( $r['options_to_clone'] as $option ) { 651 | 652 | // Skip if not set. 653 | if ( ! isset( $options_cache[ $option ] ) ) { 654 | continue; 655 | } 656 | 657 | // Fix for bug that prevents writing the ms_files_rewriting value for new networks. 658 | if ( 'ms_files_rewriting' === $option ) { 659 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery 660 | $wpdb->insert( $wpdb->sitemeta, array( 661 | 'site_id' => $new_network_id, 662 | // phpcs:ignore WordPress.VIP.SlowDBQuery 663 | 'meta_key' => $option, 664 | // phpcs:ignore WordPress.VIP.SlowDBQuery 665 | 'meta_value' => $options_cache[ $option ], 666 | ) ); 667 | } else { 668 | update_network_option( $new_network_id, $option, $options_cache[ $option ] ); 669 | } 670 | } 671 | } 672 | 673 | // Update network counts. 674 | wp_update_network_counts( $new_network_id ); 675 | 676 | // Clean the network cache. 677 | clean_network_cache( $new_network_id ); 678 | 679 | /** 680 | * Fires after a new network has been added. 681 | * 682 | * @since 1.3.0 683 | * 684 | * @param int $new_network_id ID of the added network. 685 | * @param array $r Full associative array of network arguments. 686 | */ 687 | do_action( 'add_network', $new_network_id, $r ); 688 | 689 | // Return new network ID. 690 | return $new_network_id; 691 | } 692 | endif; 693 | 694 | if ( ! function_exists( 'update_network' ) ) : 695 | /** 696 | * Modifies the domain/path of a network, and updates all of its sites. 697 | * 698 | * @since 1.3.0 699 | * 700 | * @global wpdb $wpdb WordPress database abstraction object. 701 | * 702 | * @param int $id ID of network to modify. 703 | * @param string $domain New domain for network. 704 | * @param string $path New path for network. 705 | * @return bool|WP_Error True on success, or WP_Error on failure. 706 | */ 707 | function update_network( $id, $domain, $path = '' ) { 708 | global $wpdb; 709 | 710 | $network = get_network( $id ); 711 | 712 | // Bail if network not found. 713 | if ( empty( $network ) ) { 714 | return new WP_Error( 'network_not_exist', __( 'Network does not exist.', 'wp-multi-network' ) ); 715 | } 716 | 717 | $site_id = get_main_site_id( $id ); 718 | $path = wp_sanitize_site_path( $path ); 719 | 720 | // Bail if site URL is invalid. 721 | if ( ! wp_validate_site_url( $domain, $path, strval( $site_id ) ) ) { 722 | /* translators: %s: site domain and path */ 723 | return new WP_Error( 'blog_bad', sprintf( __( 'The site "%s" is invalid, not available, or already exists.', 'wp-multi-network' ), $domain . $path ) ); 724 | } 725 | 726 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery 727 | $update_result = $wpdb->update( 728 | $wpdb->site, 729 | array( 730 | 'domain' => $domain, 731 | 'path' => $path, 732 | ), 733 | array( 734 | 'id' => $network->id, 735 | ) 736 | ); 737 | if ( is_wp_error( $update_result ) ) { 738 | return new WP_Error( 'network_not_updated', __( 'Network could not be updated.', 'wp-multi-network' ) ); 739 | } 740 | 741 | $path = ! empty( $path ) ? $path : $network->path; 742 | $full_path = untrailingslashit( $domain . $path ); 743 | $old_path = untrailingslashit( $network->domain . $network->path ); 744 | 745 | $sites = get_sites( array( 746 | 'network_id' => $network->id, 747 | ) ); 748 | 749 | // Update network site domains and paths as necessary. 750 | if ( ! empty( $sites ) ) { 751 | foreach ( $sites as $site ) { 752 | $update = array(); 753 | 754 | if ( $network->domain !== $domain ) { 755 | $update['domain'] = str_replace( $network->domain, $domain, $site->domain ); 756 | } 757 | 758 | if ( $network->path !== $path ) { 759 | $search = sprintf( '|^%s|', preg_quote( $network->path, '|' ) ); 760 | $update['path'] = preg_replace( $search, $path, $site->path, 1 ); 761 | } 762 | 763 | if ( empty( $update ) ) { 764 | continue; 765 | } 766 | 767 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery 768 | $wpdb->update( $wpdb->blogs, $update, array( 769 | 'blog_id' => (int) $site->id, 770 | ) ); 771 | 772 | $option_table = $wpdb->get_blog_prefix( $site->id ) . 'options'; 773 | 774 | // Loop through URL-dependent options and correct them. 775 | foreach ( network_options_list() as $option_name ) { 776 | $value = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$option_table} WHERE option_name = %s", $option_name ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 777 | 778 | if ( ! empty( $value ) && ( false !== strpos( $value->option_value, $old_path ) ) ) { 779 | $new_value = str_replace( $old_path, $full_path, $value->option_value ); 780 | update_blog_option( $site->id, $option_name, $new_value ); 781 | } 782 | } 783 | 784 | // Clean the blog cache. 785 | clean_blog_cache( $site->id ); 786 | } 787 | } 788 | 789 | // Update network counts. 790 | wp_update_network_counts( $network->id ); 791 | 792 | // Clean the network cache. 793 | clean_network_cache( $network->id ); 794 | 795 | /** 796 | * Fires after an existing network has been updated. 797 | * 798 | * @since 1.3.0 799 | * 800 | * @param int $network_id ID of the added network. 801 | * @param array $args Associative array of network arguments. 802 | */ 803 | do_action( 'update_network', $network->id, array( 804 | 'domain' => $network->domain, 805 | 'path' => $network->path, 806 | ) ); 807 | 808 | return true; 809 | } 810 | endif; 811 | 812 | if ( ! function_exists( 'delete_network' ) ) : 813 | /** 814 | * Deletes a network and all its sites. 815 | * 816 | * @since 1.3.0 817 | * 818 | * @global wpdb $wpdb WordPress database abstraction object. 819 | * 820 | * @param int $network_id ID of network to delete. 821 | * @param bool $delete_blogs Flag to permit site deletion - default setting 822 | * of false will prevent deletion of occupied networks. 823 | * @return bool|WP_Error True on success, or WP_Error on failure. 824 | */ 825 | function delete_network( $network_id, $delete_blogs = false ) { 826 | global $wpdb; 827 | 828 | $network = get_network( $network_id ); 829 | 830 | // Bail if network does not exist. 831 | if ( empty( $network ) ) { 832 | return new WP_Error( 'network_not_exist', __( 'Network does not exist.', 'wp-multi-network' ) ); 833 | } 834 | 835 | $sites = get_sites( array( 836 | 'network_id' => $network->id, 837 | ) ); 838 | if ( ! empty( $sites ) ) { 839 | 840 | // Bail if site deletion is off. 841 | if ( empty( $delete_blogs ) ) { 842 | return new WP_Error( 'network_not_empty', __( 'Cannot delete network with sites.', 'wp-multi-network' ) ); 843 | } 844 | 845 | if ( true === $delete_blogs ) { 846 | foreach ( $sites as $site ) { 847 | if ( wp_should_rescue_orphaned_sites() ) { 848 | move_site( $site->id, 0 ); 849 | continue; 850 | } 851 | 852 | wpmu_delete_blog( $site->id, true ); 853 | } 854 | } 855 | } 856 | 857 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery 858 | $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->site} WHERE id = %d", $network->id ) ); 859 | 860 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery 861 | $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->sitemeta} WHERE site_id = %d", $network->id ) ); 862 | 863 | // Clean the network cache. 864 | clean_network_cache( $network->id ); 865 | 866 | /** 867 | * Fires after a network has been deleted. 868 | * 869 | * @since 1.3.0 870 | * 871 | * @param WP_Network $network The deleted network object. 872 | */ 873 | do_action( 'delete_network', $network ); 874 | 875 | return true; 876 | } 877 | endif; 878 | 879 | if ( ! function_exists( 'move_site' ) ) : 880 | /** 881 | * Moves a site to a new network. 882 | * 883 | * @since 1.3.0 884 | * 885 | * @global wpdb $wpdb WordPress database abstraction object. 886 | * 887 | * @param int $site_id ID of site to move. 888 | * @param int $new_network_id ID of destination network. 889 | * @return int|bool|WP_Error New network ID on success, true if site cannot be moved, 890 | * or WP_Error on failure. 891 | */ 892 | function move_site( $site_id = 0, $new_network_id = 0 ) { 893 | global $wpdb; 894 | 895 | $site = get_site( $site_id ); 896 | 897 | // Cast network IDs to ints. 898 | $old_network_id = (int) $site->network_id; 899 | $new_network_id = (int) $new_network_id; 900 | 901 | // Bail if site does not exist. 902 | if ( empty( $site ) ) { 903 | return new WP_Error( 'blog_not_exist', __( 'Site does not exist.', 'wp-multi-network' ) ); 904 | } 905 | 906 | // Bail if site is the main site. 907 | if ( is_main_site( $site->id, $old_network_id ) ) { 908 | return true; 909 | } 910 | 911 | // Bail if no change. 912 | if ( $new_network_id === $old_network_id ) { 913 | return true; 914 | } 915 | 916 | // Update the database entry. 917 | $result = $wpdb->update( // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery 918 | $wpdb->blogs, 919 | array( 920 | 'site_id' => $new_network_id, 921 | ), 922 | array( 923 | 'blog_id' => $site->id, 924 | ) 925 | ); 926 | 927 | // Bail if error. 928 | if ( empty( $result ) ) { 929 | return new WP_Error( 'blog_not_moved', __( 'Site could not be moved.', 'wp-multi-network' ) ); 930 | } 931 | 932 | // Update old network counts. 933 | if ( 0 !== $old_network_id ) { 934 | wp_update_network_counts( $old_network_id ); 935 | } 936 | 937 | // Update new network counts. 938 | if ( 0 !== $new_network_id ) { 939 | wp_update_network_counts( $new_network_id ); 940 | } 941 | 942 | // Clean the blog cache. 943 | clean_blog_cache( $site_id ); 944 | 945 | // Clean the network caches. 946 | clean_network_cache( 947 | array_filter( 948 | array( 949 | $site->network_id, 950 | $new_network_id, 951 | ) 952 | ) 953 | ); 954 | 955 | /** 956 | * Fires after a site has been moved to a new network. 957 | * 958 | * @since 1.3.0 959 | * 960 | * @param int $site_id ID of the site that has been moved. 961 | * @param int $old_network_id ID of the original network for the site. 962 | * @param int $new_network_id ID of the network the site has been moved to. 963 | */ 964 | do_action( 'move_site', $site_id, $site->network_id, $new_network_id ); 965 | 966 | return $new_network_id; 967 | } 968 | endif; 969 | 970 | if ( ! function_exists( 'network_options_list' ) ) : 971 | /** 972 | * Lists the URL-dependent options. 973 | * 974 | * @since 1.3.0 975 | * 976 | * @return string[] List of network option names. 977 | */ 978 | function network_options_list() { 979 | $network_options = array( 980 | 'siteurl', 981 | 'home', 982 | ); 983 | 984 | /** 985 | * Filters the list of network options that depend on the domain and path of a network. 986 | * 987 | * @since 1.3.0 988 | * 989 | * @param array $network_options List of network option names. 990 | */ 991 | return apply_filters( 'network_options_list', $network_options ); 992 | } 993 | endif; 994 | 995 | if ( ! function_exists( 'network_options_to_copy' ) ) : 996 | /** 997 | * Lists the default network options to copy. 998 | * 999 | * @since 1.3.0 1000 | * 1001 | * @return array List of network $option_name => $option_label pairs. 1002 | */ 1003 | function network_options_to_copy() { 1004 | $network_options = array( 1005 | 'admin_email' => __( 'Network admin email', 'wp-multi-network' ), 1006 | 'admin_user_id' => __( 'Admin user ID - deprecated', 'wp-multi-network' ), 1007 | 'allowed_themes' => __( 'OLD List of allowed themes - deprecated', 'wp-multi-network' ), 1008 | 'allowedthemes' => __( 'List of allowed themes', 'wp-multi-network' ), 1009 | 'banned_email_domains' => __( 'Banned email domains', 'wp-multi-network' ), 1010 | 'first_post' => __( 'Content of first post on a new blog', 'wp-multi-network' ), 1011 | 'limited_email_domains' => __( 'Permitted email domains', 'wp-multi-network' ), 1012 | 'ms_files_rewriting' => __( 'Uploaded file handling', 'wp-multi-network' ), 1013 | 'site_admins' => __( 'List of network admin usernames', 'wp-multi-network' ), 1014 | 'upload_filetypes' => __( 'List of allowed file types for uploads', 'wp-multi-network' ), 1015 | 'welcome_email' => __( 'Content of welcome email', 'wp-multi-network' ), 1016 | ); 1017 | 1018 | /** 1019 | * Filters the default network options to copy. 1020 | * 1021 | * @since 1.3.0 1022 | * 1023 | * @return array List of network $option_name => $option_label pairs. 1024 | */ 1025 | return apply_filters( 'network_options_to_copy', $network_options ); 1026 | } 1027 | endif; 1028 | -------------------------------------------------------------------------------- /wp-multi-network/includes/metaboxes/edit-network.php: -------------------------------------------------------------------------------- 1 | domain ) ? Requests_IDNAEncoder::encode( $network->domain ) : ''; 22 | $path = ! empty( $network->path ) ? $network->path : '/'; 23 | 24 | ?> 25 | 26 | 27 | 28 | 29 | 30 | 33 | 39 | 40 | 41 | 44 | 48 | 49 | 50 | 51 |
31 | 32 | 34 | 38 |
42 | 43 | 45 | 46 |

47 |
52 | 53 | 64 | 65 | 66 | 67 | 68 | 69 | 72 | 76 | 77 | 78 | 79 |
70 | 71 | 73 | 74 |

75 |
80 | 81 | array( get_main_site_id( $network->id ) ), 96 | 'network_id' => $network->id, 97 | ) 98 | ); 99 | 100 | $from = get_sites( 101 | array( 102 | 'network__not_in' => array( $network->id ), 103 | ) 104 | ); 105 | 106 | ?> 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 134 | 138 | 155 | 156 |
 
118 | 133 | 135 | 136 | 137 | 139 | 154 |
157 | 158 | id; 176 | $button_text = esc_html__( 'Update', 'wp-multi-network' ); 177 | $action = 'update'; 178 | } 179 | 180 | $cancel_url = add_query_arg( 181 | array( 182 | 'page' => 'networks', 183 | ), 184 | network_admin_url( 'admin.php' ) 185 | ); 186 | 187 | ?> 188 | 189 |
190 |
191 |
192 | 196 | 197 |
198 | 199 | %s', 'wp-multi-network' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 203 | esc_html( get_network_option( $network->id, 'site_name' ) ) 204 | ); 205 | ?> 206 | 207 |
208 |
209 | 210 | %s', 'wp-multi-network' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 214 | esc_html( get_network_option( $network->id, 'blog_count' ) ) 215 | ); 216 | ?> 217 | 218 |
219 | 220 | 223 | 224 |
225 | 226 |
227 | 228 | 232 |
233 | 234 |
235 |
236 | 237 |
238 | 239 |
240 | 247 | 248 | 249 |
250 |
251 |
252 |
253 | 254 | 27 | 28 | 29 | 30 | 31 | 69 | 70 |
32 | 56 | 57 |

58 | 61 | 62 |
63 | 64 | 67 |

68 |
71 | 72 | 85 | 86 |
87 |
88 |
89 |
90 | 91 | ' . esc_html( $site->registered ) . '' 96 | ); 97 | ?> 98 | 99 |
100 |
101 | 102 | ' . esc_html( $site->domain ) . '' 107 | ); 108 | ?> 109 | 110 |
111 |
112 | 113 | ' . esc_html( $site->path ) . '' 118 | ); 119 | ?> 120 | 121 |
122 |
123 | 124 |
125 |
126 | 127 |
128 | 129 |
130 | 142 | 143 | 144 |
145 |
146 |
147 |
148 | 149 | constants(); 105 | $this->setup_globals(); 106 | $this->includes(); 107 | } 108 | 109 | /** 110 | * Sets up constants used by the plugin if they are not already defined. 111 | * 112 | * @since 1.3.0 113 | * @return void 114 | */ 115 | private function constants() { 116 | if ( ! defined( 'RESCUE_ORPHANED_BLOGS' ) ) { 117 | define( 'RESCUE_ORPHANED_BLOGS', false ); 118 | } 119 | 120 | if ( ! defined( 'WPMN_DEPRECATED' ) ) { 121 | define( 'WPMN_DEPRECATED', false ); 122 | } 123 | } 124 | 125 | /** 126 | * Sets up the global properties used by the plugin. 127 | * 128 | * @since 1.3.0 129 | * @return void 130 | */ 131 | private function setup_globals() { 132 | $this->file = __FILE__; 133 | $this->basename = plugin_basename( $this->file ); 134 | $this->plugin_dir = plugin_dir_path( $this->file ) . 'wp-multi-network/'; 135 | $this->plugin_url = plugin_dir_url( $this->file ) . 'wp-multi-network/'; 136 | } 137 | 138 | /** 139 | * Includes the required files to run the plugin. 140 | * 141 | * @since 1.3.0 142 | * @return void 143 | */ 144 | private function includes() { 145 | 146 | // Manual localization loading is no longer necessary since WP 4.6. 147 | if ( version_compare( $GLOBALS['wp_version'], '4.6', '<' ) ) { 148 | load_plugin_textdomain( 'wp-multi-network' ); 149 | } 150 | 151 | require $this->plugin_dir . 'includes/compat.php'; 152 | require $this->plugin_dir . 'includes/functions.php'; 153 | 154 | require $this->plugin_dir . 'includes/classes/class-wp-ms-networks-capabilities.php'; 155 | 156 | if ( is_blog_admin() || is_network_admin() ) { 157 | require $this->plugin_dir . 'includes/metaboxes/move-site.php'; 158 | require $this->plugin_dir . 'includes/metaboxes/edit-network.php'; 159 | 160 | require $this->plugin_dir . 'includes/classes/class-wp-ms-networks-admin.php'; 161 | 162 | $this->admin = new WP_MS_Networks_Admin(); 163 | } 164 | 165 | require $this->plugin_dir . 'includes/classes/class-wp-ms-networks-admin-bar.php'; 166 | 167 | $this->capabilities = new WP_MS_Networks_Capabilities(); 168 | $this->capabilities->add_hooks(); 169 | 170 | $this->admin_bar = new WP_MS_Networks_Admin_Bar(); 171 | $this->admin_bar->add_hooks(); 172 | 173 | if ( defined( 'WPMN_DEPRECATED' ) && ( true === WPMN_DEPRECATED ) ) { 174 | require $this->plugin_dir . 'includes/deprecated.php'; 175 | } 176 | 177 | if ( defined( 'WP_CLI' ) && WP_CLI ) { 178 | require $this->plugin_dir . 'includes/classes/class-wp-ms-network-command.php'; 179 | } 180 | 181 | // REST endpoint class only load 4.7+. 182 | if ( version_compare( $GLOBALS['wp_version'], '4.7', '>=' ) ) { 183 | require $this->plugin_dir . 'includes/classes/class-wp-ms-rest-networks-controller.php'; 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * Hooks loader into muplugins_loaded, in order to load early. 190 | * 191 | * @since 1.3.0 192 | * @return void 193 | */ 194 | function setup_multi_network() { // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed 195 | wpmn(); 196 | } 197 | add_action( 'muplugins_loaded', 'setup_multi_network' ); 198 | 199 | /** 200 | * Hook REST endpoints on rest_api_init 201 | * 202 | * @since 2.3.0 203 | * @return void 204 | */ 205 | function setup_multi_network_endpoints() { 206 | $controller = new WP_MS_REST_Networks_Controller(); 207 | $controller->register_routes(); 208 | } 209 | add_action( 'rest_api_init', 'setup_multi_network_endpoints', 99 ); 210 | 211 | /** 212 | * Returns the main WP Multi Network instance. 213 | * 214 | * It will be instantiated if not available yet. 215 | * 216 | * @since 1.7.0 217 | * @return WPMN_Loader WP Multi Network instance to use. 218 | */ 219 | function wpmn() { 220 | static $wpmn = false; 221 | 222 | if ( false === $wpmn ) { 223 | $wpmn = new WPMN_Loader(); 224 | } 225 | 226 | return $wpmn; 227 | } 228 | --------------------------------------------------------------------------------