Created
September 27, 2022 14:20
-
-
Save luizvaz/30c083573353073716e14ddbaa8a67fc to your computer and use it in GitHub Desktop.
Assinatura e Envio de XML FIORILLI padrão ABRASF 2.01 (Senador Canedo)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* User: LuizVAz | |
* Date: 26/09/2022 | |
* Time: 12:46 | |
*/ | |
class fiorilli_v001 | |
{ | |
private $rps_id; | |
private $rps_serie; | |
private $usuario; | |
private $senha; | |
public function __construct(){ | |
$this->rps_id = date('Ymd') . str_pad(time() - strtotime("today"), 5, '0'); | |
$this->rps_serie = "3"; | |
// As credenciais de homologação são sempre as mesmas | |
// mas precisa solicitar para a Fiorilli a inscrição | |
// municipal para ser usada no arquivo XML. | |
$this->usuario = "01001001000113"; | |
$this->senha = "123456"; | |
} | |
public function gerarNfse(): stdClass | |
{ | |
//Live Mode | |
$live = true; | |
//Retorno | |
$return = new stdClass(); | |
$return->status = 500; | |
$return->data = array("success" => false); | |
$rps_dir = getcwd().DIRECTORY_SEPARATOR."xml".DIRECTORY_SEPARATOR."envio".DIRECTORY_SEPARATOR; | |
$rps_id = $this->rps_id; | |
// Homologação | |
$url = 'http://fi1.fiorilli.com.br:5663/IssWeb-ejb/IssWebWS/IssWebWS?wsdl'; | |
//rps_id | |
$this->rps_id = "$rps_id"; | |
//Garante que o diretório exista | |
$fname = "${rps_dir}${rps_id}.xml"; | |
if (!file_exists("${rps_dir}")) { | |
mkdir("${rps_dir}", 0777, true); | |
} | |
//Usuário e Senha do portal | |
$user = $this->usuario; | |
$pass = $this->senha; | |
$envelope = | |
"<soapenv:Envelope \n". | |
" xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" \n". | |
" xmlns:ws=\"http://ws.issweb.fiorilli.com.br/\">\n". | |
" xmlns:xd=\"http://www.w3.org/2000/09/xmldsig#\"\n". | |
" <soapenv:Header/>\n". | |
" <soapenv:Body>\n". | |
" <ws:gerarNfse>%DadosMsg%<username>$user</username><password>$pass</password></ws:gerarNfse>\n". | |
" </soapenv:Body>\n". | |
"</soapenv:Envelope>"; | |
// Arquivo pronto | |
$xml = file_get_contents(dirname($rps_dir)."\GerarNFSe.xml"); | |
// Assinatura | |
$xml = str_replace(array("\t"), '', $xml); | |
$xml = $this->remove_accents($xml); | |
// Não remover a formatação depois de assinado | |
$xml = $this->assinaXML($xml, 'InfDeclaracaoPrestacaoServico'); | |
file_put_contents("${rps_dir}${rps_id}.sign.xml", $xml); | |
//Envio | |
if ($live) { | |
//Live Mode | |
$envelope = str_replace('%DadosMsg%', $xml, $envelope); | |
file_put_contents("${rps_dir}${rps_id}.soap", $envelope); | |
$ret = $this->sendRequest($envelope, "http://ws.issweb.fiorilli.com.br/gerarNfse", $url); | |
$html = $ret["html"]; | |
$httpcode = $ret["code"]; | |
$return->status = $httpcode; | |
file_put_contents("$fname.ret", $html); | |
} else { | |
//Test Mode | |
$fname = "${rps_dir}2022092719685.xml"; | |
$html = file_get_contents("$fname.ret"); | |
$return->status = $httpcode = 200; | |
} | |
// Converte de UTF-8 para ISO-8859 se necessário | |
// $html = utf8_decode($html); | |
if ($httpcode!=200){ | |
preg_match_all('/<faultstring>(.*?)<\/faultstring>/s', html_entity_decode($html), $matches); | |
$response = (count($matches)&&count($matches[1])?$matches[1][0]:$html); | |
$return->data['errors'][] = [ | |
"codigo" => $httpcode, | |
"mensagem" => $response, | |
"correcao" => "" | |
]; | |
return $return; | |
} | |
//Extrai | |
$return = $this->parseRetorno($html, $return); | |
return $return; | |
} | |
function assinaXML($docxml, $tagid){ | |
$URLdsig='http://www.w3.org/2000/09/xmldsig#'; | |
$URLCanonMeth='http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; | |
$URLSigMeth='http://www.w3.org/2000/09/xmldsig#rsa-sha1'; | |
$URLTransfMeth_1='http://www.w3.org/2000/09/xmldsig#enveloped-signature'; | |
$URLTransfMeth_2='http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; | |
$URLDigestMeth='http://www.w3.org/2000/09/xmldsig#sha1'; | |
try { | |
//Certificado | |
$crt_dir = getcwd().DIRECTORY_SEPARATOR.'cert'.DIRECTORY_SEPARATOR; | |
$priKEY = "{$crt_dir}pkey.pem"; | |
$keypass = file_get_contents("{$crt_dir}pass.txt"); | |
$pubKEY = "{$crt_dir}cert.pem"; | |
$xml = $docxml; | |
// obter o chave privada para a ssinatura | |
$prikeyid = openssl_pkey_get_private(file_get_contents($priKEY), $keypass); | |
// limpeza do xml com a retirada dos CR, LF e TAB | |
$order = array("\r\n", "\n", "\r", "\t"); | |
$replace = ''; | |
$xml = str_replace($order, $replace, $xml); | |
// Habilita a manipulação de erros da libxml | |
libxml_use_internal_errors(true); | |
//limpar erros anteriores que possam estar em memória | |
libxml_clear_errors(); | |
// carrega o documento no DOM | |
$xmldoc = new DOMDocument('1.0', 'utf-8'); | |
$xmldoc->preservWhiteSpace = true; //elimina espaços em branco | |
$xmldoc->formatOutput = false; | |
// muito importante deixar ativadas as opções para limpar os espacos em branco | |
// e as tags vazias | |
if ($xmldoc->loadXML($xml, LIBXML_NOBLANKS | LIBXML_NOEMPTYTAG)) { | |
$root = $xmldoc->documentElement; | |
} else { | |
$msg = "Erro ao carregar XML, provavel erro na passagem do parametro docxml ou no proprio xml!!"; | |
$errors = libxml_get_errors(); | |
if (!empty($errors)) { | |
$i = 1; | |
foreach ($errors as $error) { | |
$msg .= "\n [$i]-" . trim($error->message); | |
} | |
libxml_clear_errors(); | |
} | |
throw new Exception($msg); | |
} | |
//extrair a tag com os dados a serem assinados | |
$node = $xmldoc->getElementsByTagName($tagid)->item(0); | |
if (!isset($node)) { | |
$msg = "A tag < $tagid > não existe no XML!!"; | |
throw new Exception($msg); | |
} | |
$id = trim($node->getAttribute("Id")); | |
$idnome = preg_replace('/[^0-9]/', '', $id); | |
//extrai os dados da tag para uma string | |
$dados = $node->C14N(false, false, NULL, NULL); | |
//calcular o hash dos dados | |
$hashValue = hash('sha1', $dados, true); | |
//converte o valor para base64 para serem colocados no xml | |
$digValue = base64_encode($hashValue); | |
//monta a tag da assinatura digital | |
$Signature = $xmldoc->createElementNS($URLdsig, 'Signature'); | |
//adiciona a assinatura | |
$node->parentNode->appendChild($Signature); | |
$SignedInfo = $xmldoc->createElement('SignedInfo'); | |
$Signature->appendChild($SignedInfo); | |
//Cannocalization | |
$newNode = $xmldoc->createElement('CanonicalizationMethod'); | |
$SignedInfo->appendChild($newNode); | |
$newNode->setAttribute('Algorithm', $URLCanonMeth); | |
//SignatureMethod | |
$newNode = $xmldoc->createElement('SignatureMethod'); | |
$SignedInfo->appendChild($newNode); | |
$newNode->setAttribute('Algorithm', $URLSigMeth); | |
//Reference | |
$Reference = $xmldoc->createElement('Reference'); | |
$SignedInfo->appendChild($Reference); | |
$Reference->setAttribute('URI', '#' . $id); | |
//Transforms | |
$Transforms = $xmldoc->createElement('Transforms'); | |
$Reference->appendChild($Transforms); | |
//Transform | |
$newNode = $xmldoc->createElement('Transform'); | |
$Transforms->appendChild($newNode); | |
$newNode->setAttribute('Algorithm', $URLTransfMeth_1); | |
//Transform | |
$newNode = $xmldoc->createElement('Transform'); | |
$Transforms->appendChild($newNode); | |
$newNode->setAttribute('Algorithm', $URLTransfMeth_2); | |
//DigestMethod | |
$newNode = $xmldoc->createElement('DigestMethod'); | |
$Reference->appendChild($newNode); | |
$newNode->setAttribute('Algorithm', $URLDigestMeth); | |
//DigestValue | |
$newNode = $xmldoc->createElement('DigestValue', $digValue); | |
$Reference->appendChild($newNode); | |
// extrai os dados a serem assinados para uma string | |
$dados = $SignedInfo->C14N(false, false, NULL, NULL); | |
//inicializa a variavel que irá receber a assinatura | |
$signature = ''; | |
//executa a assinatura digital usando o resource da chave privada | |
$resp = openssl_sign($dados, $signature, $prikeyid); | |
//codifica assinatura para o padrao base64 | |
$signatureValue = base64_encode($signature); | |
//SignatureValue | |
$newNode = $xmldoc->createElement('SignatureValue', $signatureValue); | |
$Signature->appendChild($newNode); | |
//KeyInfo | |
$KeyInfo = $xmldoc->createElement('KeyInfo'); | |
$Signature->appendChild($KeyInfo); | |
//X509Data | |
$X509Data = $xmldoc->createElement('X509Data'); | |
$KeyInfo->appendChild($X509Data); | |
//carrega o certificado sem as tags de inicio e fim | |
$cert = str_replace("\r", '', file_get_contents($pubKEY)); | |
$cert = str_replace("\n", '', $cert); | |
$cert = str_replace('-----BEGIN CERTIFICATE-----', '', $cert); | |
$cert = wordwrap(trim(str_replace('-----END CERTIFICATE-----', '', $cert)), 64, "\n", true); | |
//X509Certificate | |
$newNode = $xmldoc->createElement('X509Certificate', $cert); | |
$X509Data->appendChild($newNode); | |
//grava na string o objeto DOM | |
$xml = $xmldoc->saveXML($xmldoc->documentElement); | |
// libera a memoria | |
openssl_free_key($prikeyid); | |
} catch (Exception $e) { | |
throw $e; | |
} | |
//retorna o documento assinado | |
return $xml; | |
} //fim signXML | |
public function parseRetorno($response, $return): stdClass | |
{ | |
$dom = new DOMDocument(); | |
$dom->preserveWhiteSpace = false; | |
//Verifica falha ao processar XML de RETORNO | |
if (@$dom->loadXML($response) === false){ | |
//Retorno | |
$return->data['errors'][] = [ | |
"codigo" => "500", | |
"mensagem" => "$response", | |
"correcao" => "Contate o suporte!" | |
]; | |
return $return; | |
} | |
$xp = new DOMXPath($dom); | |
$xp->registerNamespace('soap', "http://schemas.xmlsoap.org/soap/envelope/"); | |
$xp->registerNamespace('ns2', "http://www.abrasf.org.br/nfse.xsd"); | |
$xp->registerNamespace('ns3', "http://ws.issweb.fiorilli.com.br/"); | |
// MensagemRetorno | |
$nodes = $xp->query("//ns2:MensagemRetorno"); | |
if ($nodes->length != 0){ | |
if (!array_key_exists('errors', $return->data)) | |
$return->data['errors'] = []; | |
for ($i = 0; $i<$nodes->length; $i++){ | |
$msg = $nodes->item($i); | |
//Tags | |
$cod = $msg->getElementsByTagName("Codigo"); | |
if ($cod->length>0) { | |
$cod = $cod->item(0); | |
$cod = $cod->nodeValue; | |
if (strlen($cod)>5) $cod = "E00"; //Texto redundante | |
} else | |
$cod = "E00"; | |
$txt = $msg->getElementsByTagName("Mensagem"); | |
if ($txt->length>0) { | |
$txt = $txt->item(0); | |
$txt = $txt->nodeValue; | |
} else | |
$txt = ""; | |
$cor = $msg->getElementsByTagName("Correcao"); | |
if ($cor->length>0) { | |
$cor = $cor->item(0); | |
$cor = $cor->nodeValue; | |
} else | |
$cor = ""; | |
//Retorno | |
$return->data['errors'][] = [ | |
"codigo" => $cod, | |
"mensagem" => $txt, | |
"correcao" => $cor | |
]; | |
} | |
return $return; | |
} | |
// Possui Nota | |
$nodes = $xp->query("//ns2:CompNfse"); | |
if ($nodes->length != 0) { | |
$nfseId = ""; | |
$msg = $nodes->item(0); | |
$id = $msg->getElementsByTagName("InfNfse"); | |
$id = ($id->length && ($id = $id->item(0)->getAttribute("Id")))?$id:$this->rps_id; | |
$num = $msg->getElementsByTagName("Numero"); | |
$num = ($num->length)?$num->item(0)->nodeValue:$this->rps_id; | |
$cod = $msg->getElementsByTagName("CodigoVerificacao"); | |
$cod = ($cod->length)?$cod->item(0)->nodeValue:"N/D"; | |
$dat = $msg->getElementsByTagName("DataEmissao"); | |
$dat = ($dat->length)?$dat->item(0)->nodeValue:""; | |
$dat = str_ireplace(' ', 'T', $dat); | |
$return->data['nfseId'] = $id; | |
$return->data['numero'] = $num; | |
$return->data['codigoVerificacao'] = $cod; | |
$return->data['dataEmissao'] = $dat; | |
$return->data['success'] = true; | |
} | |
return $return; | |
} | |
function remove_accents($string) { | |
if ( !preg_match('/[\x80-\xff]/', $string) ) | |
return $string; | |
$chars = array( | |
// Decompositions for Latin-1 Supplement | |
chr(195).chr(128) => 'A', chr(195).chr(129) => 'A', | |
chr(195).chr(130) => 'A', chr(195).chr(131) => 'A', | |
chr(195).chr(132) => 'A', chr(195).chr(133) => 'A', | |
chr(195).chr(135) => 'C', chr(195).chr(136) => 'E', | |
chr(195).chr(137) => 'E', chr(195).chr(138) => 'E', | |
chr(195).chr(139) => 'E', chr(195).chr(140) => 'I', | |
chr(195).chr(141) => 'I', chr(195).chr(142) => 'I', | |
chr(195).chr(143) => 'I', chr(195).chr(145) => 'N', | |
chr(195).chr(146) => 'O', chr(195).chr(147) => 'O', | |
chr(195).chr(148) => 'O', chr(195).chr(149) => 'O', | |
chr(195).chr(150) => 'O', chr(195).chr(153) => 'U', | |
chr(195).chr(154) => 'U', chr(195).chr(155) => 'U', | |
chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y', | |
chr(195).chr(159) => 's', chr(195).chr(160) => 'a', | |
chr(195).chr(161) => 'a', chr(195).chr(162) => 'a', | |
chr(195).chr(163) => 'a', chr(195).chr(164) => 'a', | |
chr(195).chr(165) => 'a', chr(195).chr(167) => 'c', | |
chr(195).chr(168) => 'e', chr(195).chr(169) => 'e', | |
chr(195).chr(170) => 'e', chr(195).chr(171) => 'e', | |
chr(195).chr(172) => 'i', chr(195).chr(173) => 'i', | |
chr(195).chr(174) => 'i', chr(195).chr(175) => 'i', | |
chr(195).chr(177) => 'n', chr(195).chr(178) => 'o', | |
chr(195).chr(179) => 'o', chr(195).chr(180) => 'o', | |
chr(195).chr(181) => 'o', chr(195).chr(182) => 'o', | |
chr(195).chr(182) => 'o', chr(195).chr(185) => 'u', | |
chr(195).chr(186) => 'u', chr(195).chr(187) => 'u', | |
chr(195).chr(188) => 'u', chr(195).chr(189) => 'y', | |
chr(195).chr(191) => 'y', | |
// Decompositions for Latin Extended-A | |
chr(196).chr(128) => 'A', chr(196).chr(129) => 'a', | |
chr(196).chr(130) => 'A', chr(196).chr(131) => 'a', | |
chr(196).chr(132) => 'A', chr(196).chr(133) => 'a', | |
chr(196).chr(134) => 'C', chr(196).chr(135) => 'c', | |
chr(196).chr(136) => 'C', chr(196).chr(137) => 'c', | |
chr(196).chr(138) => 'C', chr(196).chr(139) => 'c', | |
chr(196).chr(140) => 'C', chr(196).chr(141) => 'c', | |
chr(196).chr(142) => 'D', chr(196).chr(143) => 'd', | |
chr(196).chr(144) => 'D', chr(196).chr(145) => 'd', | |
chr(196).chr(146) => 'E', chr(196).chr(147) => 'e', | |
chr(196).chr(148) => 'E', chr(196).chr(149) => 'e', | |
chr(196).chr(150) => 'E', chr(196).chr(151) => 'e', | |
chr(196).chr(152) => 'E', chr(196).chr(153) => 'e', | |
chr(196).chr(154) => 'E', chr(196).chr(155) => 'e', | |
chr(196).chr(156) => 'G', chr(196).chr(157) => 'g', | |
chr(196).chr(158) => 'G', chr(196).chr(159) => 'g', | |
chr(196).chr(160) => 'G', chr(196).chr(161) => 'g', | |
chr(196).chr(162) => 'G', chr(196).chr(163) => 'g', | |
chr(196).chr(164) => 'H', chr(196).chr(165) => 'h', | |
chr(196).chr(166) => 'H', chr(196).chr(167) => 'h', | |
chr(196).chr(168) => 'I', chr(196).chr(169) => 'i', | |
chr(196).chr(170) => 'I', chr(196).chr(171) => 'i', | |
chr(196).chr(172) => 'I', chr(196).chr(173) => 'i', | |
chr(196).chr(174) => 'I', chr(196).chr(175) => 'i', | |
chr(196).chr(176) => 'I', chr(196).chr(177) => 'i', | |
chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij', | |
chr(196).chr(180) => 'J', chr(196).chr(181) => 'j', | |
chr(196).chr(182) => 'K', chr(196).chr(183) => 'k', | |
chr(196).chr(184) => 'k', chr(196).chr(185) => 'L', | |
chr(196).chr(186) => 'l', chr(196).chr(187) => 'L', | |
chr(196).chr(188) => 'l', chr(196).chr(189) => 'L', | |
chr(196).chr(190) => 'l', chr(196).chr(191) => 'L', | |
chr(197).chr(128) => 'l', chr(197).chr(129) => 'L', | |
chr(197).chr(130) => 'l', chr(197).chr(131) => 'N', | |
chr(197).chr(132) => 'n', chr(197).chr(133) => 'N', | |
chr(197).chr(134) => 'n', chr(197).chr(135) => 'N', | |
chr(197).chr(136) => 'n', chr(197).chr(137) => 'N', | |
chr(197).chr(138) => 'n', chr(197).chr(139) => 'N', | |
chr(197).chr(140) => 'O', chr(197).chr(141) => 'o', | |
chr(197).chr(142) => 'O', chr(197).chr(143) => 'o', | |
chr(197).chr(144) => 'O', chr(197).chr(145) => 'o', | |
chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe', | |
chr(197).chr(148) => 'R',chr(197).chr(149) => 'r', | |
chr(197).chr(150) => 'R',chr(197).chr(151) => 'r', | |
chr(197).chr(152) => 'R',chr(197).chr(153) => 'r', | |
chr(197).chr(154) => 'S',chr(197).chr(155) => 's', | |
chr(197).chr(156) => 'S',chr(197).chr(157) => 's', | |
chr(197).chr(158) => 'S',chr(197).chr(159) => 's', | |
chr(197).chr(160) => 'S', chr(197).chr(161) => 's', | |
chr(197).chr(162) => 'T', chr(197).chr(163) => 't', | |
chr(197).chr(164) => 'T', chr(197).chr(165) => 't', | |
chr(197).chr(166) => 'T', chr(197).chr(167) => 't', | |
chr(197).chr(168) => 'U', chr(197).chr(169) => 'u', | |
chr(197).chr(170) => 'U', chr(197).chr(171) => 'u', | |
chr(197).chr(172) => 'U', chr(197).chr(173) => 'u', | |
chr(197).chr(174) => 'U', chr(197).chr(175) => 'u', | |
chr(197).chr(176) => 'U', chr(197).chr(177) => 'u', | |
chr(197).chr(178) => 'U', chr(197).chr(179) => 'u', | |
chr(197).chr(180) => 'W', chr(197).chr(181) => 'w', | |
chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y', | |
chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z', | |
chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z', | |
chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z', | |
chr(197).chr(190) => 'z', chr(197).chr(191) => 's' | |
); | |
$string = strtr($string, $chars); | |
return $string; | |
} | |
public function sendRequest(string $envelope, string $action, string $url): array | |
{ | |
$headers = array( | |
"Content-type: text/xml;charset=UTF-8", | |
"Accept-Encoding: gzip,deflate", | |
"SOAPAction: \"$action\"", | |
"Content-length: " . strlen($envelope), | |
); | |
$ch = curl_init(); | |
curl_setopt($ch, CURLOPT_URL, $url); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
curl_setopt($ch, CURLOPT_TIMEOUT, 300); | |
curl_setopt($ch, CURLOPT_POST, true); | |
curl_setopt($ch, CURLOPT_POSTFIELDS, $envelope); | |
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); | |
curl_setopt($ch, CURLOPT_ENCODING, "identity, deflate, gzip"); | |
//Usar proxy se a prefeitura bloquear IPs estrangeiros | |
//$proxy = '200.255.122.174:8080'; | |
//curl_setopt($ch, CURLOPT_PROXY, $proxy); | |
//curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); | |
$html = curl_exec($ch); | |
// Converte de UTF-8 para ISO-8859 | |
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
curl_close($ch); | |
return [ | |
"html" => $html, | |
"code" => $httpcode | |
]; | |
} | |
} |
Quando eu coloco essa data, retorna esse erro:
array(2) {
["success"]=>
bool(false)
["errors"]=>
array(1) {
[0]=>
array(3) {
["codigo"]=>
int(500)
["mensagem"]=>
string(40) "Unmarshalling Error: 2022-12-15T08:50:1 "
["correcao"]=>
string(0) ""
}
}
}
Realmente não aceita então?
Na verdade, o campo DataEmissao dentro da tag rps só aceita date mesmo. O que aceita dateTime é o dentro da tag Infse mas esse campo é gerado automaticamente, como posso alterar ela?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
No schema nfse.xsd da Fiorilli o formato do campo DataEmissao está como:
<xsd:element name="DataEmissao" type="xsd:dateTime" minOccurs="1" maxOccurs="1" />
O tipo xsd:dateTime possui aceita seguinte formato:
2022-12-15T08:50:1
Só que atualmente depende de como a empresa está tratando o campo.
Vi que foi enviado como
<DataEmissao>2022-09-26</DataEmissao>
Tente fazer o teste, usando o formato.
Se não funcionar, infelizmente não aceita mesmo.