├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── assets ├── import.css ├── import.js ├── intro.css └── intro.js ├── bin └── install-wp-tests.sh ├── class-command.php ├── class-logger-cli.php ├── class-logger-html.php ├── class-logger-serversentevents.php ├── class-logger.php ├── class-wxr-import-info.php ├── class-wxr-import-ui.php ├── class-wxr-importer.php ├── composer.json ├── package.json ├── phpcs.ruleset.xml ├── phpunit.xml.dist ├── plugin.php ├── templates ├── footer.php ├── header.php ├── import.php ├── intro.php ├── select-options.php └── upload.php └── tests ├── bootstrap.php └── test-importer-plugin.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | ## Composer 3 | /vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | 4 | language: php 5 | 6 | matrix: 7 | fast_finish: true 8 | include: 9 | - php: 5.2 10 | - php: 5.3 11 | - php: 5.4 12 | - php: 5.5 13 | - php: 5.6 14 | - php: 7.0 15 | - php: hhvm 16 | 17 | - php: 5.6 18 | env: WP_TRAVISCI="travis:phpvalidate" 19 | 20 | branches: 21 | only: 22 | - master 23 | 24 | env: 25 | global: 26 | - WP_TRAVISCI="travis:phpunit" 27 | 28 | # Clones WordPress and configures our testing environment. 29 | before_script: 30 | # Setup Coveralls 31 | - phpenv local 5.5 32 | - composer install --no-interaction 33 | - phpenv local --unset 34 | 35 | - bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest 36 | 37 | - cd $TRAVIS_BUILD_DIR 38 | - nvm install 6 39 | - nvm use 6 40 | - node -v 41 | - npm -v 42 | - npm install -g grunt-cli 43 | - npm install 44 | 45 | script: 46 | - grunt $WP_TRAVISCI 47 | 48 | after_script: 49 | # Push coverage off to Codecov 50 | - bash <(curl -s https://codecov.io/bash) 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | So you want to contribute to the Importer? Fantastic! There are a few rules you'll need to follow for all contributions. 4 | 5 | (There are always exceptions to these rules. :) ) 6 | 7 | ## Process 8 | 9 | 1. Ideally, start with an issue to check the need for a PR. It's possible that a feature may be rejected at an early stage, and it's better to find out before you write the code. 10 | 2. Write the code. Small, atomic commits are preferred. Explain the motivation behind the change when needed. 11 | 3. File a PR. If it isn't ready for merge yet, note that in the description. If your PR closes an existing issue, add "fixes #xxx" to the message, so that the issue will be closed when the PR is merged. 12 | 4. If needed, iterate on the code until it is ready. This includes adding unit tests. When you're ready, comment that the PR is complete. 13 | 5. A committer will review your code and offer you feedback. 14 | 6. Update with the feedback as necessary. 15 | 7. PR will be merged. 16 | 17 | Notes: 18 | 19 | * All code needs to go through peer review. Committers may not merge their own PR. 20 | * PRs should **never be squashed or rebased**. This includes when merging. Keeping the history is important for tracking motivation behind changes later. 21 | 22 | ## Best Practices 23 | 24 | All code in the Importer must be compatible with PHP 5.2. Treat this code as if it were part of WordPress core, and apply the [same best practices](https://make.wordpress.org/core/handbook/best-practices/). 25 | 26 | ### Commit Messages 27 | 28 | Commit messages should follow the [general git best practices](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html): 29 | 30 | ``` 31 | Capitalized, short (50 chars or less) summary 32 | 33 | More detailed explanatory text, if necessary. Wrap it to about 72 34 | characters or so. In some contexts, the first line is treated as the 35 | subject of an email and the rest of the text as the body. The blank 36 | line separating the summary from the body is critical (unless you omit 37 | the body entirely); tools like rebase can get confused if you run the 38 | two together. 39 | 40 | Write your commit message in the imperative: "Fix bug" and not "Fixed bug" 41 | or "Fixes bug." This convention matches up with commit messages generated 42 | by commands like git merge and git revert. 43 | 44 | Further paragraphs come after blank lines. 45 | 46 | - Bullet points are okay, too 47 | 48 | - Typically a hyphen or asterisk is used for the bullet, followed by a 49 | single space, with blank lines in between, but conventions vary here 50 | 51 | - Use a hanging indent 52 | ``` 53 | 54 | There is no need to reference issues inside commits, as all interaction with issues is handled via pull requests. 55 | 56 | 57 | ## Coding Style 58 | 59 | The coding style should match [the WordPress coding standards](https://make.wordpress.org/core/handbook/coding-standards/php/). 60 | 61 | 62 | ## Unit Tests 63 | 64 | PRs should include unit tests for any changes. These are written in PHPUnit, and should be added to the file corresponding to the class they test (that is, tests for `class-wxr-importer.php` would be in `tests/test-wxr-importer.php`). 65 | 66 | Where possible, features should be unit tested. The eventual aim is to have >90% coverage. 67 | 68 | 73 | 74 | 75 | ## Licensing 76 | 77 | By contributing code to this repository, you agree to license your code for use under the [GPL License](https://github.com/humanmade/WordPress-Importer/blob/master/LICENSE). 78 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | 'use strict'; 3 | 4 | require('phplint').gruntPlugin(grunt); 5 | grunt.initConfig( { 6 | 7 | pkg: grunt.file.readJSON( 'package.json' ), 8 | 9 | phpcs: { 10 | plugin: { 11 | src: './' 12 | }, 13 | options: { 14 | bin: "vendor/bin/phpcs --extensions=php --ignore=\"*/vendor/*,*/node_modules/*\"", 15 | standard: "phpcs.ruleset.xml" 16 | } 17 | }, 18 | 19 | phplint: { 20 | options: { 21 | limit: 10, 22 | stdout: true, 23 | stderr: true 24 | }, 25 | files: ['tests/*.php', '*.php'] 26 | }, 27 | 28 | phpunit: { 29 | 'default': { 30 | cmd: 'phpunit', 31 | args: [ '-c', 'phpunit.xml.dist' ], 32 | } 33 | } 34 | 35 | } ); 36 | grunt.loadNpmTasks( 'grunt-phpcs' ); 37 | 38 | // Testing tasks. 39 | grunt.registerMultiTask('phpunit', 'Runs PHPUnit tests, including the ajax, external-http, and multisite tests.', function() { 40 | grunt.util.spawn({ 41 | cmd: this.data.cmd, 42 | args: this.data.args, 43 | opts: {stdio: 'inherit'} 44 | }, this.async()); 45 | }); 46 | 47 | grunt.registerTask( 'test', [ 'phpcs', 'phplint', 'phpunit' ] ); 48 | grunt.util.linefeed = '\n'; 49 | 50 | // Travis CI tasks. 51 | grunt.registerTask('travis:phpvalidate', 'Runs PHPUnit Travis CI PHP code tasks.', [ 52 | 'phpcs', 53 | 'phplint' 54 | ] ); 55 | grunt.registerTask('travis:phpunit', 'Runs PHPUnit Travis CI tasks.', [ 56 | 'phpunit', 57 | ] ); 58 | }; 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | WordPress Importer - Import content into WordPress 2 | 3 | Copyright 2016-2017 by the contributors 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | 19 | This program incorporates work covered by the following copyright and 20 | permission notices: 21 | 22 | WordPress Importer - Import content into WordPress 23 | 24 | Copyright 2016-2017 by the contributors 25 | 26 | The WordPress Importer is released under the GPL 27 | 28 | =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 29 | 30 | GNU GENERAL PUBLIC LICENSE 31 | Version 2, June 1991 32 | 33 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 34 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 35 | Everyone is permitted to copy and distribute verbatim copies 36 | of this license document, but changing it is not allowed. 37 | 38 | Preamble 39 | 40 | The licenses for most software are designed to take away your 41 | freedom to share and change it. By contrast, the GNU General Public 42 | License is intended to guarantee your freedom to share and change free 43 | software--to make sure the software is free for all its users. This 44 | General Public License applies to most of the Free Software 45 | Foundation's software and to any other program whose authors commit to 46 | using it. (Some other Free Software Foundation software is covered by 47 | the GNU Lesser General Public License instead.) You can apply it to 48 | your programs, too. 49 | 50 | When we speak of free software, we are referring to freedom, not 51 | price. Our General Public Licenses are designed to make sure that you 52 | have the freedom to distribute copies of free software (and charge for 53 | this service if you wish), that you receive source code or can get it 54 | if you want it, that you can change the software or use pieces of it 55 | in new free programs; and that you know you can do these things. 56 | 57 | To protect your rights, we need to make restrictions that forbid 58 | anyone to deny you these rights or to ask you to surrender the rights. 59 | These restrictions translate to certain responsibilities for you if you 60 | distribute copies of the software, or if you modify it. 61 | 62 | For example, if you distribute copies of such a program, whether 63 | gratis or for a fee, you must give the recipients all the rights that 64 | you have. You must make sure that they, too, receive or can get the 65 | source code. And you must show them these terms so they know their 66 | rights. 67 | 68 | We protect your rights with two steps: (1) copyright the software, and 69 | (2) offer you this license which gives you legal permission to copy, 70 | distribute and/or modify the software. 71 | 72 | Also, for each author's protection and ours, we want to make certain 73 | that everyone understands that there is no warranty for this free 74 | software. If the software is modified by someone else and passed on, we 75 | want its recipients to know that what they have is not the original, so 76 | that any problems introduced by others will not reflect on the original 77 | authors' reputations. 78 | 79 | Finally, any free program is threatened constantly by software 80 | patents. We wish to avoid the danger that redistributors of a free 81 | program will individually obtain patent licenses, in effect making the 82 | program proprietary. To prevent this, we have made it clear that any 83 | patent must be licensed for everyone's free use or not licensed at all. 84 | 85 | The precise terms and conditions for copying, distribution and 86 | modification follow. 87 | 88 | GNU GENERAL PUBLIC LICENSE 89 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 90 | 91 | 0. This License applies to any program or other work which contains 92 | a notice placed by the copyright holder saying it may be distributed 93 | under the terms of this General Public License. The "Program", below, 94 | refers to any such program or work, and a "work based on the Program" 95 | means either the Program or any derivative work under copyright law: 96 | that is to say, a work containing the Program or a portion of it, 97 | either verbatim or with modifications and/or translated into another 98 | language. (Hereinafter, translation is included without limitation in 99 | the term "modification".) Each licensee is addressed as "you". 100 | 101 | Activities other than copying, distribution and modification are not 102 | covered by this License; they are outside its scope. The act of 103 | running the Program is not restricted, and the output from the Program 104 | is covered only if its contents constitute a work based on the 105 | Program (independent of having been made by running the Program). 106 | Whether that is true depends on what the Program does. 107 | 108 | 1. You may copy and distribute verbatim copies of the Program's 109 | source code as you receive it, in any medium, provided that you 110 | conspicuously and appropriately publish on each copy an appropriate 111 | copyright notice and disclaimer of warranty; keep intact all the 112 | notices that refer to this License and to the absence of any warranty; 113 | and give any other recipients of the Program a copy of this License 114 | along with the Program. 115 | 116 | You may charge a fee for the physical act of transferring a copy, and 117 | you may at your option offer warranty protection in exchange for a fee. 118 | 119 | 2. You may modify your copy or copies of the Program or any portion 120 | of it, thus forming a work based on the Program, and copy and 121 | distribute such modifications or work under the terms of Section 1 122 | above, provided that you also meet all of these conditions: 123 | 124 | a) You must cause the modified files to carry prominent notices 125 | stating that you changed the files and the date of any change. 126 | 127 | b) You must cause any work that you distribute or publish, that in 128 | whole or in part contains or is derived from the Program or any 129 | part thereof, to be licensed as a whole at no charge to all third 130 | parties under the terms of this License. 131 | 132 | c) If the modified program normally reads commands interactively 133 | when run, you must cause it, when started running for such 134 | interactive use in the most ordinary way, to print or display an 135 | announcement including an appropriate copyright notice and a 136 | notice that there is no warranty (or else, saying that you provide 137 | a warranty) and that users may redistribute the program under 138 | these conditions, and telling the user how to view a copy of this 139 | License. (Exception: if the Program itself is interactive but 140 | does not normally print such an announcement, your work based on 141 | the Program is not required to print an announcement.) 142 | 143 | These requirements apply to the modified work as a whole. If 144 | identifiable sections of that work are not derived from the Program, 145 | and can be reasonably considered independent and separate works in 146 | themselves, then this License, and its terms, do not apply to those 147 | sections when you distribute them as separate works. But when you 148 | distribute the same sections as part of a whole which is a work based 149 | on the Program, the distribution of the whole must be on the terms of 150 | this License, whose permissions for other licensees extend to the 151 | entire whole, and thus to each and every part regardless of who wrote it. 152 | 153 | Thus, it is not the intent of this section to claim rights or contest 154 | your rights to work written entirely by you; rather, the intent is to 155 | exercise the right to control the distribution of derivative or 156 | collective works based on the Program. 157 | 158 | In addition, mere aggregation of another work not based on the Program 159 | with the Program (or with a work based on the Program) on a volume of 160 | a storage or distribution medium does not bring the other work under 161 | the scope of this License. 162 | 163 | 3. You may copy and distribute the Program (or a work based on it, 164 | under Section 2) in object code or executable form under the terms of 165 | Sections 1 and 2 above provided that you also do one of the following: 166 | 167 | a) Accompany it with the complete corresponding machine-readable 168 | source code, which must be distributed under the terms of Sections 169 | 1 and 2 above on a medium customarily used for software interchange; or, 170 | 171 | b) Accompany it with a written offer, valid for at least three 172 | years, to give any third party, for a charge no more than your 173 | cost of physically performing source distribution, a complete 174 | machine-readable copy of the corresponding source code, to be 175 | distributed under the terms of Sections 1 and 2 above on a medium 176 | customarily used for software interchange; or, 177 | 178 | c) Accompany it with the information you received as to the offer 179 | to distribute corresponding source code. (This alternative is 180 | allowed only for noncommercial distribution and only if you 181 | received the program in object code or executable form with such 182 | an offer, in accord with Subsection b above.) 183 | 184 | The source code for a work means the preferred form of the work for 185 | making modifications to it. For an executable work, complete source 186 | code means all the source code for all modules it contains, plus any 187 | associated interface definition files, plus the scripts used to 188 | control compilation and installation of the executable. However, as a 189 | special exception, the source code distributed need not include 190 | anything that is normally distributed (in either source or binary 191 | form) with the major components (compiler, kernel, and so on) of the 192 | operating system on which the executable runs, unless that component 193 | itself accompanies the executable. 194 | 195 | If distribution of executable or object code is made by offering 196 | access to copy from a designated place, then offering equivalent 197 | access to copy the source code from the same place counts as 198 | distribution of the source code, even though third parties are not 199 | compelled to copy the source along with the object code. 200 | 201 | 4. You may not copy, modify, sublicense, or distribute the Program 202 | except as expressly provided under this License. Any attempt 203 | otherwise to copy, modify, sublicense or distribute the Program is 204 | void, and will automatically terminate your rights under this License. 205 | However, parties who have received copies, or rights, from you under 206 | this License will not have their licenses terminated so long as such 207 | parties remain in full compliance. 208 | 209 | 5. You are not required to accept this License, since you have not 210 | signed it. However, nothing else grants you permission to modify or 211 | distribute the Program or its derivative works. These actions are 212 | prohibited by law if you do not accept this License. Therefore, by 213 | modifying or distributing the Program (or any work based on the 214 | Program), you indicate your acceptance of this License to do so, and 215 | all its terms and conditions for copying, distributing or modifying 216 | the Program or works based on it. 217 | 218 | 6. Each time you redistribute the Program (or any work based on the 219 | Program), the recipient automatically receives a license from the 220 | original licensor to copy, distribute or modify the Program subject to 221 | these terms and conditions. You may not impose any further 222 | restrictions on the recipients' exercise of the rights granted herein. 223 | You are not responsible for enforcing compliance by third parties to 224 | this License. 225 | 226 | 7. If, as a consequence of a court judgment or allegation of patent 227 | infringement or for any other reason (not limited to patent issues), 228 | conditions are imposed on you (whether by court order, agreement or 229 | otherwise) that contradict the conditions of this License, they do not 230 | excuse you from the conditions of this License. If you cannot 231 | distribute so as to satisfy simultaneously your obligations under this 232 | License and any other pertinent obligations, then as a consequence you 233 | may not distribute the Program at all. For example, if a patent 234 | license would not permit royalty-free redistribution of the Program by 235 | all those who receive copies directly or indirectly through you, then 236 | the only way you could satisfy both it and this License would be to 237 | refrain entirely from distribution of the Program. 238 | 239 | If any portion of this section is held invalid or unenforceable under 240 | any particular circumstance, the balance of the section is intended to 241 | apply and the section as a whole is intended to apply in other 242 | circumstances. 243 | 244 | It is not the purpose of this section to induce you to infringe any 245 | patents or other property right claims or to contest validity of any 246 | such claims; this section has the sole purpose of protecting the 247 | integrity of the free software distribution system, which is 248 | implemented by public license practices. Many people have made 249 | generous contributions to the wide range of software distributed 250 | through that system in reliance on consistent application of that 251 | system; it is up to the author/donor to decide if he or she is willing 252 | to distribute software through any other system and a licensee cannot 253 | impose that choice. 254 | 255 | This section is intended to make thoroughly clear what is believed to 256 | be a consequence of the rest of this License. 257 | 258 | 8. If the distribution and/or use of the Program is restricted in 259 | certain countries either by patents or by copyrighted interfaces, the 260 | original copyright holder who places the Program under this License 261 | may add an explicit geographical distribution limitation excluding 262 | those countries, so that distribution is permitted only in or among 263 | countries not thus excluded. In such case, this License incorporates 264 | the limitation as if written in the body of this License. 265 | 266 | 9. The Free Software Foundation may publish revised and/or new versions 267 | of the General Public License from time to time. Such new versions will 268 | be similar in spirit to the present version, but may differ in detail to 269 | address new problems or concerns. 270 | 271 | Each version is given a distinguishing version number. If the Program 272 | specifies a version number of this License which applies to it and "any 273 | later version", you have the option of following the terms and conditions 274 | either of that version or of any later version published by the Free 275 | Software Foundation. If the Program does not specify a version number of 276 | this License, you may choose any version ever published by the Free Software 277 | Foundation. 278 | 279 | 10. If you wish to incorporate parts of the Program into other free 280 | programs whose distribution conditions are different, write to the author 281 | to ask for permission. For software which is copyrighted by the Free 282 | Software Foundation, write to the Free Software Foundation; we sometimes 283 | make exceptions for this. Our decision will be guided by the two goals 284 | of preserving the free status of all derivatives of our free software and 285 | of promoting the sharing and reuse of software generally. 286 | 287 | NO WARRANTY 288 | 289 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 290 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 291 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 292 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 293 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 294 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 295 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 296 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 297 | REPAIR OR CORRECTION. 298 | 299 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 300 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 301 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 302 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 303 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 304 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 305 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 306 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 307 | POSSIBILITY OF SUCH DAMAGES. 308 | 309 | END OF TERMS AND CONDITIONS 310 | 311 | How to Apply These Terms to Your New Programs 312 | 313 | If you develop a new program, and you want it to be of the greatest 314 | possible use to the public, the best way to achieve this is to make it 315 | free software which everyone can redistribute and change under these terms. 316 | 317 | To do so, attach the following notices to the program. It is safest 318 | to attach them to the start of each source file to most effectively 319 | convey the exclusion of warranty; and each file should have at least 320 | the "copyright" line and a pointer to where the full notice is found. 321 | 322 | 323 | Copyright (C) 324 | 325 | This program is free software; you can redistribute it and/or modify 326 | it under the terms of the GNU General Public License as published by 327 | the Free Software Foundation; either version 2 of the License, or 328 | (at your option) any later version. 329 | 330 | This program is distributed in the hope that it will be useful, 331 | but WITHOUT ANY WARRANTY; without even the implied warranty of 332 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 333 | GNU General Public License for more details. 334 | 335 | You should have received a copy of the GNU General Public License along 336 | with this program; if not, write to the Free Software Foundation, Inc., 337 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 338 | 339 | Also add information on how to contact you by electronic and paper mail. 340 | 341 | If the program is interactive, make it output a short notice like this 342 | when it starts in an interactive mode: 343 | 344 | Gnomovision version 69, Copyright (C) year name of author 345 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 346 | This is free software, and you are welcome to redistribute it 347 | under certain conditions; type `show c' for details. 348 | 349 | The hypothetical commands `show w' and `show c' should show the appropriate 350 | parts of the General Public License. Of course, the commands you use may 351 | be called something other than `show w' and `show c'; they could even be 352 | mouse-clicks or menu items--whatever suits your program. 353 | 354 | You should also get your employer (if you work as a programmer) or your 355 | school, if any, to sign a "copyright disclaimer" for the program, if 356 | necessary. Here is a sample; alter the names: 357 | 358 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 359 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 360 | 361 | , 1 April 1989 362 | Ty Coon, President of Vice 363 | 364 | This General Public License does not permit incorporating your program into 365 | proprietary programs. If your program is a subroutine library, you may 366 | consider it more useful to permit linking proprietary applications with the 367 | library. If this is what you want to do, use the GNU Lesser General 368 | Public License instead of this License. 369 | 370 | WRITTEN OFFER 371 | 372 | The source code for any program binaries or compressed scripts that are 373 | included with the WordPress Importer can be freely obtained at the 374 | following URL: 375 | 376 | https://github.com/humanmade/WordPress-Importer 377 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress Importer Redux 2 | This repository contains the new version of the [WordPress Importer][] currently in development. [Learn more about the rewrite](https://make.wordpress.org/core/?p=15550). 3 | 4 | Fast, lightweight, consistent. Pick three. :palm_tree: :sunglasses: 5 | 6 | [WordPress Importer]: https://wordpress.org/plugins/wordpress-importer/ 7 | 8 | ## How do I use it? 9 | 10 | ### Via the Dashboard 11 | 12 | 1. Install the plugin directly from GitHub. ([Download as a ZIP.](https://github.com/humanmade/WordPress-Importer/archive/master.zip)) 13 | 2. Activate the plugin (make sure you also deactivate the original Wordpress Importer if you have it installed). 14 | 3. Head to Tools → Import 15 | 4. Select "WordPress (v2)" 16 | 5. Follow the on-screen instructions. 17 | 18 | ### Via the CLI 19 | 20 | The plugin also includes CLI commands out of the box. 21 | 22 | Simply activate the plugin, then run: 23 | 24 | ```sh 25 | wp wxr-importer import import-file.xml 26 | ``` 27 | 28 | Run `wp help wxr-importer import` to discover what you can do via the CLI. 29 | 30 | ## Current Status 31 | 32 | The current major items are currently missing or suboptimal in the Importer: 33 | 34 | * [x] **Web UI** ([#1](https://github.com/humanmade/WordPress-Importer/issues/1)): ~~Right now, there's essentially *no* web interface for the importer. This sucks.~~ Done! 35 | 36 | * **Automatic Testing**: There's no unit tests. Boooooo. 37 | 38 | ## How can I help? 39 | 40 | The best way to help with the importer right now is to **try importing and see what breaks**. Compare the old importer to the new one, and find any inconsistent behaviour. 41 | 42 | We have a [general feedback thread](https://github.com/humanmade/WordPress-Importer/issues/7) so you can let us know how it goes. If the importer works perfectly, let us know. If something doesn't import the way you think it should, you can file a new issue, or leave a comment to check whether it's intentional first. :) 43 | 44 | ## License 45 | 46 | The WordPress Importer is licensed under the GPLv2 or later. 47 | 48 | ## Credits 49 | 50 | Original plugin created by Ryan Boren, [Jon Cave][duck_] (@joncave), [Andrew Nacin][nacin] (@nacin), and [Peter Westwood][westi] (@westi). Redux project by [Ryan McCue](https://github.com/rmccue) and [contributors](https://github.com/humanmade/WordPress-Importer/graphs/contributors). 51 | 52 | [duck_]: https://profiles.wordpress.org/duck_ 53 | [nacin]: https://profiles.wordpress.org/nacin 54 | [westi]: https://profiles.wordpress.org/westi 55 | -------------------------------------------------------------------------------- /assets/import.css: -------------------------------------------------------------------------------- 1 | .import-status { 2 | width: 100%; 3 | 4 | font-size: 14px; 5 | line-height: 16px; 6 | margin-bottom: 1em; 7 | } 8 | .import-status thead th { 9 | width: 32%; 10 | text-align: left; 11 | font-size: 16px; 12 | padding-bottom: 1em; 13 | } 14 | .import-status thead th:first-child { 15 | width: 36%; 16 | } 17 | 18 | .import-status th, 19 | .import-status td { 20 | padding: 0 0 8px; 21 | margin-bottom: 6px; 22 | } 23 | 24 | #import-log tbody { 25 | max-height: 40em; 26 | } 27 | .import-status-indicator { 28 | margin-bottom: 1em; 29 | } 30 | .import-status-indicator progress { 31 | width: 100%; 32 | } 33 | .import-status-indicator .status { 34 | text-align: center; 35 | } 36 | .import-status-indicator .status .dashicons { 37 | color: #46B450; 38 | font-size: 3rem; 39 | height: auto; 40 | width: auto; 41 | } 42 | #completed-total { 43 | display: none; 44 | } 45 | #import-status-message { 46 | border-top: 1px solid #f2f2f2; 47 | height: 40px; 48 | line-height: 40px; 49 | margin: 20px 0; 50 | } 51 | -------------------------------------------------------------------------------- /assets/import.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | var wxrImport = { 3 | complete: { 4 | posts: 0, 5 | media: 0, 6 | users: 0, 7 | comments: 0, 8 | terms: 0, 9 | }, 10 | 11 | updateDelta: function (type, delta) { 12 | this.complete[ type ] += delta; 13 | 14 | var self = this; 15 | requestAnimationFrame(function () { 16 | self.render(); 17 | }); 18 | }, 19 | updateProgress: function ( type, complete, total ) { 20 | var text = complete + '/' + total; 21 | document.getElementById( 'completed-' + type ).innerHTML = text; 22 | total = parseInt( total, 10 ); 23 | if ( 0 === total || isNaN( total ) ) { 24 | total = 1; 25 | } 26 | var percent = parseInt( complete, 10 ) / total; 27 | document.getElementById( 'progress-' + type ).innerHTML = Math.round( percent * 100 ) + '%'; 28 | document.getElementById( 'progressbar-' + type ).value = percent * 100; 29 | }, 30 | render: function () { 31 | var types = Object.keys( this.complete ); 32 | var complete = 0; 33 | var total = 0; 34 | 35 | for (var i = types.length - 1; i >= 0; i--) { 36 | var type = types[i]; 37 | this.updateProgress( type, this.complete[ type ], this.data.count[ type ] ); 38 | 39 | complete += this.complete[ type ]; 40 | total += this.data.count[ type ]; 41 | } 42 | 43 | this.updateProgress( 'total', complete, total ); 44 | } 45 | }; 46 | wxrImport.data = wxrImportData; 47 | wxrImport.render(); 48 | 49 | var evtSource = new EventSource( wxrImport.data.url ); 50 | evtSource.onmessage = function ( message ) { 51 | var data = JSON.parse( message.data ); 52 | switch ( data.action ) { 53 | case 'updateDelta': 54 | wxrImport.updateDelta( data.type, data.delta ); 55 | break; 56 | 57 | case 'complete': 58 | evtSource.close(); 59 | var import_status_msg = jQuery('#import-status-message'); 60 | import_status_msg.text( wxrImport.data.strings.complete ); 61 | import_status_msg.removeClass('notice-info'); 62 | import_status_msg.addClass('notice-success'); 63 | break; 64 | } 65 | }; 66 | evtSource.addEventListener( 'log', function ( message ) { 67 | var data = JSON.parse( message.data ); 68 | var row = document.createElement('tr'); 69 | var level = document.createElement( 'td' ); 70 | level.appendChild( document.createTextNode( data.level ) ); 71 | row.appendChild( level ); 72 | 73 | var message = document.createElement( 'td' ); 74 | message.appendChild( document.createTextNode( data.message ) ); 75 | row.appendChild( message ); 76 | 77 | jQuery('#import-log').append( row ); 78 | }); 79 | })(jQuery); 80 | -------------------------------------------------------------------------------- /assets/intro.css: -------------------------------------------------------------------------------- 1 | #plupload-upload-ui .drag-drop-status { 2 | display: none; 3 | 4 | margin: 70px auto 0; 5 | width: 400px; 6 | } 7 | #plupload-upload-ui .drag-drop-status .media-item { 8 | width: 200px; 9 | margin: 0 auto; 10 | } 11 | -------------------------------------------------------------------------------- /assets/intro.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | var options = importUploadSettings; 3 | var uploader, statusTemplate, errorTemplate; 4 | 5 | // progress and success handlers for media multi uploads 6 | var renderStatus = function ( attachment ) { 7 | var attr = attachment.attributes; 8 | var $status = jQuery.parseHTML( statusTemplate( attr ).trim() ); 9 | 10 | $('.bar', $status).width( (200 * attr.loaded) / attr.size ); 11 | $('.percent', $status).html( attr.percent + '%' ); 12 | 13 | $('.drag-drop-status').empty().append( $status ); 14 | }; 15 | var renderError = function ( message ) { 16 | var data = { 17 | message: message, 18 | }; 19 | 20 | var status = errorTemplate( data ); 21 | var $status = $('.drag-drop-status'); 22 | $status.html( status ); 23 | $status.one( 'click', 'button', function () { 24 | $status.empty().hide(); 25 | $('.drag-drop-selector').show(); 26 | }); 27 | }; 28 | var actions = { 29 | init: function () { 30 | var uploaddiv = $('#plupload-upload-ui'); 31 | 32 | if ( uploader.supports.dragdrop ) { 33 | uploaddiv.addClass('drag-drop'); 34 | } else { 35 | uploaddiv.removeClass('drag-drop'); 36 | } 37 | }, 38 | 39 | added: function ( attachment ) { 40 | $('.drag-drop-selector').hide(); 41 | $('.drag-drop-status').show(); 42 | 43 | renderStatus( attachment ); 44 | }, 45 | 46 | progress: function ( attachment ) { 47 | renderStatus( attachment ); 48 | }, 49 | 50 | success: function ( attachment ) { 51 | $('#import-selected-id').val( attachment.id ); 52 | 53 | renderStatus( attachment ); 54 | }, 55 | 56 | error: function ( message, data, file ) { 57 | renderError( message ); 58 | }, 59 | }; 60 | 61 | // init and set the uploader 62 | var init = function() { 63 | var isIE = navigator.userAgent.indexOf('Trident/') != -1 || navigator.userAgent.indexOf('MSIE ') != -1; 64 | 65 | // Make sure flash sends cookies (seems in IE it does whitout switching to urlstream mode) 66 | if ( ! isIE && 'flash' === plupload.predictRuntime( options ) && 67 | ( ! options.required_features || ! options.required_features.hasOwnProperty( 'send_binary_string' ) ) ) { 68 | 69 | options.required_features = options.required_features || {}; 70 | options.required_features.send_binary_string = true; 71 | } 72 | 73 | var instanceOptions = _.extend({}, options, actions); 74 | instanceOptions.browser = $('#plupload-browse-button'); 75 | instanceOptions.dropzone = $('#plupload-upload-ui'); 76 | 77 | uploader = new wp.Uploader(instanceOptions); 78 | }; 79 | 80 | $(document).ready(function() { 81 | statusTemplate = wp.template( 'import-upload-status' ); 82 | errorTemplate = wp.template( 'import-upload-error' ); 83 | 84 | init(); 85 | 86 | // Create the media frame. 87 | var frame = wp.media({ 88 | id: 'import-select', 89 | // Set the title of the modal. 90 | title: options.l10n.frameTitle, 91 | multiple: true, 92 | 93 | // Tell the modal to show only xml files. 94 | library: { 95 | type: '', 96 | status: 'private', 97 | }, 98 | 99 | // Customize the submit button. 100 | button: { 101 | // Set the text of the button. 102 | text: options.l10n.buttonText, 103 | // Tell the button not to close the modal, since we're 104 | // going to refresh the page when the image is selected. 105 | close: false, 106 | }, 107 | }); 108 | $('.upload-select').on( 'click', function ( event ) { 109 | event.preventDefault(); 110 | 111 | frame.open(); 112 | }); 113 | frame.on( 'select', function () { 114 | console.log( this, arguments ); 115 | var attachment = frame.state().get('selection').first().toJSON(); 116 | console.log( attachment ); 117 | 118 | var $input = $('#import-selected-id'); 119 | $input.val( attachment.id ); 120 | $input.parents('form')[0].submit(); 121 | }); 122 | }); 123 | 124 | 125 | })( jQuery ); 126 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | 14 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 15 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} 16 | 17 | download() { 18 | if [ `which curl` ]; then 19 | curl -s "$1" > "$2"; 20 | elif [ `which wget` ]; then 21 | wget -nv -O "$2" "$1" 22 | fi 23 | } 24 | 25 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then 26 | WP_TESTS_TAG="tags/$WP_VERSION" 27 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 28 | WP_TESTS_TAG="trunk" 29 | else 30 | # http serves a single offer, whereas https serves multiple. we only want one 31 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 32 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 33 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 34 | if [[ -z "$LATEST_VERSION" ]]; then 35 | echo "Latest WordPress version could not be found" 36 | exit 1 37 | fi 38 | WP_TESTS_TAG="tags/$LATEST_VERSION" 39 | fi 40 | 41 | set -ex 42 | 43 | install_wp() { 44 | 45 | if [ -d $WP_CORE_DIR ]; then 46 | return; 47 | fi 48 | 49 | mkdir -p $WP_CORE_DIR 50 | 51 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 52 | mkdir -p /tmp/wordpress-nightly 53 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip 54 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ 55 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR 56 | else 57 | if [ $WP_VERSION == 'latest' ]; then 58 | local ARCHIVE_NAME='latest' 59 | else 60 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 61 | fi 62 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz 63 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 64 | fi 65 | 66 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 67 | } 68 | 69 | install_test_suite() { 70 | # portable in-place argument for both GNU sed and Mac OSX sed 71 | if [[ $(uname -s) == 'Darwin' ]]; then 72 | local ioption='-i .bak' 73 | else 74 | local ioption='-i' 75 | fi 76 | 77 | # set up testing suite if it doesn't yet exist 78 | if [ ! -d $WP_TESTS_DIR ]; then 79 | # set up testing suite 80 | mkdir -p $WP_TESTS_DIR 81 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 82 | fi 83 | 84 | cd $WP_TESTS_DIR 85 | 86 | if [ ! -f wp-tests-config.php ]; then 87 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 88 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php 89 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 90 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 91 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 92 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 93 | fi 94 | 95 | } 96 | 97 | install_db() { 98 | # parse DB_HOST for port or socket references 99 | local PARTS=(${DB_HOST//\:/ }) 100 | local DB_HOSTNAME=${PARTS[0]}; 101 | local DB_SOCK_OR_PORT=${PARTS[1]}; 102 | local EXTRA="" 103 | 104 | if ! [ -z $DB_HOSTNAME ] ; then 105 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 106 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 107 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 108 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 109 | elif ! [ -z $DB_HOSTNAME ] ; then 110 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 111 | fi 112 | fi 113 | 114 | # create database 115 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 116 | } 117 | 118 | install_wp 119 | install_test_suite 120 | install_db 121 | -------------------------------------------------------------------------------- /class-command.php: -------------------------------------------------------------------------------- 1 | ... 10 | * : Path to one or more valid WXR files for importing. Directories are also accepted. 11 | * 12 | * [--verbose[=]] 13 | * : Should we print verbose statements? 14 | * (No value for 'info'; or one of 'emergency', 'alert', 'critical', 15 | * 'error', 'warning', 'notice', 'info', 'debug') 16 | * 17 | * [--default-author=] 18 | * : Default author ID to use if invalid user is found in the import data. 19 | */ 20 | public function import( $args, $assoc_args ) { 21 | $logger = new WP_Importer_Logger_CLI(); 22 | if ( ! empty( $assoc_args['verbose'] ) ) { 23 | if ( $assoc_args['verbose'] === true ) { 24 | $logger->min_level = 'info'; 25 | } else { 26 | $valid = $logger->level_to_numeric( $assoc_args['verbose'] ); 27 | if ( ! $valid ) { 28 | WP_CLI::error( 'Invalid verbosity level' ); 29 | return; 30 | } 31 | 32 | $logger->min_level = $assoc_args['verbose']; 33 | } 34 | } 35 | 36 | $path = realpath( $args[0] ); 37 | if ( ! $path ) { 38 | WP_CLI::error( sprintf( 'Specified file %s does not exist', $args[0] ) ); 39 | } 40 | 41 | $options = array( 42 | 'fetch_attachments' => true, 43 | ); 44 | if ( isset( $assoc_args['default-author'] ) ) { 45 | $options['default_author'] = absint( $assoc_args['default-author'] ); 46 | 47 | if ( ! get_user_by( 'ID', $options['default_author'] ) ) { 48 | WP_CLI::error( 'Invalid default author ID specified.' ); 49 | } 50 | } 51 | $importer = new WXR_Importer( $options ); 52 | $importer->set_logger( $logger ); 53 | $result = $importer->import( $path ); 54 | if ( is_wp_error( $result ) ) { 55 | WP_CLI::error( $result->get_error_message() ); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /class-logger-cli.php: -------------------------------------------------------------------------------- 1 | level_to_numeric( $level ) < $this->level_to_numeric( $this->min_level ) ) { 16 | return; 17 | } 18 | 19 | printf( 20 | '[%s] %s' . PHP_EOL, 21 | strtoupper( $level ), 22 | $message 23 | ); 24 | } 25 | 26 | public static function level_to_numeric( $level ) { 27 | $levels = array( 28 | 'emergency' => 8, 29 | 'alert' => 7, 30 | 'critical' => 6, 31 | 'error' => 5, 32 | 'warning' => 4, 33 | 'notice' => 3, 34 | 'info' => 2, 35 | 'debug' => 1, 36 | ); 37 | if ( ! isset( $levels[ $level ] ) ) { 38 | return 0; 39 | } 40 | 41 | return $levels[ $level ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /class-logger-html.php: -------------------------------------------------------------------------------- 1 | ' . __( 'Sorry, there has been an error.', 'wordpress-importer' ) . '
'; 18 | echo esc_html( $message ); 19 | echo '

'; 20 | break; 21 | 22 | case 'error': 23 | case 'warning': 24 | case 'notice': 25 | case 'info': 26 | echo '

' . esc_html( $message ) . '

'; 27 | break; 28 | 29 | case 'debug': 30 | if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) { 31 | echo '

' . esc_html( $message ) . '

'; 32 | } 33 | break; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /class-logger-serversentevents.php: -------------------------------------------------------------------------------- 1 | log( 'emergency', $message, $context ); 30 | } 31 | 32 | /** 33 | * Action must be taken immediately. 34 | * 35 | * Example: Entire website down, database unavailable, etc. This should 36 | * trigger the SMS alerts and wake you up. 37 | * 38 | * @param string $message 39 | * @param array $context 40 | * @return null 41 | */ 42 | public function alert( $message, array $context = array() ) { 43 | return $this->log( 'alert', $message, $context ); 44 | } 45 | 46 | /** 47 | * Critical conditions. 48 | * 49 | * Example: Application component unavailable, unexpected exception. 50 | * 51 | * @param string $message 52 | * @param array $context 53 | * @return null 54 | */ 55 | public function critical( $message, array $context = array() ) { 56 | return $this->log( 'critical', $message, $context ); 57 | } 58 | 59 | /** 60 | * Runtime errors that do not require immediate action but should typically 61 | * be logged and monitored. 62 | * 63 | * @param string $message 64 | * @param array $context 65 | * @return null 66 | */ 67 | public function error( $message, array $context = array()) { 68 | return $this->log( 'error', $message, $context ); 69 | } 70 | 71 | /** 72 | * Exceptional occurrences that are not errors. 73 | * 74 | * Example: Use of deprecated APIs, poor use of an API, undesirable things 75 | * that are not necessarily wrong. 76 | * 77 | * @param string $message 78 | * @param array $context 79 | * @return null 80 | */ 81 | public function warning( $message, array $context = array() ) { 82 | return $this->log( 'warning', $message, $context ); 83 | } 84 | 85 | /** 86 | * Normal but significant events. 87 | * 88 | * @param string $message 89 | * @param array $context 90 | * @return null 91 | */ 92 | public function notice( $message, array $context = array() ) { 93 | return $this->log( 'notice', $message, $context ); 94 | } 95 | 96 | /** 97 | * Interesting events. 98 | * 99 | * Example: User logs in, SQL logs. 100 | * 101 | * @param string $message 102 | * @param array $context 103 | * @return null 104 | */ 105 | public function info( $message, array $context = array() ) { 106 | return $this->log( 'info', $message, $context ); 107 | } 108 | 109 | /** 110 | * Detailed debug information. 111 | * 112 | * @param string $message 113 | * @param array $context 114 | * @return null 115 | */ 116 | public function debug( $message, array $context = array() ) { 117 | return $this->log( 'debug', $message, $context ); 118 | } 119 | 120 | /** 121 | * Logs with an arbitrary level. 122 | * 123 | * @param mixed $level 124 | * @param string $message 125 | * @param array $context 126 | * @return null 127 | */ 128 | public function log( $level, $message, array $context = array() ) { 129 | $this->messages[] = array( 130 | 'timestamp' => time(), 131 | 'level' => $level, 132 | 'message' => $message, 133 | 'context' => $context, 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /class-wxr-import-info.php: -------------------------------------------------------------------------------- 1 | 'application/xml' ) ); 30 | 31 | return $mimes; 32 | } 33 | 34 | /** 35 | * Show an update notice in the importer header. 36 | */ 37 | public function show_updates_in_header() { 38 | // Check for updates too. 39 | $updates = get_plugin_updates(); 40 | $basename = plugin_basename( __FILE__ ); 41 | if ( empty( $updates[ $basename ] ) ) { 42 | return; 43 | } 44 | 45 | $message = sprintf( 46 | esc_html__( 'A new version of this importer is available. Please update to version %s to ensure compatibility with newer export files.', 'wordpress-importer' ), 47 | $updates[ $basename ]->update->new_version 48 | ); 49 | 50 | $args = array( 51 | 'action' => 'upgrade-plugin', 52 | 'plugin' => $basename, 53 | ); 54 | $url = add_query_arg( $args, self_admin_url( 'update.php' ) ); 55 | $url = wp_nonce_url( $url, 'upgrade-plugin_' . $basename ); 56 | $link = sprintf( '%s', $url, esc_html__( 'Update Now', 'wordpress-importer' ) ); 57 | 58 | printf( '

%s

%s

', $message, $link ); 59 | } 60 | 61 | /** 62 | * Get the URL for the importer. 63 | * 64 | * @param int $step Go to step rather than start. 65 | */ 66 | protected function get_url( $step = 0 ) { 67 | $path = 'admin.php?import=wordpress'; 68 | if ( $step ) { 69 | $path = add_query_arg( 'step', (int) $step, $path ); 70 | } 71 | return admin_url( $path ); 72 | } 73 | 74 | protected function display_error( WP_Error $err, $step = 0 ) { 75 | $this->render_header(); 76 | 77 | echo '

' . __( 'Sorry, there has been an error.', 'wordpress-importer' ) . '
'; 78 | echo $err->get_error_message(); 79 | echo '

'; 80 | printf( 81 | '

Try Again

', 82 | esc_url( $this->get_url( $step ) ) 83 | ); 84 | 85 | $this->render_footer(); 86 | } 87 | 88 | /** 89 | * Handle load event for the importer. 90 | */ 91 | public function on_load() { 92 | // Skip outputting the header on our import page, so we can handle it. 93 | $_GET['noheader'] = true; 94 | } 95 | 96 | /** 97 | * Render the import page. 98 | */ 99 | public function dispatch() { 100 | $step = empty( $_GET['step'] ) ? 0 : (int) $_GET['step']; 101 | switch ( $step ) { 102 | case 0: 103 | $this->display_intro_step(); 104 | break; 105 | case 1: 106 | check_admin_referer( 'import-upload' ); 107 | $this->display_author_step(); 108 | break; 109 | case 2: 110 | $this->display_import_step(); 111 | break; 112 | } 113 | } 114 | 115 | /** 116 | * Render the importer header. 117 | */ 118 | protected function render_header() { 119 | require __DIR__ . '/templates/header.php'; 120 | } 121 | 122 | /** 123 | * Render the importer footer. 124 | */ 125 | protected function render_footer() { 126 | require __DIR__ . '/templates/footer.php'; 127 | } 128 | 129 | /** 130 | * Display introductory text and file upload form 131 | */ 132 | protected function display_intro_step() { 133 | require __DIR__ . '/templates/intro.php'; 134 | } 135 | 136 | protected function render_upload_form() { 137 | /** 138 | * Filter the maximum allowed upload size for import files. 139 | * 140 | * @since 2.3.0 141 | * 142 | * @see wp_max_upload_size() 143 | * 144 | * @param int $max_upload_size Allowed upload size. Default 1 MB. 145 | */ 146 | $max_upload_size = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); 147 | $upload_dir = wp_upload_dir(); 148 | if ( ! empty( $upload_dir['error'] ) ) { 149 | $error = '

'; 150 | $error .= esc_html__( 151 | 'Before you can upload your import file, you will need to fix the following error:', 152 | 'wordpress-importer' 153 | ); 154 | $error .= sprintf( '

%s

', $upload_dir['error'] ); 155 | echo $error; 156 | return; 157 | } 158 | 159 | // Queue the JS needed for the page 160 | $url = plugins_url( 'assets/intro.js', __FILE__ ); 161 | $deps = array( 162 | 'wp-backbone', 163 | 'wp-plupload', 164 | ); 165 | wp_enqueue_script( 'import-upload', $url, $deps, false, true ); 166 | 167 | // Set uploader settings 168 | wp_plupload_default_settings(); 169 | $settings = array( 170 | 'l10n' => array( 171 | 'frameTitle' => esc_html__( 'Select', 'wordpress-importer' ), 172 | 'buttonText' => esc_html__( 'Import', 'wordpress-importer' ), 173 | ), 174 | 'next_url' => wp_nonce_url( $this->get_url( 1 ), 'import-upload' ) . '&id={id}', 175 | 'plupload' => array( 176 | 'filters' => array( 177 | 'max_file_size' => $max_upload_size . 'b', 178 | 'mime_types' => array( 179 | array( 180 | 'title' => esc_html__( 'XML files', 'wordpress-importer' ), 181 | 'extensions' => 'xml', 182 | ), 183 | ), 184 | ), 185 | 186 | 'file_data_name' => 'import', 187 | 'multipart_params' => array( 188 | 'action' => 'wxr-import-upload', 189 | '_wpnonce' => wp_create_nonce( 'wxr-import-upload' ), 190 | ), 191 | ), 192 | ); 193 | wp_localize_script( 'import-upload', 'importUploadSettings', $settings ); 194 | 195 | wp_enqueue_style( 'wxr-import-upload', plugins_url( 'assets/intro.css', __FILE__ ), array(), '20160412' ); 196 | 197 | // Load the template 198 | remove_action( 'post-plupload-upload-ui', 'media_upload_flash_bypass' ); 199 | require __DIR__ . '/templates/upload.php'; 200 | add_action( 'post-plupload-upload-ui', 'media_upload_flash_bypass' ); 201 | } 202 | 203 | /** 204 | * Display the author picker (or upload errors). 205 | */ 206 | protected function display_author_step() { 207 | if ( isset( $_REQUEST['id'] ) ) { 208 | $err = $this->handle_select( wp_unslash( $_REQUEST['id'] ) ); 209 | } else { 210 | $err = $this->handle_upload(); 211 | } 212 | if ( is_wp_error( $err ) ) { 213 | $this->display_error( $err ); 214 | return; 215 | } 216 | 217 | $data = $this->get_data_for_attachment( $this->id ); 218 | if ( is_wp_error( $data ) ) { 219 | $this->display_error( $data ); 220 | return; 221 | } 222 | 223 | require __DIR__ . '/templates/select-options.php'; 224 | } 225 | 226 | /** 227 | * Handles the WXR upload and initial parsing of the file to prepare for 228 | * displaying author import options 229 | * 230 | * @return bool|WP_Error True on success, error object otherwise. 231 | */ 232 | protected function handle_upload() { 233 | $file = wp_import_handle_upload(); 234 | 235 | if ( isset( $file['error'] ) ) { 236 | return new WP_Error( 'wxr_importer.upload.error', esc_html( $file['error'] ), $file ); 237 | } elseif ( ! file_exists( $file['file'] ) ) { 238 | $message = sprintf( 239 | esc_html__( 'The export file could not be found at %s. It is likely that this was caused by a permissions problem.', 'wordpress-importer' ), 240 | '' . esc_html( $file['file'] ) . '' 241 | ); 242 | return new WP_Error( 'wxr_importer.upload.no_file', $message, $file ); 243 | } 244 | 245 | $this->id = (int) $file['id']; 246 | return true; 247 | } 248 | 249 | /** 250 | * Handle an async upload. 251 | * 252 | * Triggers on `async-upload.php?action=wxr-import-upload` to handle 253 | * Plupload requests from the importer. 254 | */ 255 | public function handle_async_upload() { 256 | header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) ); 257 | send_nosniff_header(); 258 | nocache_headers(); 259 | 260 | check_ajax_referer( 'wxr-import-upload' ); 261 | 262 | /* 263 | * This function does not use wp_send_json_success() / wp_send_json_error() 264 | * as the html4 Plupload handler requires a text/html content-type for older IE. 265 | * See https://core.trac.wordpress.org/ticket/31037 266 | */ 267 | 268 | $filename = wp_unslash( $_FILES['import']['name'] ); 269 | $filename = sanitize_file_name( $filename ); 270 | 271 | if ( ! current_user_can( 'upload_files' ) ) { 272 | echo wp_json_encode( array( 273 | 'success' => false, 274 | 'data' => array( 275 | 'message' => __( 'You do not have permission to upload files.' ), 276 | 'filename' => $filename, 277 | ), 278 | ) ); 279 | 280 | exit; 281 | } 282 | 283 | $file = wp_import_handle_upload(); 284 | if ( is_wp_error( $file ) ) { 285 | echo wp_json_encode( array( 286 | 'success' => false, 287 | 'data' => array( 288 | 'message' => $file->get_error_message(), 289 | 'filename' => $filename, 290 | ), 291 | ) ); 292 | 293 | wp_die(); 294 | } 295 | 296 | $attachment = wp_prepare_attachment_for_js( $file['id'] ); 297 | if ( ! $attachment ) { 298 | exit; 299 | } 300 | 301 | echo wp_json_encode( array( 302 | 'success' => true, 303 | 'data' => $attachment, 304 | ) ); 305 | 306 | exit; 307 | } 308 | 309 | /** 310 | * Handle a WXR file selected from the media browser. 311 | * 312 | * @param int|string $id Media item to import from. 313 | * @return bool|WP_Error True on success, error object otherwise. 314 | */ 315 | protected function handle_select( $id ) { 316 | if ( ! is_numeric( $id ) || intval( $id ) < 1 ) { 317 | return new WP_Error( 318 | 'wxr_importer.upload.invalid_id', 319 | __( 'Invalid media item ID.', 'wordpress-importer' ), 320 | compact( 'id' ) 321 | ); 322 | } 323 | 324 | $id = (int) $id; 325 | 326 | $attachment = get_post( $id ); 327 | if ( ! $attachment || $attachment->post_type !== 'attachment' ) { 328 | return new WP_Error( 329 | 'wxr_importer.upload.invalid_id', 330 | __( 'Invalid media item ID.', 'wordpress-importer' ), 331 | compact( 'id', 'attachment' ) 332 | ); 333 | } 334 | 335 | if ( ! current_user_can( 'read_post', $attachment->ID ) ) { 336 | return new WP_Error( 337 | 'wxr_importer.upload.sorry_dave', 338 | __( 'You cannot access the selected media item.', 'wordpress-importer' ), 339 | compact( 'id', 'attachment' ) 340 | ); 341 | } 342 | 343 | $this->id = $id; 344 | return true; 345 | } 346 | 347 | /** 348 | * Get preliminary data for an import file. 349 | * 350 | * This is a quick pre-parse to verify the file and grab authors from it. 351 | * 352 | * @param int $id Media item ID. 353 | * @return WXR_Import_Info|WP_Error Import info instance on success, error otherwise. 354 | */ 355 | protected function get_data_for_attachment( $id ) { 356 | $existing = get_post_meta( $id, '_wxr_import_info' ); 357 | if ( ! empty( $existing ) ) { 358 | $data = $existing[0]; 359 | $this->authors = $data->users; 360 | $this->version = $data->version; 361 | return $data; 362 | } 363 | 364 | $file = get_attached_file( $id ); 365 | 366 | $importer = $this->get_importer(); 367 | $data = $importer->get_preliminary_information( $file ); 368 | if ( is_wp_error( $data ) ) { 369 | return $data; 370 | } 371 | 372 | // Cache the information on the upload 373 | if ( ! update_post_meta( $id, '_wxr_import_info', $data ) ) { 374 | return new WP_Error( 375 | 'wxr_importer.upload.failed_save_meta', 376 | __( 'Could not cache information on the import.', 'wordpress-importer' ), 377 | compact( 'id' ) 378 | ); 379 | } 380 | 381 | $this->authors = $data->users; 382 | $this->version = $data->version; 383 | 384 | return $data; 385 | } 386 | 387 | /** 388 | * Display the actual import step. 389 | */ 390 | protected function display_import_step() { 391 | $args = wp_unslash( $_POST ); 392 | if ( ! isset( $args['import_id'] ) ) { 393 | // Missing import ID. 394 | $error = new WP_Error( 'wxr_importer.import.missing_id', __( 'Missing import file ID from request.', 'wordpress-importer' ) ); 395 | $this->display_error( $error ); 396 | return; 397 | } 398 | 399 | // Check the nonce. 400 | check_admin_referer( sprintf( 'wxr.import:%d', (int) $args['import_id'] ) ); 401 | 402 | $this->id = (int) $args['import_id']; 403 | $file = get_attached_file( $this->id ); 404 | 405 | $mapping = $this->get_author_mapping( $args ); 406 | $fetch_attachments = ( ! empty( $args['fetch_attachments'] ) && $this->allow_fetch_attachments() ); 407 | 408 | // Set our settings 409 | $settings = compact( 'mapping', 'fetch_attachments' ); 410 | update_post_meta( $this->id, '_wxr_import_settings', $settings ); 411 | 412 | // Time to run the import! 413 | set_time_limit( 0 ); 414 | 415 | // Ensure we're not buffered. 416 | wp_ob_end_flush_all(); 417 | flush(); 418 | 419 | $data = get_post_meta( $this->id, '_wxr_import_info', true ); 420 | require __DIR__ . '/templates/import.php'; 421 | } 422 | 423 | /** 424 | * Run an import, and send an event-stream response. 425 | * 426 | * Streams logs and success messages to the browser to allow live status 427 | * and updates. 428 | */ 429 | public function stream_import() { 430 | // Turn off PHP output compression 431 | $previous = error_reporting( error_reporting() ^ E_WARNING ); 432 | ini_set( 'output_buffering', 'off' ); 433 | ini_set( 'zlib.output_compression', false ); 434 | error_reporting( $previous ); 435 | 436 | if ( $GLOBALS['is_nginx'] ) { 437 | // Setting this header instructs Nginx to disable fastcgi_buffering 438 | // and disable gzip for this request. 439 | header( 'X-Accel-Buffering: no' ); 440 | header( 'Content-Encoding: none' ); 441 | } 442 | 443 | // Start the event stream. 444 | header( 'Content-Type: text/event-stream' ); 445 | 446 | $this->id = wp_unslash( (int) $_REQUEST['id'] ); 447 | $settings = get_post_meta( $this->id, '_wxr_import_settings', true ); 448 | if ( empty( $settings ) ) { 449 | // Tell the browser to stop reconnecting. 450 | status_header( 204 ); 451 | exit; 452 | } 453 | 454 | // 2KB padding for IE 455 | echo ':' . str_repeat( ' ', 2048 ) . "\n\n"; 456 | 457 | // Time to run the import! 458 | set_time_limit( 0 ); 459 | 460 | // Ensure we're not buffered. 461 | wp_ob_end_flush_all(); 462 | flush(); 463 | 464 | $mapping = $settings['mapping']; 465 | $this->fetch_attachments = (bool) $settings['fetch_attachments']; 466 | 467 | $importer = $this->get_importer(); 468 | if ( ! empty( $mapping['mapping'] ) ) { 469 | $importer->set_user_mapping( $mapping['mapping'] ); 470 | } 471 | if ( ! empty( $mapping['slug_overrides'] ) ) { 472 | $importer->set_user_slug_overrides( $mapping['slug_overrides'] ); 473 | } 474 | 475 | // Are we allowed to create users? 476 | if ( ! $this->allow_create_users() ) { 477 | add_filter( 'wxr_importer.pre_process.user', '__return_null' ); 478 | } 479 | 480 | // Keep track of our progress 481 | add_action( 'wxr_importer.processed.post', array( $this, 'imported_post' ), 10, 2 ); 482 | add_action( 'wxr_importer.process_failed.post', array( $this, 'imported_post' ), 10, 2 ); 483 | add_action( 'wxr_importer.process_already_imported.post', array( $this, 'already_imported_post' ), 10, 2 ); 484 | add_action( 'wxr_importer.process_skipped.post', array( $this, 'already_imported_post' ), 10, 2 ); 485 | add_action( 'wxr_importer.processed.comment', array( $this, 'imported_comment' ) ); 486 | add_action( 'wxr_importer.process_already_imported.comment', array( $this, 'imported_comment' ) ); 487 | add_action( 'wxr_importer.processed.term', array( $this, 'imported_term' ) ); 488 | add_action( 'wxr_importer.process_failed.term', array( $this, 'imported_term' ) ); 489 | add_action( 'wxr_importer.process_already_imported.term', array( $this, 'imported_term' ) ); 490 | add_action( 'wxr_importer.processed.user', array( $this, 'imported_user' ) ); 491 | add_action( 'wxr_importer.process_failed.user', array( $this, 'imported_user' ) ); 492 | 493 | // Clean up some memory 494 | unset( $settings ); 495 | 496 | // Flush once more. 497 | flush(); 498 | 499 | $file = get_attached_file( $this->id ); 500 | $err = $importer->import( $file ); 501 | 502 | // Remove the settings to stop future reconnects. 503 | delete_post_meta( $this->id, '_wxr_import_settings' ); 504 | 505 | // Let the browser know we're done. 506 | $complete = array( 507 | 'action' => 'complete', 508 | 'error' => false, 509 | ); 510 | if ( is_wp_error( $err ) ) { 511 | $complete['error'] = $err->get_error_message(); 512 | } 513 | 514 | $this->emit_sse_message( $complete ); 515 | exit; 516 | } 517 | 518 | /** 519 | * Get the importer instance. 520 | * 521 | * @return WXR_Importer 522 | */ 523 | protected function get_importer() { 524 | $importer = new WXR_Importer( $this->get_import_options() ); 525 | $logger = new WP_Importer_Logger_ServerSentEvents(); 526 | $importer->set_logger( $logger ); 527 | 528 | return $importer; 529 | } 530 | 531 | /** 532 | * Get options for the importer. 533 | * 534 | * @return array Options to pass to WXR_Importer::__construct 535 | */ 536 | protected function get_import_options() { 537 | $options = array( 538 | 'fetch_attachments' => $this->fetch_attachments, 539 | 'default_author' => get_current_user_id(), 540 | ); 541 | 542 | /** 543 | * Filter the importer options used in the admin UI. 544 | * 545 | * @param array $options Options to pass to WXR_Importer::__construct 546 | */ 547 | return apply_filters( 'wxr_importer.admin.import_options', $options ); 548 | } 549 | 550 | /** 551 | * Display import options for an individual author. That is, either create 552 | * a new user based on import info or map to an existing user 553 | * 554 | * @param int $index Index for each author in the form 555 | * @param array $author Author information, e.g. login, display name, email 556 | */ 557 | protected function author_select( $index, $author ) { 558 | esc_html_e( 'Import author:', 'wordpress-importer' ); 559 | $supports_extras = version_compare( $this->version, '1.0', '>' ); 560 | 561 | if ( $supports_extras ) { 562 | $name = sprintf( '%s (%s)', $author['display_name'], $author['user_login'] ); 563 | } else { 564 | $name = $author['display_name']; 565 | } 566 | echo ' ' . esc_html( $name ) . '
'; 567 | 568 | if ( $supports_extras ) { 569 | echo '
'; 570 | } 571 | 572 | $create_users = $this->allow_create_users(); 573 | if ( $create_users ) { 574 | if ( ! $supports_extras ) { 575 | esc_html_e( 'or create new user with login name:', 'wordpress-importer' ); 576 | $value = ''; 577 | } else { 578 | esc_html_e( 'as a new user:', 'wordpress-importer' ); 579 | $value = sanitize_user( $author['user_login'], true ); 580 | } 581 | 582 | printf( 583 | '
', 584 | $index, 585 | esc_attr( $value ) 586 | ); 587 | } 588 | 589 | if ( ! $create_users && $supports_extras ) { 590 | esc_html_e( 'assign posts to an existing user:', 'wordpress-importer' ); 591 | } else { 592 | esc_html_e( 'or assign posts to an existing user:', 'wordpress-importer' ); 593 | } 594 | 595 | wp_dropdown_users( array( 596 | 'name' => sprintf( 'user_map[%d]', $index ), 597 | 'multi' => true, 598 | 'show_option_all' => __( '- Select -', 'wordpress-importer' ), 599 | )); 600 | 601 | printf( 602 | '', 603 | (int) $index, 604 | esc_attr( $author['user_login'] ) 605 | ); 606 | 607 | // Keep the old ID for when we want to remap 608 | if ( isset( $author['ID'] ) ) { 609 | printf( 610 | '', 611 | (int) $index, 612 | esc_attr( $author['ID'] ) 613 | ); 614 | } 615 | 616 | if ( $supports_extras ) { 617 | echo '
'; 618 | } 619 | } 620 | 621 | /** 622 | * Decide whether or not the importer should attempt to download attachment files. 623 | * Default is true, can be filtered via import_allow_fetch_attachments. The choice 624 | * made at the import options screen must also be true, false here hides that checkbox. 625 | * 626 | * @return bool True if downloading attachments is allowed 627 | */ 628 | protected function allow_fetch_attachments() { 629 | return apply_filters( 'import_allow_fetch_attachments', true ); 630 | } 631 | 632 | /** 633 | * Decide whether or not the importer is allowed to create users. 634 | * Default is true, can be filtered via import_allow_create_users 635 | * 636 | * @return bool True if creating users is allowed 637 | */ 638 | protected function allow_create_users() { 639 | return apply_filters( 'import_allow_create_users', true ); 640 | } 641 | 642 | /** 643 | * Get mapping data from request data. 644 | * 645 | * Parses form request data into an internally usable mapping format. 646 | * 647 | * @param array $args Raw (UNSLASHED) POST data to parse. 648 | * @return array Map containing `mapping` and `slug_overrides` keys. 649 | */ 650 | protected function get_author_mapping( $args ) { 651 | if ( ! isset( $args['imported_authors'] ) ) { 652 | return array( 653 | 'mapping' => array(), 654 | 'slug_overrides' => array(), 655 | ); 656 | } 657 | 658 | $map = isset( $args['user_map'] ) ? (array) $args['user_map'] : array(); 659 | $new_users = isset( $args['user_new'] ) ? $args['user_new'] : array(); 660 | $old_ids = isset( $args['imported_author_ids'] ) ? (array) $args['imported_author_ids'] : array(); 661 | 662 | // Store the actual map. 663 | $mapping = array(); 664 | $slug_overrides = array(); 665 | 666 | foreach ( (array) $args['imported_authors'] as $i => $old_login ) { 667 | $old_id = isset( $old_ids[ $i ] ) ? (int) $old_ids[ $i ] : false; 668 | 669 | if ( ! empty( $map[ $i ] ) ) { 670 | $user = get_user_by( 'id', (int) $map[ $i ] ); 671 | 672 | if ( isset( $user->ID ) ) { 673 | $mapping[] = array( 674 | 'old_slug' => $old_login, 675 | 'old_id' => $old_id, 676 | 'new_id' => $user->ID, 677 | ); 678 | } 679 | } elseif ( ! empty( $new_users[ $i ] ) ) { 680 | if ( $new_users[ $i ] !== $old_login ) { 681 | $slug_overrides[ $old_login ] = $new_users[ $i ]; 682 | } 683 | } 684 | } 685 | 686 | return compact( 'mapping', 'slug_overrides' ); 687 | } 688 | 689 | /** 690 | * Emit a Server-Sent Events message. 691 | * 692 | * @param mixed $data Data to be JSON-encoded and sent in the message. 693 | */ 694 | protected function emit_sse_message( $data ) { 695 | echo "event: message\n"; 696 | echo 'data: ' . wp_json_encode( $data ) . "\n\n"; 697 | 698 | // Extra padding. 699 | echo ':' . str_repeat( ' ', 2048 ) . "\n\n"; 700 | 701 | flush(); 702 | } 703 | 704 | /** 705 | * Send message when a post has been imported. 706 | * 707 | * @param int $id Post ID. 708 | * @param array $data Post data saved to the DB. 709 | */ 710 | public function imported_post( $id, $data ) { 711 | $this->emit_sse_message( array( 712 | 'action' => 'updateDelta', 713 | 'type' => ( $data['post_type'] === 'attachment' ) ? 'media' : 'posts', 714 | 'delta' => 1, 715 | )); 716 | } 717 | 718 | /** 719 | * Send message when a post is marked as already imported. 720 | * 721 | * @param array $data Post data saved to the DB. 722 | */ 723 | public function already_imported_post( $data ) { 724 | $this->emit_sse_message( array( 725 | 'action' => 'updateDelta', 726 | 'type' => ( $data['post_type'] === 'attachment' ) ? 'media' : 'posts', 727 | 'delta' => 1, 728 | )); 729 | } 730 | 731 | /** 732 | * Send message when a comment has been imported. 733 | */ 734 | public function imported_comment() { 735 | $this->emit_sse_message( array( 736 | 'action' => 'updateDelta', 737 | 'type' => 'comments', 738 | 'delta' => 1, 739 | )); 740 | } 741 | 742 | /** 743 | * Send message when a term has been imported. 744 | */ 745 | public function imported_term() { 746 | $this->emit_sse_message( array( 747 | 'action' => 'updateDelta', 748 | 'type' => 'terms', 749 | 'delta' => 1, 750 | )); 751 | } 752 | 753 | /** 754 | * Send message when a user has been imported. 755 | */ 756 | public function imported_user() { 757 | $this->emit_sse_message( array( 758 | 'action' => 'updateDelta', 759 | 'type' => 'users', 760 | 'delta' => 1, 761 | )); 762 | } 763 | } 764 | -------------------------------------------------------------------------------- /class-wxr-importer.php: -------------------------------------------------------------------------------- 1 | ` tag at the start of the file. 33 | * 34 | * @var string 35 | */ 36 | protected $version = '1.0'; 37 | 38 | // information to import from WXR file 39 | protected $categories = array(); 40 | protected $tags = array(); 41 | protected $base_url = ''; 42 | 43 | // TODO: REMOVE THESE 44 | protected $processed_terms = array(); 45 | protected $processed_posts = array(); 46 | protected $processed_menu_items = array(); 47 | protected $menu_item_orphans = array(); 48 | protected $missing_menu_items = array(); 49 | 50 | // NEW STYLE 51 | protected $mapping = array(); 52 | protected $requires_remapping = array(); 53 | protected $exists = array(); 54 | protected $user_slug_override = array(); 55 | 56 | protected $url_remap = array(); 57 | protected $featured_images = array(); 58 | 59 | /** 60 | * Logger instance. 61 | * 62 | * @var WP_Importer_Logger 63 | */ 64 | protected $logger; 65 | 66 | /** 67 | * Constructor 68 | * 69 | * @param array $options { 70 | * @var bool $prefill_existing_posts Should we prefill `post_exists` calls? (True prefills and uses more memory, false checks once per imported post and takes longer. Default is true.) 71 | * @var bool $prefill_existing_comments Should we prefill `comment_exists` calls? (True prefills and uses more memory, false checks once per imported comment and takes longer. Default is true.) 72 | * @var bool $prefill_existing_terms Should we prefill `term_exists` calls? (True prefills and uses more memory, false checks once per imported term and takes longer. Default is true.) 73 | * @var bool $update_attachment_guids Should attachment GUIDs be updated to the new URL? (True updates the GUID, which keeps compatibility with v1, false doesn't update, and allows deduplication and reimporting. Default is false.) 74 | * @var bool $fetch_attachments Fetch attachments from the remote server. (True fetches and creates attachment posts, false skips attachments. Default is false.) 75 | * @var bool $aggressive_url_search Should we search/replace for URLs aggressively? (True searches all posts' content for old URLs and replaces, false checks for `` only. Default is false.) 76 | * @var int $default_author User ID to use if author is missing or invalid. (Default is null, which leaves posts unassigned.) 77 | * } 78 | */ 79 | public function __construct( $options = array() ) { 80 | // Initialize some important variables 81 | $empty_types = array( 82 | 'post' => array(), 83 | 'comment' => array(), 84 | 'term' => array(), 85 | 'user' => array(), 86 | ); 87 | 88 | $this->mapping = $empty_types; 89 | $this->mapping['user_slug'] = array(); 90 | $this->mapping['term_id'] = array(); 91 | $this->requires_remapping = $empty_types; 92 | $this->exists = $empty_types; 93 | 94 | $this->options = wp_parse_args( $options, array( 95 | 'prefill_existing_posts' => true, 96 | 'prefill_existing_comments' => true, 97 | 'prefill_existing_terms' => true, 98 | 'update_attachment_guids' => false, 99 | 'fetch_attachments' => false, 100 | 'aggressive_url_search' => false, 101 | 'default_author' => null, 102 | ) ); 103 | } 104 | 105 | public function set_logger( $logger ) { 106 | $this->logger = $logger; 107 | } 108 | 109 | /** 110 | * Get a stream reader for the file. 111 | * 112 | * @param string $file Path to the XML file. 113 | * @return XMLReader|WP_Error Reader instance on success, error otherwise. 114 | */ 115 | protected function get_reader( $file ) { 116 | // Avoid loading external entities for security 117 | $old_value = null; 118 | if ( function_exists( 'libxml_disable_entity_loader' ) ) { 119 | // $old_value = libxml_disable_entity_loader( true ); 120 | } 121 | 122 | $reader = new XMLReader(); 123 | $status = $reader->open( $file ); 124 | 125 | if ( ! is_null( $old_value ) ) { 126 | // libxml_disable_entity_loader( $old_value ); 127 | } 128 | 129 | if ( ! $status ) { 130 | return new WP_Error( 'wxr_importer.cannot_parse', __( 'Could not open the file for parsing', 'wordpress-importer' ) ); 131 | } 132 | 133 | return $reader; 134 | } 135 | 136 | /** 137 | * The main controller for the actual import stage. 138 | * 139 | * @param string $file Path to the WXR file for importing 140 | */ 141 | public function get_preliminary_information( $file ) { 142 | // Let's run the actual importer now, woot 143 | $reader = $this->get_reader( $file ); 144 | if ( is_wp_error( $reader ) ) { 145 | return $reader; 146 | } 147 | 148 | // Set the version to compatibility mode first 149 | $this->version = '1.0'; 150 | 151 | // Start parsing! 152 | $data = new WXR_Import_Info(); 153 | while ( $reader->read() ) { 154 | // Only deal with element opens 155 | if ( $reader->nodeType !== XMLReader::ELEMENT ) { 156 | continue; 157 | } 158 | 159 | switch ( $reader->name ) { 160 | case 'wp:wxr_version': 161 | // Upgrade to the correct version 162 | $this->version = $reader->readString(); 163 | 164 | if ( version_compare( $this->version, self::MAX_WXR_VERSION, '>' ) ) { 165 | $this->logger->warning( sprintf( 166 | __( 'This WXR file (version %s) is newer than the importer (version %s) and may not be supported. Please consider updating.', 'wordpress-importer' ), 167 | $this->version, 168 | self::MAX_WXR_VERSION 169 | ) ); 170 | } 171 | 172 | // Handled everything in this node, move on to the next 173 | $reader->next(); 174 | break; 175 | 176 | case 'generator': 177 | $data->generator = $reader->readString(); 178 | $reader->next(); 179 | break; 180 | 181 | case 'title': 182 | $data->title = $reader->readString(); 183 | $reader->next(); 184 | break; 185 | 186 | case 'wp:base_site_url': 187 | $data->siteurl = $reader->readString(); 188 | $reader->next(); 189 | break; 190 | 191 | case 'wp:base_blog_url': 192 | $data->home = $reader->readString(); 193 | $reader->next(); 194 | break; 195 | 196 | case 'wp:author': 197 | $node = $reader->expand(); 198 | 199 | $parsed = $this->parse_author_node( $node ); 200 | if ( is_wp_error( $parsed ) ) { 201 | $this->log_error( $parsed ); 202 | 203 | // Skip the rest of this post 204 | $reader->next(); 205 | break; 206 | } 207 | 208 | $data->users[] = $parsed; 209 | 210 | // Handled everything in this node, move on to the next 211 | $reader->next(); 212 | break; 213 | 214 | case 'item': 215 | $node = $reader->expand(); 216 | $parsed = $this->parse_post_node( $node ); 217 | if ( is_wp_error( $parsed ) ) { 218 | $this->log_error( $parsed ); 219 | 220 | // Skip the rest of this post 221 | $reader->next(); 222 | break; 223 | } 224 | 225 | if ( $parsed['data']['post_type'] === 'attachment' ) { 226 | $data->media_count++; 227 | } else { 228 | $data->post_count++; 229 | } 230 | $data->comment_count += count( $parsed['comments'] ); 231 | 232 | // Handled everything in this node, move on to the next 233 | $reader->next(); 234 | break; 235 | 236 | case 'wp:category': 237 | case 'wp:tag': 238 | case 'wp:term': 239 | $data->term_count++; 240 | 241 | // Handled everything in this node, move on to the next 242 | $reader->next(); 243 | break; 244 | } 245 | } 246 | 247 | $data->version = $this->version; 248 | 249 | return $data; 250 | } 251 | 252 | /** 253 | * The main controller for the actual import stage. 254 | * 255 | * @param string $file Path to the WXR file for importing 256 | */ 257 | public function parse_authors( $file ) { 258 | // Let's run the actual importer now, woot 259 | $reader = $this->get_reader( $file ); 260 | if ( is_wp_error( $reader ) ) { 261 | return $reader; 262 | } 263 | 264 | // Set the version to compatibility mode first 265 | $this->version = '1.0'; 266 | 267 | // Start parsing! 268 | $authors = array(); 269 | while ( $reader->read() ) { 270 | // Only deal with element opens 271 | if ( $reader->nodeType !== XMLReader::ELEMENT ) { 272 | continue; 273 | } 274 | 275 | switch ( $reader->name ) { 276 | case 'wp:wxr_version': 277 | // Upgrade to the correct version 278 | $this->version = $reader->readString(); 279 | 280 | if ( version_compare( $this->version, self::MAX_WXR_VERSION, '>' ) ) { 281 | $this->logger->warning( sprintf( 282 | __( 'This WXR file (version %s) is newer than the importer (version %s) and may not be supported. Please consider updating.', 'wordpress-importer' ), 283 | $this->version, 284 | self::MAX_WXR_VERSION 285 | ) ); 286 | } 287 | 288 | // Handled everything in this node, move on to the next 289 | $reader->next(); 290 | break; 291 | 292 | case 'wp:author': 293 | $node = $reader->expand(); 294 | 295 | $parsed = $this->parse_author_node( $node ); 296 | if ( is_wp_error( $parsed ) ) { 297 | $this->log_error( $parsed ); 298 | 299 | // Skip the rest of this post 300 | $reader->next(); 301 | break; 302 | } 303 | 304 | $authors[] = $parsed; 305 | 306 | // Handled everything in this node, move on to the next 307 | $reader->next(); 308 | break; 309 | } 310 | } 311 | 312 | return $authors; 313 | } 314 | 315 | /** 316 | * The main controller for the actual import stage. 317 | * 318 | * @param string $file Path to the WXR file for importing 319 | */ 320 | public function import( $file ) { 321 | add_filter( 'import_post_meta_key', array( $this, 'is_valid_meta_key' ) ); 322 | add_filter( 'http_request_timeout', array( &$this, 'bump_request_timeout' ) ); 323 | 324 | $result = $this->import_start( $file ); 325 | if ( is_wp_error( $result ) ) { 326 | return $result; 327 | } 328 | 329 | // Let's run the actual importer now, woot 330 | $reader = $this->get_reader( $file ); 331 | if ( is_wp_error( $reader ) ) { 332 | return $reader; 333 | } 334 | 335 | // Set the version to compatibility mode first 336 | $this->version = '1.0'; 337 | 338 | // Reset other variables 339 | $this->base_url = ''; 340 | 341 | // Start parsing! 342 | while ( $reader->read() ) { 343 | // Only deal with element opens 344 | if ( $reader->nodeType !== XMLReader::ELEMENT ) { 345 | continue; 346 | } 347 | 348 | switch ( $reader->name ) { 349 | case 'wp:wxr_version': 350 | // Upgrade to the correct version 351 | $this->version = $reader->readString(); 352 | 353 | if ( version_compare( $this->version, self::MAX_WXR_VERSION, '>' ) ) { 354 | $this->logger->warning( sprintf( 355 | __( 'This WXR file (version %s) is newer than the importer (version %s) and may not be supported. Please consider updating.', 'wordpress-importer' ), 356 | $this->version, 357 | self::MAX_WXR_VERSION 358 | ) ); 359 | } 360 | 361 | // Handled everything in this node, move on to the next 362 | $reader->next(); 363 | break; 364 | 365 | case 'wp:base_site_url': 366 | $this->base_url = $reader->readString(); 367 | 368 | // Handled everything in this node, move on to the next 369 | $reader->next(); 370 | break; 371 | 372 | case 'item': 373 | $node = $reader->expand(); 374 | $parsed = $this->parse_post_node( $node ); 375 | if ( is_wp_error( $parsed ) ) { 376 | $this->log_error( $parsed ); 377 | 378 | // Skip the rest of this post 379 | $reader->next(); 380 | break; 381 | } 382 | 383 | $this->process_post( $parsed['data'], $parsed['meta'], $parsed['comments'], $parsed['terms'] ); 384 | 385 | // Handled everything in this node, move on to the next 386 | $reader->next(); 387 | break; 388 | 389 | case 'wp:author': 390 | $node = $reader->expand(); 391 | 392 | $parsed = $this->parse_author_node( $node ); 393 | if ( is_wp_error( $parsed ) ) { 394 | $this->log_error( $parsed ); 395 | 396 | // Skip the rest of this post 397 | $reader->next(); 398 | break; 399 | } 400 | 401 | $status = $this->process_author( $parsed['data'], $parsed['meta'] ); 402 | if ( is_wp_error( $status ) ) { 403 | $this->log_error( $status ); 404 | } 405 | 406 | // Handled everything in this node, move on to the next 407 | $reader->next(); 408 | break; 409 | 410 | case 'wp:category': 411 | $node = $reader->expand(); 412 | 413 | $parsed = $this->parse_term_node( $node, 'category' ); 414 | if ( is_wp_error( $parsed ) ) { 415 | $this->log_error( $parsed ); 416 | 417 | // Skip the rest of this post 418 | $reader->next(); 419 | break; 420 | } 421 | 422 | $status = $this->process_term( $parsed['data'], $parsed['meta'] ); 423 | 424 | // Handled everything in this node, move on to the next 425 | $reader->next(); 426 | break; 427 | 428 | case 'wp:tag': 429 | $node = $reader->expand(); 430 | 431 | $parsed = $this->parse_term_node( $node, 'tag' ); 432 | if ( is_wp_error( $parsed ) ) { 433 | $this->log_error( $parsed ); 434 | 435 | // Skip the rest of this post 436 | $reader->next(); 437 | break; 438 | } 439 | 440 | $status = $this->process_term( $parsed['data'], $parsed['meta'] ); 441 | 442 | // Handled everything in this node, move on to the next 443 | $reader->next(); 444 | break; 445 | 446 | case 'wp:term': 447 | $node = $reader->expand(); 448 | 449 | $parsed = $this->parse_term_node( $node ); 450 | if ( is_wp_error( $parsed ) ) { 451 | $this->log_error( $parsed ); 452 | 453 | // Skip the rest of this post 454 | $reader->next(); 455 | break; 456 | } 457 | 458 | $status = $this->process_term( $parsed['data'], $parsed['meta'] ); 459 | 460 | // Handled everything in this node, move on to the next 461 | $reader->next(); 462 | break; 463 | 464 | default: 465 | // Skip this node, probably handled by something already 466 | break; 467 | } 468 | } 469 | 470 | // Now that we've done the main processing, do any required 471 | // post-processing and remapping. 472 | $this->post_process(); 473 | 474 | if ( $this->options['aggressive_url_search'] ) { 475 | $this->replace_attachment_urls_in_content(); 476 | } 477 | // $this->remap_featured_images(); 478 | 479 | $this->import_end(); 480 | } 481 | 482 | /** 483 | * Log an error instance to the logger. 484 | * 485 | * @param WP_Error $error Error instance to log. 486 | */ 487 | protected function log_error( WP_Error $error ) { 488 | $this->logger->warning( $error->get_error_message() ); 489 | 490 | // Log the data as debug info too 491 | $data = $error->get_error_data(); 492 | if ( ! empty( $data ) ) { 493 | $this->logger->debug( var_export( $data, true ) ); 494 | } 495 | } 496 | 497 | /** 498 | * Parses the WXR file and prepares us for the task of processing parsed data 499 | * 500 | * @param string $file Path to the WXR file for importing 501 | */ 502 | protected function import_start( $file ) { 503 | if ( ! is_file( $file ) ) { 504 | return new WP_Error( 'wxr_importer.file_missing', __( 'The file does not exist, please try again.', 'wordpress-importer' ) ); 505 | } 506 | 507 | // Suspend bunches of stuff in WP core 508 | wp_defer_term_counting( true ); 509 | wp_defer_comment_counting( true ); 510 | wp_suspend_cache_invalidation( true ); 511 | 512 | // Prefill exists calls if told to 513 | if ( $this->options['prefill_existing_posts'] ) { 514 | $this->prefill_existing_posts(); 515 | } 516 | if ( $this->options['prefill_existing_comments'] ) { 517 | $this->prefill_existing_comments(); 518 | } 519 | if ( $this->options['prefill_existing_terms'] ) { 520 | $this->prefill_existing_terms(); 521 | } 522 | 523 | /** 524 | * Begin the import. 525 | * 526 | * Fires before the import process has begun. If you need to suspend 527 | * caching or heavy processing on hooks, do so here. 528 | */ 529 | do_action( 'import_start' ); 530 | } 531 | 532 | /** 533 | * Performs post-import cleanup of files and the cache 534 | */ 535 | protected function import_end() { 536 | // Re-enable stuff in core 537 | wp_suspend_cache_invalidation( false ); 538 | wp_cache_flush(); 539 | foreach ( get_taxonomies() as $tax ) { 540 | delete_option( "{$tax}_children" ); 541 | _get_term_hierarchy( $tax ); 542 | } 543 | 544 | wp_defer_term_counting( false ); 545 | wp_defer_comment_counting( false ); 546 | 547 | /** 548 | * Complete the import. 549 | * 550 | * Fires after the import process has finished. If you need to update 551 | * your cache or re-enable processing, do so here. 552 | */ 553 | do_action( 'import_end' ); 554 | } 555 | 556 | /** 557 | * Set the user mapping. 558 | * 559 | * @param array $mapping List of map arrays (containing `old_slug`, `old_id`, `new_id`) 560 | */ 561 | public function set_user_mapping( $mapping ) { 562 | foreach ( $mapping as $map ) { 563 | if ( empty( $map['old_slug'] ) || empty( $map['old_id'] ) || empty( $map['new_id'] ) ) { 564 | $this->logger->warning( __( 'Invalid author mapping', 'wordpress-importer' ) ); 565 | $this->logger->debug( var_export( $map, true ) ); 566 | continue; 567 | } 568 | 569 | $old_slug = $map['old_slug']; 570 | $old_id = $map['old_id']; 571 | $new_id = $map['new_id']; 572 | 573 | $this->mapping['user'][ $old_id ] = $new_id; 574 | $this->mapping['user_slug'][ $old_slug ] = $new_id; 575 | } 576 | } 577 | 578 | /** 579 | * Set the user slug overrides. 580 | * 581 | * Allows overriding the slug in the import with a custom/renamed version. 582 | * 583 | * @param string[] $overrides Map of old slug to new slug. 584 | */ 585 | public function set_user_slug_overrides( $overrides ) { 586 | foreach ( $overrides as $original => $renamed ) { 587 | $this->user_slug_override[ $original ] = $renamed; 588 | } 589 | } 590 | 591 | /** 592 | * Parse a post node into post data. 593 | * 594 | * @param DOMElement $node Parent node of post data (typically `item`). 595 | * @return array|WP_Error Post data array on success, error otherwise. 596 | */ 597 | protected function parse_post_node( $node ) { 598 | $data = array(); 599 | $meta = array(); 600 | $comments = array(); 601 | $terms = array(); 602 | 603 | foreach ( $node->childNodes as $child ) { 604 | // We only care about child elements 605 | if ( $child->nodeType !== XML_ELEMENT_NODE ) { 606 | continue; 607 | } 608 | 609 | switch ( $child->tagName ) { 610 | case 'wp:post_type': 611 | $data['post_type'] = $child->textContent; 612 | break; 613 | 614 | case 'title': 615 | $data['post_title'] = $child->textContent; 616 | break; 617 | 618 | case 'guid': 619 | $data['guid'] = $child->textContent; 620 | break; 621 | 622 | case 'dc:creator': 623 | $data['post_author'] = $child->textContent; 624 | break; 625 | 626 | case 'content:encoded': 627 | $data['post_content'] = $child->textContent; 628 | break; 629 | 630 | case 'excerpt:encoded': 631 | $data['post_excerpt'] = $child->textContent; 632 | break; 633 | 634 | case 'wp:post_id': 635 | $data['post_id'] = $child->textContent; 636 | break; 637 | 638 | case 'wp:post_date': 639 | $data['post_date'] = $child->textContent; 640 | break; 641 | 642 | case 'wp:post_date_gmt': 643 | $data['post_date_gmt'] = $child->textContent; 644 | break; 645 | 646 | case 'wp:comment_status': 647 | $data['comment_status'] = $child->textContent; 648 | break; 649 | 650 | case 'wp:ping_status': 651 | $data['ping_status'] = $child->textContent; 652 | break; 653 | 654 | case 'wp:post_name': 655 | $data['post_name'] = $child->textContent; 656 | break; 657 | 658 | case 'wp:status': 659 | $data['post_status'] = $child->textContent; 660 | 661 | if ( $data['post_status'] === 'auto-draft' ) { 662 | // Bail now 663 | return new WP_Error( 664 | 'wxr_importer.post.cannot_import_draft', 665 | __( 'Cannot import auto-draft posts' ), 666 | $data 667 | ); 668 | } 669 | break; 670 | 671 | case 'wp:post_parent': 672 | $data['post_parent'] = $child->textContent; 673 | break; 674 | 675 | case 'wp:menu_order': 676 | $data['menu_order'] = $child->textContent; 677 | break; 678 | 679 | case 'wp:post_password': 680 | $data['post_password'] = $child->textContent; 681 | break; 682 | 683 | case 'wp:is_sticky': 684 | $data['is_sticky'] = $child->textContent; 685 | break; 686 | 687 | case 'wp:attachment_url': 688 | $data['attachment_url'] = $child->textContent; 689 | break; 690 | 691 | case 'wp:postmeta': 692 | $meta_item = $this->parse_meta_node( $child ); 693 | if ( ! empty( $meta_item ) ) { 694 | $meta[] = $meta_item; 695 | } 696 | break; 697 | 698 | case 'wp:comment': 699 | $comment_item = $this->parse_comment_node( $child ); 700 | if ( ! empty( $comment_item ) ) { 701 | $comments[] = $comment_item; 702 | } 703 | break; 704 | 705 | case 'category': 706 | $term_item = $this->parse_category_node( $child ); 707 | if ( ! empty( $term_item ) ) { 708 | $terms[] = $term_item; 709 | } 710 | break; 711 | } 712 | } 713 | 714 | return compact( 'data', 'meta', 'comments', 'terms' ); 715 | } 716 | 717 | /** 718 | * Create new posts based on import information 719 | * 720 | * Posts marked as having a parent which doesn't exist will become top level items. 721 | * Doesn't create a new post if: the post type doesn't exist, the given post ID 722 | * is already noted as imported or a post with the same title and date already exists. 723 | * Note that new/updated terms, comments and meta are imported for the last of the above. 724 | */ 725 | protected function process_post( $data, $meta, $comments, $terms ) { 726 | /** 727 | * Pre-process post data. 728 | * 729 | * @param array $data Post data. (Return empty to skip.) 730 | * @param array $meta Meta data. 731 | * @param array $comments Comments on the post. 732 | * @param array $terms Terms on the post. 733 | */ 734 | $data = apply_filters( 'wxr_importer.pre_process.post', $data, $meta, $comments, $terms ); 735 | if ( empty( $data ) ) { 736 | return false; 737 | } 738 | 739 | $original_id = isset( $data['post_id'] ) ? (int) $data['post_id'] : 0; 740 | $parent_id = isset( $data['post_parent'] ) ? (int) $data['post_parent'] : 0; 741 | $author_id = isset( $data['post_author'] ) ? (int) $data['post_author'] : 0; 742 | 743 | // Have we already processed this? 744 | if ( isset( $this->mapping['post'][ $original_id ] ) ) { 745 | return; 746 | } 747 | 748 | $post_type_object = get_post_type_object( $data['post_type'] ); 749 | 750 | // Is this type even valid? 751 | if ( ! $post_type_object ) { 752 | $this->logger->warning( sprintf( 753 | __( 'Failed to import "%s": Invalid post type %s', 'wordpress-importer' ), 754 | $data['post_title'], 755 | $data['post_type'] 756 | ) ); 757 | return false; 758 | } 759 | 760 | $post_exists = $this->post_exists( $data ); 761 | if ( $post_exists ) { 762 | $this->logger->info( sprintf( 763 | __( '%s "%s" already exists.', 'wordpress-importer' ), 764 | $post_type_object->labels->singular_name, 765 | $data['post_title'] 766 | ) ); 767 | 768 | /** 769 | * Post processing already imported. 770 | * 771 | * @param array $data Raw data imported for the post. 772 | */ 773 | do_action( 'wxr_importer.process_already_imported.post', $data ); 774 | 775 | // Even though this post already exists, new comments might need importing 776 | $this->process_comments( $comments, $original_id, $data, $post_exists ); 777 | 778 | return false; 779 | } 780 | 781 | // Map the parent post, or mark it as one we need to fix 782 | $requires_remapping = false; 783 | if ( $parent_id ) { 784 | if ( isset( $this->mapping['post'][ $parent_id ] ) ) { 785 | $data['post_parent'] = $this->mapping['post'][ $parent_id ]; 786 | } else { 787 | $meta[] = array( 'key' => '_wxr_import_parent', 'value' => $parent_id ); 788 | $requires_remapping = true; 789 | 790 | $data['post_parent'] = 0; 791 | } 792 | } 793 | 794 | // Map the author, or mark it as one we need to fix 795 | $author = sanitize_user( $data['post_author'], true ); 796 | if ( empty( $author ) ) { 797 | // Missing or invalid author, use default if available. 798 | $data['post_author'] = $this->options['default_author']; 799 | } elseif ( isset( $this->mapping['user_slug'][ $author ] ) ) { 800 | $data['post_author'] = $this->mapping['user_slug'][ $author ]; 801 | } else { 802 | $meta[] = array( 'key' => '_wxr_import_user_slug', 'value' => $author ); 803 | $requires_remapping = true; 804 | 805 | $data['post_author'] = (int) get_current_user_id(); 806 | } 807 | 808 | // Does the post look like it contains attachment images? 809 | if ( preg_match( self::REGEX_HAS_ATTACHMENT_REFS, $data['post_content'] ) ) { 810 | $meta[] = array( 'key' => '_wxr_import_has_attachment_refs', 'value' => true ); 811 | $requires_remapping = true; 812 | } 813 | 814 | // Whitelist to just the keys we allow 815 | $postdata = array( 816 | 'import_id' => $data['post_id'], 817 | ); 818 | $allowed = array( 819 | 'post_author' => true, 820 | 'post_date' => true, 821 | 'post_date_gmt' => true, 822 | 'post_content' => true, 823 | 'post_excerpt' => true, 824 | 'post_title' => true, 825 | 'post_status' => true, 826 | 'post_name' => true, 827 | 'comment_status' => true, 828 | 'ping_status' => true, 829 | 'guid' => true, 830 | 'post_parent' => true, 831 | 'menu_order' => true, 832 | 'post_type' => true, 833 | 'post_password' => true, 834 | ); 835 | foreach ( $data as $key => $value ) { 836 | if ( ! isset( $allowed[ $key ] ) ) { 837 | continue; 838 | } 839 | 840 | $postdata[ $key ] = $data[ $key ]; 841 | } 842 | 843 | $postdata = apply_filters( 'wp_import_post_data_processed', $postdata, $data ); 844 | 845 | if ( 'attachment' === $postdata['post_type'] ) { 846 | if ( ! $this->options['fetch_attachments'] ) { 847 | $this->logger->notice( sprintf( 848 | __( 'Skipping attachment "%s", fetching attachments disabled' ), 849 | $data['post_title'] 850 | ) ); 851 | /** 852 | * Post processing skipped. 853 | * 854 | * @param array $data Raw data imported for the post. 855 | * @param array $meta Raw meta data, already processed by {@see process_post_meta}. 856 | */ 857 | do_action( 'wxr_importer.process_skipped.post', $data, $meta ); 858 | return false; 859 | } 860 | $remote_url = ! empty( $data['attachment_url'] ) ? $data['attachment_url'] : $data['guid']; 861 | $post_id = $this->process_attachment( $postdata, $meta, $remote_url ); 862 | } else { 863 | $post_id = wp_insert_post( $postdata, true ); 864 | do_action( 'wp_import_insert_post', $post_id, $original_id, $postdata, $data ); 865 | } 866 | 867 | if ( is_wp_error( $post_id ) ) { 868 | $this->logger->error( sprintf( 869 | __( 'Failed to import "%s" (%s)', 'wordpress-importer' ), 870 | $data['post_title'], 871 | $post_type_object->labels->singular_name 872 | ) ); 873 | $this->logger->debug( $post_id->get_error_message() ); 874 | 875 | /** 876 | * Post processing failed. 877 | * 878 | * @param WP_Error $post_id Error object. 879 | * @param array $data Raw data imported for the post. 880 | * @param array $meta Raw meta data, already processed by {@see process_post_meta}. 881 | * @param array $comments Raw comment data, already processed by {@see process_comments}. 882 | * @param array $terms Raw term data, already processed. 883 | */ 884 | do_action( 'wxr_importer.process_failed.post', $post_id, $data, $meta, $comments, $terms ); 885 | return false; 886 | } 887 | 888 | // Ensure stickiness is handled correctly too 889 | if ( $data['is_sticky'] === '1' ) { 890 | stick_post( $post_id ); 891 | } 892 | 893 | // map pre-import ID to local ID 894 | $this->mapping['post'][ $original_id ] = (int) $post_id; 895 | if ( $requires_remapping ) { 896 | $this->requires_remapping['post'][ $post_id ] = true; 897 | } 898 | $this->mark_post_exists( $data, $post_id ); 899 | 900 | $this->logger->info( sprintf( 901 | __( 'Imported "%s" (%s)', 'wordpress-importer' ), 902 | $data['post_title'], 903 | $post_type_object->labels->singular_name 904 | ) ); 905 | $this->logger->debug( sprintf( 906 | __( 'Post %d remapped to %d', 'wordpress-importer' ), 907 | $original_id, 908 | $post_id 909 | ) ); 910 | 911 | // Handle the terms too 912 | $terms = apply_filters( 'wp_import_post_terms', $terms, $post_id, $data ); 913 | 914 | if ( ! empty( $terms ) ) { 915 | $term_ids = array(); 916 | foreach ( $terms as $term ) { 917 | $taxonomy = $term['taxonomy']; 918 | $key = sha1( $taxonomy . ':' . $term['slug'] ); 919 | 920 | if ( isset( $this->mapping['term'][ $key ] ) ) { 921 | $term_ids[ $taxonomy ][] = (int) $this->mapping['term'][ $key ]; 922 | } else { 923 | $meta[] = array( 'key' => '_wxr_import_term', 'value' => $term ); 924 | $requires_remapping = true; 925 | } 926 | } 927 | 928 | foreach ( $term_ids as $tax => $ids ) { 929 | $tt_ids = wp_set_post_terms( $post_id, $ids, $tax ); 930 | do_action( 'wp_import_set_post_terms', $tt_ids, $ids, $tax, $post_id, $data ); 931 | } 932 | } 933 | 934 | $this->process_comments( $comments, $post_id, $data ); 935 | $this->process_post_meta( $meta, $post_id, $data ); 936 | 937 | if ( 'nav_menu_item' === $data['post_type'] ) { 938 | $this->process_menu_item_meta( $post_id, $data, $meta ); 939 | } 940 | 941 | /** 942 | * Post processing completed. 943 | * 944 | * @param int $post_id New post ID. 945 | * @param array $data Raw data imported for the post. 946 | * @param array $meta Raw meta data, already processed by {@see process_post_meta}. 947 | * @param array $comments Raw comment data, already processed by {@see process_comments}. 948 | * @param array $terms Raw term data, already processed. 949 | */ 950 | do_action( 'wxr_importer.processed.post', $post_id, $data, $meta, $comments, $terms ); 951 | } 952 | 953 | /** 954 | * Attempt to create a new menu item from import data 955 | * 956 | * Fails for draft, orphaned menu items and those without an associated nav_menu 957 | * or an invalid nav_menu term. If the post type or term object which the menu item 958 | * represents doesn't exist then the menu item will not be imported (waits until the 959 | * end of the import to retry again before discarding). 960 | * 961 | * @param array $item Menu item details from WXR file 962 | */ 963 | protected function process_menu_item_meta( $post_id, $data, $meta ) { 964 | 965 | $item_type = get_post_meta( $post_id, '_menu_item_type', true ); 966 | $original_object_id = get_post_meta( $post_id, '_menu_item_object_id', true ); 967 | $object_id = null; 968 | 969 | $this->logger->debug( sprintf( 'Processing menu item %s', $item_type ) ); 970 | 971 | $requires_remapping = false; 972 | switch ( $item_type ) { 973 | case 'taxonomy': 974 | if ( isset( $this->mapping['term_id'][ $original_object_id ] ) ) { 975 | $object_id = $this->mapping['term_id'][ $original_object_id ]; 976 | } else { 977 | add_post_meta( $post_id, '_wxr_import_menu_item', wp_slash( $original_object_id ) ); 978 | $requires_remapping = true; 979 | } 980 | break; 981 | 982 | case 'post_type': 983 | if ( isset( $this->mapping['post'][ $original_object_id ] ) ) { 984 | $object_id = $this->mapping['post'][ $original_object_id ]; 985 | } else { 986 | add_post_meta( $post_id, '_wxr_import_menu_item', wp_slash( $original_object_id ) ); 987 | $requires_remapping = true; 988 | } 989 | break; 990 | 991 | case 'custom': 992 | // Custom refers to itself, wonderfully easy. 993 | $object_id = $post_id; 994 | break; 995 | 996 | default: 997 | // associated object is missing or not imported yet, we'll retry later 998 | $this->missing_menu_items[] = $item; 999 | $this->logger->debug( 'Unknown menu item type' ); 1000 | break; 1001 | } 1002 | 1003 | if ( $requires_remapping ) { 1004 | $this->requires_remapping['post'][ $post_id ] = true; 1005 | } 1006 | 1007 | if ( empty( $object_id ) ) { 1008 | // Nothing needed here. 1009 | return; 1010 | } 1011 | 1012 | $this->logger->debug( sprintf( 'Menu item %d mapped to %d', $original_object_id, $object_id ) ); 1013 | update_post_meta( $post_id, '_menu_item_object_id', wp_slash( $object_id ) ); 1014 | } 1015 | 1016 | /** 1017 | * If fetching attachments is enabled then attempt to create a new attachment 1018 | * 1019 | * @param array $post Attachment post details from WXR 1020 | * @param string $url URL to fetch attachment from 1021 | * @return int|WP_Error Post ID on success, WP_Error otherwise 1022 | */ 1023 | protected function process_attachment( $post, $meta, $remote_url ) { 1024 | // try to use _wp_attached file for upload folder placement to ensure the same location as the export site 1025 | // e.g. location is 2003/05/image.jpg but the attachment post_date is 2010/09, see media_handle_upload() 1026 | $post['upload_date'] = $post['post_date']; 1027 | foreach ( $meta as $meta_item ) { 1028 | if ( $meta_item['key'] !== '_wp_attached_file' ) { 1029 | continue; 1030 | } 1031 | 1032 | if ( preg_match( '%^[0-9]{4}/[0-9]{2}%', $meta_item['value'], $matches ) ) { 1033 | $post['upload_date'] = $matches[0]; 1034 | } 1035 | break; 1036 | } 1037 | 1038 | // if the URL is absolute, but does not contain address, then upload it assuming base_site_url 1039 | if ( preg_match( '|^/[\w\W]+$|', $remote_url ) ) { 1040 | $remote_url = rtrim( $this->base_url, '/' ) . $remote_url; 1041 | } 1042 | 1043 | $upload = $this->fetch_remote_file( $remote_url, $post ); 1044 | if ( is_wp_error( $upload ) ) { 1045 | return $upload; 1046 | } 1047 | 1048 | $info = wp_check_filetype( $upload['file'] ); 1049 | if ( ! $info ) { 1050 | return new WP_Error( 'attachment_processing_error', __( 'Invalid file type', 'wordpress-importer' ) ); 1051 | } 1052 | 1053 | $post['post_mime_type'] = $info['type']; 1054 | 1055 | // WP really likes using the GUID for display. Allow updating it. 1056 | // See https://core.trac.wordpress.org/ticket/33386 1057 | if ( $this->options['update_attachment_guids'] ) { 1058 | $post['guid'] = $upload['url']; 1059 | } 1060 | 1061 | // as per wp-admin/includes/upload.php 1062 | $post_id = wp_insert_attachment( $post, $upload['file'] ); 1063 | if ( is_wp_error( $post_id ) ) { 1064 | return $post_id; 1065 | } 1066 | 1067 | $attachment_metadata = wp_generate_attachment_metadata( $post_id, $upload['file'] ); 1068 | wp_update_attachment_metadata( $post_id, $attachment_metadata ); 1069 | 1070 | // Map this image URL later if we need to 1071 | $this->url_remap[ $remote_url ] = $upload['url']; 1072 | 1073 | // If we have a HTTPS URL, ensure the HTTP URL gets replaced too 1074 | if ( substr( $remote_url, 0, 8 ) === 'https://' ) { 1075 | $insecure_url = 'http' . substr( $remote_url, 5 ); 1076 | $this->url_remap[ $insecure_url ] = $upload['url']; 1077 | } 1078 | 1079 | if ( $this->options['aggressive_url_search'] ) { 1080 | // remap resized image URLs, works by stripping the extension and remapping the URL stub. 1081 | /*if ( preg_match( '!^image/!', $info['type'] ) ) { 1082 | $parts = pathinfo( $remote_url ); 1083 | $name = basename( $parts['basename'], ".{$parts['extension']}" ); // PATHINFO_FILENAME in PHP 5.2 1084 | 1085 | $parts_new = pathinfo( $upload['url'] ); 1086 | $name_new = basename( $parts_new['basename'], ".{$parts_new['extension']}" ); 1087 | 1088 | $this->url_remap[$parts['dirname'] . '/' . $name] = $parts_new['dirname'] . '/' . $name_new; 1089 | }*/ 1090 | } 1091 | 1092 | return $post_id; 1093 | } 1094 | 1095 | /** 1096 | * Parse a meta node into meta data. 1097 | * 1098 | * @param DOMElement $node Parent node of meta data (typically `wp:postmeta` or `wp:commentmeta`). 1099 | * @return array|null Meta data array on success, or null on error. 1100 | */ 1101 | protected function parse_meta_node( $node ) { 1102 | foreach ( $node->childNodes as $child ) { 1103 | // We only care about child elements 1104 | if ( $child->nodeType !== XML_ELEMENT_NODE ) { 1105 | continue; 1106 | } 1107 | 1108 | switch ( $child->tagName ) { 1109 | case 'wp:meta_key': 1110 | $key = $child->textContent; 1111 | break; 1112 | 1113 | case 'wp:meta_value': 1114 | $value = $child->textContent; 1115 | break; 1116 | } 1117 | } 1118 | 1119 | if ( empty( $key ) || empty( $value ) ) { 1120 | return null; 1121 | } 1122 | 1123 | return compact( 'key', 'value' ); 1124 | } 1125 | 1126 | /** 1127 | * Process and import post meta items. 1128 | * 1129 | * @param array $meta List of meta data arrays 1130 | * @param int $post_id Post to associate with 1131 | * @param array $post Post data 1132 | * @return int|WP_Error Number of meta items imported on success, error otherwise. 1133 | */ 1134 | protected function process_post_meta( $meta, $post_id, $post ) { 1135 | if ( empty( $meta ) ) { 1136 | return true; 1137 | } 1138 | 1139 | foreach ( $meta as $meta_item ) { 1140 | /** 1141 | * Pre-process post meta data. 1142 | * 1143 | * @param array $meta_item Meta data. (Return empty to skip.) 1144 | * @param int $post_id Post the meta is attached to. 1145 | */ 1146 | $meta_item = apply_filters( 'wxr_importer.pre_process.post_meta', $meta_item, $post_id ); 1147 | if ( empty( $meta_item ) ) { 1148 | return false; 1149 | } 1150 | 1151 | $key = apply_filters( 'import_post_meta_key', $meta_item['key'], $post_id, $post ); 1152 | $value = false; 1153 | 1154 | if ( '_edit_last' === $key ) { 1155 | $value = intval( $meta_item['value'] ); 1156 | if ( ! isset( $this->mapping['user'][ $value ] ) ) { 1157 | // Skip! 1158 | continue; 1159 | } 1160 | 1161 | $value = $this->mapping['user'][ $value ]; 1162 | } 1163 | 1164 | if ( $key ) { 1165 | // export gets meta straight from the DB so could have a serialized string 1166 | if ( ! $value ) { 1167 | $value = maybe_unserialize( $meta_item['value'] ); 1168 | } 1169 | 1170 | add_post_meta( $post_id, $key, $value ); 1171 | do_action( 'import_post_meta', $post_id, $key, $value ); 1172 | 1173 | // if the post has a featured image, take note of this in case of remap 1174 | if ( '_thumbnail_id' === $key ) { 1175 | $this->featured_images[ $post_id ] = (int) $value; 1176 | } 1177 | } 1178 | } 1179 | 1180 | return true; 1181 | } 1182 | 1183 | /** 1184 | * Parse a comment node into comment data. 1185 | * 1186 | * @param DOMElement $node Parent node of comment data (typically `wp:comment`). 1187 | * @return array Comment data array. 1188 | */ 1189 | protected function parse_comment_node( $node ) { 1190 | $data = array( 1191 | 'commentmeta' => array(), 1192 | ); 1193 | 1194 | foreach ( $node->childNodes as $child ) { 1195 | // We only care about child elements 1196 | if ( $child->nodeType !== XML_ELEMENT_NODE ) { 1197 | continue; 1198 | } 1199 | 1200 | switch ( $child->tagName ) { 1201 | case 'wp:comment_id': 1202 | $data['comment_id'] = $child->textContent; 1203 | break; 1204 | case 'wp:comment_author': 1205 | $data['comment_author'] = $child->textContent; 1206 | break; 1207 | 1208 | case 'wp:comment_author_email': 1209 | $data['comment_author_email'] = $child->textContent; 1210 | break; 1211 | 1212 | case 'wp:comment_author_IP': 1213 | $data['comment_author_IP'] = $child->textContent; 1214 | break; 1215 | 1216 | case 'wp:comment_author_url': 1217 | $data['comment_author_url'] = $child->textContent; 1218 | break; 1219 | 1220 | case 'wp:comment_user_id': 1221 | $data['comment_user_id'] = $child->textContent; 1222 | break; 1223 | 1224 | case 'wp:comment_date': 1225 | $data['comment_date'] = $child->textContent; 1226 | break; 1227 | 1228 | case 'wp:comment_date_gmt': 1229 | $data['comment_date_gmt'] = $child->textContent; 1230 | break; 1231 | 1232 | case 'wp:comment_content': 1233 | $data['comment_content'] = $child->textContent; 1234 | break; 1235 | 1236 | case 'wp:comment_approved': 1237 | $data['comment_approved'] = $child->textContent; 1238 | break; 1239 | 1240 | case 'wp:comment_type': 1241 | $data['comment_type'] = $child->textContent; 1242 | break; 1243 | 1244 | case 'wp:comment_parent': 1245 | $data['comment_parent'] = $child->textContent; 1246 | break; 1247 | 1248 | case 'wp:commentmeta': 1249 | $meta_item = $this->parse_meta_node( $child ); 1250 | if ( ! empty( $meta_item ) ) { 1251 | $data['commentmeta'][] = $meta_item; 1252 | } 1253 | break; 1254 | } 1255 | } 1256 | 1257 | return $data; 1258 | } 1259 | 1260 | /** 1261 | * Process and import comment data. 1262 | * 1263 | * @param array $comments List of comment data arrays. 1264 | * @param int $post_id Post to associate with. 1265 | * @param array $post Post data. 1266 | * @return int|WP_Error Number of comments imported on success, error otherwise. 1267 | */ 1268 | protected function process_comments( $comments, $post_id, $post, $post_exists = false ) { 1269 | 1270 | $comments = apply_filters( 'wp_import_post_comments', $comments, $post_id, $post ); 1271 | if ( empty( $comments ) ) { 1272 | return 0; 1273 | } 1274 | 1275 | $num_comments = 0; 1276 | 1277 | // Sort by ID to avoid excessive remapping later 1278 | usort( $comments, array( $this, 'sort_comments_by_id' ) ); 1279 | 1280 | foreach ( $comments as $key => $comment ) { 1281 | /** 1282 | * Pre-process comment data 1283 | * 1284 | * @param array $comment Comment data. (Return empty to skip.) 1285 | * @param int $post_id Post the comment is attached to. 1286 | */ 1287 | $comment = apply_filters( 'wxr_importer.pre_process.comment', $comment, $post_id ); 1288 | if ( empty( $comment ) ) { 1289 | return false; 1290 | } 1291 | 1292 | $original_id = isset( $comment['comment_id'] ) ? (int) $comment['comment_id'] : 0; 1293 | $parent_id = isset( $comment['comment_parent'] ) ? (int) $comment['comment_parent'] : 0; 1294 | $author_id = isset( $comment['comment_user_id'] ) ? (int) $comment['comment_user_id'] : 0; 1295 | 1296 | // if this is a new post we can skip the comment_exists() check 1297 | // TODO: Check comment_exists for performance 1298 | if ( $post_exists ) { 1299 | $existing = $this->comment_exists( $comment ); 1300 | if ( $existing ) { 1301 | 1302 | /** 1303 | * Comment processing already imported. 1304 | * 1305 | * @param array $comment Raw data imported for the comment. 1306 | */ 1307 | do_action( 'wxr_importer.process_already_imported.comment', $comment ); 1308 | 1309 | $this->mapping['comment'][ $original_id ] = $existing; 1310 | continue; 1311 | } 1312 | } 1313 | 1314 | // Remove meta from the main array 1315 | $meta = isset( $comment['commentmeta'] ) ? $comment['commentmeta'] : array(); 1316 | unset( $comment['commentmeta'] ); 1317 | 1318 | // Map the parent comment, or mark it as one we need to fix 1319 | $requires_remapping = false; 1320 | if ( $parent_id ) { 1321 | if ( isset( $this->mapping['comment'][ $parent_id ] ) ) { 1322 | $comment['comment_parent'] = $this->mapping['comment'][ $parent_id ]; 1323 | } else { 1324 | // Prepare for remapping later 1325 | $meta[] = array( 'key' => '_wxr_import_parent', 'value' => $parent_id ); 1326 | $requires_remapping = true; 1327 | 1328 | // Wipe the parent for now 1329 | $comment['comment_parent'] = 0; 1330 | } 1331 | } 1332 | 1333 | // Map the author, or mark it as one we need to fix 1334 | if ( $author_id ) { 1335 | if ( isset( $this->mapping['user'][ $author_id ] ) ) { 1336 | $comment['user_id'] = $this->mapping['user'][ $author_id ]; 1337 | } else { 1338 | // Prepare for remapping later 1339 | $meta[] = array( 'key' => '_wxr_import_user', 'value' => $author_id ); 1340 | $requires_remapping = true; 1341 | 1342 | // Wipe the user for now 1343 | $comment['user_id'] = 0; 1344 | } 1345 | } 1346 | 1347 | // Run standard core filters 1348 | $comment['comment_post_ID'] = $post_id; 1349 | $comment = wp_filter_comment( $comment ); 1350 | 1351 | // wp_insert_comment expects slashed data 1352 | $comment_id = wp_insert_comment( wp_slash( $comment ) ); 1353 | $this->mapping['comment'][ $original_id ] = $comment_id; 1354 | if ( $requires_remapping ) { 1355 | $this->requires_remapping['comment'][ $comment_id ] = true; 1356 | } 1357 | $this->mark_comment_exists( $comment, $comment_id ); 1358 | 1359 | /** 1360 | * Comment has been imported. 1361 | * 1362 | * @param int $comment_id New comment ID 1363 | * @param array $comment Comment inserted (`comment_id` item refers to the original ID) 1364 | * @param int $post_id Post parent of the comment 1365 | * @param array $post Post data 1366 | */ 1367 | do_action( 'wp_import_insert_comment', $comment_id, $comment, $post_id, $post ); 1368 | 1369 | // Process the meta items 1370 | foreach ( $meta as $meta_item ) { 1371 | $value = maybe_unserialize( $meta_item['value'] ); 1372 | add_comment_meta( $comment_id, wp_slash( $meta_item['key'] ), wp_slash( $value ) ); 1373 | } 1374 | 1375 | /** 1376 | * Post processing completed. 1377 | * 1378 | * @param int $post_id New post ID. 1379 | * @param array $comment Raw data imported for the comment. 1380 | * @param array $meta Raw meta data, already processed by {@see process_post_meta}. 1381 | * @param array $post_id Parent post ID. 1382 | */ 1383 | do_action( 'wxr_importer.processed.comment', $comment_id, $comment, $meta, $post_id ); 1384 | 1385 | $num_comments++; 1386 | } 1387 | 1388 | return $num_comments; 1389 | } 1390 | 1391 | protected function parse_category_node( $node ) { 1392 | $data = array( 1393 | // Default taxonomy to "category", since this is a `` tag 1394 | 'taxonomy' => 'category', 1395 | ); 1396 | $meta = array(); 1397 | 1398 | if ( $node->hasAttribute( 'domain' ) ) { 1399 | $data['taxonomy'] = $node->getAttribute( 'domain' ); 1400 | } 1401 | if ( $node->hasAttribute( 'nicename' ) ) { 1402 | $data['slug'] = $node->getAttribute( 'nicename' ); 1403 | } 1404 | 1405 | $data['name'] = $node->textContent; 1406 | 1407 | if ( empty( $data['slug'] ) ) { 1408 | return null; 1409 | } 1410 | 1411 | // Just for extra compatibility 1412 | if ( $data['taxonomy'] === 'tag' ) { 1413 | $data['taxonomy'] = 'post_tag'; 1414 | } 1415 | 1416 | return $data; 1417 | } 1418 | 1419 | /** 1420 | * Callback for `usort` to sort comments by ID 1421 | * 1422 | * @param array $a Comment data for the first comment 1423 | * @param array $b Comment data for the second comment 1424 | * @return int 1425 | */ 1426 | public static function sort_comments_by_id( $a, $b ) { 1427 | if ( empty( $a['comment_id'] ) ) { 1428 | return 1; 1429 | } 1430 | 1431 | if ( empty( $b['comment_id'] ) ) { 1432 | return -1; 1433 | } 1434 | 1435 | return $a['comment_id'] - $b['comment_id']; 1436 | } 1437 | 1438 | protected function parse_author_node( $node ) { 1439 | $data = array(); 1440 | $meta = array(); 1441 | foreach ( $node->childNodes as $child ) { 1442 | // We only care about child elements 1443 | if ( $child->nodeType !== XML_ELEMENT_NODE ) { 1444 | continue; 1445 | } 1446 | 1447 | switch ( $child->tagName ) { 1448 | case 'wp:author_login': 1449 | $data['user_login'] = $child->textContent; 1450 | break; 1451 | 1452 | case 'wp:author_id': 1453 | $data['ID'] = $child->textContent; 1454 | break; 1455 | 1456 | case 'wp:author_email': 1457 | $data['user_email'] = $child->textContent; 1458 | break; 1459 | 1460 | case 'wp:author_display_name': 1461 | $data['display_name'] = $child->textContent; 1462 | break; 1463 | 1464 | case 'wp:author_first_name': 1465 | $data['first_name'] = $child->textContent; 1466 | break; 1467 | 1468 | case 'wp:author_last_name': 1469 | $data['last_name'] = $child->textContent; 1470 | break; 1471 | } 1472 | } 1473 | 1474 | return compact( 'data', 'meta' ); 1475 | } 1476 | 1477 | protected function process_author( $data, $meta ) { 1478 | /** 1479 | * Pre-process user data. 1480 | * 1481 | * @param array $data User data. (Return empty to skip.) 1482 | * @param array $meta Meta data. 1483 | */ 1484 | $data = apply_filters( 'wxr_importer.pre_process.user', $data, $meta ); 1485 | if ( empty( $data ) ) { 1486 | return false; 1487 | } 1488 | 1489 | // Have we already handled this user? 1490 | $original_id = isset( $data['ID'] ) ? $data['ID'] : 0; 1491 | $original_slug = $data['user_login']; 1492 | 1493 | if ( isset( $this->mapping['user'][ $original_id ] ) ) { 1494 | $existing = $this->mapping['user'][ $original_id ]; 1495 | 1496 | // Note the slug mapping if we need to too 1497 | if ( ! isset( $this->mapping['user_slug'][ $original_slug ] ) ) { 1498 | $this->mapping['user_slug'][ $original_slug ] = $existing; 1499 | } 1500 | 1501 | return false; 1502 | } 1503 | 1504 | if ( isset( $this->mapping['user_slug'][ $original_slug ] ) ) { 1505 | $existing = $this->mapping['user_slug'][ $original_slug ]; 1506 | 1507 | // Ensure we note the mapping too 1508 | $this->mapping['user'][ $original_id ] = $existing; 1509 | 1510 | return false; 1511 | } 1512 | 1513 | // Allow overriding the user's slug 1514 | $login = $original_slug; 1515 | if ( isset( $this->user_slug_override[ $login ] ) ) { 1516 | $login = $this->user_slug_override[ $login ]; 1517 | } 1518 | 1519 | $userdata = array( 1520 | 'user_login' => sanitize_user( $login, true ), 1521 | 'user_pass' => wp_generate_password(), 1522 | ); 1523 | 1524 | $allowed = array( 1525 | 'user_email' => true, 1526 | 'display_name' => true, 1527 | 'first_name' => true, 1528 | 'last_name' => true, 1529 | ); 1530 | foreach ( $data as $key => $value ) { 1531 | if ( ! isset( $allowed[ $key ] ) ) { 1532 | continue; 1533 | } 1534 | 1535 | $userdata[ $key ] = $data[ $key ]; 1536 | } 1537 | 1538 | $user_id = wp_insert_user( wp_slash( $userdata ) ); 1539 | if ( is_wp_error( $user_id ) ) { 1540 | $this->logger->error( sprintf( 1541 | __( 'Failed to import user "%s"', 'wordpress-importer' ), 1542 | $userdata['user_login'] 1543 | ) ); 1544 | $this->logger->debug( $user_id->get_error_message() ); 1545 | 1546 | /** 1547 | * User processing failed. 1548 | * 1549 | * @param WP_Error $user_id Error object. 1550 | * @param array $userdata Raw data imported for the user. 1551 | */ 1552 | do_action( 'wxr_importer.process_failed.user', $user_id, $userdata ); 1553 | return false; 1554 | } 1555 | 1556 | if ( $original_id ) { 1557 | $this->mapping['user'][ $original_id ] = $user_id; 1558 | } 1559 | $this->mapping['user_slug'][ $original_slug ] = $user_id; 1560 | 1561 | $this->logger->info( sprintf( 1562 | __( 'Imported user "%s"', 'wordpress-importer' ), 1563 | $userdata['user_login'] 1564 | ) ); 1565 | $this->logger->debug( sprintf( 1566 | __( 'User %d remapped to %d', 'wordpress-importer' ), 1567 | $original_id, 1568 | $user_id 1569 | ) ); 1570 | 1571 | // TODO: Implement meta handling once WXR includes it 1572 | /** 1573 | * User processing completed. 1574 | * 1575 | * @param int $user_id New user ID. 1576 | * @param array $userdata Raw data imported for the user. 1577 | */ 1578 | do_action( 'wxr_importer.processed.user', $user_id, $userdata ); 1579 | } 1580 | 1581 | protected function parse_term_node( $node, $type = 'term' ) { 1582 | $data = array(); 1583 | $meta = array(); 1584 | 1585 | $tag_name = array( 1586 | 'id' => 'wp:term_id', 1587 | 'taxonomy' => 'wp:term_taxonomy', 1588 | 'slug' => 'wp:term_slug', 1589 | 'parent' => 'wp:term_parent', 1590 | 'name' => 'wp:term_name', 1591 | 'description' => 'wp:term_description', 1592 | ); 1593 | $taxonomy = null; 1594 | 1595 | // Special casing! 1596 | switch ( $type ) { 1597 | case 'category': 1598 | $tag_name['slug'] = 'wp:category_nicename'; 1599 | $tag_name['parent'] = 'wp:category_parent'; 1600 | $tag_name['name'] = 'wp:cat_name'; 1601 | $tag_name['description'] = 'wp:category_description'; 1602 | $tag_name['taxonomy'] = null; 1603 | 1604 | $data['taxonomy'] = 'category'; 1605 | break; 1606 | 1607 | case 'tag': 1608 | $tag_name['slug'] = 'wp:tag_slug'; 1609 | $tag_name['parent'] = null; 1610 | $tag_name['name'] = 'wp:tag_name'; 1611 | $tag_name['description'] = 'wp:tag_description'; 1612 | $tag_name['taxonomy'] = null; 1613 | 1614 | $data['taxonomy'] = 'post_tag'; 1615 | break; 1616 | } 1617 | 1618 | foreach ( $node->childNodes as $child ) { 1619 | // We only care about child elements 1620 | if ( $child->nodeType !== XML_ELEMENT_NODE ) { 1621 | continue; 1622 | } 1623 | 1624 | $key = array_search( $child->tagName, $tag_name ); 1625 | if ( $key ) { 1626 | $data[ $key ] = $child->textContent; 1627 | } 1628 | } 1629 | 1630 | if ( empty( $data['taxonomy'] ) ) { 1631 | return null; 1632 | } 1633 | 1634 | // Compatibility with WXR 1.0 1635 | if ( $data['taxonomy'] === 'tag' ) { 1636 | $data['taxonomy'] = 'post_tag'; 1637 | } 1638 | 1639 | return compact( 'data', 'meta' ); 1640 | } 1641 | 1642 | protected function process_term( $data, $meta ) { 1643 | /** 1644 | * Pre-process term data. 1645 | * 1646 | * @param array $data Term data. (Return empty to skip.) 1647 | * @param array $meta Meta data. 1648 | */ 1649 | $data = apply_filters( 'wxr_importer.pre_process.term', $data, $meta ); 1650 | if ( empty( $data ) ) { 1651 | return false; 1652 | } 1653 | 1654 | $original_id = isset( $data['id'] ) ? (int) $data['id'] : 0; 1655 | $parent_id = isset( $data['parent'] ) ? (int) $data['parent'] : 0; 1656 | 1657 | $mapping_key = sha1( $data['taxonomy'] . ':' . $data['slug'] ); 1658 | $existing = $this->term_exists( $data ); 1659 | if ( $existing ) { 1660 | 1661 | /** 1662 | * Term processing already imported. 1663 | * 1664 | * @param array $data Raw data imported for the term. 1665 | */ 1666 | do_action( 'wxr_importer.process_already_imported.term', $data ); 1667 | 1668 | $this->mapping['term'][ $mapping_key ] = $existing; 1669 | $this->mapping['term_id'][ $original_id ] = $existing; 1670 | return false; 1671 | } 1672 | 1673 | // WP really likes to repeat itself in export files 1674 | if ( isset( $this->mapping['term'][ $mapping_key ] ) ) { 1675 | return false; 1676 | } 1677 | 1678 | $termdata = array(); 1679 | $allowed = array( 1680 | 'slug' => true, 1681 | 'description' => true, 1682 | ); 1683 | 1684 | // Map the parent comment, or mark it as one we need to fix 1685 | // TODO: add parent mapping and remapping 1686 | /*$requires_remapping = false; 1687 | if ( $parent_id ) { 1688 | if ( isset( $this->mapping['term'][ $parent_id ] ) ) { 1689 | $data['parent'] = $this->mapping['term'][ $parent_id ]; 1690 | } else { 1691 | // Prepare for remapping later 1692 | $meta[] = array( 'key' => '_wxr_import_parent', 'value' => $parent_id ); 1693 | $requires_remapping = true; 1694 | 1695 | // Wipe the parent for now 1696 | $data['parent'] = 0; 1697 | } 1698 | }*/ 1699 | 1700 | foreach ( $data as $key => $value ) { 1701 | if ( ! isset( $allowed[ $key ] ) ) { 1702 | continue; 1703 | } 1704 | 1705 | $termdata[ $key ] = $data[ $key ]; 1706 | } 1707 | 1708 | $result = wp_insert_term( $data['name'], $data['taxonomy'], $termdata ); 1709 | if ( is_wp_error( $result ) ) { 1710 | $this->logger->warning( sprintf( 1711 | __( 'Failed to import %s %s', 'wordpress-importer' ), 1712 | $data['taxonomy'], 1713 | $data['name'] 1714 | ) ); 1715 | $this->logger->debug( $result->get_error_message() ); 1716 | do_action( 'wp_import_insert_term_failed', $result, $data ); 1717 | 1718 | /** 1719 | * Term processing failed. 1720 | * 1721 | * @param WP_Error $result Error object. 1722 | * @param array $data Raw data imported for the term. 1723 | * @param array $meta Meta data supplied for the term. 1724 | */ 1725 | do_action( 'wxr_importer.process_failed.term', $result, $data, $meta ); 1726 | return false; 1727 | } 1728 | 1729 | $term_id = $result['term_id']; 1730 | 1731 | $this->mapping['term'][ $mapping_key ] = $term_id; 1732 | $this->mapping['term_id'][ $original_id ] = $term_id; 1733 | 1734 | $this->logger->info( sprintf( 1735 | __( 'Imported "%s" (%s)', 'wordpress-importer' ), 1736 | $data['name'], 1737 | $data['taxonomy'] 1738 | ) ); 1739 | $this->logger->debug( sprintf( 1740 | __( 'Term %d remapped to %d', 'wordpress-importer' ), 1741 | $original_id, 1742 | $term_id 1743 | ) ); 1744 | 1745 | do_action( 'wp_import_insert_term', $term_id, $data ); 1746 | 1747 | /** 1748 | * Term processing completed. 1749 | * 1750 | * @param int $term_id New term ID. 1751 | * @param array $data Raw data imported for the term. 1752 | */ 1753 | do_action( 'wxr_importer.processed.term', $term_id, $data ); 1754 | } 1755 | 1756 | /** 1757 | * Attempt to download a remote file attachment 1758 | * 1759 | * @param string $url URL of item to fetch 1760 | * @param array $post Attachment details 1761 | * @return array|WP_Error Local file location details on success, WP_Error otherwise 1762 | */ 1763 | protected function fetch_remote_file( $url, $post ) { 1764 | // extract the file name and extension from the url 1765 | $file_name = basename( $url ); 1766 | 1767 | // get placeholder file in the upload dir with a unique, sanitized filename 1768 | $upload = wp_upload_bits( $file_name, 0, '', $post['upload_date'] ); 1769 | if ( $upload['error'] ) { 1770 | return new WP_Error( 'upload_dir_error', $upload['error'] ); 1771 | } 1772 | 1773 | // fetch the remote url and write it to the placeholder file 1774 | $response = wp_remote_get( $url, array( 1775 | 'stream' => true, 1776 | 'filename' => $upload['file'], 1777 | ) ); 1778 | 1779 | // request failed 1780 | if ( is_wp_error( $response ) ) { 1781 | unlink( $upload['file'] ); 1782 | return $response; 1783 | } 1784 | 1785 | $code = (int) wp_remote_retrieve_response_code( $response ); 1786 | 1787 | // make sure the fetch was successful 1788 | if ( $code !== 200 ) { 1789 | unlink( $upload['file'] ); 1790 | return new WP_Error( 1791 | 'import_file_error', 1792 | sprintf( 1793 | __( 'Remote server returned %1$d %2$s for %3$s', 'wordpress-importer' ), 1794 | $code, 1795 | get_status_header_desc( $code ), 1796 | $url 1797 | ) 1798 | ); 1799 | } 1800 | 1801 | $filesize = filesize( $upload['file'] ); 1802 | $headers = wp_remote_retrieve_headers( $response ); 1803 | 1804 | if ( isset( $headers['content-length'] ) && $filesize !== (int) $headers['content-length'] ) { 1805 | unlink( $upload['file'] ); 1806 | return new WP_Error( 'import_file_error', __( 'Remote file is incorrect size', 'wordpress-importer' ) ); 1807 | } 1808 | 1809 | if ( 0 === $filesize ) { 1810 | unlink( $upload['file'] ); 1811 | return new WP_Error( 'import_file_error', __( 'Zero size file downloaded', 'wordpress-importer' ) ); 1812 | } 1813 | 1814 | $max_size = (int) $this->max_attachment_size(); 1815 | if ( ! empty( $max_size ) && $filesize > $max_size ) { 1816 | unlink( $upload['file'] ); 1817 | $message = sprintf( __( 'Remote file is too large, limit is %s', 'wordpress-importer' ), size_format( $max_size ) ); 1818 | return new WP_Error( 'import_file_error', $message ); 1819 | } 1820 | 1821 | return $upload; 1822 | } 1823 | 1824 | protected function post_process() { 1825 | // Time to tackle any left-over bits 1826 | if ( ! empty( $this->requires_remapping['post'] ) ) { 1827 | $this->post_process_posts( $this->requires_remapping['post'] ); 1828 | } 1829 | if ( ! empty( $this->requires_remapping['comment'] ) ) { 1830 | $this->post_process_comments( $this->requires_remapping['comment'] ); 1831 | } 1832 | } 1833 | 1834 | protected function post_process_posts( $todo ) { 1835 | foreach ( $todo as $post_id => $_ ) { 1836 | $this->logger->debug( sprintf( 1837 | // Note: title intentionally not used to skip extra processing 1838 | // for when debug logging is off 1839 | __( 'Running post-processing for post %d', 'wordpress-importer' ), 1840 | $post_id 1841 | ) ); 1842 | 1843 | $data = array(); 1844 | 1845 | $parent_id = get_post_meta( $post_id, '_wxr_import_parent', true ); 1846 | if ( ! empty( $parent_id ) ) { 1847 | // Have we imported the parent now? 1848 | if ( isset( $this->mapping['post'][ $parent_id ] ) ) { 1849 | $data['post_parent'] = $this->mapping['post'][ $parent_id ]; 1850 | } else { 1851 | $this->logger->warning( sprintf( 1852 | __( 'Could not find the post parent for "%s" (post #%d)', 'wordpress-importer' ), 1853 | get_the_title( $post_id ), 1854 | $post_id 1855 | ) ); 1856 | $this->logger->debug( sprintf( 1857 | __( 'Post %d was imported with parent %d, but could not be found', 'wordpress-importer' ), 1858 | $post_id, 1859 | $parent_id 1860 | ) ); 1861 | } 1862 | } 1863 | 1864 | $author_slug = get_post_meta( $post_id, '_wxr_import_user_slug', true ); 1865 | if ( ! empty( $author_slug ) ) { 1866 | // Have we imported the user now? 1867 | if ( isset( $this->mapping['user_slug'][ $author_slug ] ) ) { 1868 | $data['post_author'] = $this->mapping['user_slug'][ $author_slug ]; 1869 | } else { 1870 | $this->logger->warning( sprintf( 1871 | __( 'Could not find the author for "%s" (post #%d)', 'wordpress-importer' ), 1872 | get_the_title( $post_id ), 1873 | $post_id 1874 | ) ); 1875 | $this->logger->debug( sprintf( 1876 | __( 'Post %d was imported with author "%s", but could not be found', 'wordpress-importer' ), 1877 | $post_id, 1878 | $author_slug 1879 | ) ); 1880 | } 1881 | } 1882 | 1883 | $has_attachments = get_post_meta( $post_id, '_wxr_import_has_attachment_refs', true ); 1884 | if ( ! empty( $has_attachments ) ) { 1885 | $post = get_post( $post_id ); 1886 | $content = $post->post_content; 1887 | 1888 | // Replace all the URLs we've got 1889 | $new_content = str_replace( array_keys( $this->url_remap ), $this->url_remap, $content ); 1890 | if ( $new_content !== $content ) { 1891 | $data['post_content'] = $new_content; 1892 | } 1893 | } 1894 | 1895 | if ( get_post_type( $post_id ) === 'nav_menu_item' ) { 1896 | $this->post_process_menu_item( $post_id ); 1897 | } 1898 | 1899 | // Do we have updates to make? 1900 | if ( empty( $data ) ) { 1901 | $this->logger->debug( sprintf( 1902 | __( 'Post %d was marked for post-processing, but none was required.', 'wordpress-importer' ), 1903 | $post_id 1904 | ) ); 1905 | continue; 1906 | } 1907 | 1908 | // Run the update 1909 | $data['ID'] = $post_id; 1910 | $result = wp_update_post( $data, true ); 1911 | if ( is_wp_error( $result ) ) { 1912 | $this->logger->warning( sprintf( 1913 | __( 'Could not update "%s" (post #%d) with mapped data', 'wordpress-importer' ), 1914 | get_the_title( $post_id ), 1915 | $post_id 1916 | ) ); 1917 | $this->logger->debug( $result->get_error_message() ); 1918 | continue; 1919 | } 1920 | 1921 | // Clear out our temporary meta keys 1922 | delete_post_meta( $post_id, '_wxr_import_parent' ); 1923 | delete_post_meta( $post_id, '_wxr_import_user_slug' ); 1924 | delete_post_meta( $post_id, '_wxr_import_has_attachment_refs' ); 1925 | } 1926 | } 1927 | 1928 | protected function post_process_menu_item( $post_id ) { 1929 | $menu_object_id = get_post_meta( $post_id, '_wxr_import_menu_item', true ); 1930 | if ( empty( $menu_object_id ) ) { 1931 | // No processing needed! 1932 | return; 1933 | } 1934 | 1935 | $menu_item_type = get_post_meta( $post_id, '_menu_item_type', true ); 1936 | switch ( $menu_item_type ) { 1937 | case 'taxonomy': 1938 | if ( isset( $this->mapping['term_id'][ $menu_object_id ] ) ) { 1939 | $menu_object = $this->mapping['term_id'][ $menu_object_id ]; 1940 | } 1941 | break; 1942 | 1943 | case 'post_type': 1944 | if ( isset( $this->mapping['post'][ $menu_object_id ] ) ) { 1945 | $menu_object = $this->mapping['post'][ $menu_object_id ]; 1946 | } 1947 | break; 1948 | 1949 | default: 1950 | // Cannot handle this. 1951 | return; 1952 | } 1953 | 1954 | if ( ! empty( $menu_object ) ) { 1955 | update_post_meta( $post_id, '_menu_item_object_id', wp_slash( $menu_object ) ); 1956 | } else { 1957 | $this->logger->warning( sprintf( 1958 | __( 'Could not find the menu object for "%s" (post #%d)', 'wordpress-importer' ), 1959 | get_the_title( $post_id ), 1960 | $post_id 1961 | ) ); 1962 | $this->logger->debug( sprintf( 1963 | __( 'Post %d was imported with object "%d" of type "%s", but could not be found', 'wordpress-importer' ), 1964 | $post_id, 1965 | $menu_object_id, 1966 | $menu_item_type 1967 | ) ); 1968 | } 1969 | 1970 | delete_post_meta( $post_id, '_wxr_import_menu_item' ); 1971 | } 1972 | 1973 | 1974 | protected function post_process_comments( $todo ) { 1975 | foreach ( $todo as $comment_id => $_ ) { 1976 | $data = array(); 1977 | 1978 | $parent_id = get_comment_meta( $comment_id, '_wxr_import_parent', true ); 1979 | if ( ! empty( $parent_id ) ) { 1980 | // Have we imported the parent now? 1981 | if ( isset( $this->mapping['comment'][ $parent_id ] ) ) { 1982 | $data['comment_parent'] = $this->mapping['comment'][ $parent_id ]; 1983 | } else { 1984 | $this->logger->warning( sprintf( 1985 | __( 'Could not find the comment parent for comment #%d', 'wordpress-importer' ), 1986 | $comment_id 1987 | ) ); 1988 | $this->logger->debug( sprintf( 1989 | __( 'Comment %d was imported with parent %d, but could not be found', 'wordpress-importer' ), 1990 | $comment_id, 1991 | $parent_id 1992 | ) ); 1993 | } 1994 | } 1995 | 1996 | $author_id = get_comment_meta( $comment_id, '_wxr_import_user', true ); 1997 | if ( ! empty( $author_id ) ) { 1998 | // Have we imported the user now? 1999 | if ( isset( $this->mapping['user'][ $author_id ] ) ) { 2000 | $data['user_id'] = $this->mapping['user'][ $author_id ]; 2001 | } else { 2002 | $this->logger->warning( sprintf( 2003 | __( 'Could not find the author for comment #%d', 'wordpress-importer' ), 2004 | $comment_id 2005 | ) ); 2006 | $this->logger->debug( sprintf( 2007 | __( 'Comment %d was imported with author %d, but could not be found', 'wordpress-importer' ), 2008 | $comment_id, 2009 | $author_id 2010 | ) ); 2011 | } 2012 | } 2013 | 2014 | // Do we have updates to make? 2015 | if ( empty( $data ) ) { 2016 | continue; 2017 | } 2018 | 2019 | // Run the update 2020 | $data['comment_ID'] = $comment_ID; 2021 | $result = wp_update_comment( wp_slash( $data ) ); 2022 | if ( empty( $result ) ) { 2023 | $this->logger->warning( sprintf( 2024 | __( 'Could not update comment #%d with mapped data', 'wordpress-importer' ), 2025 | $comment_id 2026 | ) ); 2027 | continue; 2028 | } 2029 | 2030 | // Clear out our temporary meta keys 2031 | delete_comment_meta( $comment_id, '_wxr_import_parent' ); 2032 | delete_comment_meta( $comment_id, '_wxr_import_user' ); 2033 | } 2034 | } 2035 | 2036 | /** 2037 | * Use stored mapping information to update old attachment URLs 2038 | */ 2039 | protected function replace_attachment_urls_in_content() { 2040 | global $wpdb; 2041 | // make sure we do the longest urls first, in case one is a substring of another 2042 | uksort( $this->url_remap, array( $this, 'cmpr_strlen' ) ); 2043 | 2044 | foreach ( $this->url_remap as $from_url => $to_url ) { 2045 | // remap urls in post_content 2046 | $query = $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s)", $from_url, $to_url ); 2047 | $wpdb->query( $query ); 2048 | 2049 | // remap enclosure urls 2050 | $query = $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_key='enclosure'", $from_url, $to_url ); 2051 | $result = $wpdb->query( $query ); 2052 | } 2053 | } 2054 | 2055 | /** 2056 | * Update _thumbnail_id meta to new, imported attachment IDs 2057 | */ 2058 | function remap_featured_images() { 2059 | // cycle through posts that have a featured image 2060 | foreach ( $this->featured_images as $post_id => $value ) { 2061 | if ( isset( $this->processed_posts[ $value ] ) ) { 2062 | $new_id = $this->processed_posts[ $value ]; 2063 | 2064 | // only update if there's a difference 2065 | if ( $new_id !== $value ) { 2066 | update_post_meta( $post_id, '_thumbnail_id', $new_id ); 2067 | } 2068 | } 2069 | } 2070 | } 2071 | 2072 | /** 2073 | * Decide if the given meta key maps to information we will want to import 2074 | * 2075 | * @param string $key The meta key to check 2076 | * @return string|bool The key if we do want to import, false if not 2077 | */ 2078 | public function is_valid_meta_key( $key ) { 2079 | // skip attachment metadata since we'll regenerate it from scratch 2080 | // skip _edit_lock as not relevant for import 2081 | if ( in_array( $key, array( '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock' ) ) ) { 2082 | return false; 2083 | } 2084 | 2085 | return $key; 2086 | } 2087 | 2088 | /** 2089 | * Decide what the maximum file size for downloaded attachments is. 2090 | * Default is 0 (unlimited), can be filtered via import_attachment_size_limit 2091 | * 2092 | * @return int Maximum attachment file size to import 2093 | */ 2094 | protected function max_attachment_size() { 2095 | return apply_filters( 'import_attachment_size_limit', 0 ); 2096 | } 2097 | 2098 | /** 2099 | * Added to http_request_timeout filter to force timeout at 60 seconds during import 2100 | * 2101 | * @access protected 2102 | * @return int 60 2103 | */ 2104 | function bump_request_timeout($val) { 2105 | return 60; 2106 | } 2107 | 2108 | // return the difference in length between two strings 2109 | function cmpr_strlen( $a, $b ) { 2110 | return strlen( $b ) - strlen( $a ); 2111 | } 2112 | 2113 | /** 2114 | * Prefill existing post data. 2115 | * 2116 | * This preloads all GUIDs into memory, allowing us to avoid hitting the 2117 | * database when we need to check for existence. With larger imports, this 2118 | * becomes prohibitively slow to perform SELECT queries on each. 2119 | * 2120 | * By preloading all this data into memory, it's a constant-time lookup in 2121 | * PHP instead. However, this does use a lot more memory, so for sites doing 2122 | * small imports onto a large site, it may be a better tradeoff to use 2123 | * on-the-fly checking instead. 2124 | */ 2125 | protected function prefill_existing_posts() { 2126 | global $wpdb; 2127 | $posts = $wpdb->get_results( "SELECT ID, guid FROM {$wpdb->posts}" ); 2128 | 2129 | foreach ( $posts as $item ) { 2130 | $this->exists['post'][ $item->guid ] = $item->ID; 2131 | } 2132 | } 2133 | 2134 | /** 2135 | * Does the post exist? 2136 | * 2137 | * @param array $data Post data to check against. 2138 | * @return int|bool Existing post ID if it exists, false otherwise. 2139 | */ 2140 | protected function post_exists( $data ) { 2141 | // Constant-time lookup if we prefilled 2142 | $exists_key = $data['guid']; 2143 | 2144 | if ( $this->options['prefill_existing_posts'] ) { 2145 | return isset( $this->exists['post'][ $exists_key ] ) ? $this->exists['post'][ $exists_key ] : false; 2146 | } 2147 | 2148 | // No prefilling, but might have already handled it 2149 | if ( isset( $this->exists['post'][ $exists_key ] ) ) { 2150 | return $this->exists['post'][ $exists_key ]; 2151 | } 2152 | 2153 | // Still nothing, try post_exists, and cache it 2154 | $exists = post_exists( $data['post_title'], $data['post_content'], $data['post_date'] ); 2155 | $this->exists['post'][ $exists_key ] = $exists; 2156 | 2157 | return $exists; 2158 | } 2159 | 2160 | /** 2161 | * Mark the post as existing. 2162 | * 2163 | * @param array $data Post data to mark as existing. 2164 | * @param int $post_id Post ID. 2165 | */ 2166 | protected function mark_post_exists( $data, $post_id ) { 2167 | $exists_key = $data['guid']; 2168 | $this->exists['post'][ $exists_key ] = $post_id; 2169 | } 2170 | 2171 | /** 2172 | * Prefill existing comment data. 2173 | * 2174 | * @see self::prefill_existing_posts() for justification of why this exists. 2175 | */ 2176 | protected function prefill_existing_comments() { 2177 | global $wpdb; 2178 | $posts = $wpdb->get_results( "SELECT comment_ID, comment_author, comment_date FROM {$wpdb->comments}" ); 2179 | 2180 | foreach ( $posts as $item ) { 2181 | $exists_key = sha1( $item->comment_author . ':' . $item->comment_date ); 2182 | $this->exists['comment'][ $exists_key ] = $item->comment_ID; 2183 | } 2184 | } 2185 | 2186 | /** 2187 | * Does the comment exist? 2188 | * 2189 | * @param array $data Comment data to check against. 2190 | * @return int|bool Existing comment ID if it exists, false otherwise. 2191 | */ 2192 | protected function comment_exists( $data ) { 2193 | $exists_key = sha1( $data['comment_author'] . ':' . $data['comment_date'] ); 2194 | 2195 | // Constant-time lookup if we prefilled 2196 | if ( $this->options['prefill_existing_comments'] ) { 2197 | return isset( $this->exists['comment'][ $exists_key ] ) ? $this->exists['comment'][ $exists_key ] : false; 2198 | } 2199 | 2200 | // No prefilling, but might have already handled it 2201 | if ( isset( $this->exists['comment'][ $exists_key ] ) ) { 2202 | return $this->exists['comment'][ $exists_key ]; 2203 | } 2204 | 2205 | // Still nothing, try comment_exists, and cache it 2206 | $exists = comment_exists( $data['comment_author'], $data['comment_date'] ); 2207 | $this->exists['comment'][ $exists_key ] = $exists; 2208 | 2209 | return $exists; 2210 | } 2211 | 2212 | /** 2213 | * Mark the comment as existing. 2214 | * 2215 | * @param array $data Comment data to mark as existing. 2216 | * @param int $comment_id Comment ID. 2217 | */ 2218 | protected function mark_comment_exists( $data, $comment_id ) { 2219 | $exists_key = sha1( $data['comment_author'] . ':' . $data['comment_date'] ); 2220 | $this->exists['comment'][ $exists_key ] = $comment_id; 2221 | } 2222 | 2223 | /** 2224 | * Prefill existing term data. 2225 | * 2226 | * @see self::prefill_existing_posts() for justification of why this exists. 2227 | */ 2228 | protected function prefill_existing_terms() { 2229 | global $wpdb; 2230 | $query = "SELECT t.term_id, tt.taxonomy, t.slug FROM {$wpdb->terms} AS t"; 2231 | $query .= " JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id"; 2232 | $terms = $wpdb->get_results( $query ); 2233 | 2234 | foreach ( $terms as $item ) { 2235 | $exists_key = sha1( $item->taxonomy . ':' . $item->slug ); 2236 | $this->exists['term'][ $exists_key ] = $item->term_id; 2237 | } 2238 | } 2239 | 2240 | /** 2241 | * Does the term exist? 2242 | * 2243 | * @param array $data Term data to check against. 2244 | * @return int|bool Existing term ID if it exists, false otherwise. 2245 | */ 2246 | protected function term_exists( $data ) { 2247 | $exists_key = sha1( $data['taxonomy'] . ':' . $data['slug'] ); 2248 | 2249 | // Constant-time lookup if we prefilled 2250 | if ( $this->options['prefill_existing_terms'] ) { 2251 | return isset( $this->exists['term'][ $exists_key ] ) ? $this->exists['term'][ $exists_key ] : false; 2252 | } 2253 | 2254 | // No prefilling, but might have already handled it 2255 | if ( isset( $this->exists['term'][ $exists_key ] ) ) { 2256 | return $this->exists['term'][ $exists_key ]; 2257 | } 2258 | 2259 | // Still nothing, try comment_exists, and cache it 2260 | $exists = term_exists( $data['slug'], $data['taxonomy'] ); 2261 | if ( is_array( $exists ) ) { 2262 | $exists = $exists['term_id']; 2263 | } 2264 | 2265 | $this->exists['term'][ $exists_key ] = $exists; 2266 | 2267 | return $exists; 2268 | } 2269 | 2270 | /** 2271 | * Mark the term as existing. 2272 | * 2273 | * @param array $data Term data to mark as existing. 2274 | * @param int $term_id Term ID. 2275 | */ 2276 | protected function mark_term_exists( $data, $term_id ) { 2277 | $exists_key = sha1( $data['taxonomy'] . ':' . $data['slug'] ); 2278 | $this->exists['term'][ $exists_key ] = $term_id; 2279 | } 2280 | } 2281 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "humanmade/wordpress-importer", 3 | "description" : "WordPress importer plugin", 4 | "license" : "GPLv2", 5 | "authors" : [ 6 | { 7 | "name" : "Contributors", 8 | "homepage" : "https://github.com/humanmade/WordPress-Importer/graphs/contributors" 9 | } 10 | ], 11 | "type" : "wordpress-plugin", 12 | "minimum-stability" : "dev", 13 | "prefer-stable" : true, 14 | "support" : { 15 | "wiki" : "https://github.com/humanmade/WordPress-Importer/wiki", 16 | "source" : "https://github.com/humanmade/WordPress-Importer/releases", 17 | "issues" : "https://github.com/humanmade/WordPress-Importer/issues" 18 | }, 19 | "keywords" : [ 20 | "wordpress", 21 | "plugin", 22 | "importer" 23 | ], 24 | "require" : { 25 | "php" : ">=5.2", 26 | "composer/installers" : "~1.0" 27 | }, 28 | "require-dev": { 29 | "wp-coding-standards/wpcs": "dev-master", 30 | "squizlabs/php_codesniffer": "~2.9", 31 | "fig-r/psr2r-sniffer": "~0.4" 32 | }, 33 | "autoload" : { }, 34 | "autoload-dev" : { }, 35 | "extra" : { 36 | "branch-alias" : { 37 | "dev-dev" : "0.1.x-dev" 38 | }, 39 | "installer-paths" : { 40 | "vendor/{$name}" : [ 41 | "type:wordpress-plugin" 42 | ] 43 | } 44 | }, 45 | "scripts": { 46 | "post-install-cmd": "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs", 47 | "post-update-cmd" : "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "grunt": "^0.4.5", 4 | "grunt-phpcs": "^0.4.0", 5 | "phplint": "^1.6.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /phpcs.ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sniffs for the coding standards of the WP-API plugin 4 | 5 | 6 | 7 | vendor/* 8 | node_modules/* 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | tests 13 | 14 | 15 | 16 | 17 | ./tests 18 | 19 | 20 | ./ 21 | ./plugin.php 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /plugin.php: -------------------------------------------------------------------------------- 1 | posts, pages, comments, custom fields, categories, and tags from a WordPress export (WXR) file.', 'wordpress-importer' ), 42 | array( $GLOBALS['wxr_importer'], 'dispatch' ) 43 | ); 44 | 45 | add_action( 'load-importer-wordpress', array( $GLOBALS['wxr_importer'], 'on_load' ) ); 46 | add_action( 'wp_ajax_wxr-import', array( $GLOBALS['wxr_importer'], 'stream_import' ) ); 47 | } 48 | add_action( 'admin_init', 'wpimportv2_init' ); 49 | -------------------------------------------------------------------------------- /templates/footer.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/import.php: -------------------------------------------------------------------------------- 1 | 'wxr-import', 8 | 'id' => $this->id, 9 | ); 10 | $url = add_query_arg( urlencode_deep( $args ), admin_url( 'admin-ajax.php' ) ); 11 | 12 | $script_data = array( 13 | 'count' => array( 14 | 'posts' => $data->post_count, 15 | 'media' => $data->media_count, 16 | 'users' => count( $data->users ), 17 | 'comments' => $data->comment_count, 18 | 'terms' => $data->term_count, 19 | ), 20 | 'url' => $url, 21 | 'strings' => array( 22 | 'complete' => __( 'Import complete!', 'wordpress-importer' ), 23 | ), 24 | ); 25 | 26 | $url = plugins_url( 'assets/import.js', dirname( __FILE__ ) ); 27 | wp_enqueue_script( 'wxr-importer-import', $url, array( 'jquery' ), '20160909', true ); 28 | wp_localize_script( 'wxr-importer-import', 'wxrImportData', $script_data ); 29 | 30 | wp_enqueue_style( 'wxr-importer-import', plugins_url( 'assets/import.css', dirname( __FILE__ ) ), array(), '20160909' ); 31 | 32 | $this->render_header(); 33 | ?> 34 |
35 |
36 |

37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 58 | 61 | 65 | 66 | 67 | 76 | 79 | 83 | 84 | 85 | 86 | 95 | 98 | 102 | 103 | 104 | 105 | 114 | 117 | 121 | 122 | 123 | 124 | 133 | 136 | 140 | 141 | 142 |
50 | 51 | post_count, 'wordpress-importer' ), 54 | $data->post_count 55 | )); 56 | ?> 57 | 59 | 0/0 60 | 62 | 63 | 0% 64 |
68 | 69 | media_count, 'wordpress-importer' ), 72 | $data->media_count 73 | )); 74 | ?> 75 | 77 | 0/0 78 | 80 | 81 | 0% 82 |
87 | 88 | users ), 'wordpress-importer' ), 91 | count( $data->users ) 92 | )); 93 | ?> 94 | 96 | 0/0 97 | 99 | 100 | 0% 101 |
106 | 107 | comment_count, 'wordpress-importer' ), 110 | $data->comment_count 111 | )); 112 | ?> 113 | 115 | 0/0 116 | 118 | 119 | 0% 120 |
125 | 126 | term_count, 'wordpress-importer' ), 129 | $data->term_count 130 | )); 131 | ?> 132 | 134 | 0/0 135 | 137 | 138 | 0% 139 |
143 | 144 |
145 |
146 | 147 |
148 |
149 | 0/0 150 | 0% 151 |
152 |
153 |
154 |
155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 |
166 | 167 | render_footer(); 170 | -------------------------------------------------------------------------------- /templates/intro.php: -------------------------------------------------------------------------------- 1 | render_header(); 9 | 10 | ?> 11 |
12 |
13 |

14 |

18 |

22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 | render_upload_form() ?> 30 | 31 |

35 | 39 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | render_footer(); 49 | -------------------------------------------------------------------------------- /templates/select-options.php: -------------------------------------------------------------------------------- 1 | render_header(); 7 | 8 | $generator = $data->generator; 9 | if ( preg_match( '#^http://wordpress\.org/\?v=(\d+\.\d+\.\d+)$#', $generator, $matches ) ) { 10 | $generator = sprintf( __( 'WordPress %s', 'wordpress-importer' ), $matches[1] ); 11 | } 12 | 13 | ?> 14 |
15 |
16 |

17 |

18 | 19 |
20 |
21 |

22 |
    23 |
  • 24 | 25 | post_count, 'wordpress-importer' ), 28 | $data->post_count 29 | )); 30 | ?> 31 |
  • 32 |
  • 33 | 34 | media_count, 'wordpress-importer' ), 37 | $data->media_count 38 | )); 39 | ?> 40 |
  • 41 |
  • 42 | 43 | users ), 'wordpress-importer' ), 46 | count( $data->users ) 47 | )); 48 | ?> 49 |
  • 50 |
  • 51 | 52 | comment_count, 'wordpress-importer' ), 55 | $data->comment_count 56 | )); 57 | ?> 58 |
  • 59 |
  • 60 | 61 | term_count, 'wordpress-importer' ), 64 | $data->term_count 65 | )); 66 | ?> 67 |
  • 68 |
69 |
70 |
71 |

72 |
    73 |
  • 74 | %2$s', 'wordpress-importer' ), 77 | esc_url( $data->home ), 78 | esc_html( $data->title ) 79 | ), 'data' ); 80 | ?> 81 |
  • 82 |
  • 83 | 89 |
  • 90 |
  • 91 | version 95 | )); 96 | ?> 97 |
  • 98 |
99 |
100 |
101 |

102 |
    103 |
  • 104 |
  • 105 |
106 |
107 |
108 |
109 |
110 | 111 |
112 | 113 | users ) ) : ?> 114 | 115 |

116 |

admins entries.', 'wordpress-importer' ), 119 | 'data' 120 | ); 121 | ?>

122 | 123 | allow_create_users() ) : ?> 124 | 125 |

126 | 127 | 128 | 129 |
    130 | 131 | users as $index => $users ) : ?> 132 | 133 |
  1. author_select( $index, $users['data'] ); ?>
  2. 134 | 135 | 136 | 137 |
138 | 139 | 140 | 141 | allow_fetch_attachments() ) : ?> 142 | 143 |

144 |

145 | 146 | 148 |

149 | 150 | 151 | 152 | 153 | id ) ) ?> 154 | 155 | 156 | 157 |
158 | 159 | render_footer(); 162 | -------------------------------------------------------------------------------- /templates/upload.php: -------------------------------------------------------------------------------- 1 |
2 | 10 | 11 |
12 |
13 |

14 |

15 |

16 |
17 |
18 |
19 | 20 | 28 |
29 | 30 |
31 | 39 |

40 | 41 | 42 | 43 | 44 |

45 | 46 |
47 | 55 |
56 | 57 |

61 | 62 | 83 | 84 | 91 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | assertTrue( class_exists( 'WXR_Importer' ) ); 9 | } 10 | } 11 | --------------------------------------------------------------------------------