import { DOMParser } from '@xmldom/xmldom' ; import { select , SelectedValue } from 'xpath' ; import { uniq , last , zipObject , notEmpty } from './utility' ; import camelCase from 'camelcase' ; const dom = DOMParser ; interface ExtractorField { key : string ; localPath : string [ ] | string [ ] [ ] ; attributes : string [ ] ; index ? : string [ ] ; attributePath ? : string [ ] ; context ? : boolean ; } export type ExtractorFields = ExtractorField [ ] ; function buildAbsoluteXPath ( paths ) { return paths . reduce ( ( currentPath , name ) => { let appendedPath = currentPath ; const isWildcard = name . startsWith ( '~' ) ; if ( isWildcard ) { const pathName = name . replace ( '~' , '' ) ; appendedPath = currentPath + `/*[contains(local-name(), '${ pathName }')]` ; } if ( ! isWildcard ) { appendedPath = currentPath + `/*[local-name(.)='${ name }']` ; } return appendedPath ; } , '' ) ; } function buildAttributeXPath ( attributes ) { if ( attributes . length === 0 ) { return '/text()' ; } if ( attributes . length === 1 ) { return `/@${ attributes [ 0 ] }` ; } const filters = attributes . map ( attribute => `name()='${ attribute }'` ) . join ( ' or ' ) ; return `/@*[${ filters }]` ; } export const loginRequestFields = [ { key : 'request' , localPath : [ 'AuthnRequest' ] , attributes : [ 'ID' , 'IssueInstant' , 'Destination' , 'AssertionConsumerServiceURL' ] } , { key : 'issuer' , localPath : [ 'AuthnRequest' , 'Issuer' ] , attributes : [ ] } , { key : 'nameIDPolicy' , localPath : [ 'AuthnRequest' , 'NameIDPolicy' ] , attributes : [ 'Format' , 'AllowCreate' ] } , { key : 'authnContextClassRef' , localPath : [ 'AuthnRequest' , 'AuthnContextClassRef' ] , attributes : [ ] } , { key : 'signature' , localPath : [ 'AuthnRequest' , 'Signature' ] , attributes : [ ] , context : true } ] ; export const loginResponseStatusFields = [ { key : 'top' , localPath : [ 'Response' , 'Status' , 'StatusCode' ] , attributes : [ 'Value' ] , } , { key : 'second' , localPath : [ 'Response' , 'Status' , 'StatusCode' , 'StatusCode' ] , attributes : [ 'Value' ] , } ] ; export const logoutResponseStatusFields = [ { key : 'top' , localPath : [ 'LogoutResponse' , 'Status' , 'StatusCode' ] , attributes : [ 'Value' ] } , { key : 'second' , localPath : [ 'LogoutResponse' , 'Status' , 'StatusCode' , 'StatusCode' ] , attributes : [ 'Value' ] , } ] ; export const loginResponseFields = assertion => [ { key : 'conditions' , localPath : [ 'Assertion' , 'Conditions' ] , attributes : [ 'NotBefore' , 'NotOnOrAfter' ] , shortcut : assertion } , { key : 'response' , localPath : [ 'Response' ] , attributes : [ 'ID' , 'IssueInstant' , 'Destination' , 'InResponseTo' ] , } , { key : 'audience' , localPath : [ 'Assertion' , 'Conditions' , 'AudienceRestriction' , 'Audience' ] , attributes : [ ] , shortcut : assertion } , { key : 'issuer' , localPath : [ 'Assertion' , 'Issuer' ] , attributes : [ ] , shortcut : assertion } , { key : 'nameID' , localPath : [ 'Assertion' , 'Subject' , 'NameID' ] , attributes : [ ] , shortcut : assertion } , { key : 'sessionIndex' , localPath : [ 'Assertion' , 'AuthnStatement' ] , attributes : [ 'AuthnInstant' , 'SessionNotOnOrAfter' , 'SessionIndex' ] , shortcut : assertion } , { key : 'attributes' , localPath : [ 'Assertion' , 'AttributeStatement' , 'Attribute' ] , index : [ 'Name' ] , attributePath : [ 'AttributeValue' ] , attributes : [ ] , shortcut : assertion } ] ; export const logoutRequestFields = [ { key : 'request' , localPath : [ 'LogoutRequest' ] , attributes : [ 'ID' , 'IssueInstant' , 'Destination' ] } , { key : 'issuer' , localPath : [ 'LogoutRequest' , 'Issuer' ] , attributes : [ ] } , { key : 'nameID' , localPath : [ 'LogoutRequest' , 'NameID' ] , attributes : [ ] } , { key : 'signature' , localPath : [ 'LogoutRequest' , 'Signature' ] , attributes : [ ] , context : true } ] ; export const logoutResponseFields = [ { key : 'response' , localPath : [ 'LogoutResponse' ] , attributes : [ 'ID' , 'Destination' , 'InResponseTo' ] } , { key : 'issuer' , localPath : [ 'LogoutResponse' , 'Issuer' ] , attributes : [ ] } , { key : 'signature' , localPath : [ 'LogoutResponse' , 'Signature' ] , attributes : [ ] , context : true } ] ; export function extract ( context , fields ) { const rootDoc = new dom ( ) . parseFromString ( context ) ; return fields . reduce ( ( result , field ) => { const key = field . key ; const localPath = field . localPath ; const attributes = field . attributes ; const isEntire = field . context ; const shortcut = field . shortcut ; const index = field . index ; const attributePath = field . attributePath ; let targetDoc = rootDoc ; if ( shortcut ) { targetDoc = new dom ( ) . parseFromString ( shortcut ) ; } if ( localPath . every ( path => Array . isArray ( path ) ) ) { const multiXPaths = localPath . map ( path => { return `${ buildAbsoluteXPath ( path ) }/text()` ; } ) . join ( ' | ' ) ; return { ... result , [ key ] : uniq ( select ( multiXPaths , targetDoc ) . map ( ( n ) => n . nodeValue ) . filter ( notEmpty ) ) } ; } const baseXPath = buildAbsoluteXPath ( localPath ) ; const attributeXPath = buildAttributeXPath ( attributes ) ; if ( index && attributePath ) { const indexPath = buildAttributeXPath ( index ) ; const fullLocalXPath = `${ baseXPath }${ indexPath }` ; const parentNodes = select ( baseXPath , targetDoc ) ; const parentAttributes = select ( fullLocalXPath , targetDoc ) . map ( ( n ) => n . value ) ; const childXPath = buildAbsoluteXPath ( [ last ( localPath ) ] . concat ( attributePath ) ) ; const childAttributeXPath = buildAttributeXPath ( attributes ) ; const fullChildXPath = `${ childXPath }${ childAttributeXPath }` ; const childAttributes = parentNodes . map ( node => { const nodeDoc = new dom ( ) . parseFromString ( node . toString ( ) ) ; if ( attributes . length === 0 ) { const childValues = select ( fullChildXPath , nodeDoc ) . map ( ( n ) => n . nodeValue ) ; if ( childValues . length === 1 ) { return childValues [ 0 ] ; } return childValues ; } if ( attributes . length > 0 ) { const childValues = select ( fullChildXPath , nodeDoc ) . map ( ( n ) => n . value ) ; if ( childValues . length === 1 ) { return childValues [ 0 ] ; } return childValues ; } return null ; } ) ; const obj = zipObject ( parentAttributes , childAttributes , false ) ; return { ... result , [ key ] : obj } ; } if ( isEntire ) { const node = select ( baseXPath , targetDoc ) ; let value = null ; if ( node . length === 1 ) { value = node [ 0 ] . toString ( ) ; } if ( node . length > 1 ) { value = node . map ( n => n . toString ( ) ) ; } return { ... result , [ key ] : value } ; } if ( attributes . length > 1 ) { const baseNode = select ( baseXPath , targetDoc ) . map ( n => n . toString ( ) ) ; const childXPath = `${ buildAbsoluteXPath ( [ last ( localPath ) ] ) }${ attributeXPath }` ; const attributeValues = baseNode . map ( ( node ) => { const nodeDoc = new dom ( ) . parseFromString ( node ) ; const values = select ( childXPath , nodeDoc ) . reduce ( ( r , n ) => { r [ camelCase ( n . name ) ] = n . value ; return r ; } , { } ) ; return values ; } ) ; return { ... result , [ key ] : attributeValues . length === 1 ? attributeValues [ 0 ] : attributeValues } ; } if ( attributes . length === 1 ) { const fullPath = `${ baseXPath }${ attributeXPath }` ; const attributeValues = select ( fullPath , targetDoc ) . map ( ( n ) => n . value ) ; return { ... result , [ key ] : attributeValues [ 0 ] } ; } if ( attributes . length === 0 ) { let attributeValue = null ; const node = select ( baseXPath , targetDoc ) ; if ( node . length === 1 ) { const fullPath = `string(${ baseXPath }${ attributeXPath })` ; attributeValue = select ( fullPath , targetDoc ) ; } if ( node . length > 1 ) { attributeValue = node . filter ( ( n ) => n . firstChild ) . map ( ( n ) => n . firstChild ! . nodeValue ) ; } return { ... result , [ key ] : attributeValue } ; } return result ; } , { } ) ; }