3737
3838import panos
3939import panos .errors as err
40+
41+ # Defined before sub-module imports below: panos.userid imports _xpath_safe
42+ # from this module, so the symbol must exist by the time `from panos import
43+ # userid` triggers userid's module body.
44+ SELF = "/%s"
45+ ENTRY = "/entry[@name=%s]"
46+ MEMBER = "/member[text()=%s]"
47+
48+
49+ def _xpath_safe (val ):
50+ """Return val as an XPath 1.0 string literal, safe to inject into a predicate.
51+
52+ XPath 1.0 has no escape syntax for quotes inside string literals, so a value
53+ containing quotes must be wrapped in the opposite quote, or split into a
54+ concat() expression when both quote types are present.
55+ """
56+ val = "" if val is None else str (val )
57+ if "'" not in val :
58+ return "'" + val + "'"
59+ if '"' not in val :
60+ return '"' + val + '"'
61+ parts = val .split ("'" )
62+ return "concat('" + "', \" '\" , '" .join (parts ) + "')"
63+
64+
4065from panos import (
4166 chunk_instances_for_delete_similar ,
4267 isstring ,
4974logger = panos .getlogger (__name__ )
5075
5176Root = panos .enum ("DEVICE" , "VSYS" , "MGTCONFIG" , "PANORAMA" , "PANORAMA_VSYS" )
52- SELF = "/%s"
53- ENTRY = "/entry[@name='%s']"
54- MEMBER = "/member[text()='%s']"
5577
5678
5779# PanObject type
@@ -341,7 +363,7 @@ def xpath(self, root=None):
341363 # xpath was asked for.
342364 addon = p .XPATH
343365 if p .SUFFIX is not None :
344- addon += p .SUFFIX % (p .uid ,)
366+ addon += p .SUFFIX % (_xpath_safe ( p .uid ) ,)
345367 path .insert (0 , addon )
346368 if p .__class__ .__name__ == "Firewall" and p .parent is not None :
347369 if p .parent .__class__ .__name__ == "DeviceGroup" :
@@ -413,7 +435,7 @@ def _root_xpath_vsys(self, vsys, label="vsys"):
413435 xpath = "/config/shared"
414436 else :
415437 xpath = "/config/devices/entry[@name='localhost.localdomain']"
416- xpath += "/{0}/entry[@name=' {1}' ]" .format (label , vsys or "vsys1" )
438+ xpath += "/{0}/entry[@name={1}]" .format (label , _xpath_safe ( vsys or "vsys1" ) )
417439
418440 return xpath
419441
@@ -477,7 +499,7 @@ def element(self, with_children=True, comparable=False):
477499 regex ,
478500 matchedvar .path
479501 + "/"
480- + "entry[@name='%s' ]" % entry_value [0 ],
502+ + "entry[@name=%s ]" % _xpath_safe ( entry_value [0 ]) ,
481503 section ,
482504 )
483505 entryvar = matchedvar
@@ -861,7 +883,9 @@ def _get_param_specific_info(self, variable):
861883 entry_value = panos .string_or_list (getattr (self , matchedvar .variable ))
862884 varpath = re .sub (
863885 regex ,
864- matchedvar .path + "/" + "entry[@name='%s']" % entry_value [0 ],
886+ matchedvar .path
887+ + "/"
888+ + "entry[@name=%s]" % _xpath_safe (entry_value [0 ]),
865889 varpath ,
866890 )
867891 else :
@@ -1449,7 +1473,9 @@ def _parse_xml(cls, xml, variables=None):
14491473 replacement = replacement [0 ]
14501474 path = re .sub (
14511475 regex ,
1452- matchedvar .path + "/" + "entry[@name='%s']" % replacement ,
1476+ matchedvar .path
1477+ + "/"
1478+ + "entry[@name=%s]" % _xpath_safe (replacement ),
14531479 path ,
14541480 )
14551481 else :
@@ -1984,10 +2010,10 @@ def delete_similar(self):
19842010 prefix = ""
19852011 xpath = self .xpath_nosuffix ()
19862012 if self .SUFFIX == ENTRY :
1987- joiner = "@name=' {0}' "
2013+ joiner = "@name={0}"
19882014 prefix = "entry"
19892015 elif self .SUFFIX == MEMBER :
1990- joiner = "text()=' {0}' "
2016+ joiner = "text()={0}"
19912017 prefix = "member"
19922018
19932019 # After some testing, PAN-OS seems to be able to handle a DELETE API call
@@ -2000,7 +2026,7 @@ def delete_similar(self):
20002026 "{0}/{1}[{2}]" .format (
20012027 xpath ,
20022028 prefix ,
2003- " or " .join (joiner .format (x .uid ) for x in chunk ),
2029+ " or " .join (joiner .format (_xpath_safe ( x .uid ) ) for x in chunk ),
20042030 ),
20052031 retry_on_peer = self .HA_SYNC ,
20062032 )
@@ -2037,7 +2063,9 @@ def _perform_vsys_dict_import_delete(self, dev, vsys_dict):
20372063 """Iterates over a vsys_dict, deleting the import for all instances."""
20382064 for vsys_spec in vsys_dict .values ():
20392065 for objs in vsys_spec .values ():
2040- members = " or " .join ("text()='{0}'" .format (x .uid ) for x in objs )
2066+ members = " or " .join (
2067+ "text()={0}" .format (_xpath_safe (x .uid )) for x in objs
2068+ )
20412069 xpath = "{0}/member[{1}]" .format (objs [0 ].xpath_import_base (), members )
20422070 # API complains if you try to do this in one delete statement,
20432071 # so do one delete per vsys per path, just like when we set the
@@ -2432,7 +2460,7 @@ class VersionedPanObject(PanObject):
24322460
24332461 _DEFAULT_NAME = None
24342462 _TEMPLATE_DEVICE_XPATH = "/config/devices/entry[@name='localhost.localdomain']"
2435- _TEMPLATE_VSYS_XPATH = _TEMPLATE_DEVICE_XPATH + "/vsys/entry[@name=' {vsys}' ]"
2463+ _TEMPLATE_VSYS_XPATH = _TEMPLATE_DEVICE_XPATH + "/vsys/entry[@name={vsys}]"
24362464 _TEMPLATE_MGTCONFIG_XPATH = "/config/mgt-config"
24372465
24382466 def __init__ (self , * args , ** kwargs ):
@@ -2639,7 +2667,7 @@ def element(self, with_children=True, comparable=False):
26392667 if ap .startswith ("entry " ):
26402668 junk , var_to_use = ap .split ()
26412669 sol_value = panos .string_or_list (settings [var_to_use ])[0 ]
2642- finder = "entry[@name=' {0}' ]" .format (sol_value )
2670+ finder = "entry[@name={0}]" .format (_xpath_safe ( sol_value ) )
26432671 tag = "entry"
26442672 attribs ["name" ] = sol_value
26452673 elif ap == "entry[@name='localhost.localdomain']" :
@@ -2726,8 +2754,8 @@ def _get_param_specific_info(self, param):
27262754 p = None
27272755 if token .startswith ("entry " ):
27282756 junk , var_to_use = token .split ()
2729- p = "entry[name=' {0}' ]" .format (
2730- * (x for x in self ._value_as_list (settings [var_to_use ]))
2757+ p = "entry[name={0}]" .format (
2758+ * (_xpath_safe ( x ) for x in self ._value_as_list (settings [var_to_use ]))
27312759 )
27322760 else :
27332761 p = None
@@ -2836,7 +2864,7 @@ def XPATH(self):
28362864 """Returns the version specific xpath of this object."""
28372865 panos_version = self .retrieve_panos_version ()
28382866 val = self ._xpaths ._get_versioned_value (panos_version , self .parent )
2839- return val .format (vsys = self .vsys or "vsys1" )
2867+ return val .format (vsys = _xpath_safe ( self .vsys or "vsys1" ) )
28402868
28412869
28422870class VersionedParamPath (VersioningSupport ):
@@ -3199,7 +3227,7 @@ def parse_xml(self, xml, settings, possibilities):
31993227 return
32003228 settings [entry_var ] = ans .attrib ["name" ]
32013229 sol_val = panos .string_or_list (settings [entry_var ])[0 ]
3202- path_str = "entry[@name=' {0}' ]" .format (sol_val )
3230+ path_str = "entry[@name={0}]" .format (_xpath_safe ( sol_val ) )
32033231 else :
32043232 # Standard path part
32053233 try :
@@ -3421,8 +3449,8 @@ def delete_import(self, vsys=None):
34213449 p = p .parent
34223450
34233451 if vsys != "shared" and vsys is not None and self .XPATH_IMPORT is not None :
3424- xpath = "{0}/member[text()=' {1}' ]" .format (
3425- self .xpath_import_base (vsys ), self .uid
3452+ xpath = "{0}/member[text()={1}]" .format (
3453+ self .xpath_import_base (vsys ), _xpath_safe ( self .uid )
34263454 )
34273455 device = self .nearest_pandevice ()
34283456 device .active ().xapi .delete (xpath , retry_on_peer = True )
0 commit comments