Prevents line breaks in delivery service remapText field (UI and API) (#4305) (#4317)

* prevents line breaks in ds.remapText (UI and API)

* adds test to ensure ds.remapText cannot include a line break

* updated fixtures to include valid line breaks for edge/mid header rewrites, regex remap and tr request/response headers

* adds period to GoDoc

* no need for ng-pattern

* textarea does not support pattern so switching back to ng-pattern

(cherry picked from commit c025d2297168fc46d4fd4a9693aad9161bdc0015)
diff --git a/lib/go-tc/deliveryservices.go b/lib/go-tc/deliveryservices.go
index 2042ae8..69a5de3 100644
--- a/lib/go-tc/deliveryservices.go
+++ b/lib/go-tc/deliveryservices.go
@@ -405,6 +405,7 @@
 	isDNSName := validation.NewStringRule(govalidator.IsDNSName, "must be a valid hostname")
 	noPeriods := validation.NewStringRule(tovalidate.NoPeriods, "cannot contain periods")
 	noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
+	noLineBreaks := validation.NewStringRule(tovalidate.NoLineBreaks, "cannot contain line breaks")
 	errs := tovalidate.ToErrors(validation.Errors{
 		"active":              validation.Validate(ds.Active, validation.NotNil),
 		"cdnId":               validation.Validate(ds.CDNID, validation.Required),
@@ -415,6 +416,7 @@
 		"geoProvider":         validation.Validate(ds.GeoProvider, validation.NotNil),
 		"logsEnabled":         validation.Validate(ds.LogsEnabled, validation.NotNil),
 		"regionalGeoBlocking": validation.Validate(ds.RegionalGeoBlocking, validation.NotNil),
+		"remapText":           validation.Validate(ds.RemapText, noLineBreaks),
 		"routingName":         validation.Validate(ds.RoutingName, isDNSName, noPeriods, validation.Length(1, 48)),
 		"typeId":              validation.Validate(ds.TypeID, validation.Required, validation.Min(1)),
 		"xmlId":               validation.Validate(ds.XMLID, noSpaces, noPeriods, validation.Length(1, 48)),
diff --git a/lib/go-tc/tovalidate/rules.go b/lib/go-tc/tovalidate/rules.go
index f5d6622..e2f1555 100644
--- a/lib/go-tc/tovalidate/rules.go
+++ b/lib/go-tc/tovalidate/rules.go
@@ -28,6 +28,11 @@
 	return !strings.ContainsAny(str, " ")
 }
 
+// NoLineBreaks returns true if the string has no line breaks.
+func NoLineBreaks(str string) bool {
+	return !strings.ContainsAny(str, "\n\r")
+}
+
 // IsAlphanumericUnderscoreDash returns true if the string consists of only alphanumeric, underscore, or dash characters.
 func IsAlphanumericUnderscoreDash(str string) bool {
 	return rxAlphanumericUnderscoreDash.MatchString(str)
diff --git a/traffic_ops/testing/api/v14/deliveryservices_test.go b/traffic_ops/testing/api/v14/deliveryservices_test.go
index c56ddf3..69e74b7 100644
--- a/traffic_ops/testing/api/v14/deliveryservices_test.go
+++ b/traffic_ops/testing/api/v14/deliveryservices_test.go
@@ -36,6 +36,7 @@
 	WithObjs(t, []TCObj{CDNs, Types, Tenants, Users, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, DeliveryServices}, func() {
 		UpdateTestDeliveryServices(t)
 		UpdateNullableTestDeliveryServices(t)
+		UpdateDeliveryServiceWithInvalidRemapText(t)
 		GetTestDeliveryServices(t)
 		DeliveryServiceMinorVersionsTest(t)
 		DeliveryServiceTenancyTest(t)
@@ -189,6 +190,39 @@
 	}
 }
 
+// UpdateDeliveryServiceWithInvalidRemapText ensures that a delivery service can't be updated with a remap text value with a line break in it.
+func UpdateDeliveryServiceWithInvalidRemapText(t *testing.T) {
+	firstDS := testData.DeliveryServices[0]
+
+	dses, _, err := TOSession.GetDeliveryServicesNullable()
+	if err != nil {
+		t.Fatalf("cannot GET Delivery Services: %v", err)
+	}
+
+	remoteDS := tc.DeliveryServiceNullable{}
+	found := false
+	for _, ds := range dses {
+		if ds.XMLID == nil || ds.ID == nil {
+			continue
+		}
+		if *ds.XMLID == firstDS.XMLID {
+			found = true
+			remoteDS = ds
+			break
+		}
+	}
+	if !found {
+		t.Fatalf("GET Delivery Services missing: %v", firstDS.XMLID)
+	}
+
+	updatedRemapText := "@plugin=tslua.so @pparam=/opt/trafficserver/etc/trafficserver/remapPlugin1.lua\nline2"
+	remoteDS.RemapText = &updatedRemapText
+
+	if _, err := TOSession.UpdateDeliveryServiceNullable(strconv.Itoa(*remoteDS.ID), &remoteDS); err == nil {
+		t.Errorf("Delivery service updated with invalid remap text: %v", updatedRemapText)
+	}
+}
+
 func DeleteTestDeliveryServices(t *testing.T) {
 	dses, _, err := TOSession.GetDeliveryServices()
 	if err != nil {
diff --git a/traffic_ops/testing/api/v14/tc-fixtures.json b/traffic_ops/testing/api/v14/tc-fixtures.json
index cb12363..c813a95 100644
--- a/traffic_ops/testing/api/v14/tc-fixtures.json
+++ b/traffic_ops/testing/api/v14/tc-fixtures.json
@@ -258,7 +258,7 @@
             "dnsBypassIp6": "",
             "dnsBypassTtl": 30,
             "dscp": 40,
-            "edgeHeaderRewrite": "edgeHeader1",
+            "edgeHeaderRewrite": "edgeHeader1\nedgeHeader2",
             "exampleURLs": [
                 "http://ccr.ds1.example.net",
                 "https://ccr.ds1.example.net"
@@ -287,7 +287,7 @@
                 }
             ],
             "maxDnsAnswers": 0,
-            "midHeaderRewrite": "midHeader1",
+            "midHeaderRewrite": "midHeader1\nmidHeader2",
             "missLat": 41.881944,
             "missLong": -87.627778,
             "multiSiteOrigin": false,
@@ -298,7 +298,7 @@
             "protocol": 2,
             "qstringIgnore": 1,
             "rangeRequestHandling": 0,
-            "regexRemap": "rr1",
+            "regexRemap": "rr1\nrr2",
             "regionalGeoBlocking": false,
             "remapText": "@plugin=tslua.so @pparam=/opt/trafficserver/etc/trafficserver/remapPlugin1.lua",
             "routingName": "ccr-ds1",
@@ -325,7 +325,7 @@
             "dnsBypassIp6": "",
             "dnsBypassTtl": 30,
             "dscp": 40,
-            "edgeHeaderRewrite": "edgeRewrite2",
+            "edgeHeaderRewrite": "edgeRewrite1\nedgeHeader2",
             "exampleURLs": [
                 "http://ccr.ds2.example.net",
                 "https://ccr.ds2x.example.net"
@@ -355,7 +355,7 @@
             ],
             "maxDnsAnswers": 0,
             "maxOriginConnections": -1,
-            "midHeaderRewrite": "midRewrite2",
+            "midHeaderRewrite": "midHeader1\nmidHeader2",
             "missLat": 41.881944,
             "missLong": -87.627778,
             "multiSiteOrigin": false,
@@ -366,7 +366,7 @@
             "protocol": 2,
             "qstringIgnore": 1,
             "rangeRequestHandling": 0,
-            "regexRemap": "regexRemap2",
+            "regexRemap": "rr1\nrr2",
             "regionalGeoBlocking": false,
             "remapText": "@plugin=tslua.so @pparam=/opt/trafficserver/etc/trafficserver/ds2plugin.lua",
             "routingName": "ccr-ds2",
@@ -393,7 +393,7 @@
             "dnsBypassIp6": "",
             "dnsBypassTtl": 30,
             "dscp": 40,
-            "edgeHeaderRewrite": "edgeRewrite3",
+            "edgeHeaderRewrite": "edgeRewrite1\nedgeHeader2",
             "exampleURLs": [
                 "http://ccr.ds3.example.net",
                 "https://ccr.ds3x.example.net"
@@ -423,7 +423,7 @@
             ],
             "maxDnsAnswers": 0,
             "maxOriginConnections": 0,
-            "midHeaderRewrite": "midRewrite3",
+            "midHeaderRewrite": "midHeader1\nmidHeader2",
             "missLat": 41.881944,
             "missLong": -87.627778,
             "multiSiteOrigin": false,
@@ -434,7 +434,7 @@
             "protocol": 2,
             "qstringIgnore": 1,
             "rangeRequestHandling": 0,
-            "regexRemap": "regexRemap3",
+            "regexRemap": "rr1\nrr2",
             "regionalGeoBlocking": false,
             "remapText": "@plugin=tslua.so @pparam=/opt/trafficserver/etc/trafficserver/ds3plugin.lua",
             "routingName": "ccr-ds3",
@@ -520,7 +520,7 @@
             "dnsBypassIp6": "",
             "dnsBypassTtl": 30,
             "dscp": 40,
-            "edgeHeaderRewrite": "edgeHeader1",
+            "edgeHeaderRewrite": "edgeRewrite1\nedgeHeader2",
             "fqPacingRate": 42,
             "geoLimit": 0,
             "geoLimitCountries": "",
@@ -538,7 +538,7 @@
             "longDesc2": "ds1",
             "maxDnsAnswers": 0,
             "maxOriginConnections": 1000,
-            "midHeaderRewrite": "midHeader1",
+            "midHeaderRewrite": "midHeader1\nmidHeader2",
             "missLat": 41.881944,
             "missLong": -87.627778,
             "multiSiteOrigin": false,
@@ -549,7 +549,7 @@
             "protocol": 2,
             "qstringIgnore": 1,
             "rangeRequestHandling": 0,
-            "regexRemap": "rr1",
+            "regexRemap": "rr1\nrr2",
             "regionalGeoBlocking": false,
             "remapText": "@plugin=tslua.so @pparam=/opt/trafficserver/etc/trafficserver/remapPlugin1.lua",
             "routingName": "cdn",
@@ -558,8 +558,8 @@
             "sslKeyVersion": 2,
             "tenantId": 1,
             "tenant": "root",
-            "trRequestHeaders": "X-Foo",
-            "trResponseHeaders": "Access-Control-Allow-Origin: *",
+            "trRequestHeaders": "X-Foo\nX-Bar",
+            "trResponseHeaders": "Access-Control-Allow-Origin: *\nContent-Type: text/html; charset=utf-8",
             "type": "HTTP_LIVE",
             "xmlId": "ds-test-minor-versions",
             "anonymousBlockingEnabled": true
@@ -578,7 +578,7 @@
             "dnsBypassIp6": "",
             "dnsBypassTtl": 30,
             "dscp": 40,
-            "edgeHeaderRewrite": "edgeHeader1",
+            "edgeHeaderRewrite": "edgeRewrite1\nedgeHeader2",
             "exampleURLs": [
                 "http://ccr.msods1.example.net",
                 "https://ccr.msods1.example.net"
@@ -607,7 +607,7 @@
                 }
             ],
             "maxDnsAnswers": 0,
-            "midHeaderRewrite": "midHeader1",
+            "midHeaderRewrite": "midHeader1\nmidHeader2",
             "missLat": 41.881944,
             "missLong": -87.627778,
             "multiSiteOrigin": true,
@@ -618,7 +618,7 @@
             "protocol": 2,
             "qstringIgnore": 1,
             "rangeRequestHandling": 0,
-            "regexRemap": "rr1",
+            "regexRemap": "rr1\nrr2",
             "regionalGeoBlocking": false,
             "remapText": "@plugin=tslua.so @pparam=/opt/trafficserver/etc/trafficserver/remapPlugin1.lua",
             "routingName": "ccr-msods1",
@@ -645,7 +645,7 @@
             "dnsBypassIp6": "",
             "dnsBypassTtl": 30,
             "dscp": 40,
-            "edgeHeaderRewrite": "edgeHeader1",
+            "edgeHeaderRewrite": "edgeRewrite1\nedgeHeader2",
             "exampleURLs": [
                 "http://ccr.ds1nat.example.net",
                 "https://ccr.ds1nat.example.net"
@@ -674,7 +674,7 @@
                 }
             ],
             "maxDnsAnswers": 0,
-            "midHeaderRewrite": "midHeader1",
+            "midHeaderRewrite": "midHeader1\nmidHeader2",
             "missLat": 41.881944,
             "missLong": -87.627778,
             "multiSiteOrigin": false,
@@ -685,7 +685,7 @@
             "protocol": 2,
             "qstringIgnore": 1,
             "rangeRequestHandling": 0,
-            "regexRemap": "rr1",
+            "regexRemap": "rr1\nrr2",
             "regionalGeoBlocking": false,
             "remapText": "@plugin=tslua.so @pparam=/opt/trafficserver/etc/trafficserver/remapPlugin1.lua",
             "routingName": "ccr-ds1nat",
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
index 779e224..9b8a43b 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
@@ -534,7 +534,8 @@
                         </div>
                         </label>
                         <div class="col-md-10 col-sm-10 col-xs-12">
-                            <textarea id="remapText" name="remapText" class="form-control" ng-model="deliveryService.remapText" rows="3"></textarea>
+                            <textarea id="remapText" name="remapText" class="form-control" ng-model="deliveryService.remapText" ng-pattern="/^[^\n\r]*$/" rows="3"></textarea>
+                            <small class="input-error" ng-show="hasPropertyError(cacheConfig.remapText, 'pattern')">No Line Breaks</small>
                             <aside class="current-value" ng-if="settings.isRequest" ng-show="open() && deliveryService.remapText != dsCurrent.remapText">
                                 <h3>Current Value</h3>
                                 <pre>{{::dsCurrent.remapText}}</pre>
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
index a491062..e0f1878 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
@@ -534,7 +534,8 @@
                         </div>
                         </label>
                         <div class="col-md-10 col-sm-10 col-xs-12">
-                            <textarea id="remapText" name="remapText" class="form-control" ng-model="deliveryService.remapText" rows="3"></textarea>
+                            <textarea id="remapText" name="remapText" class="form-control" ng-model="deliveryService.remapText" ng-pattern="/^[^\n\r]*$/" rows="3"></textarea>
+                            <small class="input-error" ng-show="hasPropertyError(cacheConfig.remapText, 'pattern')">No Line Breaks</small>
                             <aside class="current-value" ng-if="settings.isRequest" ng-show="open() && deliveryService.remapText != dsCurrent.remapText">
                                 <h3>Current Value</h3>
                                 <pre>{{::dsCurrent.remapText}}</pre>
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
index 4db5f01..3c14f0d 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
@@ -273,7 +273,8 @@
                         </div>
                         </label>
                         <div class="col-md-10 col-sm-10 col-xs-12">
-                            <textarea id="remapText" name="remapText" class="form-control" ng-model="deliveryService.remapText" rows="3"></textarea>
+                            <textarea id="remapText" name="remapText" class="form-control" ng-model="deliveryService.remapText" ng-pattern="/^[^\n\r]*$/" rows="3" required></textarea>
+                            <small class="input-error" ng-show="hasPropertyError(cacheConfig.remapText, 'pattern')">No Line Breaks</small>
                             <aside class="current-value" ng-if="settings.isRequest" ng-show="open() && deliveryService.remapText != dsCurrent.remapText">
                                 <h3>Current Value</h3>
                                 <pre>{{::dsCurrent.remapText}}</pre>