-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.php
More file actions
1422 lines (1237 loc) · 70.6 KB
/
app.php
File metadata and controls
1422 lines (1237 loc) · 70.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/*
Plugin Name: MDMAP
Description: Point any extra domain or subdomain at a WordPress page, post, or archive — without redirects. The mapped domain always stays in the visitor's address bar.
Version: 2.0
Author: Jeff Caldwell
License: GPL3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
Text Domain: mdmap_app
Domain Path: /languages
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
// If this file is called directly, abort.
if( !defined( 'ABSPATH' ) ){
die('...');
}
// support for older php versions
if( !defined( 'PHP_INT_MIN' ) ){
define('PHP_INT_MIN', ~PHP_INT_MAX);
}
if( !class_exists( 'MultipleDomainMapper' ) ){
class MultipleDomainMapper{
//The unique instance of the plugin.
private static $instance;
//Gets an instance of our plugin.
public static function get_instance(){
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
//variables
private $mappings = false;
private $settings = false;
private $originalRequestURI = false;
private $currentURI = false;
private $currentMapping = array(
'match' => false,
'factor' => PHP_INT_MIN
);
private $saveMappingsButtonDisabled = false;
private $pluginVersion = '2.0';
private $pluginBasename;
private $menuHookSuffix;
private $homeURLMatchLength;
private $siteHost;
private $mappedHost = null;
//constructor
private function __construct(){
$this->pluginBasename = plugin_basename(__FILE__);
$this->homeURLMatchLength = strlen(str_ireplace('http://', '', str_ireplace('https://', '', str_ireplace('www.', '', get_home_url()))));
$this->siteHost = parse_url(get_site_url(), PHP_URL_HOST);
//retrieve options
$this->setMappings(get_option('mdmap_app_mappings'));
$this->setSettings(get_option('mdmap_app_settings'));
//backend
add_action( 'plugins_loaded', array( $this, 'set_textdomain' ) );
add_action( 'admin_menu', array( $this, 'add_menu_page' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
//set current uri
$phpServerVar = (!empty($this->getSettings()) && isset($this->getSettings()['php_server'])) ? $this->getSettings()['php_server'] : 'HTTP_HOST';
// Fall back to HTTP_HOST when SERVER_NAME doesn't reflect the actual requested host (e.g. behind a proxy or with domain aliases)
if( $phpServerVar === 'SERVER_NAME' && !empty($_SERVER['HTTP_HOST']) && $_SERVER['HTTP_HOST'] !== $_SERVER['SERVER_NAME'] ){
$phpServerVar = 'HTTP_HOST';
}
$this->setCurrentURI($_SERVER[$phpServerVar] . $_SERVER['REQUEST_URI']);
//process request
add_filter( 'do_parse_request', array( $this, 'parse_request' ), 10, 3 );
add_filter( 'redirect_canonical', array( $this, 'check_canonical_redirect' ), 10, 2 );
add_action( 'template_redirect', array( $this, 'handle_redirect' ), 1 );
//some hooks to change occurences of orignal domain to mapped domain
$this->replace_uris();
//hook some stuff into our own actions
add_action( 'plugins_loaded', array( $this, 'hookMDMAction'), 20);
//html head
add_action('wp_head', array( $this, 'output_custom_head_code' ), 20);
//canonical tag, noindex + per-mapping tracking snippet
add_action('wp_head', array( $this, 'output_canonical_tag' ), 1);
add_action('wp_head', array( $this, 'output_noindex_tag' ), 2);
add_action('wp_head', array( $this, 'output_tracking_snippet' ), 5);
//admin bar badge showing the active mapped domain
add_action('admin_bar_menu', array( $this, 'admin_bar_badge' ), 100);
//open graph url replacement (Yoast + RankMath)
add_filter('wpseo_opengraph_url', array( $this, 'replace_og_url' ), 10);
add_filter('rank_math/opengraph/facebook/og_url', array( $this, 'replace_og_url' ), 10);
//per-mapping site name / tagline / og image overrides (only fire when on a mapped domain)
add_filter('pre_option_blogname', array( $this, 'override_blogname' ));
add_filter('pre_option_blogdescription', array( $this, 'override_blogdescription' ));
add_filter('wpseo_replacements', array( $this, 'override_yoast_replacements' ));
add_filter('wpseo_opengraph_site_name', array( $this, 'override_og_site_name' ));
add_filter('rank_math/opengraph/facebook/og_site_name', array( $this, 'override_og_site_name' ));
add_filter('wpseo_opengraph_image', array( $this, 'override_og_image' ));
add_filter('rank_math/opengraph/facebook/og_image', array( $this, 'override_og_image' ));
add_filter('wpseo_twitter_image', array( $this, 'override_og_image' ));
add_filter('rank_math/opengraph/twitter/twitter_image', array( $this, 'override_og_image' ));
//canonical url replacement for SEO plugins (we suppress our own tag when one of these is active)
add_filter('wpseo_canonical', array( $this, 'replace_canonical' ), 10);
add_filter('rank_math/frontend/canonical', array( $this, 'replace_canonical' ), 10);
//keep home/search links on the mapped domain (front-end of a mapped page only; self-guarded)
add_filter('home_url', array( $this, 'replace_home_url' ), 10, 3);
//rest api response domain replacement
add_filter('rest_post_dispatch', array( $this, 'rest_response_replace' ), 10, 3);
//flush all supported page caches when mappings or settings change
add_action('updated_option', array( $this, 'maybe_flush_caches' ), 10, 3);
//per-mapping robots.txt sitemap override
add_filter('robots_txt', array( $this, 'filter_robots_txt' ), 10, 2);
//ajax endpoints
add_action('wp_ajax_mdmap_health_check', array( $this, 'ajax_health_check' ));
add_action('wp_ajax_mdmap_export_mappings', array( $this, 'ajax_export_mappings' ));
add_action('wp_ajax_mdmap_import_mappings', array( $this, 'ajax_import_mappings' ));
//settings link on the plugins list row
add_filter('plugin_action_links_' . $this->pluginBasename, array( $this, 'add_settings_link' ));
}
//setters/getters
private function setMappings($mappings){
$this->mappings = $mappings;
}
public function getMappings(){
return $this->mappings;
}
private function setSettings($settings){
$this->settings = $settings;
}
public function getSettings(){
return $this->settings;
}
private function setCurrentURI($uri){
// Strip port from host portion (HTTP_HOST can include port e.g. example.com:8080)
$uri = preg_replace('/^([^\/]+):\d+(\/|$)/', '$1$2', $uri);
$this->currentURI = trailingslashit( $uri );
}
public function getCurrentURI(){
return $this->currentURI;
}
private function setCurrentMapping($mapping){
$this->currentMapping = $mapping;
$this->mappedHost = !empty($mapping['match']['domain'])
? (parse_url('dummyprotocol://' . $mapping['match']['domain'], PHP_URL_HOST) ?? $mapping['match']['domain'])
: null;
}
public function getCurrentMapping(){
return $this->currentMapping;
}
private function setOriginalRequestURI($uri){
$this->originalRequestURI = $uri;
}
public function getOriginalRequestURI(){
return $this->originalRequestURI;
}
public function getOriginalURI(){
global $wp;
return home_url( $wp->request );
}
//set textdomain
public function set_textdomain(){
load_plugin_textdomain( 'mdmap_app', false, dirname( $this->pluginBasename ) . '/languages/' );
}
//enqueue scripts and styles in admin
public function admin_scripts($hook){
if($hook !== $this->menuHookSuffix) return;
//custom assets
wp_enqueue_style( 'mdmap_app_adminstyle', plugin_dir_url( __FILE__ ) . 'assets/css/admin.css', array(), $this->pluginVersion );
wp_register_script( 'mdmap_app_adminscript', plugin_dir_url( __FILE__ ) . 'assets/js/admin.js', array('jquery', 'jquery-ui-accordion', 'jquery-ui-sortable'), $this->pluginVersion, true );
wp_localize_script( 'mdmap_app_adminscript', 'localizedObj', array(
'removedMessage' => esc_html__('This mapping will be deleted when you save. Click Undo to keep it.', 'mdmap_app'),
'undoMessage' => esc_html__('Undo', 'mdmap_app'),
'dismissMessage' => __( 'Dismiss this notice.', 'mdmap_app' ),
'healthOk' => esc_html__('Reachable', 'mdmap_app'),
'healthFail' => esc_html__('Unreachable', 'mdmap_app'),
'healthError' => esc_html__('Check failed', 'mdmap_app'),
'exportLabel' => esc_html__('Export JSON', 'mdmap_app'),
'importSuccess' => esc_html__('Mappings imported! Reloading…', 'mdmap_app'),
'importError' => esc_html__('Import failed — please check the file and try again.', 'mdmap_app'),
'ajaxUrl' => admin_url('admin-ajax.php'),
'healthNonce' => wp_create_nonce('mdmap_health_check'),
'exportNonce' => wp_create_nonce('mdmap_export'),
'importNonce' => wp_create_nonce('mdmap_import'),
) );
wp_enqueue_script( 'mdmap_app_adminscript' );
}
//generate menu entry
public function add_menu_page(){
// check user capabilities
if (!current_user_can('manage_options')) {
return;
}
$this->menuHookSuffix = add_submenu_page( 'tools.php', esc_html__('MDMAP', 'mdmap_app'), esc_html__('MDMAP', 'mdmap_app'), 'manage_options', $this->pluginBasename, array( $this, 'output_menu_page') );
$this->register_settings();
}
//add a "Settings" link to the plugin's row on the Plugins screen
public function add_settings_link($links){
$url = admin_url('tools.php?page=' . $this->pluginBasename);
array_unshift($links, '<a href="' . esc_url($url) . '">' . esc_html__('Settings', 'mdmap_app') . '</a>');
return $links;
}
//generate menu page output
public function output_menu_page(){
// check user capabilities
if (!current_user_can('manage_options')) {
return;
}
//find out active tab
$valid_tabs = array('settings', 'advanced', 'help');
$raw_tab = isset($_GET['tab']) ? $_GET['tab'] : '';
$active_tab = in_array($raw_tab, $valid_tabs, true) ? $raw_tab : 'mappings';
$active_tab_name = $active_tab === 'mappings' ? esc_html__('Mappings', 'mdmap_app') : ucfirst($active_tab);
echo '<div class="wrap mdmap_app_wrap">';
//page title
echo '<h1>' . get_admin_page_title() . '</h1>';
//updated notices
if ( isset( $_GET['settings-updated'] ) ) {
add_settings_error( 'mdmap_app_messages', 'mdmap_app_message', sprintf(esc_html__( '%s saved successfully', 'mdmap_app' ), esc_html($active_tab_name)), 'updated' );
}
settings_errors( 'mdmap_app_messages' );
//page intro
echo '<p>' . esc_html__('Point any extra domain or subdomain at a page, post, or archive on this site — no redirects. Visitors always see the mapped domain in their address bar. New here? Check the Help tab for setup instructions.', 'mdmap_app') . '</p>';
//tabs
echo '<h2 class="nav-tab-wrapper">';
echo '<a href="?page='. $this->pluginBasename .'&tab=mappings" class="nav-tab ' . ($active_tab == 'mappings' ? 'nav-tab-active ' : '') . '">' . esc_html__('Mappings', 'mdmap_app') . '</a>';
echo '<a href="?page='. $this->pluginBasename .'&tab=settings" class="nav-tab ' . ($active_tab == 'settings' ? 'nav-tab-active ' : '') . '">' . esc_html__('Settings', 'mdmap_app') . '</a>';
echo '<a href="?page='. $this->pluginBasename .'&tab=advanced" class="nav-tab nav-tab-featured ' . ($active_tab == 'advanced' ? 'nav-tab-active ' : '') . '">' . esc_html__('Advanced', 'mdmap_app') . '</a>';
echo '<a href="?page='. $this->pluginBasename .'&tab=help" class="nav-tab ' . ($active_tab == 'help' ? 'nav-tab-active ' : '') . '">' . esc_html__('Help', 'mdmap_app') . '</a>';
echo '</h2>';
//main form
echo '<form action="options.php" method="post">';
//inputs based on current tab
switch($active_tab){
case 'settings':{
add_settings_section(
'mdmap_app_section_settings',
esc_html__('Domain mapping settings', 'mdmap_app'),
array($this, 'section_settings_callback'),
$this->pluginBasename
);
add_settings_field(
'mdmap_app_field_settings_phpserver',
esc_html__('PHP Server Variable:', 'mdmap_app'),
array($this, 'field_settings_phpserver_callback'),
$this->pluginBasename,
'mdmap_app_section_settings'
);
add_settings_field(
'mdmap_app_field_settings_compatibilitymode',
esc_html__('Compatibility mode:', 'mdmap_app'),
array($this, 'field_settings_compatibilitymode_callback'),
$this->pluginBasename,
'mdmap_app_section_settings'
);
add_settings_field(
'mdmap_app_field_settings_excluded_domains',
esc_html__('Excluded domains:', 'mdmap_app'),
array($this, 'field_settings_excluded_domains_callback'),
$this->pluginBasename,
'mdmap_app_section_settings'
);
do_action('mdmap_appa_settings_tab');
settings_fields('mdmap_app_settings_group');
do_settings_sections( $this->pluginBasename );
break 1;
}
case 'advanced':{
echo '<h2>' . esc_html__('Developer Hooks', 'mdmap_app') . '</h2>';
echo '<p>' . esc_html__('MDMAP provides action and filter hooks for developers to extend its behaviour.', 'mdmap_app') . '</p>';
echo '<ul>';
echo '<li>' . esc_html__('Actions prefix:', 'mdmap_app') . ' <code>mdmap_appa_</code></li>';
echo '<li>' . esc_html__('Filters prefix:', 'mdmap_app') . ' <code>mdmap_appf_</code></li>';
echo '</ul>';
echo '<p>' . esc_html__('Search for these prefixes in the plugin source to see all available hooks.', 'mdmap_app') . '</p>';
break 1;
}
case 'help':{
echo '<h2>' . esc_html__('Setup', 'mdmap_app') . '</h2>';
echo '<p>' . esc_html__('Before adding any mappings, each extra domain needs to point to the same web root as your main WordPress site. Two things to set up:', 'mdmap_app') . '</p>';
echo '<ol>';
echo '<li>' . esc_html__('Set the A-record of each extra domain to your main site\'s IP address (done through your domain registrar\'s DNS settings).', 'mdmap_app') . '</li>';
echo '<li>' . esc_html__('Configure your hosting to route all domains to the same WordPress directory (virtual host, domain alias, or parked domain).', 'mdmap_app') . '</li>';
echo '</ol>';
echo '<p>' . esc_html__('Quick test: drop a file in your WordPress root and confirm it\'s accessible from both domains before adding any mappings.', 'mdmap_app') . '</p>';
echo '<p>' . esc_html__('Using nginx? Switch the PHP Server Variable to HTTP_HOST in the Settings tab.', 'mdmap_app') . '</p>';
break 1;
}
default:{ //default is our mappings tab
add_settings_section(
'mdmap_app_section_mappings',
esc_html__('Domain mappings', 'mdmap_app'),
array($this, 'section_mappings_callback'),
$this->pluginBasename
);
add_settings_field(
'mdmap_app_field_mappings_uris',
esc_html__('Your domain mappings:', 'mdmap_app'),
array($this, 'field_mappings_uris_callback'),
$this->pluginBasename,
'mdmap_app_section_mappings'
);
settings_fields('mdmap_app_mappings_group');
do_settings_sections( $this->pluginBasename );
break 1;
}
}
//dynamic submit button
if($active_tab != 'help' && $active_tab != 'advanced'){
if($active_tab != 'mappings' || $this->saveMappingsButtonDisabled == false){
submit_button(sprintf(esc_html__('Save %s', 'mdmap_app'), $active_tab_name));
}
}
echo '</form>';
echo '</div>';
}
//register settings
private function register_settings(){
register_setting( 'mdmap_app_settings_group', 'mdmap_app_settings', array(
'sanitize_callback' => array($this, 'sanitize_settings_group'),
'show_in_rest' => false
) );
register_setting( 'mdmap_app_mappings_group', 'mdmap_app_mappings', array(
'sanitize_callback' => array($this, 'sanitize_mappings_group'),
'show_in_rest' => false
) );
}
//generate options fields output for the settings tab
public function section_settings_callback(){
echo esc_html__('Advanced server settings — most sites can leave these at their defaults.', 'mdmap_app');
}
public function field_settings_phpserver_callback(){
$options = $this->getSettings();
if(empty($options)) $options = array();
$options['php_server'] = isset($options['php_server']) ? $options['php_server'] : 'SERVER_NAME';
echo sprintf('<p>%s <a target="_blank" href="https://wordpress.org/support/topic/server_name-instead-of-http_host/">%s</a>.</p>',
esc_html__('Most sites work fine with the default. If your mappings aren\'t resolving correctly, try HTTP_HOST — see', 'mdmap_app'),
esc_html__('this support thread', 'mdmap_app')
);
echo '<p><label><input type="radio" name="mdmap_app_settings[php_server]" value="SERVER_NAME" '. checked('SERVER_NAME', $options['php_server'], false ) . ' />$_SERVER["SERVER_NAME"] ('. esc_html__('Default', 'mdmap_app') .')</label></p>';
echo '<p><label><input type="radio" name="mdmap_app_settings[php_server]" value="HTTP_HOST" '. checked('HTTP_HOST', $options['php_server'], false ) .' />$_SERVER["HTTP_HOST"] ('. esc_html__('recommended for nginx', 'mdmap_app') .')</label></p>';
}
public function field_settings_compatibilitymode_callback(){
$options = $this->getSettings();
if(empty($options)) $options = array();
$options['compatibilitymode'] = isset($options['compatibilitymode']) ? $options['compatibilitymode'] : 0;
echo sprintf('<p>%s</p>',
esc_html__('Disables domain replacement inside wp-admin. Useful if a page builder or visual editor has trouble loading mapped pages.', 'mdmap_app')
);
echo '<p><label><input type="radio" name="mdmap_app_settings[compatibilitymode]" value="0" '. checked('0', $options['compatibilitymode'], false ) . ' />Off ('. esc_html__('Default', 'mdmap_app') .')</label></p>';
echo '<p><label><input type="radio" name="mdmap_app_settings[compatibilitymode]" value="1" '. checked('1', $options['compatibilitymode'], false ) .' />On</label></p>';
}
public function field_settings_excluded_domains_callback(){
$options = $this->getSettings();
$excluded = !empty($options['excluded_domains']) ? $options['excluded_domains'] : '';
if( defined('ICL_SITEPRESS_VERSION') || defined('POLYLANG_VERSION') ){
echo '<p class="description">' . esc_html__('A multilingual plugin (WPML or Polylang) is active. Add the language-specific domains it manages here to prevent the mapper from processing them.', 'mdmap_app') . '</p>';
}
echo '<textarea name="mdmap_app_settings[excluded_domains]" rows="4" class="large-text code">' . esc_textarea($excluded) . '</textarea>';
echo '<p class="description">' . esc_html__('One domain per line. The mapper ignores requests arriving on these domains.', 'mdmap_app') . '</p>';
}
//generate options fields output for the mappings tab
public function section_mappings_callback(){
echo '<strong>' . esc_html__('Left field', 'mdmap_app') . '</strong>: ';
echo esc_html__('enter the domain you want to use. http/https and www/non-www are handled automatically — one entry per domain is all you need.', 'mdmap_app');
echo '<br />';
echo '<strong>' . esc_html__('Right field', 'mdmap_app') . '</strong>: ';
echo esc_html__('enter the WordPress path this domain should point to. All pages beneath that path are included automatically.', 'mdmap_app');
}
public function field_mappings_uris_callback(){
$options = $this->getMappings();
if(empty($options)) $options = array();
echo '<section class="mdmap_app_mappings">';
$cnt = 0;
if(isset($options['mappings']) && !empty($options['mappings'])){
foreach($options['mappings'] as $mapping){
$mappingClass = 'mdmap_app_mapping' . ($this->isMappingEnabled($mapping) ? '' : ' mdmap_app_mapping_disabled');
echo '<article class="'. apply_filters( 'mdmap_appf_mapping_class', $mappingClass ) .'">';
echo '<div class="mdmap_app_mapping_header">';
echo '<div><div class="mdmap_app_input_wrap"><span class="mdmap_app_input_prefix">http[s]://</span><input type="text" name="mdmap_app_mappings[cnt_'.$cnt.'][domain]" value="' . esc_attr($mapping['domain']) . '" /></div></div>';
echo '<div class="mdmap_app_mapping_arrow">»</div>';
echo '<div><div class="mdmap_app_input_wrap"><span class="mdmap_app_input_prefix">'. esc_url(get_home_url()) .'</span><input type="text" name="mdmap_app_mappings[cnt_'.$cnt.'][path]" value="' . esc_attr($mapping['path']) . '" /></div></div>';
echo '</div>';
echo '<div class="mdmap_app_mapping_body">';
echo '<span class="mdmap_app_mapping_body_icon mdmap_app_delete_mapping"><a href="#" title="' . esc_html__('Remove this mapping', 'mdmap_app') . '">' . esc_html__('Remove', 'mdmap_app') . ' <i>✗</i></a></span>';
do_action('mdmap_appa_after_mapping_body', $cnt, $mapping);
echo '</div>';
echo '</article>';
$cnt++;
}
}
echo '</section>';
echo '<section class="mdmap_app_new_mapping">';
echo '<article class="'. apply_filters( 'mdmap_appf_mapping_class', 'mdmap_app_mapping mdmap_app_mapping_new' ) .'">';
echo '<div class="mdmap_app_mapping_header">';
echo '<div><div class="mdmap_app_input_wrap"><span class="mdmap_app_input_prefix">http[s]://</span><input type="text" name="mdmap_app_mappings[cnt_new][domain]" placeholder="[www.]newdomain.com" /></div><div class="mdmap_app_input_hint">' . esc_html__('Enter the domain you want to map.', 'mdmap_app') . '</div></div>';
echo '<div class="mdmap_app_mapping_arrow">»</div>';
echo '<div><div class="mdmap_app_input_wrap"><span class="mdmap_app_input_prefix">'. get_home_url() .'</span><input type="text" name="mdmap_app_mappings[cnt_new][path]" placeholder="/mappedpage" /></div><div class="mdmap_app_input_hint">' . esc_html__('Enter the path to the desired root for this mapping', 'mdmap_app') . '</div></div>';
echo '</div>';
echo '<div class="mdmap_app_mapping_body">';
do_action('mdmap_appa_after_mapping_body', 'new', false);
echo '</div>';
echo '</article>';
echo '</section>';
echo '<div class="mdmap_app_io_toolbar">';
echo '<button type="button" id="mdmap_export_btn" class="button">' . esc_html__('Export Mappings', 'mdmap_app') . '</button>';
echo '<label for="mdmap_import_file" class="button">' . esc_html__('Import Mappings', 'mdmap_app') . '</label>';
echo '<input type="file" id="mdmap_import_file" accept=".json" style="display:none" />';
echo '</div>';
//calculate and maybe show warning for higher max_input_vars needed
$numberOfSettings = 14; //domain, path, customheadcode, redirection, enabled (+hidden companion), noindex, passthrough, sitename, sitetagline, ogimage, ga4id, robotssitemap, sortorder
if($cnt >= (intval(ini_get('max_input_vars')) / $numberOfSettings - 100)){
$this->saveMappingsButtonDisabled = true;
echo '<section class="notice notice-error">';
echo '<p>';
echo sprintf(
esc_html__('Heads up! Your server allows a maximum of %1$s %2$s. With %3$s mapping(s) at %4$s vars each (%5$s total), you\'re approaching the limit. Increase %6$s to save more mappings.', 'mdmap_app'),
esc_html(ini_get('max_input_vars')),
'<em>max_input_vars</em>',
esc_html($cnt),
esc_html($numberOfSettings),
esc_html($cnt . ' x ' . $numberOfSettings . ' = ' . ($cnt*$numberOfSettings)),
'<em>max_input_vars</em>'
);
echo ' <a href="https://duckduckgo.com/?q=php+increase+max_input_vars" target="_blank">' . esc_html__('How to increase max_input_vars', 'mdmap_app') . '</a>';
echo '</p>';
echo '<p>';
echo esc_html__('The Save button has been hidden to prevent partial data loss. Increase max_input_vars and reload to restore it.', 'mdmap_app');
echo '</p>';
echo '</section>';
}
}
//function to show additional input fields in mapping body
public function render_advanced_mapping_inputs($cnt, $mapping){
$isNew = ($cnt === 'new');
//the new-mapping row passes false; normalise so the field reads below are warning-free
if(!is_array($mapping)) $mapping = array();
//enabled/disabled toggle — shown for all mappings including new
$isEnabled = ($isNew || !isset($mapping['enabled']) || intval($mapping['enabled']) !== 0);
echo '<div class="mdmap_app_mapping_additional_input mdmap_app_toggle_row">';
//hidden companion so an unchecked box still submits a 0 — the checkbox value wins when checked
echo '<input type="hidden" name="mdmap_app_mappings[cnt_'.$cnt.'][enabled]" value="0" />';
echo '<label class="mdmap_app_toggle_label">';
echo '<input type="checkbox" name="mdmap_app_mappings[cnt_'.$cnt.'][enabled]" value="1" ' . checked($isEnabled, true, false) . ' />';
echo ' ' . esc_html__('Active', 'mdmap_app');
echo '</label>';
if(!$isNew){
echo '<button type="button" class="button button-small mdmap_app_health_btn" data-domain="' . esc_attr($mapping['domain']) . '">' . esc_html__('Test connection', 'mdmap_app') . '</button>';
echo '<span class="mdmap_app_health_result"></span>';
}
echo '</div>';
//hidden field carrying this row's position; the admin JS renumbers these on drag-to-reorder.
//only existing rows participate in the sortable (the new row lives outside that container).
if(!$isNew){
echo '<input type="hidden" class="mdmap_app_sortorder" name="mdmap_app_mappings[cnt_'.$cnt.'][sortorder]" value="' . esc_attr($cnt) . '" />';
}
echo '<div class="mdmap_app_mapping_additional_input">';
echo '<p class="mdmap_app_mapping_additional_input_header">' . esc_html__('Custom <head> code (this domain only)', 'mdmap_app') . '</p>';
echo '<textarea name="mdmap_app_mappings[cnt_'.$cnt.'][customheadcode]" placeholder="' . esc_attr__('e.g. <meta name="google-site-verification" content="…" />', 'mdmap_app') . '">' . esc_textarea(html_entity_decode($mapping['customheadcode'] ?? '')) . '</textarea>';
echo '</div>';
echo '<div class="mdmap_app_mapping_additional_input">';
echo '<p class="mdmap_app_mapping_additional_input_header">' . esc_html__('301 Redirect to mapped domain', 'mdmap_app') . '</p>';
echo '<label><input type="checkbox" name="mdmap_app_mappings[cnt_'.$cnt.'][redirection]" value="301" ' . checked( !empty($mapping['redirection']), true, false ) . ' />' . esc_html__('Redirect visitors who arrive at the original path to this domain instead.', 'mdmap_app') . '</label>';
echo '</div>';
echo '<div class="mdmap_app_mapping_additional_input">';
echo '<p class="mdmap_app_mapping_additional_input_header">' . esc_html__('Noindex original URL', 'mdmap_app') . '</p>';
echo '<label><input type="checkbox" name="mdmap_app_mappings[cnt_'.$cnt.'][noindex]" value="1" ' . checked( !empty($mapping['noindex']), true, false ) . ' />' . esc_html__('Add a noindex tag to the original path — search engines will index only the mapped domain.', 'mdmap_app') . '</label>';
echo '</div>';
echo '<div class="mdmap_app_mapping_additional_input">';
echo '<p class="mdmap_app_mapping_additional_input_header">' . esc_html__('Pass through unmatched paths', 'mdmap_app') . '</p>';
echo '<label><input type="checkbox" name="mdmap_app_mappings[cnt_'.$cnt.'][passthrough]" value="1" ' . checked( !empty($mapping['passthrough']), true, false ) . ' />' . esc_html__('When a request on this domain doesn\'t resolve under the mapped path, serve the same path from the main site instead of 404.', 'mdmap_app') . '</label>';
echo '<p class="description">' . esc_html__('Useful when the alternate domain acts as a branded alias of the main site. Any public top-level page on the main site becomes reachable from this domain — review before enabling on a site with private pages.', 'mdmap_app') . '</p>';
echo '</div>';
echo '<div class="mdmap_app_mapping_additional_input">';
echo '<p class="mdmap_app_mapping_additional_input_header">' . esc_html__('Site name (this domain only)', 'mdmap_app') . '</p>';
echo '<input type="text" class="regular-text" name="mdmap_app_mappings[cnt_'.$cnt.'][sitename]" value="' . esc_attr($mapping['sitename'] ?? '') . '" placeholder="' . esc_attr__('Leave empty to use the main site name', 'mdmap_app') . '" />';
echo '<p class="description">' . esc_html__('Replaces the site name in <title> tags, Open Graph site_name, RSS feeds, and SEO plugin output while visitors browse this mapped domain.', 'mdmap_app') . '</p>';
echo '</div>';
echo '<div class="mdmap_app_mapping_additional_input">';
echo '<p class="mdmap_app_mapping_additional_input_header">' . esc_html__('Site tagline (this domain only)', 'mdmap_app') . '</p>';
echo '<input type="text" class="regular-text" name="mdmap_app_mappings[cnt_'.$cnt.'][sitetagline]" value="' . esc_attr($mapping['sitetagline'] ?? '') . '" placeholder="' . esc_attr__('Leave empty to use the main site tagline', 'mdmap_app') . '" />';
echo '<p class="description">' . esc_html__('Replaces the site tagline (blogdescription) and Yoast/RankMath %sitedesc% expansions when visitors are on this mapped domain.', 'mdmap_app') . '</p>';
echo '</div>';
echo '<div class="mdmap_app_mapping_additional_input">';
echo '<p class="mdmap_app_mapping_additional_input_header">' . esc_html__('Default Open Graph image (this domain only)', 'mdmap_app') . '</p>';
echo '<input type="text" class="regular-text" name="mdmap_app_mappings[cnt_'.$cnt.'][ogimage]" value="' . esc_attr($mapping['ogimage'] ?? '') . '" placeholder="https://example.com/share-card.jpg" />';
echo '<p class="description">' . esc_html__('Used as a fallback og:image / twitter:image when a page on this mapped domain has no specific share image set. Per-page Yoast/RankMath images still take precedence.', 'mdmap_app') . '</p>';
echo '</div>';
echo '<div class="mdmap_app_mapping_additional_input">';
echo '<p class="mdmap_app_mapping_additional_input_header">' . esc_html__('Analytics ID (GA4 or GTM, this domain only)', 'mdmap_app') . '</p>';
echo '<input type="text" class="regular-text" name="mdmap_app_mappings[cnt_'.$cnt.'][ga4id]" value="' . esc_attr($mapping['ga4id'] ?? '') . '" placeholder="G-XXXXXXXXXX" />';
echo '<p class="description">' . esc_html__('Injects a gtag.js snippet when visitors are browsing this mapped domain.', 'mdmap_app') . '</p>';
echo '</div>';
echo '<div class="mdmap_app_mapping_additional_input">';
echo '<p class="mdmap_app_mapping_additional_input_header">' . esc_html__('robots.txt Sitemap URL (this domain only)', 'mdmap_app') . '</p>';
echo '<input type="text" class="regular-text" name="mdmap_app_mappings[cnt_'.$cnt.'][robotssitemap]" value="' . esc_attr($mapping['robotssitemap'] ?? '') . '" placeholder="https://example.com/sitemap.xml" />';
echo '<p class="description">' . esc_html__('Overrides the Sitemap: line in robots.txt while visitors browse this domain.', 'mdmap_app') . '</p>';
echo '</div>';
}
//sanitize options fields input
public function sanitize_settings_group($options){
if(empty($options)){
return $options;
}
//be sure that only a correct server-value will be saved
$options['php_server'] = (isset($options['php_server']) && ( $options['php_server'] == 'SERVER_NAME' || $options['php_server'] == 'HTTP_HOST' )) ? $options['php_server'] : 'SERVER_NAME';
//sanitize excluded domains — strip protocols and trailing slashes; one per line
if(isset($options['excluded_domains'])){
$lines = array_filter(array_map('trim', explode("\n", $options['excluded_domains'])));
$clean = array();
foreach($lines as $line){
$line = preg_replace('#^https?://#i', '', $line);
$line = trim($line, '/');
if(!empty($line)) $clean[] = sanitize_text_field($line);
}
$options['excluded_domains'] = implode("\n", $clean);
}
return apply_filters( 'mdmap_appf_save_settings', $options );
}
public function sanitize_mappings_group($options){
//do nothing on empty input
if(empty($options)){
return $options;
}
//prepare mappings array
$mappings = array();
foreach($options as $key=>$val){
//search for mappings and prepare them for database
if(stripos( $key, 'cnt_' ) !== false){
//only save not empty inputs
$domain = str_replace([']', '['], '', trim(trim($val['domain']), '/'));
$path = trim(trim( isset($val['path']) ? $val['path'] : '' ), '/');
if($domain != ''/* && $path != ''*/){
//validate inputs
$parsedDomain = parse_url($domain);
$parsedPath = parse_url($path);
if($parsedDomain != false && $parsedPath != false){
//if we get only the host-representation we temporary add a protocol, so we can use the benefit from parse_url to strip the query
//note: this will also be run for each already saved mapping, since we strip the protocol on save...
if(!isset($parsedDomain['host'])){
$parsedDomain = parse_url('dummyprotocol://' . $domain);
}
//save only host name (and path, if provided) with stripped slashes
$trimmedDomainPath = trim(trim( (isset($parsedDomain['path']) ? $parsedDomain['path'] : '') ), '/');
$val['domain'] = trim(trim(isset($parsedDomain['host']) ? $parsedDomain['host'] : ''), '/') . (!empty($trimmedDomainPath) ? '/' . $trimmedDomainPath : '');
//save path with leading slash
$val['path'] = '/' . $path;
//reject root path - mapping "/" would intercept all site traffic
if( $val['path'] === '/' ){
if(function_exists('add_settings_error')) add_settings_error( 'mdmap_app_messages', 'mdmap_app_error_code', esc_html__('Mapping to "/" is not allowed — it would intercept all site traffic.', 'mdmap_app'), 'error' );
unset($options[$key]);
continue;
}
//iterate over existing mappings and check, if this path has already been used
$saveMapping = true;
foreach($mappings as $existingMapping){
if($existingMapping['path'] === $val['path']){
$saveMapping = false;
}
if($this->stripWww($existingMapping['domain']) === $this->stripWww($val['domain'])){
$saveMapping = false;
}
}
//sanitize html-head-code: allow only safe head elements
if(!empty($val['customheadcode'])){
$allowed_head_tags = array(
'meta' => array('name'=>true,'content'=>true,'property'=>true,'charset'=>true,'http-equiv'=>true),
'link' => array('rel'=>true,'href'=>true,'type'=>true,'media'=>true,'sizes'=>true,'hreflang'=>true),
'script' => array('type'=>true,'src'=>true,'async'=>true,'defer'=>true,'id'=>true),
'style' => array('type'=>true),
'noscript' => array(),
);
$val['customheadcode'] = wp_kses($val['customheadcode'], $allowed_head_tags);
}
//only allow integers (statuscode) for redirection
if(!empty($val['redirection'])) $val['redirection'] = intval($val['redirection']);
//enabled flag (1 = active, 0 = disabled; absent means active — backward compat)
$val['enabled'] = isset($val['enabled']) ? intval($val['enabled']) : 1;
//noindex on original path
$val['noindex'] = !empty($val['noindex']) ? 1 : 0;
//pass-through unmatched paths to the un-rewritten path on the main site
$val['passthrough'] = !empty($val['passthrough']) ? 1 : 0;
//per-mapping site name override (empty = no override)
$val['sitename'] = isset($val['sitename']) ? sanitize_text_field($val['sitename']) : '';
//per-mapping site tagline override (empty = no override)
$val['sitetagline'] = isset($val['sitetagline']) ? sanitize_text_field($val['sitetagline']) : '';
//per-mapping default Open Graph image url (empty = no override)
$val['ogimage'] = !empty($val['ogimage']) ? esc_url_raw($val['ogimage']) : '';
//ga4 / gtm measurement id
if(!empty($val['ga4id'])){
$val['ga4id'] = strtoupper(sanitize_text_field($val['ga4id']));
if(!preg_match('/^(G-[A-Z0-9]+|GTM-[A-Z0-9]+)$/', $val['ga4id'])) $val['ga4id'] = '';
}else{
$val['ga4id'] = '';
}
//per-mapping robots.txt sitemap url
$val['robotssitemap'] = !empty($val['robotssitemap']) ? esc_url_raw($val['robotssitemap']) : '';
//explicit sort order from drag-to-reorder
$val['sortorder'] = isset($val['sortorder']) ? intval($val['sortorder']) : 999;
if($saveMapping){
//mapping should be saved and is filtered before
//use domain as index, so we do not have any duplicates -> this index will never be used or stored, but we convert it to md5 so it can not be confusing later
$mappings[md5($val['domain'])] = apply_filters('mdmap_appf_save_mapping', $val);
}else{
//check for existence, since this may be called in an upgrade process earlier, when this is not available yet
if(function_exists('add_settings_error')) add_settings_error( 'mdmap_app_messages', 'mdmap_app_error_code', esc_html__('At least one mapping with duplicate domain or path has been dropped.', 'mdmap_app'), 'error' );
}
}else{
//check for existence, since this may be called in an upgrade process earlier, when this is not available yet
if(function_exists('add_settings_error')) add_settings_error( 'mdmap_app_messages', 'mdmap_app_error_code', esc_html__('One or more mappings had an invalid domain or path and were skipped.', 'mdmap_app'), 'error' );
}
//if we have only one input filled
}else if(!($val['domain'] == '' && $val['path'] == '')){
//check for existence, since this may be called in an upgrade process earlier, when this is not available yet
if(function_exists('add_settings_error')) add_settings_error( 'mdmap_app_messages', 'mdmap_app_error_code', esc_html__('One or more mappings were skipped — both a domain and a path are required.', 'mdmap_app'), 'error' );
}
//remove original mapping (cnt_) from options array
unset($options[$key]);
}
}
//sort: use explicit drag order when present; fall back to alphabetical by domain
$hasSortOrder = false;
foreach($mappings as $m){
if(isset($m['sortorder']) && $m['sortorder'] !== 999){ $hasSortOrder = true; break; }
}
if($hasSortOrder){
usort($mappings, function($a, $b){ return intval($a['sortorder'] ?? 999) - intval($b['sortorder'] ?? 999); });
}else{
$sort_key = apply_filters('mdmap_appf_mapping_sort', 'domain');
usort($mappings, function($a, $b) use ($sort_key) { return strcmp($a[$sort_key], $b[$sort_key]); });
}
//add filtered and sorted mappings to options array
if(!empty($mappings)) $options['mappings'] = $mappings;
return apply_filters( 'mdmap_appf_save_mappings', $options );
}
//change the request, check for matching mappings
public function parse_request($do_parse, $instance, $extra_query_vars){
//store current request uri as fallback for the originalRequestURI variable, no matter if we have a match or not
$this->setOriginalRequestURI($_SERVER['REQUEST_URI']);
//definitely no request-mapping in backend
if(is_admin()) return $do_parse;
//skip if the incoming domain is on the excluded list (e.g. WPML/Polylang language domains)
if($this->isCurrentDomainExcluded()) return $do_parse;
//loop mappings and compare match of mapping against each other
$mappings = $this->getMappings();
if(!empty($mappings) && isset($mappings['mappings']) && !empty($mappings['mappings'])){
foreach($mappings['mappings'] as $mapping){
//skip disabled mappings
if(!$this->isMappingEnabled($mapping)) continue;
$matchCompare = $this->uriMatch($this->getCurrentURI(), $mapping, true);
//then enable custom matching by filtering
$matchCompare = apply_filters( 'mdmap_appf_uri_match', $matchCompare, $this->getCurrentURI(), $mapping, true );
//if the current mapping fits better, use this instead the previous one
if($matchCompare !== false && isset($matchCompare['factor']) && $matchCompare['factor'] > $this->getCurrentMapping()['factor']){
$this->setCurrentMapping($matchCompare);
}
}
//we have a matching mapping -> let the magic happen
if(!empty($this->getCurrentMapping()['match'])){
//set request uri to our original mapping path AND if we have a longer query, we need to append it
$newRequestURI = trailingslashit($this->getCurrentMapping()['match']['path'] . substr($this->stripWww($this->getCurrentURI()), strlen($this->stripWww($this->getCurrentMapping()['match']['domain']))));
//enable additional filtering on the request_uri
$newRequestURI = apply_filters('mdmap_appf_request_uri', $newRequestURI, $this->getCurrentURI(), $this->getCurrentMapping());
//robots.txt: leave the request untouched so WP serves its virtual robots.txt
//(is_robots() stays true). currentMapping is still set, so filter_robots_txt()
//can inject the per-mapping Sitemap line for this domain.
$incomingPath = parse_url($this->getOriginalRequestURI(), PHP_URL_PATH);
$isRobotsRequest = ($incomingPath === '/robots.txt');
//pass-through: when this mapping opts in, and the rewritten path doesn't resolve
//to any real page/post but the original (un-rewritten) path does, keep the original.
//lets pages outside the mapping's subtree remain reachable under the mapped domain.
$passthrough = !empty($this->getCurrentMapping()['match']['passthrough']);
if( $isRobotsRequest ){
//leave REQUEST_URI as /robots.txt
}else if( $passthrough && !$this->pathHasContent($newRequestURI) && $this->pathHasContent($this->getOriginalRequestURI()) ){
//leave REQUEST_URI as the original path — currentMapping stays set so canonical/og/admin-bar still use the mapped domain
}else{
$_SERVER['REQUEST_URI'] = $newRequestURI;
}
}
}
return $do_parse;
}
//redirect visitors from the original path to the mapped domain (when redirection is enabled for that mapping)
public function handle_redirect(){
//only fire when we are NOT on a mapped domain
if( !empty($this->getCurrentMapping()['match']) ) return;
$mappings = $this->getMappings();
if( empty($mappings) || !isset($mappings['mappings']) ) return;
$bestMatch = array( 'match' => false, 'factor' => PHP_INT_MIN );
foreach( $mappings['mappings'] as $mapping ){
//skip disabled mappings
if(!$this->isMappingEnabled($mapping)) continue;
//skip mappings that have no redirection configured
if( empty($mapping['redirection']) ) continue;
//check if the current path falls under this mapping's target path
$matchCompare = $this->uriMatch( $this->getCurrentURI(), $mapping, false );
if( $matchCompare !== false && $matchCompare['factor'] > $bestMatch['factor'] ){
$bestMatch = $matchCompare;
}
}
if( empty($bestMatch['match']) ) return;
$mapping = $bestMatch['match'];
$protocol = is_ssl() ? 'https' : 'http';
//confirm REQUEST_URI actually begins with the mapping path before slicing
$requestPath = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
if( !$this->pathUnderBase( $requestPath, $mapping['path'] ) ) return;
//extra path beyond the mapped base (e.g. /product-a/subpage -> /subpage)
//substr can return false in PHP 7 if offset >= string length; normalise to empty string
$extraPath = substr( $_SERVER['REQUEST_URI'], strlen( $mapping['path'] ) );
if( $extraPath === false ) $extraPath = '';
$redirectUrl = $protocol . '://' . $mapping['domain'] . '/' . ltrim( $extraPath, '/' );
wp_redirect( $redirectUrl, intval( $mapping['redirection'] ) );
exit;
}
//hook into the canonical redirect to avoid infinite redirection loops
public function check_canonical_redirect($redirect_url, $requested_url){
//are we on a mapped page? suppress ALL canonical redirects.
//WordPress will try to redirect the mapped domain back to the primary domain's canonical URL
//(e.g. secondary.com/ -> mainsite.com/products-page/). Allowing this creates a loop when
//redirection is also enabled for that mapping: mainsite.com/products-page/ -> secondary.com/ -> repeat.
//The mapped domain IS the canonical address, so no further canonical redirects should happen.
if($this->getCurrentMapping()['match'] != false){
return false;
}
//standard return value
return $redirect_url;
}
//strip leading www. subdomain from a host string
private function stripWww($host){
return preg_replace('/^www\./i', '', $host);
}
//strip port number from a host string (e.g. example.com:8080 -> example.com)
private function stripPort($host){
return preg_replace('/:\d+$/', '', $host);
}
//standard function to check an uri against a mapping
private function uriMatch($uri, $mapping, $reverse = false){
//strip protocol from uri
$uri = str_ireplace('http://', '', str_ireplace('https://', '', $uri));
//strip www-subdomain from uri for matching purpose
$uri = $this->stripWww($uri);
//do we check match at parsing the site or when replacing uris in the page?
if($reverse){
$arg2 = $this->stripWww($mapping['domain']);
$matchingPosCompare = 0;
}else{
$arg2 = $mapping['path'];
$matchingPosCompare = $this->homeURLMatchLength;
}
//check if arg2 is part of uri and starts where we want to
$matchingPos = stripos(trailingslashit( $uri ), trailingslashit( $arg2 ) );
if( $matchingPos !== false && $matchingPos === $matchingPosCompare ){
//use length of match as factor
return array(
'match' => $mapping,
'factor' => strlen(trailingslashit($arg2))
);
}
return false;
}
//aggregation of all filters to replace the uri in the current page
private function replace_uris(){
//retrieve settings for compatibility mode
$options = $this->getSettings();
if(empty($options)) $options = array();
$options['compatibilitymode'] = isset($options['compatibilitymode']) ? $options['compatibilitymode'] : 0;
//single views
if( !($options['compatibilitymode'] && is_admin()) ){
add_filter('page_link', array($this, 'replace_uri'), 20);
add_filter('post_link', array($this, 'replace_uri'), 20);
add_filter('post_type_link', array($this, 'replace_uri'), 20);
add_filter('attachment_link', array($this, 'replace_uri'), 20);
//get_comment_author_link ... not necessary (seems to use the "author_link")
//get_comment_author_uri_link ... this is the url the author can fill out - should not be touched
//comment_reply_link ... leave this out until we manage to keep user logged in on addon-domains
//remove_action('wp_head', 'wp_shortlink_wp_head', 10, 0); ... guess we should not add this...
}
//revoke mapping for the preview-button
add_filter('preview_post_link', array($this, 'unreplace_uri'));
//archive views
add_filter('paginate_links', array($this, 'replace_uri'), 10);
add_filter('day_link', array($this, 'replace_uri'), 20);
add_filter('month_link', array($this, 'replace_uri'), 20);
add_filter('year_link', array($this, 'replace_uri'), 20);
add_filter('author_link', array($this, 'replace_uri'), 10);
add_filter('term_link', array($this, 'replace_uri'), 10);
//feed url (if someone matches a domain to a feed...)
add_filter('feed_link', array($this, 'replace_uri'), 10);
add_filter('self_link', array($this, 'replace_uri'), 10);
add_filter('author_feed_link', array($this, 'replace_uri'), 10);
//nav menu objects that do not use the standard link builders (like custom hrefs in the menu)
add_filter('wp_nav_menu_objects', array($this, 'replace_menu_uri'));
//content elements - do not map in wp-admin
if(!is_admin()){
add_filter( 'script_loader_src', array($this, 'replace_domain'), 10 );
add_filter( 'style_loader_src', array($this, 'replace_domain'), 10 );
add_filter( 'stylesheet_directory_uri', array($this, 'replace_domain'), 10 );
add_filter( 'template_directory_uri', array($this, 'replace_domain'), 10 );
add_filter( 'the_content', array($this, 'replace_domain'), 10 );
add_filter( 'get_header_image_tag', array($this, 'replace_domain'), 10 );
add_filter( 'wp_get_attachment_image_src', array($this, 'replace_src_domain'), 10 );
add_filter( 'wp_calculate_image_srcset', array($this, 'replace_srcset_domain'), 10 );
}
//yoast sitemaps
add_filter( 'wpseo_xml_sitemap_post_url', array($this, 'replace_yoast_xml_sitemap_post_url'), 0, 2 );
add_filter( 'wpseo_sitemap_entry', array($this, 'replace_yoast_sitemap_entry'), 10, 3 );
//core WordPress sitemaps (wp-sitemap.xml, default since WP 5.5)
add_filter( 'wp_sitemaps_posts_entry', array($this, 'replace_sitemap_entry'), 10 );
add_filter( 'wp_sitemaps_taxonomies_entry', array($this, 'replace_sitemap_entry'), 10 );
add_filter( 'wp_sitemaps_users_entry', array($this, 'replace_sitemap_entry'), 10 );
//rankmath sitemaps
add_filter( 'rank_math/sitemap/entry', array($this, 'replace_sitemap_entry'), 10 );
//elementor preview url
add_filter( 'elementor/document/urls/preview', array($this, 'replace_elementor_preview_url') );
}
//all the helpers for the above filters
public function replace_uri($originalURI){
//loop mappings and compare match of mapping against each other
$mappings = $this->getMappings();
if(!empty($mappings) && isset($mappings['mappings']) && !empty($mappings['mappings'])){
$bestMatch = array(
'match' => false,
'factor' => PHP_INT_MIN
);
foreach($mappings['mappings'] as $mapping){
//skip disabled mappings
if(!$this->isMappingEnabled($mapping)) continue;
//first use our standard matching function
$matchCompare = $this->uriMatch($originalURI, $mapping, false);
//then enable custom matching by filtering
$matchCompare = apply_filters( 'mdmap_appf_uri_match', $matchCompare, $originalURI, $mapping, false );
//if the current mapping fits better, use this instead the previous one
if($matchCompare !== false && isset($matchCompare['factor']) && $matchCompare['factor'] > $bestMatch['factor']){
$bestMatch = $matchCompare;
}
}
//we have a matching mapping -> let the magic happen
if(!empty($bestMatch['match'])){
$uriParsed = parse_url($originalURI);
$newURI = str_ireplace( trailingslashit( ($uriParsed['host'] ?? '') . $bestMatch['match']['path'] ), trailingslashit( $bestMatch['match']['domain'] ), $originalURI );
return apply_filters('mdmap_appf_filtered_uri', $newURI, $originalURI, $bestMatch);
}
}
return $originalURI;
}
//keep home/search links on the mapped domain while a visitor is browsing one.
//tightly scoped: front-end of a mapped page only; never during admin/ajax/cron/rest/feed;
//subtree links fall back to replace_uri, and only the bare site root is repointed at the mapped root.
public function replace_home_url($url, $path = '', $orig_scheme = null){
if(empty($this->getCurrentMapping()['match'])) return $url;
if(is_admin() || wp_doing_ajax() || wp_doing_cron() || (defined('REST_REQUEST') && REST_REQUEST) || is_feed()) return $url;
if($orig_scheme === 'rest') return $url; //leave the REST API base alone
if(!apply_filters('mdmap_appf_rewrite_home_url', true, $url, $path)) return $url;
//links that fall under a mapping's subtree are already handled by replace_uri
$mapped = $this->replace_uri($url);
if($mapped !== $url) return $mapped;
//otherwise only repoint the bare site root (home link, search form action, etc.)
if(!is_string($path) || trim($path, '/') === ''){
$mapping = $this->getCurrentMapping()['match'];
$protocol = is_ssl() ? 'https' : 'http';
$slash = (is_string($path) && $path !== '') ? '/' : '';