-
-
Save luizvaz/30c083573353073716e14ddbaa8a67fc to your computer and use it in GitHub Desktop.
<?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 | |
]; | |
} | |
} |
É 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.
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.
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?
Esse é o arquivo XML usado no envio:
GerarNFSe.xml