<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[2137] 2013/frederickding/importer/trunk: Add author mapping process step.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://gsoc.trac.wordpress.org/changeset/2137">2137</a></dd>
<dt>Author</dt> <dd>frederick.ding</dd>
<dt>Date</dt> <dd>2013-07-23 00:16:52 +0000 (Tue, 23 Jul 2013)</dd>
</dl>

<h3>Log Message</h3>
<pre>Add author mapping process step.

- Modified to use internal class variable $params (with getters/setters)
  to eliminate dependence on $_POST, which wouldn't be usable from a cron job
- See <a href="http://gsoc.trac.wordpress.org/ticket/327">#327</a></pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#2013frederickdingimportertrunkclasswordpressimporterphp">2013/frederickding/importer/trunk/class-wordpress-importer.php</a></li>
<li><a href="#2013frederickdingimportertrunkteststestclasswordpressimporterphp">2013/frederickding/importer/trunk/tests/test-class-wordpress-importer.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="2013frederickdingimportertrunkclasswordpressimporterphp"></a>
<div class="modfile"><h4>Modified: 2013/frederickding/importer/trunk/class-wordpress-importer.php (2136 => 2137)</h4>
<pre class="diff"><span>
<span class="info">--- 2013/frederickding/importer/trunk/class-wordpress-importer.php   2013-07-18 21:52:42 UTC (rev 2136)
+++ 2013/frederickding/importer/trunk/class-wordpress-importer.php      2013-07-23 00:16:52 UTC (rev 2137)
</span><span class="lines">@@ -33,12 +33,25 @@
</span><span class="cx">  
</span><span class="cx">  protected $authors = array();
</span><span class="cx">  
</span><ins>+       protected $processed_authors = array();
+       
+       protected $author_mappings = array();
+       
</ins><span class="cx">   protected $_data = array();
</span><ins>+       
+       /**
+        * Stores user-submitted choices on how to proceed with the process.
+        * 
+        * @var array
+        */
+       public $params = array();
</ins><span class="cx"> 
</span><span class="cx">  /**
</span><del>-        * Parses the WXR file and prepares us for the task of processing parsed
-        * data
</del><ins>+         * Parses the WXR file and prepares us to process parsed data.
</ins><span class="cx">    * 
</span><ins>+        * This method is synchronous and is NOT for use in a cron-scheduled job; it
+        * should be invoked by a controller UI before do_import() is scheduled.
+        * 
</ins><span class="cx">    * @param string $file
</span><span class="cx">   *              Path to the WXR file for parsing
</span><span class="cx">   * @return array WP_Error data if successful, or an instance of WP_Error if
</span><span class="lines">@@ -70,6 +83,9 @@
</span><span class="cx">          $this->version = $version;
</span><span class="cx">          $this->wxr_file = $file;
</span><span class="cx">          $this->_data = $data;
</span><ins>+               if ( $user = get_current_user_id() > 0 ) {
+                       $this->set_param( 'admin_user_id', $user );
+               }
</ins><span class="cx">           
</span><span class="cx">          /**
</span><span class="cx">           *
</span><span class="lines">@@ -99,6 +115,9 @@
</span><span class="cx">                  $done = false;
</span><span class="cx">                  
</span><span class="cx">                  do {
</span><ins>+                               // we could probably be doing call_user_func() or using some 
+                               // Reflection, but this makes more sense for readability and 
+                               // process-specific modifications 
</ins><span class="cx">                           switch ( $this->progress[$file] ) {
</span><span class="cx">                                  case 'start':
</span><span class="cx">                                  case 'authors':
</span><span class="lines">@@ -243,13 +262,123 @@
</span><span class="cx">          $this->advance();
</span><span class="cx">          return true;
</span><span class="cx">  }
</span><del>-       
</del><ins>+
</ins><span class="cx">   /**
</span><ins>+        * Map old author logins to local user IDs based on decisions.
</ins><span class="cx">    *
</span><del>-        * @todo
</del><ins>+         * Can map to an existing user, create a new user, or falls back to the
+        * current user in case of an error with the other cases.
</ins><span class="cx">    */
</span><del>-       protected function process_author_mappings() {}
</del><ins>+        protected function process_author_mappings () {
+               $create_users = $this->is_allowed_user_creation();
+               
+               // adapted from WP_Import; however, we don't have the luxury of
+               // directly accessing $_POST, so we rely instead on DB-stored params
+               $imported_authors = $this->get_param( 'imported_authors', array() );
+               
+               if ( is_wp_error( $imported_authors ) ) {
+                       $this->error = $imported_authors;
+                       return false;
+               }
+               
+               foreach ( (array) $imported_authors as $i => $old_login ) {
+                       // Multisite adds strtolower to sanitize_user. Need to sanitize here
+                       // to stop breakage in process_posts.
+                       $sanitized_old_login = sanitize_user( $old_login, true );
+                       
+                       /*
+                        * $this->authors should have already been set by process_authors(),
+                        * although author_id will be missing if author information was
+                        * pulled out of post meta
+                        */
+                       $old_id = isset( $this->authors[$old_login]['author_id'] ) ? intval( 
+                                       $this->authors[$old_login]['author_id'] ) : false;
+                       
+                       $user_map = $this->get_param( 'user_map', array() );
+                       if ( ! empty( $user_map[$i] ) ) {
+                               // the mapping old->new was defined from the form
+                               $user = get_userdata( intval( $user_map[$i] ) );
+                               if ( isset( $user->ID ) ) {
+                                       if ( $old_id )
+                                               $this->processed_authors[$old_id] = $user->ID;
+                                       $this->author_mappings[$sanitized_old_login] = $user->ID;
+                               }
+                       } elseif ( $create_users ) {
+                               // make a user for an unmapped author only if allowed
+                               $user_new = $this->get_param( 'user_new', array() );
+                               if ( ! empty( $user_new[$i] ) ) {
+                                       $user_id = wp_create_user( $user_new[$i], 
+                                                       wp_generate_password() );
+                               } elseif ( $this->version != '1.0' ) {
+                                       $user_data = array(
+                                                       'user_login' => $old_login,
+                                                       'user_pass' => wp_generate_password(),
+                                                       'user_email' => isset( 
+                                                                       $this->authors[$old_login]['author_email'] ) ? $this->authors[$old_login]['author_email'] : '',
+                                                       'display_name' => $this->authors[$old_login]['author_display_name'],
+                                                       'first_name' => isset( 
+                                                                       $this->authors[$old_login]['author_first_name'] ) ? $this->authors[$old_login]['author_first_name'] : '',
+                                                       'last_name' => isset( 
+                                                                       $this->authors[$old_login]['author_last_name'] ) ? $this->authors[$old_login]['author_last_name'] : ''
+                                       );
+                                       $user_id = wp_insert_user( $user_data );
+                               }
+                               
+                               if ( ! is_wp_error( $user_id ) ) {
+                                       // successful user creation; add to mappings
+                                       if ( $old_id )
+                                               $this->processed_authors[$old_id] = $user_id;
+                                       $this->author_mappings[$sanitized_old_login] = $user_id;
+                               } else {
+                                       // set a warning in the importer class, and include the 
+                                       // WP_Error object emitted by wp_insert_user() as data.
+                                       $this->warnings[] = new WP_Error(
+                                                       'import_process_author_mappings_warning',
+                                                       __(
+                                                                       'Failed to create new user for %s. Their posts will be attributed to the current user.',
+                                                                       'wordpress-importer' ),
+                                                       esc_html( $this->authors[$old_login]['author_display_name'] ),
+                                                       $user_id );
+                                       // really wish WP_Error were like an exception and that we 
+                                       // could use try...catch...finally.
+                               }
+                       }
+                               
+                       /*
+                        * Failsafe: if the user_id was invalid, default to the current user.
+                        * 
+                        * Cron note: can't use logged in user; instead default to the user
+                        * ID stored in params, which should be the admin user who triggered
+                        * the import. It is possible to override this from an external
+                        * scope by calling $importer->set_param('admin_user_id', $a_value)
+                        * after do_parse().
+                        */
+                       if ( ! isset( $this->author_mappings[$sanitized_old_login] ) ) {
+                               if ( $old_id )
+                                       $this->processed_authors[$old_id] = (int) $this->get_param( 
+                                                       'admin_user_id', 0 );
+                               $this->author_mappings[$sanitized_old_login] = (int) $this->get_param( 
+                                               'admin_user_id', 0 );
+                       }
+               }
</ins><span class="cx"> 
</span><ins>+               /*
+                * At this point, theoretically all authors should be mapped. However,
+                * emit a warning if the number of mapped authors is fewer than the
+                * number of authors identified from the import data.
+                */
+               if ( count( $this->processed_authors ) < count( $this->authors ) ) {
+                       $this->warnings[] = new WP_Error(
+                                       'import_process_author_mappings_warning',
+                                       __(
+                                                       'Some authors may not have been attributed correctly.',
+                                                       'wordpress-importer' ) );
+               }
+
+               $this->advance();
+               return true;
+       }
+
</ins><span class="cx">   /**
</span><span class="cx">   *
</span><span class="cx">   * @todo
</span><span class="lines">@@ -297,7 +426,7 @@
</span><span class="cx">   * 
</span><span class="cx">   * @param string $key
</span><span class="cx">   *              The meta key to check
</span><del>-        * @return string bool key if we do want to import, false if not
</del><ins>+         * @return string|bool key if we do want to import, false if not
</ins><span class="cx">    */
</span><span class="cx">  public function is_valid_meta_key( $key ) {
</span><span class="cx">          // skip attachment metadata since we'll regenerate it from scratch
</span><span class="lines">@@ -351,4 +480,50 @@
</span><span class="cx">  public function get_warnings() {
</span><span class="cx">          return $this->warnings;
</span><span class="cx">  }
</span><ins>+
+       /**
+        * Stores parameters typically used for user input.
+        *
+        * Previously, some of these params would have been directly accessed with
+        * $_POST; now they are stored.
+        *
+        * @param string $name          
+        * @param mixed $value          
+        * @return boolean WP_Error if unsuccessful, an error object
+        */
+       public function set_param ( $name, $value ) {
+               if ( ! isset( $this->wxr_file ) ) {
+                       // cannot set user options before WXR data is parsed
+                       return new WP_Error( 'import_param_set_error', 
+                                       __( 'Cannot set param without parsed data', 
+                                                       'wordpress-importer' ) );
+               }
+               if ( ! is_array( $this->params[$this->wxr_file] ) )
+                       $this->params[$this->wxr_file] = array();
+               $this->params[$this->wxr_file][$name] = $value;
+               
+               return true;
+       }
+
+       /**
+        * Retrieves parameters typically used for user input.
+        *
+        * @param string $name          
+        * @param mixed $default                
+        * @return mixed WP_Error requested parameter, or if unsuccessful, an error
+        *         object
+        */
+       public function get_param ( $name, $default = null ) {
+               if ( ! isset( $this->wxr_file ) ) {
+                       // cannot get user options before WXR data is parsed
+                       return new WP_Error( 'import_param_get_error', 
+                                       __( 'Cannot get param without parsed data', 
+                                                       'wordpress-importer' ) );
+               }
+               if ( isset( $this->params[$this->wxr_file] ) &&
+                                isset( $this->params[$this->wxr_file][$name] ) )
+                       return $this->params[$this->wxr_file][$name];
+               
+               return $default;
+       }
</ins><span class="cx"> }
</span><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="2013frederickdingimportertrunkteststestclasswordpressimporterphp"></a>
<div class="modfile"><h4>Modified: 2013/frederickding/importer/trunk/tests/test-class-wordpress-importer.php (2136 => 2137)</h4>
<pre class="diff"><span>
<span class="info">--- 2013/frederickding/importer/trunk/tests/test-class-wordpress-importer.php        2013-07-18 21:52:42 UTC (rev 2136)
+++ 2013/frederickding/importer/trunk/tests/test-class-wordpress-importer.php   2013-07-23 00:16:52 UTC (rev 2137)
</span><span class="lines">@@ -93,25 +93,42 @@
</span><span class="cx">          $this->assertEquals( 'finish', $instance->advance( 'finish' ) );
</span><span class="cx">  }
</span><span class="cx">  
</span><del>-       public function test_process_authors() {
-               $instance = new WordPress_Importer();
-               
-               $this->assertFalse(self::call($instance, 'process_authors'));
</del><ins>+        public function test_get_error() {
+           // clean instance, no errors
+           $instance = new WordPress_Importer();
+       
+           $this->assertNull($instance->get_error());
</ins><span class="cx">   }
</span><span class="cx">  
</span><del>-       public function test_get_error() {
-               // clean instance, no errors
</del><ins>+        public function test_get_warnings() {
+           // clean instance, no warnings
+           $instance = new WordPress_Importer();
+       
+           $this->assertInternalType('array', $instance->get_warnings());
+           $this->assertEmpty($instance->get_warnings());
+       }
+       
+       /**
+        * @depends test_get_error
+        */
+       public function test_process_authors_standalone() {
</ins><span class="cx">           $instance = new WordPress_Importer();
</span><span class="cx">          
</span><del>-               $this->assertNull($instance->get_error());
</del><ins>+                $this->assertFalse(self::call($instance, 'process_authors'));
+               $this->assertWPError($instance->get_error());
+               
+               return $instance;
</ins><span class="cx">   }
</span><span class="cx">  
</span><del>-       public function test_get_warnings() {
-               // clean instance, no warnings
-               $instance = new WordPress_Importer();
</del><ins>+        /**
+        * @depends test_process_authors_standalone
+        * @depends test_get_error
+        */
+       public function test_process_author_mappings_standalone(WordPress_Importer $instance) {
</ins><span class="cx">   
</span><del>-               $this->assertInternalType('array', $instance->get_warnings());
-               $this->assertEmpty($instance->get_warnings());
</del><ins>+            $this->assertFalse(self::call($instance, 'process_author_mappings'));
+           $this->assertWPError($instance->get_error());
+           
</ins><span class="cx">   }
</span><span class="cx">  
</span><span class="cx">  /**
</span></span></pre>
</div>
</div>

</body>
</html>