Skip to content

Instantly share code, notes, and snippets.

@luizvaz
Created September 27, 2022 14:20
Show Gist options
  • Save luizvaz/30c083573353073716e14ddbaa8a67fc to your computer and use it in GitHub Desktop.
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)
<?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
];
}
}
@luizvaz
Copy link
Author

luizvaz commented Sep 27, 2022

Esse é o arquivo XML usado no envio:
GerarNFSe.xml

@WallisonBreno
Copy link

É possível gerar nfse nesse mesmo padrão mas com o campo DataEmissao com data e hora? quando eu uso o formato data e hora no xml dá erro de assinatura.

@luizvaz
Copy link
Author

luizvaz commented Dec 21, 2022

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.

@WallisonBreno
Copy link

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?

@WallisonBreno
Copy link

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