Changeset 3802902
- Timestamp:
- 11/15/2026 08:24:44 AM (less than one hour ago)
- Location:
- transtrix
- Files:
-
- 18 added
- 44 edited
- 1 copied
-
tags/0.3.0 (copied) (copied from transtrix/trunk )
-
tags/0.3.0/assets/admin/logos/easy-digital-downloads.svg (added)
-
tags/0.3.0/assets/admin/src/components/SourceLogo.jsx (modified) (1 diff)
-
tags/0.3.0/dist/admin.asset.php (modified) (1 diff)
-
tags/0.3.0/dist/admin.js (modified) (1 diff)
-
tags/0.3.0/includes/Cli/Cli_Command.php (modified) (2 diffs)
-
tags/0.3.0/includes/Contracts/Module.php (modified) (1 diff)
-
tags/0.3.0/includes/Import/Account_Importer.php (added)
-
tags/0.3.0/includes/Import/Restore_Point.php (modified) (1 diff)
-
tags/0.3.0/includes/Import/Three_Pass_Importer.php (modified) (10 diffs)
-
tags/0.3.0/includes/Media/Media_Collector.php (modified) (3 diffs)
-
tags/0.3.0/includes/Media/Media_Importer.php (modified) (8 diffs)
-
tags/0.3.0/includes/Modules/Core/Core_Exporter.php (modified) (5 diffs)
-
tags/0.3.0/includes/Modules/EDD (added)
-
tags/0.3.0/includes/Modules/EDD/EDD_Customers.php (added)
-
tags/0.3.0/includes/Modules/EDD/EDD_Discounts.php (added)
-
tags/0.3.0/includes/Modules/EDD/EDD_Hints.php (added)
-
tags/0.3.0/includes/Modules/EDD/EDD_Module.php (added)
-
tags/0.3.0/includes/Modules/EDD/EDD_Orders.php (added)
-
tags/0.3.0/includes/Modules/EDD/EDD_Rows.php (added)
-
tags/0.3.0/includes/Modules/Woo/Woo_Orders.php (modified) (9 diffs)
-
tags/0.3.0/includes/Package/Package_Writer.php (modified) (4 diffs)
-
tags/0.3.0/includes/Reference/Hint_Path.php (modified) (5 diffs)
-
tags/0.3.0/includes/Reference/Reference_Hint.php (modified) (8 diffs)
-
tags/0.3.0/includes/Reference/Reference_Walker.php (modified) (6 diffs)
-
tags/0.3.0/languages/transtrix.pot (modified) (10 diffs)
-
tags/0.3.0/package.json (modified) (1 diff)
-
tags/0.3.0/readme.txt (modified) (6 diffs)
-
tags/0.3.0/transtrix.php (modified) (2 diffs)
-
tags/0.3.0/vendor/composer/autoload_classmap.php (modified) (2 diffs)
-
tags/0.3.0/vendor/composer/autoload_static.php (modified) (2 diffs)
-
tags/0.3.0/vendor/composer/installed.php (modified) (2 diffs)
-
trunk/assets/admin/logos/easy-digital-downloads.svg (added)
-
trunk/assets/admin/src/components/SourceLogo.jsx (modified) (1 diff)
-
trunk/dist/admin.asset.php (modified) (1 diff)
-
trunk/dist/admin.js (modified) (1 diff)
-
trunk/includes/Cli/Cli_Command.php (modified) (2 diffs)
-
trunk/includes/Contracts/Module.php (modified) (1 diff)
-
trunk/includes/Import/Account_Importer.php (added)
-
trunk/includes/Import/Restore_Point.php (modified) (1 diff)
-
trunk/includes/Import/Three_Pass_Importer.php (modified) (10 diffs)
-
trunk/includes/Media/Media_Collector.php (modified) (3 diffs)
-
trunk/includes/Media/Media_Importer.php (modified) (8 diffs)
-
trunk/includes/Modules/Core/Core_Exporter.php (modified) (5 diffs)
-
trunk/includes/Modules/EDD (added)
-
trunk/includes/Modules/EDD/EDD_Customers.php (added)
-
trunk/includes/Modules/EDD/EDD_Discounts.php (added)
-
trunk/includes/Modules/EDD/EDD_Hints.php (added)
-
trunk/includes/Modules/EDD/EDD_Module.php (added)
-
trunk/includes/Modules/EDD/EDD_Orders.php (added)
-
trunk/includes/Modules/EDD/EDD_Rows.php (added)
-
trunk/includes/Modules/Woo/Woo_Orders.php (modified) (9 diffs)
-
trunk/includes/Package/Package_Writer.php (modified) (4 diffs)
-
trunk/includes/Reference/Hint_Path.php (modified) (5 diffs)
-
trunk/includes/Reference/Reference_Hint.php (modified) (8 diffs)
-
trunk/includes/Reference/Reference_Walker.php (modified) (6 diffs)
-
trunk/languages/transtrix.pot (modified) (10 diffs)
-
trunk/package.json (modified) (1 diff)
-
trunk/readme.txt (modified) (6 diffs)
-
trunk/transtrix.php (modified) (2 diffs)
-
trunk/vendor/composer/autoload_classmap.php (modified) (2 diffs)
-
trunk/vendor/composer/autoload_static.php (modified) (2 diffs)
-
trunk/vendor/composer/installed.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
transtrix/tags/0.3.0/assets/admin/src/components/SourceLogo.jsx
r3759911 r3802902 26 26 'wp-travel-engine': 'wp-travel-engine.png', 27 27 woocommerce: 'woocommerce.svg', 28 edd: 'easy-digital-downloads.svg', 28 29 }; 29 30 -
transtrix/tags/0.3.0/dist/admin.asset.php
r3790546 r3802902 1 <?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-date', 'wp-element', 'wp-i18n'), 'version' => ' 3911087dc07eb7c69eee');1 <?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-date', 'wp-element', 'wp-i18n'), 'version' => '0aabe6dbf55be35a8696'); -
transtrix/tags/0.3.0/dist/admin.js
r3790546 r3802902 549 549 color: #0284c7; 550 550 } 551 `,Ga=Qa,Za={};Za.wordpress=(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("circle",{cx:"36",cy:"36",r:"36",fill:"#fff"}),(0,l.jsx)("g",{transform:"translate(6 6) scale(2.5)",fill:"#21759B",children:(0,l.jsx)("path",{d:"M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11m-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.609-3.582.609M1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.211 12M12 0C5.385 0 0 5.385 0 12s5.385 12 12 12 12-5.385 12-12S18.615 0 12 0"})})]}),Za["wp-core"]=Za.wordpress,Za.core=Za.wordpress,Za["full-site"]=Za.wordpress;const Ya={wptravelengine:"wp-travel-engine.png","wp-travel-engine":"wp-travel-engine.png",woocommerce:"woocommerce.svg" },Xa=({moduleId:e,size:t=72})=>Ya[e]&&Ia.logosUrl?(0,l.jsx)("img",{src:Ia.logosUrl+Ya[e],alt:"",width:t,height:t,style:{display:"block",margin:"0 auto",borderRadius:Math.round(.22*t)}}):!Za[e]&&Ia.siteLogo?(0,l.jsx)("img",{src:Ia.siteLogo,alt:"",width:t,height:t,style:{display:"block",margin:"0 auto",borderRadius:16,objectFit:"contain"}}):Za[e]?(0,l.jsx)("svg",{viewBox:"0 0 72 72",width:t,height:t,"aria-hidden":"true",style:{display:"block",margin:"0 auto"},children:Za[e]}):(0,l.jsx)("div",{style:{width:t,margin:"0 auto"},children:(0,l.jsx)(Ga,{size:t})}),Ja=Qr.div`551 `,Ga=Qa,Za={};Za.wordpress=(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("circle",{cx:"36",cy:"36",r:"36",fill:"#fff"}),(0,l.jsx)("g",{transform:"translate(6 6) scale(2.5)",fill:"#21759B",children:(0,l.jsx)("path",{d:"M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11m-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.609-3.582.609M1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.211 12M12 0C5.385 0 0 5.385 0 12s5.385 12 12 12 12-5.385 12-12S18.615 0 12 0"})})]}),Za["wp-core"]=Za.wordpress,Za.core=Za.wordpress,Za["full-site"]=Za.wordpress;const Ya={wptravelengine:"wp-travel-engine.png","wp-travel-engine":"wp-travel-engine.png",woocommerce:"woocommerce.svg",edd:"easy-digital-downloads.svg"},Xa=({moduleId:e,size:t=72})=>Ya[e]&&Ia.logosUrl?(0,l.jsx)("img",{src:Ia.logosUrl+Ya[e],alt:"",width:t,height:t,style:{display:"block",margin:"0 auto",borderRadius:Math.round(.22*t)}}):!Za[e]&&Ia.siteLogo?(0,l.jsx)("img",{src:Ia.siteLogo,alt:"",width:t,height:t,style:{display:"block",margin:"0 auto",borderRadius:16,objectFit:"contain"}}):Za[e]?(0,l.jsx)("svg",{viewBox:"0 0 72 72",width:t,height:t,"aria-hidden":"true",style:{display:"block",margin:"0 auto"},children:Za[e]}):(0,l.jsx)("div",{style:{width:t,margin:"0 auto"},children:(0,l.jsx)(Ga,{size:t})}),Ja=Qr.div` 552 552 display: flex; 553 553 flex-direction: column; -
transtrix/tags/0.3.0/includes/Cli/Cli_Command.php
r3790546 r3802902 252 252 * 253 253 * @since 0.1.0 254 * @since 0.3.0 Also reports the orders, customers and codes it deleted. 254 255 * 255 256 * @param array<int,string> $args Positional arguments. … … 277 278 WP_CLI::success( 278 279 sprintf( 279 'Deleted %d posts and %d terms, restored %d posts.',280 'Deleted %d posts, %d terms and %d other objects (orders, customers, codes), restored %d posts.', 280 281 $undone['posts_deleted'], 281 282 $undone['terms_deleted'], 283 $undone['objects_deleted'], 282 284 $undone['posts_restored'] 283 285 ) -
transtrix/tags/0.3.0/includes/Contracts/Module.php
r3790546 r3802902 212 212 213 213 /** 214 * Top-level upload directories whose files must keep their place. 215 * 216 * A store that sells files keeps them in a directory the web server will 217 * not serve (Easy Digital Downloads' `uploads/edd`). Imported into the 218 * usual dated directory, a paid file would get a public URL. 219 * 220 * @since 0.3.0 221 * 222 * @return string[] 223 */ 224 public function protected_upload_dirs(): array { 225 return array(); 226 } 227 228 /** 214 229 * Last chance to widen or narrow the job before the package is written. 215 230 * -
transtrix/tags/0.3.0/includes/Import/Restore_Point.php
r3790546 r3802902 137 137 public function record_created_object( string $kind, int $id ): void { 138 138 $this->created_objects[ $kind ][] = $id; 139 } 140 141 /** 142 * Non-post objects this import created so far, kind => ids. 143 * 144 * @since 0.3.0 145 * 146 * @return array<string,int[]> 147 */ 148 public function created_objects(): array { 149 return array_map( 150 static function ( $ids ) { 151 return array_map( 'intval', (array) $ids ); 152 }, 153 $this->created_objects 154 ); 139 155 } 140 156 -
transtrix/tags/0.3.0/includes/Import/Three_Pass_Importer.php
r3790546 r3802902 101 101 102 102 /** 103 * Source attachment URL => source attachment id, built once per step. 104 * 105 * @var array<string,int>|null 106 */ 107 private $media_urls = null; 108 109 /** 103 110 * Taxonomies the package uses that this site does not register. 104 111 * … … 423 430 424 431 /** 432 * Source attachment URL => source attachment id, for attachment URL hints. 433 * 434 * @since 0.3.0 435 * 436 * @return array<string,int> 437 */ 438 private function media_urls(): array { 439 if ( null === $this->media_urls ) { 440 $this->media_urls = array(); 441 442 foreach ( $this->lists()['media'] as $record ) { 443 if ( ! empty( $record['url'] ) && ! empty( $record['id'] ) ) { 444 $this->media_urls[ (string) $record['url'] ] = (int) $record['id']; 445 } 446 } 447 } 448 449 return $this->media_urls; 450 } 451 452 /** 425 453 * Before the first record: what every later record is keyed by. 426 454 * … … 500 528 * 501 529 * @since 0.2.0 530 * @since 0.3.0 Also hands over the objects import_payload() created. 502 531 * 503 532 * @param Report $report Outcome report. … … 512 541 // one, before the terms that decide it were linked in pass three. 513 542 $this->job->set_state( 'imported_ids', array_values( $this->imported ) ); 543 // And what import_payload() created, kind => ids: an order's stats are 544 // rebuilt once, here, not per order. 545 $this->job->set_state( 'created_objects', $this->restore->created_objects() ); 514 546 $this->module->after_import( $this->job, $report ); 515 547 } … … 682 714 * 683 715 * @since 0.2.0 716 * @since 0.3.0 Keeps the module's protected upload directories. 684 717 * 685 718 * @param array<string,mixed> $record One media record. … … 689 722 private function create_media( array $record, Report $report ): void { 690 723 if ( $this->may_upload() ) { 691 ( new Media_Importer( $this->package->work_dir() ) )->import( array( $record ), $this->map, $report, $this->restore );724 ( new Media_Importer( $this->package->work_dir(), $this->module->protected_upload_dirs() ) )->import( array( $record ), $this->map, $report, $this->restore ); 692 725 } 693 726 } … … 771 804 * 772 805 * @since 0.2.0 806 * @since 0.3.0 Rewrites attachment URLs too. 773 807 * 774 808 * @param array<string,mixed> $record One post record. … … 785 819 $new_id = $this->imported[ $old_id ]; 786 820 $meta = isset( $record['meta'] ) && is_array( $record['meta'] ) ? $record['meta'] : array(); 787 $walker = new Reference_Walker( );821 $walker = new Reference_Walker( $this->media_urls() ); 788 822 $map = $this->map; 789 823 … … 1163 1197 * 1164 1198 * @since 0.1.0 1199 * @since 0.3.0 Reads `<id>_<number>` under a suffixed hint. 1165 1200 * 1166 1201 * @param array<string,mixed> $record One post record. … … 1183 1218 1184 1219 foreach ( (array) $meta[ $parsed['key'] ] as $value ) { 1185 foreach ( Hint_Path::collect_at( $value, $parsed['segments'] ) as $id ) {1220 foreach ( Hint_Path::collect_at( $value, $parsed['segments'], $hint->is_suffixed() ) as $id ) { 1186 1221 if ( ! isset( $in_scope[ $id ] ) ) { 1187 1222 $missing[] = $id; -
transtrix/tags/0.3.0/includes/Media/Media_Collector.php
r3790546 r3802902 40 40 * @since 0.1.0 41 41 * @since 0.2.0 Also collects the generated sizes and their metadata. 42 * @since 0.3.0 Records the source URL and upload sub-directory. 42 43 * 43 44 * @param int $attachment_id Attachment id. … … 67 68 $filename = wp_basename( $file ); 68 69 $relative_path = 'media/' . $attachment_id . '/' . $filename; 70 $subdir = dirname( (string) get_post_meta( $attachment_id, '_wp_attached_file', true ) ); 69 71 70 72 $this->files[ $relative_path ] = array( … … 85 87 'post_parent' => (int) $post->post_parent, 86 88 'post_date_gmt' => $post->post_date_gmt, 89 // Where it lived, so a link to it can follow it and a protected 90 // directory can keep it. 91 'url' => (string) wp_get_attachment_url( $attachment_id ), 92 'subdir' => '.' === $subdir ? '' : $subdir, 87 93 ); 88 94 -
transtrix/tags/0.3.0/includes/Media/Media_Importer.php
r3790546 r3802902 38 38 39 39 /** 40 * Top-level upload directories whose files keep their place. 41 * 42 * @var string[] 43 */ 44 private $protected_dirs; 45 46 /** 40 47 * Constructor. 41 48 * 42 49 * @since 0.1.0 43 * 44 * @param string $work_dir Trailing-slashed extraction directory. 45 */ 46 public function __construct( string $work_dir ) { 47 $this->work_dir = trailingslashit( $work_dir ); 50 * @since 0.3.0 Takes $protected_dirs. 51 * 52 * @param string $work_dir Trailing-slashed extraction directory. 53 * @param string[] $protected_dirs Top-level upload directories whose files keep their place. 54 */ 55 public function __construct( string $work_dir, array $protected_dirs = array() ) { 56 $this->work_dir = trailingslashit( $work_dir ); 57 $this->protected_dirs = $protected_dirs; 48 58 } 49 59 … … 52 62 * 53 63 * @since 0.1.0 64 * @since 0.3.0 A protected file matches only a copy in its protected directory. 54 65 * 55 66 * @param array<int,array<string,mixed>> $records Media records from the package. … … 68 79 } 69 80 70 $existing = $this->find_by_checksum( (string) ( $record['sha256'] ?? '' ) );81 $existing = $this->find_by_checksum( (string) ( $record['sha256'] ?? '' ), $this->protected_subdir( $record ) ); 71 82 72 83 if ( $existing > 0 ) { … … 93 104 * 94 105 * @since 0.1.0 106 * @since 0.3.0 A protected file only matches a copy inside the same protected directory. 95 107 * 96 108 * @param string $checksum sha256 of the file. 109 * @param string $subdir Protected sub-directory the file belongs in, or ''. 97 110 * @return int Attachment id, or 0. 98 111 */ 99 private function find_by_checksum( string $checksum ): int {112 private function find_by_checksum( string $checksum, string $subdir = '' ): int { 100 113 if ( '' === $checksum ) { 101 114 return 0; … … 106 119 'post_type' => 'attachment', 107 120 'post_status' => 'any', 108 'posts_per_page' => 1,121 'posts_per_page' => '' === $subdir ? 1 : 20, 109 122 'fields' => 'ids', 110 123 'no_found_rows' => true, … … 115 128 ); 116 129 117 return empty( $found ) ? 0 : (int) $found[0]; 130 if ( '' === $subdir ) { 131 return empty( $found ) ? 0 : (int) $found[0]; 132 } 133 134 // A public copy of a paid file is not a match: linking to it would 135 // hand out the file the protected directory exists to hold back. 136 $top = strtok( $subdir, '/' ) . '/'; 137 138 foreach ( $found as $id ) { 139 if ( 0 === strpos( (string) get_post_meta( (int) $id, '_wp_attached_file', true ), $top ) ) { 140 return (int) $id; 141 } 142 } 143 144 return 0; 145 } 146 147 /** 148 * The upload sub-directory a protected file must keep, or '' for any other file. 149 * 150 * The path comes from the package, so only plain directory names pass; a 151 * protected file with any other path still lands in its protected root. 152 * 153 * @since 0.3.0 154 * 155 * @param array<string,mixed> $record One media record. 156 * @return string 157 */ 158 private function protected_subdir( array $record ): string { 159 $subdir = (string) ( $record['subdir'] ?? '' ); 160 $top = explode( '/', $subdir )[0]; 161 162 if ( '' === $top || ! in_array( $top, $this->protected_dirs, true ) ) { 163 return ''; 164 } 165 166 return 1 === preg_match( '#^[A-Za-z0-9_-]+(/[A-Za-z0-9_-]+)*$#', $subdir ) ? $subdir : $top; 118 167 } 119 168 … … 123 172 * @since 0.1.0 124 173 * @since 0.2.0 Copies the packaged sizes instead of rebuilding them. 174 * @since 0.3.0 Keeps a protected file in its protected directory. 125 175 * 126 176 * @param array<string,mixed> $record One media record. … … 158 208 ); 159 209 160 $upload = wp_handle_sideload( $sideload, array( 'test_form' => false ) ); 210 $subdir = $this->protected_subdir( $record ); 211 $keep_dir = static function ( array $dirs ) use ( $subdir ): array { 212 $dirs['subdir'] = '/' . $subdir; 213 $dirs['path'] = $dirs['basedir'] . $dirs['subdir']; 214 $dirs['url'] = $dirs['baseurl'] . $dirs['subdir']; 215 216 return $dirs; 217 }; 218 219 if ( '' !== $subdir ) { 220 add_filter( 'upload_dir', $keep_dir, PHP_INT_MAX ); 221 } 222 223 try { 224 $upload = wp_handle_sideload( $sideload, array( 'test_form' => false ) ); 225 } finally { 226 remove_filter( 'upload_dir', $keep_dir, PHP_INT_MAX ); 227 } 161 228 162 229 if ( ! empty( $upload['error'] ) || empty( $upload['file'] ) ) { -
transtrix/tags/0.3.0/includes/Modules/Core/Core_Exporter.php
r3790546 r3802902 186 186 * 187 187 * @since 0.1.0 188 * @since 0.3.0 Reads `<id>_<number>` under a suffixed hint. 188 189 * 189 190 * @param int[] $post_ids Selected post ids. … … 210 211 foreach ( $post_ids as $post_id ) { 211 212 foreach ( get_post_meta( (int) $post_id, $parsed['key'], false ) as $value ) { 212 $ids = array_merge( $ids, Hint_Path::collect_at( $value, $parsed['segments'] ) );213 $ids = array_merge( $ids, Hint_Path::collect_at( $value, $parsed['segments'], $hint->is_suffixed() ) ); 213 214 } 214 215 } … … 310 311 311 312 /** 313 * Attachment ids behind the URLs at a path. 314 * 315 * @since 0.3.0 316 * 317 * @param mixed $value The whole meta value. 318 * @param string[] $segments Path to the URLs. 319 * @return int[] 320 */ 321 private function url_ids( $value, array $segments ): array { 322 $ids = array(); 323 324 Hint_Path::rewrite_at( 325 maybe_unserialize( $value ), 326 $segments, 327 static function ( $url ) use ( &$ids ) { 328 if ( is_string( $url ) && '' !== $url ) { 329 $ids[] = attachment_url_to_postid( $url ); 330 } 331 332 return $url; 333 } 334 ); 335 336 return $ids; 337 } 338 339 /** 312 340 * Attachment ids a post references, through hints and through its content. 313 341 * 314 342 * @since 0.1.0 343 * @since 0.3.0 Follows attachment URL hints too. 315 344 * 316 345 * @param int $post_id Post id. … … 323 352 $hint = Reference_Hint::from( (string) $notation ); 324 353 325 if ( ! $hint instanceof Reference_Hint || Reference_Hint::TYPE_ATTACHMENT !== $hint->type() ) {354 if ( ! $hint instanceof Reference_Hint || ! in_array( $hint->type(), array( Reference_Hint::TYPE_ATTACHMENT, Reference_Hint::TYPE_ATTACHMENT_URL ), true ) ) { 326 355 continue; 327 356 } … … 332 361 333 362 foreach ( get_post_meta( $post_id, $parsed['key'], false ) as $value ) { 334 $ids = array_merge( $ids, Hint_Path::collect_at( $value, $parsed['segments'] ) );363 $ids = array_merge( $ids, Reference_Hint::TYPE_ATTACHMENT_URL === $hint->type() ? $this->url_ids( $value, $parsed['segments'] ) : Hint_Path::collect_at( $value, $parsed['segments'] ) ); 335 364 } 336 365 } -
transtrix/tags/0.3.0/includes/Modules/Woo/Woo_Orders.php
r3790546 r3802902 8 8 namespace Transtrix\Modules\Woo; 9 9 10 use Transtrix\Import\Account_Importer; 10 11 use Transtrix\Import\Restore_Point; 11 12 use Transtrix\Reference\Id_Map; … … 306 307 * 307 308 * @since 0.1.0 309 * @since 0.3.0 The account fields come from Account_Importer. 308 310 * 309 311 * @param int $user_id User id. … … 311 313 */ 312 314 private static function customer_record( int $user_id ): ?array { 313 $ user = get_userdata( $user_id );314 315 if ( ! $user instanceof \WP_User) {315 $record = Account_Importer::export( $user_id ); 316 317 if ( null === $record ) { 316 318 return null; 317 319 } … … 319 321 $customer = new \WC_Customer( $user_id ); 320 322 321 return array( 322 'id' => $user_id, 323 'user_login' => $user->user_login, 324 'user_email' => $user->user_email, 325 'display_name' => $user->display_name, 326 'first_name' => $user->first_name, 327 'last_name' => $user->last_name, 328 'user_registered' => $user->user_registered, 329 'billing' => $customer->get_billing( 'edit' ), 330 'shipping' => $customer->get_shipping( 'edit' ), 331 'is_paying' => $customer->get_is_paying_customer( 'edit' ), 323 return array_merge( 324 $record, 325 array( 326 'billing' => $customer->get_billing( 'edit' ), 327 'shipping' => $customer->get_shipping( 'edit' ), 328 'is_paying' => $customer->get_is_paying_customer( 'edit' ), 329 ) 332 330 ); 333 331 } … … 396 394 * 397 395 * @since 0.1.0 396 * @since 0.3.0 Matches and creates the account through Account_Importer. 398 397 * 399 398 * @param array<string,mixed> $record Customer record. … … 404 403 */ 405 404 private static function import_customer( array $record, Id_Map $map, Restore_Point $restore, Report $report ): void { 406 $old_id = (int) ( $record['id'] ?? 0 ); 407 $email = sanitize_email( (string) ( $record['user_email'] ?? '' ) ); 408 409 if ( '' === $email ) { 405 $account = Account_Importer::resolve( $record, 'customer', self::KIND_CUSTOMER, $map, $restore, $report ); 406 407 if ( Account_Importer::MATCHED === $account['status'] ) { 408 $report->add_skipped(); 409 } 410 411 if ( Account_Importer::CREATED !== $account['status'] ) { 410 412 return; 411 413 } 412 414 413 $existing = get_user_by( 'email', $email ); 414 415 if ( $existing instanceof \WP_User ) { 416 // A staff account with the same email is not this shopper: tying 417 // imported orders to an administrator would show them as theirs. 418 // Their orders arrive as guest orders, with the email kept. 419 if ( array() !== array_diff( (array) $existing->roles, array( 'customer', 'subscriber' ) ) ) { 420 $report->add_warning( 421 $email, 422 __( 'This email belongs to a staff account here, so its orders were imported as guest orders.', 'transtrix' ) 423 ); 424 return; 425 } 426 427 $map->add( 'user', $old_id, (int) $existing->ID ); 428 $report->add_skipped(); 429 return; 430 } 431 432 $login = sanitize_user( (string) ( $record['user_login'] ?? '' ), true ); 433 $login = '' === $login || username_exists( $login ) ? sanitize_user( strstr( $email, '@', true ) . '-' . wp_generate_password( 4, false, false ), true ) : $login; 434 435 // wp_insert_user() sends no notification. The password is random: hashes 436 // do not travel, so the customer sets a new one with "Lost password". 437 $user_id = wp_insert_user( 438 array( 439 'user_login' => $login, 440 'user_email' => $email, 441 'user_pass' => wp_generate_password( 24 ), 442 'display_name' => (string) ( $record['display_name'] ?? '' ), 443 'first_name' => (string) ( $record['first_name'] ?? '' ), 444 'last_name' => (string) ( $record['last_name'] ?? '' ), 445 'user_registered' => (string) ( $record['user_registered'] ?? '' ), 446 'role' => 'customer', 447 ) 448 ); 449 450 if ( is_wp_error( $user_id ) ) { 451 $report->add_failure( $email, $user_id->get_error_message() ); 452 return; 453 } 454 455 $restore->record_created_object( self::KIND_CUSTOMER, (int) $user_id ); 456 $map->add( 'user', $old_id, (int) $user_id ); 457 458 $customer = new \WC_Customer( (int) $user_id ); 415 $customer = new \WC_Customer( $account['user_id'] ); 459 416 460 417 foreach ( array( 'billing', 'shipping' ) as $type ) { … … 758 715 * 759 716 * @since 0.1.0 717 * @since 0.3.0 Accounts are undone through Account_Importer. 760 718 * 761 719 * @param string $kind KIND_ORDER or KIND_CUSTOMER. … … 777 735 778 736 if ( self::KIND_CUSTOMER === $kind ) { 779 require_once ABSPATH . 'wp-admin/includes/user.php'; 780 781 foreach ( $ids as $id ) { 782 // Kept once the account has a life of its own here: a new role, 783 // content they wrote, or an order placed since the import. 784 // Deleting it would take that with it. 785 if ( self::customer_in_use( (int) $id ) ) { 786 continue; 787 } 788 789 if ( wp_delete_user( $id ) ) { 790 ++$deleted; 791 } 792 } 737 $deleted += Account_Importer::rollback( $ids, 'customer', array( __CLASS__, 'has_orders' ) ); 793 738 } 794 739 … … 797 742 798 743 /** 799 * Whether an imported customer account is now used for more than the import.800 * 801 * @since 0. 1.0744 * Whether an account has an order in any status. 745 * 746 * @since 0.3.0 802 747 * 803 748 * @param int $user_id User id. 804 749 * @return bool 805 750 */ 806 private static function customer_in_use( int $user_id ): bool { 807 $user = get_userdata( $user_id ); 808 809 if ( ! $user instanceof \WP_User ) { 810 return true; 811 } 812 813 if ( array( 'customer' ) !== array_values( (array) $user->roles ) || count_user_posts( $user_id, get_post_types(), false ) > 0 ) { 814 return true; 815 } 816 751 public static function has_orders( int $user_id ): bool { 817 752 return function_exists( 'wc_get_orders' ) && array() !== (array) wc_get_orders( 818 753 array( -
transtrix/tags/0.3.0/includes/Package/Package_Writer.php
r3790546 r3802902 89 89 90 90 /** 91 * Meta key prefixes that must not be written: entries given as 'prefix*'. 92 * 93 * @var string[] 94 */ 95 private $excluded_meta_prefixes = array(); 96 97 /** 91 98 * Constructor. 92 99 * 93 100 * @since 0.1.0 101 * @since 0.3.0 A trailing `*` on an excluded key excludes every key with that prefix. 94 102 * 95 103 * @param Job $job The export job. … … 97 105 */ 98 106 public function __construct( Job $job, array $excluded_meta_keys = array() ) { 99 $this->job = $job; 100 $this->report = new Report(); 101 $this->media = new Media_Collector(); 102 $this->excluded_meta_keys = array_flip( array_map( 'strval', $excluded_meta_keys ) ); 107 $this->job = $job; 108 $this->report = new Report(); 109 $this->media = new Media_Collector(); 110 111 foreach ( array_map( 'strval', $excluded_meta_keys ) as $key ) { 112 if ( '*' === substr( $key, -1 ) ) { 113 $this->excluded_meta_prefixes[] = substr( $key, 0, -1 ); 114 } else { 115 $this->excluded_meta_keys[ $key ] = 1; 116 } 117 } 103 118 } 104 119 … … 178 193 // A6: a denied key never reaches the package, so it cannot leak by 179 194 // being written, inspected in the zip, or shipped to a third party. 180 if ( isset( $this->excluded_meta_keys[ $key ]) ) {195 if ( $this->is_excluded( (string) $key ) ) { 181 196 continue; 182 197 } … … 360 375 return ( $parent instanceof \WP_Term ) ? $parent->slug : ''; 361 376 } 377 378 /** 379 * Whether the module forbids a meta key, by name or by prefix. 380 * 381 * @since 0.3.0 382 * 383 * @param string $key Meta key. 384 * @return bool 385 */ 386 private function is_excluded( string $key ): bool { 387 if ( isset( $this->excluded_meta_keys[ $key ] ) ) { 388 return true; 389 } 390 391 foreach ( $this->excluded_meta_prefixes as $prefix ) { 392 if ( 0 === strpos( $key, $prefix ) ) { 393 return true; 394 } 395 } 396 397 return false; 398 } 362 399 } -
transtrix/tags/0.3.0/includes/Reference/Hint_Path.php
r3790546 r3802902 145 145 * 146 146 * @since 0.1.0 147 * @since 0.3.0 Takes $suffixed. 147 148 * 148 149 * @param mixed $value The whole meta value. 149 150 * @param string[] $segments Path segments; empty means the whole value. 151 * @param bool $suffixed Also read `<id>_<number>` strings. 150 152 * @return int[] 151 153 */ 152 public static function collect_at( $value, array $segments ): array {154 public static function collect_at( $value, array $segments, bool $suffixed = false ): array { 153 155 $found = array(); 154 156 … … 156 158 $value, 157 159 $segments, 158 static function ( $leaf ) use ( &$found ) {159 $found = array_merge( $found, self::ids_in( $leaf ) );160 static function ( $leaf ) use ( &$found, $suffixed ) { 161 $found = array_merge( $found, self::ids_in( $leaf, $suffixed ) ); 160 162 161 163 return $leaf; … … 170 172 * 171 173 * @since 0.1.0 172 * 173 * @param mixed $value Any value. 174 * @since 0.3.0 Takes $suffixed. 175 * 176 * @param mixed $value Any value. 177 * @param bool $suffixed Also read `<id>_<number>` strings, as the id. 174 178 * @return int[] 175 179 */ 176 public static function ids_in( $value ): array {180 public static function ids_in( $value, bool $suffixed = false ): array { 177 181 if ( is_string( $value ) && is_serialized( $value ) ) { 178 182 $value = unserialize( $value, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- allowed_classes is false; serialized values are rewritten in place. … … 183 187 184 188 foreach ( $value as $item ) { 185 $ids = array_merge( $ids, self::ids_in( $item ) );189 $ids = array_merge( $ids, self::ids_in( $item, $suffixed ) ); 186 190 } 187 191 … … 205 209 } 206 210 211 if ( $suffixed && 1 === preg_match( '/^(\d+)_\d+$/', $value, $parts ) ) { 212 return array( (int) $parts[1] ); 213 } 214 207 215 return array(); 208 216 } -
transtrix/tags/0.3.0/includes/Reference/Reference_Hint.php
r3790546 r3802902 26 26 27 27 /** 28 * A URL of an attachment's file, rewritten to the imported attachment's URL. 29 */ 30 const TYPE_ATTACHMENT_URL = 'attachment_url'; 31 32 /** 28 33 * Reference type, one of the TYPE_* constants. 29 34 * … … 52 57 53 58 /** 59 * Whether an id may carry a `_<number>` suffix that is kept as written. 60 * 61 * Easy Digital Downloads names "download 123, price option 2" as `123_2`. 62 * The leading number is the id; the suffix is local to that download. 63 * 64 * @var bool 65 */ 66 private $suffixed = false; 67 68 /** 54 69 * Constructor. 55 70 * 56 71 * @since 0.1.0 72 * @since 0.3.0 Takes $suffixed. 57 73 * 58 74 * @param string $type Reference type. 59 75 * @param bool $is_list Whether the value holds a list of ids. 60 76 * @param string $taxonomy Taxonomy a term hint is confined to, or ''. 61 */ 62 public function __construct( string $type, bool $is_list = false, string $taxonomy = '' ) { 77 * @param bool $suffixed Whether an id may carry a kept `_<number>` suffix. 78 */ 79 public function __construct( string $type, bool $is_list = false, string $taxonomy = '', bool $suffixed = false ) { 63 80 $this->type = $type; 64 81 $this->is_list = $is_list; 65 82 $this->taxonomy = $taxonomy; 83 $this->suffixed = $suffixed; 66 84 } 67 85 … … 70 88 * 71 89 * @since 0.1.0 90 * @since 0.3.0 A trailing `^` on the type ('post^') allows `<id>_<number>`. 72 91 * 73 92 * @param string $notation For example 'post[]' or 'attachment'. … … 77 96 $is_list = ( '[]' === substr( $notation, -2 ) ); 78 97 $type = $is_list ? substr( $notation, 0, -2 ) : $notation; 98 $suffixed = ( '^' === substr( $type, -1 ) ); 99 $type = $suffixed ? substr( $type, 0, -1 ) : $type; 79 100 $taxonomy = ''; 80 101 … … 89 110 self::TYPE_TERM, 90 111 self::TYPE_USER, 112 self::TYPE_ATTACHMENT_URL, 91 113 ); 92 114 … … 99 121 } 100 122 101 return new self( $type, $is_list, $taxonomy );123 return new self( $type, $is_list, $taxonomy, $suffixed ); 102 124 } 103 125 … … 125 147 126 148 /** 149 * Whether an id may carry a kept `_<number>` suffix. 150 * 151 * @since 0.3.0 152 * 153 * @return bool 154 */ 155 public function is_suffixed(): bool { 156 return $this->suffixed; 157 } 158 159 /** 127 160 * The object type used as the Id_Map bucket. 128 161 * … … 131 164 * 132 165 * @since 0.1.0 166 * @since 0.3.0 Attachment URL hints use the post bucket too. 133 167 * 134 168 * @return string 135 169 */ 136 170 public function map_bucket(): string { 137 if ( self::TYPE_ATTACHMENT === $this->type ) {171 if ( self::TYPE_ATTACHMENT === $this->type || self::TYPE_ATTACHMENT_URL === $this->type ) { 138 172 return self::TYPE_POST; 139 173 } -
transtrix/tags/0.3.0/includes/Reference/Reference_Walker.php
r3790546 r3802902 30 30 31 31 /** 32 * Source attachment URL => source attachment id, for attachment_url hints. 33 * 34 * @var array<string,int> 35 */ 36 private $urls; 37 38 /** 39 * Constructor. 40 * 41 * @since 0.3.0 42 * 43 * @param array<string,int> $urls Source attachment URL => source attachment id. 44 */ 45 public function __construct( array $urls = array() ) { 46 $this->urls = $urls; 47 } 48 49 /** 32 50 * Rewrite every id inside a value. 33 51 * … … 54 72 * 55 73 * @since 0.1.0 74 * @since 0.3.0 Hands attachment URL hints to remap_url(). 56 75 * 57 76 * @param mixed $value Current node. … … 68 87 } 69 88 89 if ( Reference_Hint::TYPE_ATTACHMENT_URL === $hint->type() ) { 90 return is_string( $value ) ? $this->remap_url( $value, $hint, $map ) : $value; 91 } 92 70 93 if ( is_int( $value ) ) { 71 94 return $this->remap( $value, $hint, $map ); … … 84 107 * 85 108 * @since 0.1.0 109 * @since 0.3.0 Reads `<id>_<number>` under a suffixed hint. 86 110 * 87 111 * @param string $value Current node. … … 121 145 if ( '' !== $value && ctype_digit( $value ) ) { 122 146 return (string) $this->remap( (int) $value, $hint, $map ); 147 } 148 149 if ( $hint->is_suffixed() && 1 === preg_match( '/^(\d+)(_\d+)$/', $value, $parts ) ) { 150 return $this->remap( (int) $parts[1], $hint, $map ) . $parts[2]; 123 151 } 124 152 … … 140 168 141 169 /** 170 * Point a source attachment URL at the imported attachment's URL. 171 * 172 * A URL the package carries no attachment for (an external file, a path 173 * typed by hand) is not a reference and is left alone. 174 * 175 * @since 0.3.0 176 * 177 * @param string $url Source-site URL. 178 * @param Reference_Hint $hint Reference hint. 179 * @param Id_Map $map The id map. 180 * @return string 181 */ 182 private function remap_url( string $url, Reference_Hint $hint, Id_Map $map ): string { 183 if ( ! isset( $this->urls[ $url ] ) ) { 184 return $url; 185 } 186 187 $old = (int) $this->urls[ $url ]; 188 $new = $this->remap( $old, $hint, $map ); 189 190 if ( $new === $old && ! $map->has( $hint->map_bucket(), $old ) ) { 191 return $url; 192 } 193 194 $new_url = wp_get_attachment_url( $new ); 195 196 return is_string( $new_url ) && '' !== $new_url ? $new_url : $url; 197 } 198 199 /** 142 200 * Translate one id, recording it when the map has no entry. 143 201 * -
transtrix/tags/0.3.0/languages/transtrix.pot
r3790546 r3802902 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Transtrix – Site Migration, Import & Export 0. 2.0\n"5 "Project-Id-Version: Transtrix – Site Migration, Import & Export 0.3.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/transtrix\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-11- 08T06:19:00+00:00\n"12 "POT-Creation-Date: 2026-11-15T08:23:26+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 272 272 #: dist/admin.js:979 273 273 msgid "Installed plugins, so the new site has the same features." 274 msgstr "" 275 276 #: includes/Import/Account_Importer.php:73 277 msgid "This email belongs to a staff account here, so its orders were imported as guest orders." 274 278 msgstr "" 275 279 … … 282 286 283 287 #. translators: %s: comma-separated content type names. 284 #: includes/Import/Three_Pass_Importer.php:4 38288 #: includes/Import/Three_Pass_Importer.php:466 285 289 #, php-format 286 290 msgid "Not imported, because this site has no place to store them: %s." 287 291 msgstr "" 288 292 289 #: includes/Import/Three_Pass_Importer.php:4 47293 #: includes/Import/Three_Pass_Importer.php:475 290 294 msgid "Orders, customers and store settings in this package were not imported. Only an administrator can import them." 291 295 msgstr "" 292 296 293 #: includes/Import/Three_Pass_Importer.php:4 52297 #: includes/Import/Three_Pass_Importer.php:480 294 298 #: dist/admin.js:979 295 299 #: dist/admin.js:1135 … … 297 301 msgstr "" 298 302 299 #: includes/Import/Three_Pass_Importer.php:4 52303 #: includes/Import/Three_Pass_Importer.php:480 300 304 msgid "Images and files were left out: your account cannot upload files." 301 305 msgstr "" 302 306 303 307 #. translators: %s: plugin or module name, e.g. WooCommerce. 304 #: includes/Import/Three_Pass_Importer.php: 591308 #: includes/Import/Three_Pass_Importer.php:623 305 309 #, php-format 306 310 msgid "This package needs %s, which is not active on this site. Install and activate it, then import again. Nothing has been changed." … … 308 312 309 313 #. translators: %s: comma-separated content type names. 310 #: includes/Import/Three_Pass_Importer.php:6 28314 #: includes/Import/Three_Pass_Importer.php:660 311 315 #, php-format 312 316 msgid "This package contains content types it is not allowed to import: %s. Nothing has been changed." … … 314 318 315 319 #. translators: %s: comma-separated content type names. 316 #: includes/Import/Three_Pass_Importer.php:6 36320 #: includes/Import/Three_Pass_Importer.php:668 317 321 #, php-format 318 322 msgid "This site has no place to store these content types: %s. Activate the plugin that adds them, then import again. Nothing has been changed." 319 323 msgstr "" 320 324 321 #: includes/Import/Three_Pass_Importer.php:7 31325 #: includes/Import/Three_Pass_Importer.php:764 322 326 msgid "You are not allowed to create or change this item." 323 327 msgstr "" 324 328 325 #: includes/Import/Three_Pass_Importer.php:8 19329 #: includes/Import/Three_Pass_Importer.php:853 326 330 msgid "This hidden field was left out: only an administrator can import it." 327 331 msgstr "" 328 332 329 333 #. translators: %s: comma-separated list of ids. 330 #: includes/Import/Three_Pass_Importer.php:8 57334 #: includes/Import/Three_Pass_Importer.php:891 331 335 #, php-format 332 336 msgid "These linked items could not be found: %s" 333 337 msgstr "" 334 338 335 #: includes/Import/Three_Pass_Importer.php:10 30339 #: includes/Import/Three_Pass_Importer.php:1064 336 340 msgid "This site has no place for these categories, so they were not added. Activate the plugin that adds them, then import again." 337 341 msgstr "" 338 342 339 #: includes/Media/Media_Collector.php:5 0343 #: includes/Media/Media_Collector.php:51 340 344 msgid "This item is not a media file." 341 345 msgstr "" 342 346 343 #: includes/Media/Media_Collector.php:5 6347 #: includes/Media/Media_Collector.php:57 344 348 msgid "The media file is missing from the server." 345 349 msgstr "" 346 350 347 #: includes/Media/Media_Collector.php:6 4351 #: includes/Media/Media_Collector.php:65 348 352 msgid "The media file could not be read." 349 353 msgstr "" 350 354 351 #: includes/Media/Media_Importer.php: 66355 #: includes/Media/Media_Importer.php:77 352 356 msgid "(unnamed media)" 353 357 msgstr "" 354 358 355 #: includes/Media/Media_Importer.php: 66359 #: includes/Media/Media_Importer.php:77 356 360 msgid "This media item has no ID in the package." 357 361 msgstr "" 358 362 359 #: includes/Media/Media_Importer.php:1 34363 #: includes/Media/Media_Importer.php:184 360 364 msgid "The package does not contain this media file." 361 365 msgstr "" 362 366 363 #: includes/Media/Media_Importer.php:1 41367 #: includes/Media/Media_Importer.php:191 364 368 msgid "This media file is not a type this site accepts." 365 369 msgstr "" 366 370 367 #: includes/Media/Media_Importer.php: 163371 #: includes/Media/Media_Importer.php:230 368 372 msgid "The media file could not be saved." 369 373 msgstr "" … … 380 384 msgid "Posts, pages and media, with their categories and tags." 381 385 msgstr "" 386 387 #: includes/Modules/EDD/EDD_Customers.php:209 388 msgid "Easy Digital Downloads could not save this customer." 389 msgstr "" 390 391 #. translators: %s: discount code. 392 #: includes/Modules/EDD/EDD_Discounts.php:190 393 #, php-format 394 msgid "Discount code %s" 395 msgstr "" 396 397 #: includes/Modules/EDD/EDD_Discounts.php:214 398 msgid "Easy Digital Downloads could not save this discount code." 399 msgstr "" 400 401 #. translators: %s: comma-separated ids. 402 #: includes/Modules/EDD/EDD_Discounts.php:235 403 #, php-format 404 msgid "Some downloads or categories this code is limited to were not in the package, so those limits were removed: %s" 405 msgstr "" 406 407 #: includes/Modules/EDD/EDD_Module.php:73 408 #: includes/Modules/EDD/EDD_Module.php:102 409 msgid "Easy Digital Downloads" 410 msgstr "" 411 412 #. translators: %s: Easy Digital Downloads version. 413 #: includes/Modules/EDD/EDD_Module.php:101 414 #, php-format 415 msgid "Easy Digital Downloads v%s" 416 msgstr "" 417 418 #: includes/Modules/EDD/EDD_Module.php:113 419 msgid "Downloads with their prices, files and bundles, plus categories, tags and discount codes. Orders, refunds and customers can come too; importing them never charges or emails anyone. Files for sale stay protected on the new site." 420 msgstr "" 421 422 #: includes/Modules/EDD/EDD_Module.php:204 423 msgid "Discount codes" 424 msgstr "" 425 426 #: includes/Modules/EDD/EDD_Module.php:207 427 #: includes/Modules/Woo/Woo_Module.php:327 428 msgid "Orders" 429 msgstr "" 430 431 #: includes/Modules/EDD/EDD_Module.php:208 432 #: includes/Modules/Woo/Woo_Module.php:328 433 msgid "Customers" 434 msgstr "" 435 436 #: includes/Modules/EDD/EDD_Module.php:347 437 msgid "Easy Digital Downloads has a database upgrade to finish on this site. Open Downloads in the dashboard, finish the upgrade it asks for, then import again. Nothing has been changed." 438 msgstr "" 439 440 #. translators: %s: comma-separated names of Easy Digital Downloads data tables. 441 #: includes/Modules/EDD/EDD_Module.php:367 442 #, php-format 443 msgid "Easy Digital Downloads is active, but its setup is incomplete: some of its data tables are missing (%s). Deactivate and reactivate Easy Digital Downloads, then import again. Nothing has been changed." 444 msgstr "" 445 446 #. translators: %d: order number on the source site. 447 #: includes/Modules/EDD/EDD_Orders.php:277 448 #: includes/Modules/Woo/Woo_Orders.php:444 449 #, php-format 450 msgid "Order #%d" 451 msgstr "" 452 453 #: includes/Modules/EDD/EDD_Orders.php:306 454 msgid "Easy Digital Downloads could not save this order." 455 msgstr "" 456 457 #: includes/Modules/EDD/EDD_Orders.php:310 458 msgid "A refund of this order could not be saved." 459 msgstr "" 460 461 #. translators: %d: number of order lines. 462 #: includes/Modules/EDD/EDD_Orders.php:373 463 #, php-format 464 msgid "%d line refers to a download that was not in this package, so it keeps its name and price but links to no download." 465 msgid_plural "%d lines refer to downloads that were not in this package, so they keep their names and prices but link to no download." 466 msgstr[0] "" 467 msgstr[1] "" 382 468 383 469 #: includes/Modules/Woo/Woo_Module.php:78 … … 402 488 msgstr "" 403 489 404 #: includes/Modules/Woo/Woo_Module.php:327405 msgid "Orders"406 msgstr ""407 408 #: includes/Modules/Woo/Woo_Module.php:328409 msgid "Customers"410 msgstr ""411 412 #: includes/Modules/Woo/Woo_Orders.php:422413 msgid "This email belongs to a staff account here, so its orders were imported as guest orders."414 msgstr ""415 416 #. translators: %d: order number on the source site.417 #: includes/Modules/Woo/Woo_Orders.php:487418 #, php-format419 msgid "Order #%d"420 msgstr ""421 422 490 #. translators: %d: number of order lines. 423 #: includes/Modules/Woo/Woo_Orders.php:5 49491 #: includes/Modules/Woo/Woo_Orders.php:506 424 492 #, php-format 425 493 msgid "%d line refers to a product that was not in this package, so it keeps its name and price but links to no product." … … 502 570 msgstr "" 503 571 504 #: includes/Package/Package_Writer.php:1 67572 #: includes/Package/Package_Writer.php:182 505 573 msgid "This item no longer exists on this site." 506 574 msgstr "" 507 575 508 #: includes/Package/Package_Writer.php:2 77509 #: includes/Package/Package_Writer.php:2 81510 #: includes/Package/Package_Writer.php: 288511 #: includes/Package/Package_Writer.php:3 13512 #: includes/Package/Package_Writer.php:3 38576 #: includes/Package/Package_Writer.php:292 577 #: includes/Package/Package_Writer.php:296 578 #: includes/Package/Package_Writer.php:303 579 #: includes/Package/Package_Writer.php:328 580 #: includes/Package/Package_Writer.php:353 513 581 msgid "The package could not be written. Check that the uploads folder has free space." 514 582 msgstr "" 515 583 516 #: includes/Package/Package_Writer.php:3 20584 #: includes/Package/Package_Writer.php:335 517 585 msgid "The package could not be created. Check that the uploads folder is writable." 518 586 msgstr "" -
transtrix/tags/0.3.0/package.json
r3790546 r3802902 1 1 { 2 2 "name": "transtrix", 3 "version": "0. 2.0",3 "version": "0.3.0", 4 4 "description": "Admin app for Transtrix.", 5 5 "license": "GPL-2.0-or-later", -
transtrix/tags/0.3.0/readme.txt
r3790546 r3802902 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0. 2.07 Stable tag: 0.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 63 63 * **WP Travel Engine:** trips with their packages, coupons, extra services and accommodations, plus the destinations, activities, trip types, difficulty levels and trip tags they use. Bookings, customers, payments and enquiries are ticked too, so untick them if you want only the trips. When you bring bookings, Transtrix recounts the seats sold for each trip on the new site. 64 64 * **WooCommerce:** products with their variations, images and attributes, plus coupons, categories, tags and shipping classes. Orders, refunds and customers can come too, or you can untick them. An imported order arrives as a record of a past sale: Transtrix charges no card, sends no email, leaves stock alone and keeps the original totals. It works with High-Performance Order Storage on or off. 65 * **Easy Digital Downloads:** downloads with their prices, files and images, plus bundles, discount codes, categories and tags. Paid files stay in the protected Easy Digital Downloads folder on the new site. Orders, refunds and customers can come too, or you can untick them. An imported order arrives as a record of a past sale: Transtrix charges no card, sends no receipt, leaves discount code usage alone and keeps the original totals. Transtrix then recounts sales and earnings for each download and customer on the new site. Needs Easy Digital Downloads 3.0 or newer. 65 66 66 67 Transtrix shows a content type only when the plugin behind it is active. … … 148 149 = Can I leave bookings, orders and customer data out? = 149 150 150 Yes. Every content type starts ticked, so untick Bookings, Customers, Payments and Enquiries in WP Travel Engine, or Orders and Customers in WooCommerce , before you build the package.151 Yes. Every content type starts ticked, so untick Bookings, Customers, Payments and Enquiries in WP Travel Engine, or Orders and Customers in WooCommerce or Easy Digital Downloads, before you build the package. 151 152 152 153 = Can I undo a selected-content import? = … … 154 155 Yes. Open **History**, find the import and choose **Undo this import**. Transtrix removes what it added and puts back what it changed. Restore points last 30 days by default. 155 156 156 = Do imported WooCommerce or ders charge customers or send emails? =157 158 No. Transtrix calls no payment gateway, blocks outgoing mail during the import, and does not reduce stock again.157 = Do imported WooCommerce or Easy Digital Downloads orders charge customers or send emails? = 158 159 No. Transtrix calls no payment gateway, blocks outgoing mail during the import, and does not reduce stock or use up discount codes again. 159 160 160 161 = My export is large. Will it time out? = … … 182 183 183 184 == Changelog == 185 186 = 0.3.0 - 15th November 2026 = 187 * New Feature: Added migration for Easy Digital Downloads downloads, bundles and discount codes, with their files, images, categories and tags. 188 * New Feature: Added the option to include Easy Digital Downloads orders, refunds and customers. Imported orders charge no card, send no receipt and keep the original totals. 184 189 185 190 = 0.2.0 - 8th November 2026 = … … 201 206 == Upgrade Notice == 202 207 208 = 0.3.0 = 209 Adds Easy Digital Downloads: move downloads, files and discount codes, and optionally orders, refunds and customers. Update both the source and destination sites. 210 203 211 = 0.2.0 = 204 212 Faster imports that bring image sizes along, no 500-item export limit, and fixes for large uploads and imports that timed out. Update both the source and destination sites. -
transtrix/tags/0.3.0/transtrix.php
r3790546 r3802902 4 4 * Plugin URI: https://transtrix.com/ 5 5 * Description: Migrate a whole WordPress site, or move only the pages, posts, trips and products you pick, with their images, categories and links. Works with WP Travel Engine and WooCommerce. 6 * Version: 0. 2.06 * Version: 0.3.0 7 7 * Requires at least: 6.6 8 8 * Requires PHP: 7.4 … … 18 18 defined( 'ABSPATH' ) || exit; 19 19 20 define( 'TRANSTRIX_VERSION', '0. 2.0' );20 define( 'TRANSTRIX_VERSION', '0.3.0' ); 21 21 define( 'TRANSTRIX_FILE', __FILE__ ); 22 22 define( 'TRANSTRIX_PATH', plugin_dir_path( __FILE__ ) ); -
transtrix/tags/0.3.0/vendor/composer/autoload_classmap.php
r3790546 r3802902 24 24 'Transtrix\\Fullsite\\Serialized_Replacer' => $baseDir . '/includes/Fullsite/Serialized_Replacer.php', 25 25 'Transtrix\\Fullsite\\Tasks' => $baseDir . '/includes/Fullsite/Tasks.php', 26 'Transtrix\\Import\\Account_Importer' => $baseDir . '/includes/Import/Account_Importer.php', 26 27 'Transtrix\\Import\\Conflict_Resolver' => $baseDir . '/includes/Import/Conflict_Resolver.php', 27 28 'Transtrix\\Import\\Import_Task' => $baseDir . '/includes/Import/Import_Task.php', … … 33 34 'Transtrix\\Modules\\Core\\Core_Importer' => $baseDir . '/includes/Modules/Core/Core_Importer.php', 34 35 'Transtrix\\Modules\\Core\\Core_Module' => $baseDir . '/includes/Modules/Core/Core_Module.php', 36 'Transtrix\\Modules\\EDD\\EDD_Customers' => $baseDir . '/includes/Modules/EDD/EDD_Customers.php', 37 'Transtrix\\Modules\\EDD\\EDD_Discounts' => $baseDir . '/includes/Modules/EDD/EDD_Discounts.php', 38 'Transtrix\\Modules\\EDD\\EDD_Hints' => $baseDir . '/includes/Modules/EDD/EDD_Hints.php', 39 'Transtrix\\Modules\\EDD\\EDD_Module' => $baseDir . '/includes/Modules/EDD/EDD_Module.php', 40 'Transtrix\\Modules\\EDD\\EDD_Orders' => $baseDir . '/includes/Modules/EDD/EDD_Orders.php', 41 'Transtrix\\Modules\\EDD\\EDD_Rows' => $baseDir . '/includes/Modules/EDD/EDD_Rows.php', 35 42 'Transtrix\\Modules\\WPTravelEngine\\WPTravelEngine_Hints' => $baseDir . '/includes/Modules/WPTravelEngine/WPTravelEngine_Hints.php', 36 43 'Transtrix\\Modules\\WPTravelEngine\\WPTravelEngine_Module' => $baseDir . '/includes/Modules/WPTravelEngine/WPTravelEngine_Module.php', -
transtrix/tags/0.3.0/vendor/composer/autoload_static.php
r3790546 r3802902 39 39 'Transtrix\\Fullsite\\Serialized_Replacer' => __DIR__ . '/../..' . '/includes/Fullsite/Serialized_Replacer.php', 40 40 'Transtrix\\Fullsite\\Tasks' => __DIR__ . '/../..' . '/includes/Fullsite/Tasks.php', 41 'Transtrix\\Import\\Account_Importer' => __DIR__ . '/../..' . '/includes/Import/Account_Importer.php', 41 42 'Transtrix\\Import\\Conflict_Resolver' => __DIR__ . '/../..' . '/includes/Import/Conflict_Resolver.php', 42 43 'Transtrix\\Import\\Import_Task' => __DIR__ . '/../..' . '/includes/Import/Import_Task.php', … … 48 49 'Transtrix\\Modules\\Core\\Core_Importer' => __DIR__ . '/../..' . '/includes/Modules/Core/Core_Importer.php', 49 50 'Transtrix\\Modules\\Core\\Core_Module' => __DIR__ . '/../..' . '/includes/Modules/Core/Core_Module.php', 51 'Transtrix\\Modules\\EDD\\EDD_Customers' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Customers.php', 52 'Transtrix\\Modules\\EDD\\EDD_Discounts' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Discounts.php', 53 'Transtrix\\Modules\\EDD\\EDD_Hints' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Hints.php', 54 'Transtrix\\Modules\\EDD\\EDD_Module' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Module.php', 55 'Transtrix\\Modules\\EDD\\EDD_Orders' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Orders.php', 56 'Transtrix\\Modules\\EDD\\EDD_Rows' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Rows.php', 50 57 'Transtrix\\Modules\\WPTravelEngine\\WPTravelEngine_Hints' => __DIR__ . '/../..' . '/includes/Modules/WPTravelEngine/WPTravelEngine_Hints.php', 51 58 'Transtrix\\Modules\\WPTravelEngine\\WPTravelEngine_Module' => __DIR__ . '/../..' . '/includes/Modules/WPTravelEngine/WPTravelEngine_Module.php', -
transtrix/tags/0.3.0/vendor/composer/installed.php
r3790546 r3802902 2 2 'root' => array( 3 3 'name' => 'aayushk777/transtrix', 4 'pretty_version' => 'v0. 2.0',5 'version' => '0. 2.0.0',6 'reference' => ' 0c34b30edd9bcdfb09c998cb88ce418e687bb8de',4 'pretty_version' => 'v0.3.0', 5 'version' => '0.3.0.0', 6 'reference' => '396d3f7184662acef9e40ff6bbe35d8a691ee2c2', 7 7 'type' => 'wordpress-plugin', 8 8 'install_path' => __DIR__ . '/../../', … … 12 12 'versions' => array( 13 13 'aayushk777/transtrix' => array( 14 'pretty_version' => 'v0. 2.0',15 'version' => '0. 2.0.0',16 'reference' => ' 0c34b30edd9bcdfb09c998cb88ce418e687bb8de',14 'pretty_version' => 'v0.3.0', 15 'version' => '0.3.0.0', 16 'reference' => '396d3f7184662acef9e40ff6bbe35d8a691ee2c2', 17 17 'type' => 'wordpress-plugin', 18 18 'install_path' => __DIR__ . '/../../', -
transtrix/trunk/assets/admin/src/components/SourceLogo.jsx
r3759911 r3802902 26 26 'wp-travel-engine': 'wp-travel-engine.png', 27 27 woocommerce: 'woocommerce.svg', 28 edd: 'easy-digital-downloads.svg', 28 29 }; 29 30 -
transtrix/trunk/dist/admin.asset.php
r3790546 r3802902 1 <?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-date', 'wp-element', 'wp-i18n'), 'version' => ' 3911087dc07eb7c69eee');1 <?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-date', 'wp-element', 'wp-i18n'), 'version' => '0aabe6dbf55be35a8696'); -
transtrix/trunk/dist/admin.js
r3790546 r3802902 549 549 color: #0284c7; 550 550 } 551 `,Ga=Qa,Za={};Za.wordpress=(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("circle",{cx:"36",cy:"36",r:"36",fill:"#fff"}),(0,l.jsx)("g",{transform:"translate(6 6) scale(2.5)",fill:"#21759B",children:(0,l.jsx)("path",{d:"M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11m-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.609-3.582.609M1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.211 12M12 0C5.385 0 0 5.385 0 12s5.385 12 12 12 12-5.385 12-12S18.615 0 12 0"})})]}),Za["wp-core"]=Za.wordpress,Za.core=Za.wordpress,Za["full-site"]=Za.wordpress;const Ya={wptravelengine:"wp-travel-engine.png","wp-travel-engine":"wp-travel-engine.png",woocommerce:"woocommerce.svg" },Xa=({moduleId:e,size:t=72})=>Ya[e]&&Ia.logosUrl?(0,l.jsx)("img",{src:Ia.logosUrl+Ya[e],alt:"",width:t,height:t,style:{display:"block",margin:"0 auto",borderRadius:Math.round(.22*t)}}):!Za[e]&&Ia.siteLogo?(0,l.jsx)("img",{src:Ia.siteLogo,alt:"",width:t,height:t,style:{display:"block",margin:"0 auto",borderRadius:16,objectFit:"contain"}}):Za[e]?(0,l.jsx)("svg",{viewBox:"0 0 72 72",width:t,height:t,"aria-hidden":"true",style:{display:"block",margin:"0 auto"},children:Za[e]}):(0,l.jsx)("div",{style:{width:t,margin:"0 auto"},children:(0,l.jsx)(Ga,{size:t})}),Ja=Qr.div`551 `,Ga=Qa,Za={};Za.wordpress=(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("circle",{cx:"36",cy:"36",r:"36",fill:"#fff"}),(0,l.jsx)("g",{transform:"translate(6 6) scale(2.5)",fill:"#21759B",children:(0,l.jsx)("path",{d:"M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11m-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.609-3.582.609M1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.211 12M12 0C5.385 0 0 5.385 0 12s5.385 12 12 12 12-5.385 12-12S18.615 0 12 0"})})]}),Za["wp-core"]=Za.wordpress,Za.core=Za.wordpress,Za["full-site"]=Za.wordpress;const Ya={wptravelengine:"wp-travel-engine.png","wp-travel-engine":"wp-travel-engine.png",woocommerce:"woocommerce.svg",edd:"easy-digital-downloads.svg"},Xa=({moduleId:e,size:t=72})=>Ya[e]&&Ia.logosUrl?(0,l.jsx)("img",{src:Ia.logosUrl+Ya[e],alt:"",width:t,height:t,style:{display:"block",margin:"0 auto",borderRadius:Math.round(.22*t)}}):!Za[e]&&Ia.siteLogo?(0,l.jsx)("img",{src:Ia.siteLogo,alt:"",width:t,height:t,style:{display:"block",margin:"0 auto",borderRadius:16,objectFit:"contain"}}):Za[e]?(0,l.jsx)("svg",{viewBox:"0 0 72 72",width:t,height:t,"aria-hidden":"true",style:{display:"block",margin:"0 auto"},children:Za[e]}):(0,l.jsx)("div",{style:{width:t,margin:"0 auto"},children:(0,l.jsx)(Ga,{size:t})}),Ja=Qr.div` 552 552 display: flex; 553 553 flex-direction: column; -
transtrix/trunk/includes/Cli/Cli_Command.php
r3790546 r3802902 252 252 * 253 253 * @since 0.1.0 254 * @since 0.3.0 Also reports the orders, customers and codes it deleted. 254 255 * 255 256 * @param array<int,string> $args Positional arguments. … … 277 278 WP_CLI::success( 278 279 sprintf( 279 'Deleted %d posts and %d terms, restored %d posts.',280 'Deleted %d posts, %d terms and %d other objects (orders, customers, codes), restored %d posts.', 280 281 $undone['posts_deleted'], 281 282 $undone['terms_deleted'], 283 $undone['objects_deleted'], 282 284 $undone['posts_restored'] 283 285 ) -
transtrix/trunk/includes/Contracts/Module.php
r3790546 r3802902 212 212 213 213 /** 214 * Top-level upload directories whose files must keep their place. 215 * 216 * A store that sells files keeps them in a directory the web server will 217 * not serve (Easy Digital Downloads' `uploads/edd`). Imported into the 218 * usual dated directory, a paid file would get a public URL. 219 * 220 * @since 0.3.0 221 * 222 * @return string[] 223 */ 224 public function protected_upload_dirs(): array { 225 return array(); 226 } 227 228 /** 214 229 * Last chance to widen or narrow the job before the package is written. 215 230 * -
transtrix/trunk/includes/Import/Restore_Point.php
r3790546 r3802902 137 137 public function record_created_object( string $kind, int $id ): void { 138 138 $this->created_objects[ $kind ][] = $id; 139 } 140 141 /** 142 * Non-post objects this import created so far, kind => ids. 143 * 144 * @since 0.3.0 145 * 146 * @return array<string,int[]> 147 */ 148 public function created_objects(): array { 149 return array_map( 150 static function ( $ids ) { 151 return array_map( 'intval', (array) $ids ); 152 }, 153 $this->created_objects 154 ); 139 155 } 140 156 -
transtrix/trunk/includes/Import/Three_Pass_Importer.php
r3790546 r3802902 101 101 102 102 /** 103 * Source attachment URL => source attachment id, built once per step. 104 * 105 * @var array<string,int>|null 106 */ 107 private $media_urls = null; 108 109 /** 103 110 * Taxonomies the package uses that this site does not register. 104 111 * … … 423 430 424 431 /** 432 * Source attachment URL => source attachment id, for attachment URL hints. 433 * 434 * @since 0.3.0 435 * 436 * @return array<string,int> 437 */ 438 private function media_urls(): array { 439 if ( null === $this->media_urls ) { 440 $this->media_urls = array(); 441 442 foreach ( $this->lists()['media'] as $record ) { 443 if ( ! empty( $record['url'] ) && ! empty( $record['id'] ) ) { 444 $this->media_urls[ (string) $record['url'] ] = (int) $record['id']; 445 } 446 } 447 } 448 449 return $this->media_urls; 450 } 451 452 /** 425 453 * Before the first record: what every later record is keyed by. 426 454 * … … 500 528 * 501 529 * @since 0.2.0 530 * @since 0.3.0 Also hands over the objects import_payload() created. 502 531 * 503 532 * @param Report $report Outcome report. … … 512 541 // one, before the terms that decide it were linked in pass three. 513 542 $this->job->set_state( 'imported_ids', array_values( $this->imported ) ); 543 // And what import_payload() created, kind => ids: an order's stats are 544 // rebuilt once, here, not per order. 545 $this->job->set_state( 'created_objects', $this->restore->created_objects() ); 514 546 $this->module->after_import( $this->job, $report ); 515 547 } … … 682 714 * 683 715 * @since 0.2.0 716 * @since 0.3.0 Keeps the module's protected upload directories. 684 717 * 685 718 * @param array<string,mixed> $record One media record. … … 689 722 private function create_media( array $record, Report $report ): void { 690 723 if ( $this->may_upload() ) { 691 ( new Media_Importer( $this->package->work_dir() ) )->import( array( $record ), $this->map, $report, $this->restore );724 ( new Media_Importer( $this->package->work_dir(), $this->module->protected_upload_dirs() ) )->import( array( $record ), $this->map, $report, $this->restore ); 692 725 } 693 726 } … … 771 804 * 772 805 * @since 0.2.0 806 * @since 0.3.0 Rewrites attachment URLs too. 773 807 * 774 808 * @param array<string,mixed> $record One post record. … … 785 819 $new_id = $this->imported[ $old_id ]; 786 820 $meta = isset( $record['meta'] ) && is_array( $record['meta'] ) ? $record['meta'] : array(); 787 $walker = new Reference_Walker( );821 $walker = new Reference_Walker( $this->media_urls() ); 788 822 $map = $this->map; 789 823 … … 1163 1197 * 1164 1198 * @since 0.1.0 1199 * @since 0.3.0 Reads `<id>_<number>` under a suffixed hint. 1165 1200 * 1166 1201 * @param array<string,mixed> $record One post record. … … 1183 1218 1184 1219 foreach ( (array) $meta[ $parsed['key'] ] as $value ) { 1185 foreach ( Hint_Path::collect_at( $value, $parsed['segments'] ) as $id ) {1220 foreach ( Hint_Path::collect_at( $value, $parsed['segments'], $hint->is_suffixed() ) as $id ) { 1186 1221 if ( ! isset( $in_scope[ $id ] ) ) { 1187 1222 $missing[] = $id; -
transtrix/trunk/includes/Media/Media_Collector.php
r3790546 r3802902 40 40 * @since 0.1.0 41 41 * @since 0.2.0 Also collects the generated sizes and their metadata. 42 * @since 0.3.0 Records the source URL and upload sub-directory. 42 43 * 43 44 * @param int $attachment_id Attachment id. … … 67 68 $filename = wp_basename( $file ); 68 69 $relative_path = 'media/' . $attachment_id . '/' . $filename; 70 $subdir = dirname( (string) get_post_meta( $attachment_id, '_wp_attached_file', true ) ); 69 71 70 72 $this->files[ $relative_path ] = array( … … 85 87 'post_parent' => (int) $post->post_parent, 86 88 'post_date_gmt' => $post->post_date_gmt, 89 // Where it lived, so a link to it can follow it and a protected 90 // directory can keep it. 91 'url' => (string) wp_get_attachment_url( $attachment_id ), 92 'subdir' => '.' === $subdir ? '' : $subdir, 87 93 ); 88 94 -
transtrix/trunk/includes/Media/Media_Importer.php
r3790546 r3802902 38 38 39 39 /** 40 * Top-level upload directories whose files keep their place. 41 * 42 * @var string[] 43 */ 44 private $protected_dirs; 45 46 /** 40 47 * Constructor. 41 48 * 42 49 * @since 0.1.0 43 * 44 * @param string $work_dir Trailing-slashed extraction directory. 45 */ 46 public function __construct( string $work_dir ) { 47 $this->work_dir = trailingslashit( $work_dir ); 50 * @since 0.3.0 Takes $protected_dirs. 51 * 52 * @param string $work_dir Trailing-slashed extraction directory. 53 * @param string[] $protected_dirs Top-level upload directories whose files keep their place. 54 */ 55 public function __construct( string $work_dir, array $protected_dirs = array() ) { 56 $this->work_dir = trailingslashit( $work_dir ); 57 $this->protected_dirs = $protected_dirs; 48 58 } 49 59 … … 52 62 * 53 63 * @since 0.1.0 64 * @since 0.3.0 A protected file matches only a copy in its protected directory. 54 65 * 55 66 * @param array<int,array<string,mixed>> $records Media records from the package. … … 68 79 } 69 80 70 $existing = $this->find_by_checksum( (string) ( $record['sha256'] ?? '' ) );81 $existing = $this->find_by_checksum( (string) ( $record['sha256'] ?? '' ), $this->protected_subdir( $record ) ); 71 82 72 83 if ( $existing > 0 ) { … … 93 104 * 94 105 * @since 0.1.0 106 * @since 0.3.0 A protected file only matches a copy inside the same protected directory. 95 107 * 96 108 * @param string $checksum sha256 of the file. 109 * @param string $subdir Protected sub-directory the file belongs in, or ''. 97 110 * @return int Attachment id, or 0. 98 111 */ 99 private function find_by_checksum( string $checksum ): int {112 private function find_by_checksum( string $checksum, string $subdir = '' ): int { 100 113 if ( '' === $checksum ) { 101 114 return 0; … … 106 119 'post_type' => 'attachment', 107 120 'post_status' => 'any', 108 'posts_per_page' => 1,121 'posts_per_page' => '' === $subdir ? 1 : 20, 109 122 'fields' => 'ids', 110 123 'no_found_rows' => true, … … 115 128 ); 116 129 117 return empty( $found ) ? 0 : (int) $found[0]; 130 if ( '' === $subdir ) { 131 return empty( $found ) ? 0 : (int) $found[0]; 132 } 133 134 // A public copy of a paid file is not a match: linking to it would 135 // hand out the file the protected directory exists to hold back. 136 $top = strtok( $subdir, '/' ) . '/'; 137 138 foreach ( $found as $id ) { 139 if ( 0 === strpos( (string) get_post_meta( (int) $id, '_wp_attached_file', true ), $top ) ) { 140 return (int) $id; 141 } 142 } 143 144 return 0; 145 } 146 147 /** 148 * The upload sub-directory a protected file must keep, or '' for any other file. 149 * 150 * The path comes from the package, so only plain directory names pass; a 151 * protected file with any other path still lands in its protected root. 152 * 153 * @since 0.3.0 154 * 155 * @param array<string,mixed> $record One media record. 156 * @return string 157 */ 158 private function protected_subdir( array $record ): string { 159 $subdir = (string) ( $record['subdir'] ?? '' ); 160 $top = explode( '/', $subdir )[0]; 161 162 if ( '' === $top || ! in_array( $top, $this->protected_dirs, true ) ) { 163 return ''; 164 } 165 166 return 1 === preg_match( '#^[A-Za-z0-9_-]+(/[A-Za-z0-9_-]+)*$#', $subdir ) ? $subdir : $top; 118 167 } 119 168 … … 123 172 * @since 0.1.0 124 173 * @since 0.2.0 Copies the packaged sizes instead of rebuilding them. 174 * @since 0.3.0 Keeps a protected file in its protected directory. 125 175 * 126 176 * @param array<string,mixed> $record One media record. … … 158 208 ); 159 209 160 $upload = wp_handle_sideload( $sideload, array( 'test_form' => false ) ); 210 $subdir = $this->protected_subdir( $record ); 211 $keep_dir = static function ( array $dirs ) use ( $subdir ): array { 212 $dirs['subdir'] = '/' . $subdir; 213 $dirs['path'] = $dirs['basedir'] . $dirs['subdir']; 214 $dirs['url'] = $dirs['baseurl'] . $dirs['subdir']; 215 216 return $dirs; 217 }; 218 219 if ( '' !== $subdir ) { 220 add_filter( 'upload_dir', $keep_dir, PHP_INT_MAX ); 221 } 222 223 try { 224 $upload = wp_handle_sideload( $sideload, array( 'test_form' => false ) ); 225 } finally { 226 remove_filter( 'upload_dir', $keep_dir, PHP_INT_MAX ); 227 } 161 228 162 229 if ( ! empty( $upload['error'] ) || empty( $upload['file'] ) ) { -
transtrix/trunk/includes/Modules/Core/Core_Exporter.php
r3790546 r3802902 186 186 * 187 187 * @since 0.1.0 188 * @since 0.3.0 Reads `<id>_<number>` under a suffixed hint. 188 189 * 189 190 * @param int[] $post_ids Selected post ids. … … 210 211 foreach ( $post_ids as $post_id ) { 211 212 foreach ( get_post_meta( (int) $post_id, $parsed['key'], false ) as $value ) { 212 $ids = array_merge( $ids, Hint_Path::collect_at( $value, $parsed['segments'] ) );213 $ids = array_merge( $ids, Hint_Path::collect_at( $value, $parsed['segments'], $hint->is_suffixed() ) ); 213 214 } 214 215 } … … 310 311 311 312 /** 313 * Attachment ids behind the URLs at a path. 314 * 315 * @since 0.3.0 316 * 317 * @param mixed $value The whole meta value. 318 * @param string[] $segments Path to the URLs. 319 * @return int[] 320 */ 321 private function url_ids( $value, array $segments ): array { 322 $ids = array(); 323 324 Hint_Path::rewrite_at( 325 maybe_unserialize( $value ), 326 $segments, 327 static function ( $url ) use ( &$ids ) { 328 if ( is_string( $url ) && '' !== $url ) { 329 $ids[] = attachment_url_to_postid( $url ); 330 } 331 332 return $url; 333 } 334 ); 335 336 return $ids; 337 } 338 339 /** 312 340 * Attachment ids a post references, through hints and through its content. 313 341 * 314 342 * @since 0.1.0 343 * @since 0.3.0 Follows attachment URL hints too. 315 344 * 316 345 * @param int $post_id Post id. … … 323 352 $hint = Reference_Hint::from( (string) $notation ); 324 353 325 if ( ! $hint instanceof Reference_Hint || Reference_Hint::TYPE_ATTACHMENT !== $hint->type() ) {354 if ( ! $hint instanceof Reference_Hint || ! in_array( $hint->type(), array( Reference_Hint::TYPE_ATTACHMENT, Reference_Hint::TYPE_ATTACHMENT_URL ), true ) ) { 326 355 continue; 327 356 } … … 332 361 333 362 foreach ( get_post_meta( $post_id, $parsed['key'], false ) as $value ) { 334 $ids = array_merge( $ids, Hint_Path::collect_at( $value, $parsed['segments'] ) );363 $ids = array_merge( $ids, Reference_Hint::TYPE_ATTACHMENT_URL === $hint->type() ? $this->url_ids( $value, $parsed['segments'] ) : Hint_Path::collect_at( $value, $parsed['segments'] ) ); 335 364 } 336 365 } -
transtrix/trunk/includes/Modules/Woo/Woo_Orders.php
r3790546 r3802902 8 8 namespace Transtrix\Modules\Woo; 9 9 10 use Transtrix\Import\Account_Importer; 10 11 use Transtrix\Import\Restore_Point; 11 12 use Transtrix\Reference\Id_Map; … … 306 307 * 307 308 * @since 0.1.0 309 * @since 0.3.0 The account fields come from Account_Importer. 308 310 * 309 311 * @param int $user_id User id. … … 311 313 */ 312 314 private static function customer_record( int $user_id ): ?array { 313 $ user = get_userdata( $user_id );314 315 if ( ! $user instanceof \WP_User) {315 $record = Account_Importer::export( $user_id ); 316 317 if ( null === $record ) { 316 318 return null; 317 319 } … … 319 321 $customer = new \WC_Customer( $user_id ); 320 322 321 return array( 322 'id' => $user_id, 323 'user_login' => $user->user_login, 324 'user_email' => $user->user_email, 325 'display_name' => $user->display_name, 326 'first_name' => $user->first_name, 327 'last_name' => $user->last_name, 328 'user_registered' => $user->user_registered, 329 'billing' => $customer->get_billing( 'edit' ), 330 'shipping' => $customer->get_shipping( 'edit' ), 331 'is_paying' => $customer->get_is_paying_customer( 'edit' ), 323 return array_merge( 324 $record, 325 array( 326 'billing' => $customer->get_billing( 'edit' ), 327 'shipping' => $customer->get_shipping( 'edit' ), 328 'is_paying' => $customer->get_is_paying_customer( 'edit' ), 329 ) 332 330 ); 333 331 } … … 396 394 * 397 395 * @since 0.1.0 396 * @since 0.3.0 Matches and creates the account through Account_Importer. 398 397 * 399 398 * @param array<string,mixed> $record Customer record. … … 404 403 */ 405 404 private static function import_customer( array $record, Id_Map $map, Restore_Point $restore, Report $report ): void { 406 $old_id = (int) ( $record['id'] ?? 0 ); 407 $email = sanitize_email( (string) ( $record['user_email'] ?? '' ) ); 408 409 if ( '' === $email ) { 405 $account = Account_Importer::resolve( $record, 'customer', self::KIND_CUSTOMER, $map, $restore, $report ); 406 407 if ( Account_Importer::MATCHED === $account['status'] ) { 408 $report->add_skipped(); 409 } 410 411 if ( Account_Importer::CREATED !== $account['status'] ) { 410 412 return; 411 413 } 412 414 413 $existing = get_user_by( 'email', $email ); 414 415 if ( $existing instanceof \WP_User ) { 416 // A staff account with the same email is not this shopper: tying 417 // imported orders to an administrator would show them as theirs. 418 // Their orders arrive as guest orders, with the email kept. 419 if ( array() !== array_diff( (array) $existing->roles, array( 'customer', 'subscriber' ) ) ) { 420 $report->add_warning( 421 $email, 422 __( 'This email belongs to a staff account here, so its orders were imported as guest orders.', 'transtrix' ) 423 ); 424 return; 425 } 426 427 $map->add( 'user', $old_id, (int) $existing->ID ); 428 $report->add_skipped(); 429 return; 430 } 431 432 $login = sanitize_user( (string) ( $record['user_login'] ?? '' ), true ); 433 $login = '' === $login || username_exists( $login ) ? sanitize_user( strstr( $email, '@', true ) . '-' . wp_generate_password( 4, false, false ), true ) : $login; 434 435 // wp_insert_user() sends no notification. The password is random: hashes 436 // do not travel, so the customer sets a new one with "Lost password". 437 $user_id = wp_insert_user( 438 array( 439 'user_login' => $login, 440 'user_email' => $email, 441 'user_pass' => wp_generate_password( 24 ), 442 'display_name' => (string) ( $record['display_name'] ?? '' ), 443 'first_name' => (string) ( $record['first_name'] ?? '' ), 444 'last_name' => (string) ( $record['last_name'] ?? '' ), 445 'user_registered' => (string) ( $record['user_registered'] ?? '' ), 446 'role' => 'customer', 447 ) 448 ); 449 450 if ( is_wp_error( $user_id ) ) { 451 $report->add_failure( $email, $user_id->get_error_message() ); 452 return; 453 } 454 455 $restore->record_created_object( self::KIND_CUSTOMER, (int) $user_id ); 456 $map->add( 'user', $old_id, (int) $user_id ); 457 458 $customer = new \WC_Customer( (int) $user_id ); 415 $customer = new \WC_Customer( $account['user_id'] ); 459 416 460 417 foreach ( array( 'billing', 'shipping' ) as $type ) { … … 758 715 * 759 716 * @since 0.1.0 717 * @since 0.3.0 Accounts are undone through Account_Importer. 760 718 * 761 719 * @param string $kind KIND_ORDER or KIND_CUSTOMER. … … 777 735 778 736 if ( self::KIND_CUSTOMER === $kind ) { 779 require_once ABSPATH . 'wp-admin/includes/user.php'; 780 781 foreach ( $ids as $id ) { 782 // Kept once the account has a life of its own here: a new role, 783 // content they wrote, or an order placed since the import. 784 // Deleting it would take that with it. 785 if ( self::customer_in_use( (int) $id ) ) { 786 continue; 787 } 788 789 if ( wp_delete_user( $id ) ) { 790 ++$deleted; 791 } 792 } 737 $deleted += Account_Importer::rollback( $ids, 'customer', array( __CLASS__, 'has_orders' ) ); 793 738 } 794 739 … … 797 742 798 743 /** 799 * Whether an imported customer account is now used for more than the import.800 * 801 * @since 0. 1.0744 * Whether an account has an order in any status. 745 * 746 * @since 0.3.0 802 747 * 803 748 * @param int $user_id User id. 804 749 * @return bool 805 750 */ 806 private static function customer_in_use( int $user_id ): bool { 807 $user = get_userdata( $user_id ); 808 809 if ( ! $user instanceof \WP_User ) { 810 return true; 811 } 812 813 if ( array( 'customer' ) !== array_values( (array) $user->roles ) || count_user_posts( $user_id, get_post_types(), false ) > 0 ) { 814 return true; 815 } 816 751 public static function has_orders( int $user_id ): bool { 817 752 return function_exists( 'wc_get_orders' ) && array() !== (array) wc_get_orders( 818 753 array( -
transtrix/trunk/includes/Package/Package_Writer.php
r3790546 r3802902 89 89 90 90 /** 91 * Meta key prefixes that must not be written: entries given as 'prefix*'. 92 * 93 * @var string[] 94 */ 95 private $excluded_meta_prefixes = array(); 96 97 /** 91 98 * Constructor. 92 99 * 93 100 * @since 0.1.0 101 * @since 0.3.0 A trailing `*` on an excluded key excludes every key with that prefix. 94 102 * 95 103 * @param Job $job The export job. … … 97 105 */ 98 106 public function __construct( Job $job, array $excluded_meta_keys = array() ) { 99 $this->job = $job; 100 $this->report = new Report(); 101 $this->media = new Media_Collector(); 102 $this->excluded_meta_keys = array_flip( array_map( 'strval', $excluded_meta_keys ) ); 107 $this->job = $job; 108 $this->report = new Report(); 109 $this->media = new Media_Collector(); 110 111 foreach ( array_map( 'strval', $excluded_meta_keys ) as $key ) { 112 if ( '*' === substr( $key, -1 ) ) { 113 $this->excluded_meta_prefixes[] = substr( $key, 0, -1 ); 114 } else { 115 $this->excluded_meta_keys[ $key ] = 1; 116 } 117 } 103 118 } 104 119 … … 178 193 // A6: a denied key never reaches the package, so it cannot leak by 179 194 // being written, inspected in the zip, or shipped to a third party. 180 if ( isset( $this->excluded_meta_keys[ $key ]) ) {195 if ( $this->is_excluded( (string) $key ) ) { 181 196 continue; 182 197 } … … 360 375 return ( $parent instanceof \WP_Term ) ? $parent->slug : ''; 361 376 } 377 378 /** 379 * Whether the module forbids a meta key, by name or by prefix. 380 * 381 * @since 0.3.0 382 * 383 * @param string $key Meta key. 384 * @return bool 385 */ 386 private function is_excluded( string $key ): bool { 387 if ( isset( $this->excluded_meta_keys[ $key ] ) ) { 388 return true; 389 } 390 391 foreach ( $this->excluded_meta_prefixes as $prefix ) { 392 if ( 0 === strpos( $key, $prefix ) ) { 393 return true; 394 } 395 } 396 397 return false; 398 } 362 399 } -
transtrix/trunk/includes/Reference/Hint_Path.php
r3790546 r3802902 145 145 * 146 146 * @since 0.1.0 147 * @since 0.3.0 Takes $suffixed. 147 148 * 148 149 * @param mixed $value The whole meta value. 149 150 * @param string[] $segments Path segments; empty means the whole value. 151 * @param bool $suffixed Also read `<id>_<number>` strings. 150 152 * @return int[] 151 153 */ 152 public static function collect_at( $value, array $segments ): array {154 public static function collect_at( $value, array $segments, bool $suffixed = false ): array { 153 155 $found = array(); 154 156 … … 156 158 $value, 157 159 $segments, 158 static function ( $leaf ) use ( &$found ) {159 $found = array_merge( $found, self::ids_in( $leaf ) );160 static function ( $leaf ) use ( &$found, $suffixed ) { 161 $found = array_merge( $found, self::ids_in( $leaf, $suffixed ) ); 160 162 161 163 return $leaf; … … 170 172 * 171 173 * @since 0.1.0 172 * 173 * @param mixed $value Any value. 174 * @since 0.3.0 Takes $suffixed. 175 * 176 * @param mixed $value Any value. 177 * @param bool $suffixed Also read `<id>_<number>` strings, as the id. 174 178 * @return int[] 175 179 */ 176 public static function ids_in( $value ): array {180 public static function ids_in( $value, bool $suffixed = false ): array { 177 181 if ( is_string( $value ) && is_serialized( $value ) ) { 178 182 $value = unserialize( $value, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- allowed_classes is false; serialized values are rewritten in place. … … 183 187 184 188 foreach ( $value as $item ) { 185 $ids = array_merge( $ids, self::ids_in( $item ) );189 $ids = array_merge( $ids, self::ids_in( $item, $suffixed ) ); 186 190 } 187 191 … … 205 209 } 206 210 211 if ( $suffixed && 1 === preg_match( '/^(\d+)_\d+$/', $value, $parts ) ) { 212 return array( (int) $parts[1] ); 213 } 214 207 215 return array(); 208 216 } -
transtrix/trunk/includes/Reference/Reference_Hint.php
r3790546 r3802902 26 26 27 27 /** 28 * A URL of an attachment's file, rewritten to the imported attachment's URL. 29 */ 30 const TYPE_ATTACHMENT_URL = 'attachment_url'; 31 32 /** 28 33 * Reference type, one of the TYPE_* constants. 29 34 * … … 52 57 53 58 /** 59 * Whether an id may carry a `_<number>` suffix that is kept as written. 60 * 61 * Easy Digital Downloads names "download 123, price option 2" as `123_2`. 62 * The leading number is the id; the suffix is local to that download. 63 * 64 * @var bool 65 */ 66 private $suffixed = false; 67 68 /** 54 69 * Constructor. 55 70 * 56 71 * @since 0.1.0 72 * @since 0.3.0 Takes $suffixed. 57 73 * 58 74 * @param string $type Reference type. 59 75 * @param bool $is_list Whether the value holds a list of ids. 60 76 * @param string $taxonomy Taxonomy a term hint is confined to, or ''. 61 */ 62 public function __construct( string $type, bool $is_list = false, string $taxonomy = '' ) { 77 * @param bool $suffixed Whether an id may carry a kept `_<number>` suffix. 78 */ 79 public function __construct( string $type, bool $is_list = false, string $taxonomy = '', bool $suffixed = false ) { 63 80 $this->type = $type; 64 81 $this->is_list = $is_list; 65 82 $this->taxonomy = $taxonomy; 83 $this->suffixed = $suffixed; 66 84 } 67 85 … … 70 88 * 71 89 * @since 0.1.0 90 * @since 0.3.0 A trailing `^` on the type ('post^') allows `<id>_<number>`. 72 91 * 73 92 * @param string $notation For example 'post[]' or 'attachment'. … … 77 96 $is_list = ( '[]' === substr( $notation, -2 ) ); 78 97 $type = $is_list ? substr( $notation, 0, -2 ) : $notation; 98 $suffixed = ( '^' === substr( $type, -1 ) ); 99 $type = $suffixed ? substr( $type, 0, -1 ) : $type; 79 100 $taxonomy = ''; 80 101 … … 89 110 self::TYPE_TERM, 90 111 self::TYPE_USER, 112 self::TYPE_ATTACHMENT_URL, 91 113 ); 92 114 … … 99 121 } 100 122 101 return new self( $type, $is_list, $taxonomy );123 return new self( $type, $is_list, $taxonomy, $suffixed ); 102 124 } 103 125 … … 125 147 126 148 /** 149 * Whether an id may carry a kept `_<number>` suffix. 150 * 151 * @since 0.3.0 152 * 153 * @return bool 154 */ 155 public function is_suffixed(): bool { 156 return $this->suffixed; 157 } 158 159 /** 127 160 * The object type used as the Id_Map bucket. 128 161 * … … 131 164 * 132 165 * @since 0.1.0 166 * @since 0.3.0 Attachment URL hints use the post bucket too. 133 167 * 134 168 * @return string 135 169 */ 136 170 public function map_bucket(): string { 137 if ( self::TYPE_ATTACHMENT === $this->type ) {171 if ( self::TYPE_ATTACHMENT === $this->type || self::TYPE_ATTACHMENT_URL === $this->type ) { 138 172 return self::TYPE_POST; 139 173 } -
transtrix/trunk/includes/Reference/Reference_Walker.php
r3790546 r3802902 30 30 31 31 /** 32 * Source attachment URL => source attachment id, for attachment_url hints. 33 * 34 * @var array<string,int> 35 */ 36 private $urls; 37 38 /** 39 * Constructor. 40 * 41 * @since 0.3.0 42 * 43 * @param array<string,int> $urls Source attachment URL => source attachment id. 44 */ 45 public function __construct( array $urls = array() ) { 46 $this->urls = $urls; 47 } 48 49 /** 32 50 * Rewrite every id inside a value. 33 51 * … … 54 72 * 55 73 * @since 0.1.0 74 * @since 0.3.0 Hands attachment URL hints to remap_url(). 56 75 * 57 76 * @param mixed $value Current node. … … 68 87 } 69 88 89 if ( Reference_Hint::TYPE_ATTACHMENT_URL === $hint->type() ) { 90 return is_string( $value ) ? $this->remap_url( $value, $hint, $map ) : $value; 91 } 92 70 93 if ( is_int( $value ) ) { 71 94 return $this->remap( $value, $hint, $map ); … … 84 107 * 85 108 * @since 0.1.0 109 * @since 0.3.0 Reads `<id>_<number>` under a suffixed hint. 86 110 * 87 111 * @param string $value Current node. … … 121 145 if ( '' !== $value && ctype_digit( $value ) ) { 122 146 return (string) $this->remap( (int) $value, $hint, $map ); 147 } 148 149 if ( $hint->is_suffixed() && 1 === preg_match( '/^(\d+)(_\d+)$/', $value, $parts ) ) { 150 return $this->remap( (int) $parts[1], $hint, $map ) . $parts[2]; 123 151 } 124 152 … … 140 168 141 169 /** 170 * Point a source attachment URL at the imported attachment's URL. 171 * 172 * A URL the package carries no attachment for (an external file, a path 173 * typed by hand) is not a reference and is left alone. 174 * 175 * @since 0.3.0 176 * 177 * @param string $url Source-site URL. 178 * @param Reference_Hint $hint Reference hint. 179 * @param Id_Map $map The id map. 180 * @return string 181 */ 182 private function remap_url( string $url, Reference_Hint $hint, Id_Map $map ): string { 183 if ( ! isset( $this->urls[ $url ] ) ) { 184 return $url; 185 } 186 187 $old = (int) $this->urls[ $url ]; 188 $new = $this->remap( $old, $hint, $map ); 189 190 if ( $new === $old && ! $map->has( $hint->map_bucket(), $old ) ) { 191 return $url; 192 } 193 194 $new_url = wp_get_attachment_url( $new ); 195 196 return is_string( $new_url ) && '' !== $new_url ? $new_url : $url; 197 } 198 199 /** 142 200 * Translate one id, recording it when the map has no entry. 143 201 * -
transtrix/trunk/languages/transtrix.pot
r3790546 r3802902 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Transtrix – Site Migration, Import & Export 0. 2.0\n"5 "Project-Id-Version: Transtrix – Site Migration, Import & Export 0.3.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/transtrix\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-11- 08T06:19:00+00:00\n"12 "POT-Creation-Date: 2026-11-15T08:23:26+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 272 272 #: dist/admin.js:979 273 273 msgid "Installed plugins, so the new site has the same features." 274 msgstr "" 275 276 #: includes/Import/Account_Importer.php:73 277 msgid "This email belongs to a staff account here, so its orders were imported as guest orders." 274 278 msgstr "" 275 279 … … 282 286 283 287 #. translators: %s: comma-separated content type names. 284 #: includes/Import/Three_Pass_Importer.php:4 38288 #: includes/Import/Three_Pass_Importer.php:466 285 289 #, php-format 286 290 msgid "Not imported, because this site has no place to store them: %s." 287 291 msgstr "" 288 292 289 #: includes/Import/Three_Pass_Importer.php:4 47293 #: includes/Import/Three_Pass_Importer.php:475 290 294 msgid "Orders, customers and store settings in this package were not imported. Only an administrator can import them." 291 295 msgstr "" 292 296 293 #: includes/Import/Three_Pass_Importer.php:4 52297 #: includes/Import/Three_Pass_Importer.php:480 294 298 #: dist/admin.js:979 295 299 #: dist/admin.js:1135 … … 297 301 msgstr "" 298 302 299 #: includes/Import/Three_Pass_Importer.php:4 52303 #: includes/Import/Three_Pass_Importer.php:480 300 304 msgid "Images and files were left out: your account cannot upload files." 301 305 msgstr "" 302 306 303 307 #. translators: %s: plugin or module name, e.g. WooCommerce. 304 #: includes/Import/Three_Pass_Importer.php: 591308 #: includes/Import/Three_Pass_Importer.php:623 305 309 #, php-format 306 310 msgid "This package needs %s, which is not active on this site. Install and activate it, then import again. Nothing has been changed." … … 308 312 309 313 #. translators: %s: comma-separated content type names. 310 #: includes/Import/Three_Pass_Importer.php:6 28314 #: includes/Import/Three_Pass_Importer.php:660 311 315 #, php-format 312 316 msgid "This package contains content types it is not allowed to import: %s. Nothing has been changed." … … 314 318 315 319 #. translators: %s: comma-separated content type names. 316 #: includes/Import/Three_Pass_Importer.php:6 36320 #: includes/Import/Three_Pass_Importer.php:668 317 321 #, php-format 318 322 msgid "This site has no place to store these content types: %s. Activate the plugin that adds them, then import again. Nothing has been changed." 319 323 msgstr "" 320 324 321 #: includes/Import/Three_Pass_Importer.php:7 31325 #: includes/Import/Three_Pass_Importer.php:764 322 326 msgid "You are not allowed to create or change this item." 323 327 msgstr "" 324 328 325 #: includes/Import/Three_Pass_Importer.php:8 19329 #: includes/Import/Three_Pass_Importer.php:853 326 330 msgid "This hidden field was left out: only an administrator can import it." 327 331 msgstr "" 328 332 329 333 #. translators: %s: comma-separated list of ids. 330 #: includes/Import/Three_Pass_Importer.php:8 57334 #: includes/Import/Three_Pass_Importer.php:891 331 335 #, php-format 332 336 msgid "These linked items could not be found: %s" 333 337 msgstr "" 334 338 335 #: includes/Import/Three_Pass_Importer.php:10 30339 #: includes/Import/Three_Pass_Importer.php:1064 336 340 msgid "This site has no place for these categories, so they were not added. Activate the plugin that adds them, then import again." 337 341 msgstr "" 338 342 339 #: includes/Media/Media_Collector.php:5 0343 #: includes/Media/Media_Collector.php:51 340 344 msgid "This item is not a media file." 341 345 msgstr "" 342 346 343 #: includes/Media/Media_Collector.php:5 6347 #: includes/Media/Media_Collector.php:57 344 348 msgid "The media file is missing from the server." 345 349 msgstr "" 346 350 347 #: includes/Media/Media_Collector.php:6 4351 #: includes/Media/Media_Collector.php:65 348 352 msgid "The media file could not be read." 349 353 msgstr "" 350 354 351 #: includes/Media/Media_Importer.php: 66355 #: includes/Media/Media_Importer.php:77 352 356 msgid "(unnamed media)" 353 357 msgstr "" 354 358 355 #: includes/Media/Media_Importer.php: 66359 #: includes/Media/Media_Importer.php:77 356 360 msgid "This media item has no ID in the package." 357 361 msgstr "" 358 362 359 #: includes/Media/Media_Importer.php:1 34363 #: includes/Media/Media_Importer.php:184 360 364 msgid "The package does not contain this media file." 361 365 msgstr "" 362 366 363 #: includes/Media/Media_Importer.php:1 41367 #: includes/Media/Media_Importer.php:191 364 368 msgid "This media file is not a type this site accepts." 365 369 msgstr "" 366 370 367 #: includes/Media/Media_Importer.php: 163371 #: includes/Media/Media_Importer.php:230 368 372 msgid "The media file could not be saved." 369 373 msgstr "" … … 380 384 msgid "Posts, pages and media, with their categories and tags." 381 385 msgstr "" 386 387 #: includes/Modules/EDD/EDD_Customers.php:209 388 msgid "Easy Digital Downloads could not save this customer." 389 msgstr "" 390 391 #. translators: %s: discount code. 392 #: includes/Modules/EDD/EDD_Discounts.php:190 393 #, php-format 394 msgid "Discount code %s" 395 msgstr "" 396 397 #: includes/Modules/EDD/EDD_Discounts.php:214 398 msgid "Easy Digital Downloads could not save this discount code." 399 msgstr "" 400 401 #. translators: %s: comma-separated ids. 402 #: includes/Modules/EDD/EDD_Discounts.php:235 403 #, php-format 404 msgid "Some downloads or categories this code is limited to were not in the package, so those limits were removed: %s" 405 msgstr "" 406 407 #: includes/Modules/EDD/EDD_Module.php:73 408 #: includes/Modules/EDD/EDD_Module.php:102 409 msgid "Easy Digital Downloads" 410 msgstr "" 411 412 #. translators: %s: Easy Digital Downloads version. 413 #: includes/Modules/EDD/EDD_Module.php:101 414 #, php-format 415 msgid "Easy Digital Downloads v%s" 416 msgstr "" 417 418 #: includes/Modules/EDD/EDD_Module.php:113 419 msgid "Downloads with their prices, files and bundles, plus categories, tags and discount codes. Orders, refunds and customers can come too; importing them never charges or emails anyone. Files for sale stay protected on the new site." 420 msgstr "" 421 422 #: includes/Modules/EDD/EDD_Module.php:204 423 msgid "Discount codes" 424 msgstr "" 425 426 #: includes/Modules/EDD/EDD_Module.php:207 427 #: includes/Modules/Woo/Woo_Module.php:327 428 msgid "Orders" 429 msgstr "" 430 431 #: includes/Modules/EDD/EDD_Module.php:208 432 #: includes/Modules/Woo/Woo_Module.php:328 433 msgid "Customers" 434 msgstr "" 435 436 #: includes/Modules/EDD/EDD_Module.php:347 437 msgid "Easy Digital Downloads has a database upgrade to finish on this site. Open Downloads in the dashboard, finish the upgrade it asks for, then import again. Nothing has been changed." 438 msgstr "" 439 440 #. translators: %s: comma-separated names of Easy Digital Downloads data tables. 441 #: includes/Modules/EDD/EDD_Module.php:367 442 #, php-format 443 msgid "Easy Digital Downloads is active, but its setup is incomplete: some of its data tables are missing (%s). Deactivate and reactivate Easy Digital Downloads, then import again. Nothing has been changed." 444 msgstr "" 445 446 #. translators: %d: order number on the source site. 447 #: includes/Modules/EDD/EDD_Orders.php:277 448 #: includes/Modules/Woo/Woo_Orders.php:444 449 #, php-format 450 msgid "Order #%d" 451 msgstr "" 452 453 #: includes/Modules/EDD/EDD_Orders.php:306 454 msgid "Easy Digital Downloads could not save this order." 455 msgstr "" 456 457 #: includes/Modules/EDD/EDD_Orders.php:310 458 msgid "A refund of this order could not be saved." 459 msgstr "" 460 461 #. translators: %d: number of order lines. 462 #: includes/Modules/EDD/EDD_Orders.php:373 463 #, php-format 464 msgid "%d line refers to a download that was not in this package, so it keeps its name and price but links to no download." 465 msgid_plural "%d lines refer to downloads that were not in this package, so they keep their names and prices but link to no download." 466 msgstr[0] "" 467 msgstr[1] "" 382 468 383 469 #: includes/Modules/Woo/Woo_Module.php:78 … … 402 488 msgstr "" 403 489 404 #: includes/Modules/Woo/Woo_Module.php:327405 msgid "Orders"406 msgstr ""407 408 #: includes/Modules/Woo/Woo_Module.php:328409 msgid "Customers"410 msgstr ""411 412 #: includes/Modules/Woo/Woo_Orders.php:422413 msgid "This email belongs to a staff account here, so its orders were imported as guest orders."414 msgstr ""415 416 #. translators: %d: order number on the source site.417 #: includes/Modules/Woo/Woo_Orders.php:487418 #, php-format419 msgid "Order #%d"420 msgstr ""421 422 490 #. translators: %d: number of order lines. 423 #: includes/Modules/Woo/Woo_Orders.php:5 49491 #: includes/Modules/Woo/Woo_Orders.php:506 424 492 #, php-format 425 493 msgid "%d line refers to a product that was not in this package, so it keeps its name and price but links to no product." … … 502 570 msgstr "" 503 571 504 #: includes/Package/Package_Writer.php:1 67572 #: includes/Package/Package_Writer.php:182 505 573 msgid "This item no longer exists on this site." 506 574 msgstr "" 507 575 508 #: includes/Package/Package_Writer.php:2 77509 #: includes/Package/Package_Writer.php:2 81510 #: includes/Package/Package_Writer.php: 288511 #: includes/Package/Package_Writer.php:3 13512 #: includes/Package/Package_Writer.php:3 38576 #: includes/Package/Package_Writer.php:292 577 #: includes/Package/Package_Writer.php:296 578 #: includes/Package/Package_Writer.php:303 579 #: includes/Package/Package_Writer.php:328 580 #: includes/Package/Package_Writer.php:353 513 581 msgid "The package could not be written. Check that the uploads folder has free space." 514 582 msgstr "" 515 583 516 #: includes/Package/Package_Writer.php:3 20584 #: includes/Package/Package_Writer.php:335 517 585 msgid "The package could not be created. Check that the uploads folder is writable." 518 586 msgstr "" -
transtrix/trunk/package.json
r3790546 r3802902 1 1 { 2 2 "name": "transtrix", 3 "version": "0. 2.0",3 "version": "0.3.0", 4 4 "description": "Admin app for Transtrix.", 5 5 "license": "GPL-2.0-or-later", -
transtrix/trunk/readme.txt
r3790546 r3802902 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0. 2.07 Stable tag: 0.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 63 63 * **WP Travel Engine:** trips with their packages, coupons, extra services and accommodations, plus the destinations, activities, trip types, difficulty levels and trip tags they use. Bookings, customers, payments and enquiries are ticked too, so untick them if you want only the trips. When you bring bookings, Transtrix recounts the seats sold for each trip on the new site. 64 64 * **WooCommerce:** products with their variations, images and attributes, plus coupons, categories, tags and shipping classes. Orders, refunds and customers can come too, or you can untick them. An imported order arrives as a record of a past sale: Transtrix charges no card, sends no email, leaves stock alone and keeps the original totals. It works with High-Performance Order Storage on or off. 65 * **Easy Digital Downloads:** downloads with their prices, files and images, plus bundles, discount codes, categories and tags. Paid files stay in the protected Easy Digital Downloads folder on the new site. Orders, refunds and customers can come too, or you can untick them. An imported order arrives as a record of a past sale: Transtrix charges no card, sends no receipt, leaves discount code usage alone and keeps the original totals. Transtrix then recounts sales and earnings for each download and customer on the new site. Needs Easy Digital Downloads 3.0 or newer. 65 66 66 67 Transtrix shows a content type only when the plugin behind it is active. … … 148 149 = Can I leave bookings, orders and customer data out? = 149 150 150 Yes. Every content type starts ticked, so untick Bookings, Customers, Payments and Enquiries in WP Travel Engine, or Orders and Customers in WooCommerce , before you build the package.151 Yes. Every content type starts ticked, so untick Bookings, Customers, Payments and Enquiries in WP Travel Engine, or Orders and Customers in WooCommerce or Easy Digital Downloads, before you build the package. 151 152 152 153 = Can I undo a selected-content import? = … … 154 155 Yes. Open **History**, find the import and choose **Undo this import**. Transtrix removes what it added and puts back what it changed. Restore points last 30 days by default. 155 156 156 = Do imported WooCommerce or ders charge customers or send emails? =157 158 No. Transtrix calls no payment gateway, blocks outgoing mail during the import, and does not reduce stock again.157 = Do imported WooCommerce or Easy Digital Downloads orders charge customers or send emails? = 158 159 No. Transtrix calls no payment gateway, blocks outgoing mail during the import, and does not reduce stock or use up discount codes again. 159 160 160 161 = My export is large. Will it time out? = … … 182 183 183 184 == Changelog == 185 186 = 0.3.0 - 15th November 2026 = 187 * New Feature: Added migration for Easy Digital Downloads downloads, bundles and discount codes, with their files, images, categories and tags. 188 * New Feature: Added the option to include Easy Digital Downloads orders, refunds and customers. Imported orders charge no card, send no receipt and keep the original totals. 184 189 185 190 = 0.2.0 - 8th November 2026 = … … 201 206 == Upgrade Notice == 202 207 208 = 0.3.0 = 209 Adds Easy Digital Downloads: move downloads, files and discount codes, and optionally orders, refunds and customers. Update both the source and destination sites. 210 203 211 = 0.2.0 = 204 212 Faster imports that bring image sizes along, no 500-item export limit, and fixes for large uploads and imports that timed out. Update both the source and destination sites. -
transtrix/trunk/transtrix.php
r3790546 r3802902 4 4 * Plugin URI: https://transtrix.com/ 5 5 * Description: Migrate a whole WordPress site, or move only the pages, posts, trips and products you pick, with their images, categories and links. Works with WP Travel Engine and WooCommerce. 6 * Version: 0. 2.06 * Version: 0.3.0 7 7 * Requires at least: 6.6 8 8 * Requires PHP: 7.4 … … 18 18 defined( 'ABSPATH' ) || exit; 19 19 20 define( 'TRANSTRIX_VERSION', '0. 2.0' );20 define( 'TRANSTRIX_VERSION', '0.3.0' ); 21 21 define( 'TRANSTRIX_FILE', __FILE__ ); 22 22 define( 'TRANSTRIX_PATH', plugin_dir_path( __FILE__ ) ); -
transtrix/trunk/vendor/composer/autoload_classmap.php
r3790546 r3802902 24 24 'Transtrix\\Fullsite\\Serialized_Replacer' => $baseDir . '/includes/Fullsite/Serialized_Replacer.php', 25 25 'Transtrix\\Fullsite\\Tasks' => $baseDir . '/includes/Fullsite/Tasks.php', 26 'Transtrix\\Import\\Account_Importer' => $baseDir . '/includes/Import/Account_Importer.php', 26 27 'Transtrix\\Import\\Conflict_Resolver' => $baseDir . '/includes/Import/Conflict_Resolver.php', 27 28 'Transtrix\\Import\\Import_Task' => $baseDir . '/includes/Import/Import_Task.php', … … 33 34 'Transtrix\\Modules\\Core\\Core_Importer' => $baseDir . '/includes/Modules/Core/Core_Importer.php', 34 35 'Transtrix\\Modules\\Core\\Core_Module' => $baseDir . '/includes/Modules/Core/Core_Module.php', 36 'Transtrix\\Modules\\EDD\\EDD_Customers' => $baseDir . '/includes/Modules/EDD/EDD_Customers.php', 37 'Transtrix\\Modules\\EDD\\EDD_Discounts' => $baseDir . '/includes/Modules/EDD/EDD_Discounts.php', 38 'Transtrix\\Modules\\EDD\\EDD_Hints' => $baseDir . '/includes/Modules/EDD/EDD_Hints.php', 39 'Transtrix\\Modules\\EDD\\EDD_Module' => $baseDir . '/includes/Modules/EDD/EDD_Module.php', 40 'Transtrix\\Modules\\EDD\\EDD_Orders' => $baseDir . '/includes/Modules/EDD/EDD_Orders.php', 41 'Transtrix\\Modules\\EDD\\EDD_Rows' => $baseDir . '/includes/Modules/EDD/EDD_Rows.php', 35 42 'Transtrix\\Modules\\WPTravelEngine\\WPTravelEngine_Hints' => $baseDir . '/includes/Modules/WPTravelEngine/WPTravelEngine_Hints.php', 36 43 'Transtrix\\Modules\\WPTravelEngine\\WPTravelEngine_Module' => $baseDir . '/includes/Modules/WPTravelEngine/WPTravelEngine_Module.php', -
transtrix/trunk/vendor/composer/autoload_static.php
r3790546 r3802902 39 39 'Transtrix\\Fullsite\\Serialized_Replacer' => __DIR__ . '/../..' . '/includes/Fullsite/Serialized_Replacer.php', 40 40 'Transtrix\\Fullsite\\Tasks' => __DIR__ . '/../..' . '/includes/Fullsite/Tasks.php', 41 'Transtrix\\Import\\Account_Importer' => __DIR__ . '/../..' . '/includes/Import/Account_Importer.php', 41 42 'Transtrix\\Import\\Conflict_Resolver' => __DIR__ . '/../..' . '/includes/Import/Conflict_Resolver.php', 42 43 'Transtrix\\Import\\Import_Task' => __DIR__ . '/../..' . '/includes/Import/Import_Task.php', … … 48 49 'Transtrix\\Modules\\Core\\Core_Importer' => __DIR__ . '/../..' . '/includes/Modules/Core/Core_Importer.php', 49 50 'Transtrix\\Modules\\Core\\Core_Module' => __DIR__ . '/../..' . '/includes/Modules/Core/Core_Module.php', 51 'Transtrix\\Modules\\EDD\\EDD_Customers' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Customers.php', 52 'Transtrix\\Modules\\EDD\\EDD_Discounts' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Discounts.php', 53 'Transtrix\\Modules\\EDD\\EDD_Hints' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Hints.php', 54 'Transtrix\\Modules\\EDD\\EDD_Module' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Module.php', 55 'Transtrix\\Modules\\EDD\\EDD_Orders' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Orders.php', 56 'Transtrix\\Modules\\EDD\\EDD_Rows' => __DIR__ . '/../..' . '/includes/Modules/EDD/EDD_Rows.php', 50 57 'Transtrix\\Modules\\WPTravelEngine\\WPTravelEngine_Hints' => __DIR__ . '/../..' . '/includes/Modules/WPTravelEngine/WPTravelEngine_Hints.php', 51 58 'Transtrix\\Modules\\WPTravelEngine\\WPTravelEngine_Module' => __DIR__ . '/../..' . '/includes/Modules/WPTravelEngine/WPTravelEngine_Module.php', -
transtrix/trunk/vendor/composer/installed.php
r3790546 r3802902 2 2 'root' => array( 3 3 'name' => 'aayushk777/transtrix', 4 'pretty_version' => 'v0. 2.0',5 'version' => '0. 2.0.0',6 'reference' => ' 0c34b30edd9bcdfb09c998cb88ce418e687bb8de',4 'pretty_version' => 'v0.3.0', 5 'version' => '0.3.0.0', 6 'reference' => '396d3f7184662acef9e40ff6bbe35d8a691ee2c2', 7 7 'type' => 'wordpress-plugin', 8 8 'install_path' => __DIR__ . '/../../', … … 12 12 'versions' => array( 13 13 'aayushk777/transtrix' => array( 14 'pretty_version' => 'v0. 2.0',15 'version' => '0. 2.0.0',16 'reference' => ' 0c34b30edd9bcdfb09c998cb88ce418e687bb8de',14 'pretty_version' => 'v0.3.0', 15 'version' => '0.3.0.0', 16 'reference' => '396d3f7184662acef9e40ff6bbe35d8a691ee2c2', 17 17 'type' => 'wordpress-plugin', 18 18 'install_path' => __DIR__ . '/../../',
Note:
See TracChangeset
for help on using the changeset viewer.