Skip to content

Commit

Permalink
Redoing how parsing raises errors and codes, then also adding some mo…
Browse files Browse the repository at this point in the history
…re error checking and doing some basic framing for future error checking
  • Loading branch information
arfrank committed Nov 22, 2010
1 parent 3318dcf commit 6e4b496
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 43 deletions.
8 changes: 4 additions & 4 deletions handlers/main.py
Expand Up @@ -212,11 +212,11 @@ def post(self,Sid):

if 200<= self.data['Response'].status_code <= 300:
logging.info('normal response')
Valid, TwimlText, self.data['TwilioCode'], self.data['TwilioMsg'] = twiml.check_twiml( self.data['Response'] )
Valid, TwimlText, self.data['TwilioCode'], self.data['TwilioMsg'] = twiml.check_twiml_content_type( self.data['Response'] )
logging.info(self.request.headers)
if Valid:
logging.info('Valid twiml check for content-type')
Valid, self.data['twiml_object'], self.data['ErrorMessage'] = twiml.parse_twiml(TwimlText, False)
Valid, self.data['twiml_object'], self.data['TwilioCode'], self.data['TwilioMsg'] = twiml.parse_twiml(TwimlText, sms = False)

Url = getattr( self.data['PhoneNumber'], self.InstanceMain[0] )

Expand All @@ -230,10 +230,10 @@ def post(self,Sid):

if 200 <= self.data['FallbackResponse'].status_code <=300:

Valid, TwimlText, self.data['TwilioCode'], self.data['TwilioMsg'] = twiml.check_twiml( self.data['FallbackResponse'] )
Valid, TwimlText, self.data['TwilioCode'], self.data['TwilioMsg'] = twiml.check_twiml_content_type( self.data['FallbackResponse'] )
if Valid:

Valid, self.data['twiml_object'], self.data['ErrorMessage'] = twiml.parse_twiml(TwimlText, True)
Valid, self.data['twiml_object'], self.data['TwilioCode'], self.data['TwilioMsg'] = twiml.parse_twiml(TwimlText, sms = True)

Url = getattr( self.data['PhoneNumber'], self.InstanceFallback[0] )

Expand Down
104 changes: 77 additions & 27 deletions helpers/twiml.py
Expand Up @@ -119,48 +119,48 @@ def emulate(url, method = 'GET', digits = None):
"""

class TwiMLSyntaxError(Exception):
def __init__(self, lineno, col, doc):
self.lineno = lineno
self.col = col
self.doc = doc
def __init__(self, error, code, msg):
self.error = error
self.code = code
self.msg = msg

def __str__(self):
return "TwiMLSyntaxError at line %i col %i near %s" \
% (self.lineno, self.col, self.doc)
return "%s\nTwilioCode: %s TwilioMsg: %s" \
% (self.error, str(self.code), self.msg)

#Returns valid, twiml_object, error message
def parse_twiml(response, sms = False):
#Returns valid, twiml_object, twilio code, twilio message
def parse_twiml(response, sms = False, allow_dial = True):
try:
rdoc = minidom.parseString(response)
except ExpatError, e:
return False, False, 'Bad TwiML Document'
return False, False, 12100, 'http://www.twilio.com/docs/errors/12100'

try:
respNode = rdoc.getElementsByTagName('Response')[0]
except IndexError, e:
return False, False, 'No response tag in TwiML'
return False, False, 12100, 'http://www.twilio.com/docs/errors/12100'

if not respNode.hasChildNodes():
return False, False, 'No child nodes, nothing to do.'
return False, False, 12100, 'http://www.twilio.com/docs/errors/12100'
nodes = respNode.childNodes
try:
twiml_object = walk_tree(nodes,'Response', sms)
twiml_object = walk_tree(nodes,'Response', sms = sms, allow_dial = allow_dial)
except TwiMLSyntaxError, e:
return False, False, e
return False, False, e.code, e.msg
#lets walk the tree and create a list [ { 'Verb' : '', 'Attr': { 'action' : '', 'Method' : 'POST' } } ]
else:
return True, twiml_object, False
return True, twiml_object, 0, ''

def retrieve_attr(node, Type, sms = False):
d = {}
for attr in node.attributes.items():
if (sms == False and attr[0] in ALLOWED_VOICE_ELEMENTS[Type]) or (sms == True and attr[0] in ALLOWED_SMS_ELEMENTS[Type]):
d[attr[0]] = attr[1]
else:
raise TwiMLSyntaxError(0, 0, 'Invalid attribute in '+Type+':('+attr[0]+'='+attr[1]+')')
raise TwiMLSyntaxError('Invalid attribute in '+Type+':('+attr[0]+'='+attr[1]+')', 12200,'http://www.twilio.com/docs/errors/12200')
return d

def walk_tree(nodes, parentType, sms = False):
def walk_tree(nodes, parentType, sms = False, allow_dial = True):
twiml = []
count = 0
for node in nodes:
Expand All @@ -169,18 +169,30 @@ def walk_tree(nodes, parentType, sms = False):
#logging.info(parentType)
#logging.info(node.nodeName.encode('ascii'))
#logging.info(sms)
if ( ( parentType == 'Response' and ( ( node.nodeName.encode('ascii') in ALLOWED_SMS_ELEMENTS and sms == True) or (node.nodeName.encode('ascii') in ALLOWED_VOICE_ELEMENTS and sms == False) ) ) or ( parentType in ALLOWED_SUBELEMENTS ) ):

#if we are at the top level of the response, and we are in the allowed verbs for the request type, or we are a valid subelement
nodeName = node.nodeName.encode( 'ascii' )

if ( ( parentType == 'Response' and ( ( nodeName in ALLOWED_SMS_ELEMENTS and sms == True) or ( nodeName in ALLOWED_VOICE_ELEMENTS and sms == False ) ) ) or ( parentType in ALLOWED_SUBELEMENTS ) ):

if allow_dial == False and nodeName == 'Dial':

raise TwiMLSyntaxError( 'Cannot Dial out from a dial call segment', 13201, 'http://www.twilio.com/docs/errors/13201' )

if parentType == 'Response' or node.nodeName.encode('ascii') in ALLOWED_SUBELEMENTS[parentType]:

twiml.append( { 'Type' : node.nodeName.encode( 'ascii' ), 'Attr' : retrieve_attr(node, node.nodeName.encode('ascii'), sms),'Children': walk_tree(node.childNodes, node.nodeName.encode('ascii'), sms) } )


check_twiml_verb_attr(node)

check_twiml_verb_children(node)

twiml.append( { 'Type' : node.nodeName.encode( 'ascii' ), 'Attr' : retrieve_attr(node, node.nodeName.encode('ascii'), sms),'Children': walk_tree(node.childNodes, node.nodeName.encode('ascii'), sms = sms) } )

else:

raise TwiMLSyntaxError(0, 0, 'Invalid TwiML nested element in '+parentType+'. Not allowed to nest '+node.nodeName.encode('ascii'))

raise TwiMLSyntaxError('Invalid TwiML nested element in '+parentType+'. Not allowed to nest '+node.nodeName.encode('ascii'), 12200, 'http://www.twilio.com/docs/errors/12200')

else:

raise TwiMLSyntaxError(0, 0, 'Invalid TwiML in '+parentType+'. Problem with '+node.nodeName.encode('ascii')+' element: '+str(count))
raise TwiMLSyntaxError( 'Invalid TwiML in ' + parentType + '. Problem with '+node.nodeName.encode('ascii')+' element ('+str(count)+')', 12200, 'http://www.twilio.com/docs/errors/12200' )

elif node.nodeType == node.TEXT_NODE and parentType != 'Response':

Expand All @@ -192,8 +204,46 @@ def walk_tree(nodes, parentType, sms = False):

return twiml

def check_twiml_verb_attr(node):
name = node.nodeName.encode( 'ascii' )
if name == 'Dial':
pass
elif name == 'Conference':
pass
elif name == 'Number':
pass

def check_twiml_dial_attr():
"""docstring for check_twiml_dial_attr"""
pass

def check_twiml_conference_attr():
"""docstring for check_twiml_conference_attr"""
pass

def check_twiml_number_attr():
"""docstring for check_twiml_number_attr"""
pass

def check_twiml_verb_children(node):
verb = node.nodeName.encode( 'ascii' )
if verb == 'Dial':
#should be a seperate function, but right now this is the only place where it matters"
if not len(node.childNodes):
raise TwiMLSyntaxError( 'The dial verb needs to have nested nouns', 12100, 'http://www.twilio.com/docs/errors/12100' )
else:
conf = False
num = False
for child in node.childNodes:
if child.nodeName.encode( 'ascii' ) == 'Conference':
conf = True
elif child.nodeName.encode( 'ascii' ) == 'Number':
num = True
if num and conf:
raise TwiMLSyntaxError( 'Cannot have nested Number and Conference in the same Dial Verb', 12100, 'http://www.twilio.com/docs/errors/12100' )


def check_twiml(response):
def check_twiml_content_type(response):
TWIML_CONTENT_TYPES = ['text/xml','application/xml','text/html']
TEXT_CONTENT_TYPES = ['text/plain']
AUDIO_CONTENT_TYPES = ['audio/mpeg', 'audio/wav', 'audio/wave', 'audio/x-wav', 'audio/aiff', 'audio/x-aifc', 'audio/x-aiff', 'audio/x-gsm', 'audio/gsm', 'audio/ulaw']
Expand Down Expand Up @@ -521,7 +571,7 @@ def get_external_twiml(Account, Action, Method, Instance, Payload, Twiml):
#parse the new twiml document
if 'Content-Length' in Response.headers and Response.headers['Content-Length'] > 0:
#return Valid, Twiml_object, ErrorMessage
Valid, Twiml_object, ErrorMessage = parse_twiml(Response.content, True if Instance.Sid[0:2] == 'SM' else False) #returns Valid, Twiml_object, ErrorMessage
Valid, Twiml_object, TwilioCode, TwilioMsg = parse_twiml(Response.content, True if Instance.Sid[0:2] == 'SM' else False) #returns Valid, Twiml_object, ErrorMessage
Twiml = None
if Valid:
#if all that works, create new twiml document
Expand All @@ -538,7 +588,7 @@ def get_external_twiml(Account, Action, Method, Instance, Payload, Twiml):
)
Twiml.put()
else:
msg+='An error occurred parsing your action\'s Twiml document, will continue parsing original\n'+ErrorMessage+'\n'
msg+='An error occurred parsing your action\'s Twiml document, will continue parsing original\n'+TwilioMsg+'\n'
return Valid, Twiml, msg

return False, False, 'Could not retrieve a valid TwiML document'
34 changes: 22 additions & 12 deletions unit_tests/twiml_helper_test.py
Expand Up @@ -35,16 +35,16 @@ def __init__(self, content_type = None, path = None):
self.TextRequests.append(FakeRequest( content_type = self.TextHeader['Content-Type']))
def test_Check_Twiml_Success_Twiml(self):
for req in self.TwimlRequests:
Valid, Twiml, TwilioCode, TwilioMsg = twiml.check_twiml( req )
Valid, Twiml, TwilioCode, TwilioMsg = twiml.check_twiml_content_type( req )
self.assertTrue(Valid)

def test_Check_Twiml_Success_Text(self):
Valid, Twiml, TwilioCode, TwilioMsg = twiml.check_twiml( self.TextRequests[0] )
Valid, Twiml, TwilioCode, TwilioMsg = twiml.check_twiml_content_type( self.TextRequests[0] )
self.assertTrue(Valid)

def test_Check_Twiml_Success_Audio(self):
for req in self.AudioRequests:
Valid, Twiml, TwilioCode, TwilioMsg = twiml.check_twiml( req )
Valid, Twiml, TwilioCode, TwilioMsg = twiml.check_twiml_content_type( req )
self.assertTrue(Valid)


Expand All @@ -54,12 +54,12 @@ def setUp(self):
pass
def test_Parse_Fail_Missing_Reponse_Twiml_Tag(self):
Twiml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><Say>Hello</Say>"
Valid, Twiml, ErrorMessage = twiml.parse_twiml(Twiml)
Valid, Twiml, TwilioCode, TwilioMsg = twiml.parse_twiml(Twiml)
self.assertFalse(Valid)

def test_Parse_Fail_No_Content(self):
Twiml = ""
Valid, Twiml, ErrorMessage = twiml.parse_twiml(Twiml)
Valid, Twiml, TwilioCode, TwilioMsg = twiml.parse_twiml(Twiml)
self.assertFalse(Valid)

def test_Parse_Fail_Nested_Elements(self):
Expand All @@ -70,31 +70,41 @@ def test_Parse_Fail_Nested_Elements(self):
<Play>http://twilio.com</Play>
</Say>
</Response>"""
Valid, Twiml, ErrorMessage = twiml.parse_twiml(Twiml)
logging.info( ErrorMessage )
Valid, Twiml, TwilioCode, TwilioMsg = twiml.parse_twiml(Twiml)
logging.info( TwilioCode, TwilioMsg )
self.assertFalse(Valid)

def test_Parse_Fail_Capitalize_Verb(self):
Twiml = """<?xml version="1.0" encoding="UTF-8" ?>
<Response>
<say>This is the second one</say>
</Response>"""
Valid, Twiml, ErrorMessage = twiml.parse_twiml(Twiml)
Valid, Twiml, TwilioCode, TwilioMsg = twiml.parse_twiml(Twiml)
self.assertFalse(Valid)

def test_Parse_Fail_Bad_Attributes(self):
Twiml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><Response><Say length=10>Hello</Say></Response>"
Valid, Twiml, ErrorMessage = twiml.parse_twiml(Twiml)
Valid, Twiml, TwilioCode, TwilioMsg = twiml.parse_twiml(Twiml)
self.assertFalse(Valid)

def test_Parse_Fail_Dial_Children_None(self):
pass
Twiml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><Response><Dial></Dial></Response>"
Valid, Twiml, TwilioCode, TwilioMsg = twiml.parse_twiml(Twiml)
self.assertFalse(Valid)
self.assertEqual(TwilioCode, 12100)

def test_Parse_Fail_Dial_Children_Both_Number_Conference(self):
pass
Twiml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><Response><Dial><Number>+12405553333</Number><Conference>Test</Conference></Dial></Response>"
Valid, Twiml, TwilioCode, TwilioMsg = twiml.parse_twiml(Twiml)
self.assertFalse(Valid)
self.assertEqual(TwilioCode, 12100)

def test_Parse_Fail_Dial_Children_MultiConference(self):
pass

def test_Parse_Fail_Dial_Children_Number_Twiml_Dial(self):
pass
Twiml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><Response><Dial><Conference>Test</Conference></Dial></Response>"
Valid, Twiml, TwilioCode, TwilioMsg = twiml.parse_twiml(Twiml, sms = False, allow_dial = False)
self.assertFalse(Valid)
self.assertEqual(TwilioCode, 13201)

0 comments on commit 6e4b496

Please sign in to comment.