Protheus e AdvPL ASP – Parte 04

Introdução

Nos posts anteriores sobre AdvPL ASP, vimos até o momento três alias virtuais usados para receber parâmetros ( HTTPGET / HTTPPOST ) e controlar variáveis de seção de usuário (HTTPSESSION). Agora, vamos ver mais dois alias virtuais: HTTPHEADIN e HTTPHEADOUT.

Alias Virtual HTTPHEADIN

Cada requisição HTTP possui um formato interno, separada em cabeçalho (ou Header) e corpo (ou Body). Atenção, estamos falando do protocolo HTTP, e não do header e body de um formulário HTML.

Quando um Web Browse ou um cliente de aplicação WEB faz uma requisição via HTTP, esta requisição é acompanhada de algumas tuplas chave/valor, colocadas no cabeçalho da requisição, que indicam por exemplo o tipo da requisição (GET, POST, …), o tipo do conteúdo enviado na requisição (texto, html, imagem, …), a codificação do conteúdo (ANSI, UTF-8 ou outro CodePage), algumas informações sobre o Web Browse ou da aplicação Cliente que está fazendo a solicitação, entre outros identificadores de uso específico.

Usando o Google Chrome, por exemplo, temos uma janela especial chamada DevTools, onde temos ferramentas que permitem investigar por dentro uma requisição feita pelo Web Browse a um determinado servidor, os parâmetros da requisição e seu retorno. Após abrir a janela do DevTools e digitar a URL http://localhost/ , a requisição enviada ao Web Server foi:

GET / HTTP/1.1 
Host: localhost 
Connection: keep-alive 
Cache-Control: max-age=0 
Upgrade-Insecure-Requests: 1 
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36 
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 
Accept-Encoding: gzip, deflate, br 
Accept-Language: pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6 
Cookie: SESSIONID=41a79408567715479073622c71988001 
If-Modified-Since: Fri, 30 Nov 2018 00:30:20 GMT

Só nesta requisição existe um universo de informações. Não vou entrar nos méritos internos de cada uma, mas apenas nas que realmente importam para o momento, e na forma de recuperá-las.

Da mesma forma que os demais alias virtuais, o alias virtual HTTPHEADIN permite fazer a leitura de um determinado valor a partir do seu nome. Por exemplo:

HTTPHEADIN->CONNECTION deve retornar a string “keep-alive”
HTTPHEADIN->HOST deve retornar a string “localhost”

Agora, o que acontece se eu tentar recuperar o valor de “User-agent” ? HTTPHEADIN->USER-AGENT não é um formato válido para o AdvPL, o compilador vai entender que você quer pegar o valor HTTPHEADIN->USER e fazer uma subtração operação aritmética de subtração da variável “AGENT” , que não existe.

Pensando nesta possibilidade, qualquer caractere que faz parte do nome de uma chave no alias virtual HTTPHEADIN, que não seja válido como nome de variável ou de identificador, será trocado pelo caractere “_” (underline). Desse modo, para recuperar o valor de User-Agent, você deve usar HTTPHEADIN->USER_AGENT

HTTPHEADIN – Valores adicionais

Quando o Web Browse ou a aplicação cliente fez uma requisição HTTP ao Web Server, esta conexão foi aberta sobre o protocolo TCP/IP. Logo, mesmo que a requisição HTTP possa ser minimalista e não ter muitos detalhes, é possível obter dentro do prococolo TCP/IP qual é o IP de Origem da conexão, e qual é a porta TCP usada na conexão aberta pelo Cliente. O IP da Conexão, por exemplo, pode ser recuperado e gravado em um LOG de requisições ou de atividade do Web Site, para uso posterior em estatísticas ou mesmo auditoria de operações. Para obter estes valores, usamos respectivamente:

HTTPEADIN->REMOTE_ADDR
HTTPEADIN->REMOTE_PORT

Estes campos retornam respectivamente um valor “C” caractere, contendo o IP Client da Conexão, e um valor “N” numérico contendo a porta dinâmica TCP da máquina Client que fez a requisição.

Seguindo a mesma linha dos posts anteriores, vamos criar um arquivo chamado headinfo.aph, e dentro dele colocar o seguinte conteúdo:

<html><body>
<pre>
Host ........: <%=HTTPHEADIN->HOST%>
Connection...: <%=HTTPHEADIN->CONNECTION%>
User-Agent ..: <%=HTTPHEADIN->User_Agent%>
Remote Addr..: <%=HTTPHEADIN->REMOTE_ADDR%>
Remote Port..: <%=HTTPHEADIN->REMOTE_PORT%>
</pre>
</body></html>

E, dentro do fonte ASPThreads.prw, inserir mais uma entrada na lista de páginas, para ao receber o link “headinfo.apw”, processar e retornar a função H_HeadInfo() — que corresponde ao arquivo headinfo.aph compilado.

case cAspPage == 'headinfo'
  cReturn := H_HEADINFO()

E, ao testar esta chamada no meu ambiente, usando http://localhost/headinfo.apw, eu obtive o seguinte resultado na tela do Web Browse:

WEB HeadInfo

Agora, vamos ver o que acontece se eu acessar esta página usando um iPhone? Vejamos, primeiro eu preciso achar o IP da minha máquina na rede de casa. Como estou usando Wi-fi, basta eu usar o prompt de comando do Windows (cmd) e usar o comando IPCONFIG, e procurar o endereço IPV4 da interface de rede Wi-fi. Como meu sistema operacional está em Inglês, eu consigo usar alguns comandos adicionais para filtrar os resultados:

ipconfig | findstr /i "Address Adapter"

ipconfig 1

A interface e o valor que eu quero estão destacados em vermelho. Agora, acessando o Chrome do meu telefone, eu digito a URL

http://192.168.0.4/headinfo.apw

E com ela eu recupero as seguintes informações:

IMG_3661

Alias Virtual HTTPHEADOUT

Bem, da mesma forma que uma requisição HTTP é dividida em cabeçalho e corpo, uma resposta de uma requisição também têm esta mesma divisão. Quando você solicita através de um Web Browse ou Cliente HTTP um link de uma página em AdvPL ASP, o próprio Web Server do Protheus monta um cabeçalho de retorno da requisição, com alguns valores pré-definidos. Vou usar a mesma requisição do ultimo teste, e rodar ela com a janela do DevTools aberta. As informações que eu quero estão em uma aba chamada “Response Headers”, que devem mostrar algo assim:

HTTP/1.1 200 OK
Date: Fri, 30 Nov 2018 01:34:01 GMT
Server: Application Web Server
MIME-version: 1.0
Content-type: text/html
Last-modified: Fri, 30 Nov 2018 01:34:01 GMT
Set-cookie: SESSIONID=68c0be02ceaec73ddfcf14281c4f1909 
X-Frame-Options: SAMEORIGIN
Content-Length: 284

Este cabeçalho de retorno foi montado pelo Web Server do Protheus, logo que a página AdvPL ASP foi procecssada, e a função ASPConn() retornou a string cHTML. A requisição foi processada com sucesso (código de retorno HTTP 200), a data de processamento, o retorno deve ser tratado pelo Browser como um texto html (definido pelo Content-type), e o tamanho do HTML retornado foi de 284 bytes.

Lembram-se no post anterior, quando eu falei de SESSIONS de usuário? O Web Server do Protheus retorna um Cookie de memória chamado SESSIONID, gerado para esta instância de Browse, para saber nas próximas requisições que se trata da mesma instância e/ou usuário. Pode comparar este valor com o valor recebido demonstrado no exemplo dos Headers da requisição HTTP — os valores estão diferentes, pois entre um teste e outro, eu fechei o Browser e abri novamente. Mais para frente vamos ver um alias virtual que permite lidar com Cookies de memória, então vamos entrar neste assunto novamente e com maior profundidade.

Voltando ao alias virtual HTTPHEADOUT, ele foi criado para que você possa, durante o processamento de uma requisição de AdvPL ASP, criar um novo valor de retorno, para ser acrescentado no Header de retorno da requisição HTTP para o Web Browser ou Cliente que fez a solicitação. PAra ver a lista de campos mais usadas, veja os links de referência no final do post.

Normalmente você não precisa utilizar diretamente o alias HTTPHEADOUT, inclusive por que ele têm a restrição de não permitir a criação de campos de retorno usando por exemplo o símbolo “-“, existem outras funções feitas especificamente para mexer em valores default. O uso projetado para este alias virtual é a possibilidade de criar um header com um nome de identificador exclusivo, para uma integração entre sistemas por exemplo.

Conclusão

Falta pouco para cobrir o básico do AdvPL ASP. Se você começou a ler sobre este assunto agora, pegue os posts desde o primeiro da sequência de AdvPL ASP, o entendimento de boa parte do conteúdo dos posts daqui para a frente requer esta leitura.

Agradeço novamente a audiência, e desejo a todos TERABYTES DE SUCECSSO !!! 

Referências

CRUD em AdvPL ASP – Parte 01

Introdução

Nos posts anteriores sobre o CRUD em AdvPL, o programa de exemplo partiu de uma agenda de contatos, escrita originalmente para ser executado via SmartClient. Agora, vamos aproveitar algumas partes do “núcleo” do programa agenda.prw, e criar uma interface de manutenção para WEB, usando AdvPL ASP. Eu recomendo fortemente que você, caso ainda não tenha lido, leia os posts sobre AdvPL ASP e CRUD, eles estão acessíveis através das respectivas categorias no menu inicial superior do BLOG, e também nas referências no final deste post.

Agenda em AdvPL ASP

Inicialmente, vamos aproveitar o programa aspthreads.prw, que serve de casca para execução das nossas aplicações em AdvPL ASP, para desviar a execução do código para um novo fonte, chamado wagenda.prw, quando a URL informada for http://localhost/agenda.apw

Para isso, de posse do fonte aspthreads.prw, dentro da função U_ASPConn(), criamos uma nova entrada no DO CASE para a agenda, acrescentando as linhas abaixo:

  case cAspPage == 'agenda'
    // Executa a agenda em AdvPL ASP 
    // Os controles e páginas estão encapsulados pela função U_WAgenda()
    cReturn := U_WAGENDA()

Agora, vamos criar o fonte wagenda.prw, por partes, para ele funcionar como uma máquina de estado de tela única, de forma similar ao que fizemos com o programa no SmartClient. Algumas funções do fonte agenda.prw deverão ser alteradas, para serem visíveis para este novo fonte. Inicialmente, precisamos de uma tela APH para desenhar a interface da agenda. Vamos começar o fonte wagenda.prw de forma simples, e ir incrementando ele aos poucos.

#include "protheus.ch"

User Function WAgenda()
Local cHtml := ''
If empty(HTTPSESSION->LOGIN)
  // Usuário ainda não está logado. 
  // Retorna para ele a tela de login
  cHtml := H_WLogin()
Else
  cHtml := H_WAgenda()
Endif
Return cHtml

Aqui fazemos uso de uma variável de SESSION que o programa mesmo vai criar, para exigir que apenas um usuário autenticado — que passou primeiro pela página de LOGIN — tenha acesso à agenda. Para maiores detalhes sobre o funcionamento das SESSIONS no AdvPL ASP, consulte o post Protheus e AdvPL ASP – Parte 03.

Caso o usuário abra diretamente a página da agenda (http://localhost/agenda,apw), na verdade ele var receber uma tela HTML com um formulário de Login. Vamos ver como esta tela seria — arquivo wLogin.aph

*** OBSERVAÇÃO : Por hora os arquivos APH dos exemplos abaixo estão todos usando o CODEPAGE ANSI (ou CP1252) , e foram criadas manualmente usando o IDE do Protheus. Estas páginas também utilizam o padrão HTML5. PAra pbter mais detalhes sobre como o Web Browse trata as versões de HTML e JavaScript, uma excelente fonte de informações é o site W3SCHOOLS

<% 
/* ----------------------------------------------------------------
Login da Agenda
---------------------------------------------------------------- */ 
%> 
<!DOCTYPE html>
<html>
<head>
<meta charset="ANSI">
<title>LOGIN</title>
<style>
html, body { height: 100%; } 
html { display: table; margin: auto; }
body { display: table-cell; vertical-align: middle; }
.agbutton {
display: inline-block;
text-decoration : none;
width: 120px;
height: 18px;
background: rgb(240, 240, 240);
text-align: center;
color: black;
padding-top: 4px;
}
.agget { 
display: block;
width: 110px;
height: 22px; 
color: black; 
padding-top: 6px; 
text-align: right;
}
.aginput { 
width: 320px;
height: 22px; 
color: black; 
padding-top: 0px; 
padding-right: 10px; 
text-align: left;
}
</style>

function doLogin() { document.getElementById("F_LOGIN").submit(); };

</head>
<body style="font-family:Courier New;font-size: 12px;background-color:rgb(128,128,128);">

<form id="F_LOGIN" action="/login.apw" method="post">
<table style="border-spacing: 1px; background: rgb(192,192,192);">
<tr><td colspan="2" style="width: 500px; height: 22px; color: white; padding-top: 4px; background: rgb(128,0,0);">
<center>LOGIN</center></td></tr>
<tr><td class="agget">Usuário</td> <td class="aginput"><input id="I_USER" type="text" name="USER" size="50" ></td></tr>
<tr><td class="agget">Senha</td> <td class="aginput"><input id="I_PASS" type="password" name="PASS" size="32" ></td></tr>
<% If !empty(HTTPPOST->_ERRORMSG) %>
<tr><td colspan="2" style="width: 500px; height: 22px; color: white; padding-top: 4px; background: rgb(128,0,0);">
<center><%=HTTPPOST->_ERRORMSG%></center>
</td></tr>
<% Endif %>
<tr>
<td class="agget">&nbsp;</td>
<td>
<a class="agbutton" id="btnConfirm" href="javascript:void(0)" 
  onclick="doLogin()">Confirmar</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a class="agbutton" id="btnVoltar" href="/">Voltar</a>
</td>
</tr>
</table>
</form>
</body>
</html>

Somente nesta tela nós temos a utilização de vários recursos do Web Browser, inclusive JavaScript, e recursos do AdvPL ASP. Vamos ver um por um, e por quê foi optado por ser feito desta forma.

Formulário HTML

Inicialmente, para ser possível o Web Browse realizar um POST informando campos e valores informados pelo usuário para o Web Server, precisamos criar um formulário HTML usando a tag form, e dentro do corpo do formulário colocar tags de input para permitir o usuário informar valores para os campos determinados.

<form id="F_LOGIN" action="/login.apw" method="post">

Damos a este formulário um identificador (id), para poder nomear este formulário para ele ser tornar-se acessível de forma nomeada para um script em JavaScript, que vai ser executado dentro da página. Você escolhe o nome do formulário, eu resolvi usar um prefixo “F_” para saber dentro dos meus fontes que este componente é um form.

Outra escolha importante foi o método do formulário. Eu poderia usar um formulário do tipo GET, mas isto não seria nada elegante para uma tela de login, por várias razões. Primeira, os formulários com método GET colocariam os nomes e valores dos campos input na URL. Isso expõe a senha do usuário na URL. E, os Web Browses costumam fazer cache e permitir Bookmark de URL passando parâmetros via GET. Nada bom para uma operação de autenticação.

Quando usamos POST, o Browse não faz cache dos dados submetidos, e os dados são enviados internamente do Web Browse ao Web Server, dentro do corpo da requisição HTTP.

Links e JavaScript

Existem componentes no HTML, como o button, que permitem a criação de botões em um Web Site. Os botões normalmente disparam ações em JavaScript, e podem ter seu layout alterado usando CSS (ou folhas de estilo). Eu poderia usar botões, porém opter por usar o componente âncora (a) do HTML, usando CSS para dar a aparência de um botão, e internamente usar o evento de disparo (onclick) do componente para chamar uma função JavaScript declarada dentro da minha página, vide exemplo do botão de Login.

<a class="agbutton" id="btnConfirm" href="javascript:void(0)" onclick="doLogin()"

Já o botão para voltar à página principal do site, usa o componente âncora (a) apontando diretamente para a URL raiz do site “/”, sem usar javascript.

<a class="agbutton" id="btnVoltar" href="/">Voltar</a>

Outro detalhe interessante é o uso de CSS. Para você que já ouviu falar nisso, basicamente eu posso criar uma classe de layout, ou estender classes já existentes dos componentes padrão, para mudar a forma de apresentação destes componentes. As definições de estilo são feitas dentro da tag style, onde a definição pode estar dentro do póprio HTML, ente as tuplas <style> e </style>, ou mesmo em um arquivo separado, onde usamos um parâmetro da tag style para indicar onde está o arquivo. No momento, o estilo está dentro da página atual. Assim que acrescentarmos mais páginas ao projeto, colocamos as definições de estilo comuns em um arquivo de estilo separado, e fazemos referência a este arquivo nas páginas da aplicação.

Validação do Login

A ação do formulário, isto é, a URL para a qual a requisição de POST gerada no momento que o formulário for submetido também é importante. No caso, vamos chamar a aplicação login,apw, responsável por receber os campos do POST (Usuário e Senha)  deste formulário, para verificar se o usuário deve ter acesso ou não para a página da agenda. Para isso, acrescentamos mais uma entrada no programa ASPThreads.prw, vide abaixo:

case cAspPage == 'login'
  cReturn := U_WLOGIN()

E, para realizar a tarefa de validação do login, vamos criar o arquivo wlogin.prw, que vai conter a função U_WLOGIN() — e inclusive uma de Logoff.

#include "protheus.ch"

User Function WLogin()
Local cHtml := ''
Local cUser := ''
Local cPass := ''

If HTTPPOST->USER != NIL .and. HTTPPOST->PASS != NIL
  // Houve um POST , informando usuario e senha 
  // Valida usuario e senha informados 
  cUser := Upper(alltrim(HTTPPOST->USER))
  cPass := HTTPPOST->PASS
  If cUser == "ADMIN" .AND. cPass == ""
    // Usuário logado com sucesso 
    // Alimenta a session LOGIN 
    HTTPSESSION->LOGIN := cUser
  Else
    // Informa mensagem de erro usando o alias virtual HTTPPOST
    HTTPPOST->_ERRORMSG := "Usuário ou senha inválidos."
  Endif
Endif

If empty(HTTPSESSION->LOGIN)
  // Usuário ainda não está logado, retorna a tela de login
  cHtml := H_WLogin()
Else
  // Usuário atual está logado. Redireciona ele para a agenda
  HTTPSRCODE(307,"Login Redirection")
  HTTPHEADOUT->Location := "/agenda.apw"
  Return ""
Endif
Return cHtml

/* -------------------------------------------------------
Logoff de Usuario 
Retorna Limpa as variaveis de session do usuario 
e retorna a página de indice do site 
------------------------------------------------------- */
USER Function WLogOff()
HTTPFREESESSION()
return H_INDEX()

Olhando ao mesmo tempo a página wlogin.aph, e a função u_wlogin(), reparem que ambas usaram um campo do alias virtual HTTPPOST, que não estava em nenhum formilário — HTTPPOST->_ERRORMSG — onde a criação deste campo é feita diretamente no alias virtual HTTPPOST, dentro da aplicação, e a verificação da existência e do conteúdo deste campo é feita dentro do arquivo windex.aph.

Sim, eu posso criar em tempo de programação um identificador e um conteúdo, nos alias curtuais HTTPGET e HTTPPOST, e depois veremos outros casos onde isto também é possível. Desta forma, ao invés de precisarmos declarar variáveis PRIVATE dentro de um fonte prw para enviar dados de programa para um aph, criamos um ou mais campos no alias virtual HTTPPOST por exemplo, tomando o cuidado destes campos não conflitarem com nomes de campos vindos do Web Browse ou de formulários.

Como isso vai funcionar — passo a passo

O usuário abre o Web Browse e entra com a URL http://localhost/agenda.apw . A função U_ASPConn() do fonte ASPThreads.prw será chamada, e vai executar a função U_WAgenda()

case cAspPage == 'agenda'
  cReturn := U_WAGENDA()

A função U_WAgenda() vai consultar a HTTPSESSION->LOGIN, que não existe ainda, indicando que o usuário ainda não foi autenticado, retornando para ele o conteúdo do arquivo wlogin.aph . Este arquivo retorna uma página de login centralizada no Web Browse, solicitando informar usuário e senha.

Web Login

Após informar o usuário ADMIN e a senha em branco, e clicar no botão Confirmar, a ação do formulário será submeter os campos digitados em uma requisição POST, para a URL http://localhost/login.apw, que por sua vez vai chamar a função U_WLOGIN()

case cAspPage == 'login'
  cReturn := U_WLOGIN()

Dentro da função U_WLOGIN(), uma vez verificado que houve o envio de parâmetros de POST na requisição, verificado pelo recebimento dos campos USER e PASS, declarados no formulário HTML, caso o usuário e senha informados sejam aptos de realizar a operação, a HTTPSESSION->LOGIN será criada e alimentada com o ID do usuário. Caso contrário, a variável de POST _ERRORMSG será criada com uma mensagem de erro de Login. Uma vez que o usuário esteja logado com sucesso, retornamos para o Browse mediante instruções especiais — que ainda não foram vistas por aqui — para redirecionar o Browse a abrir a página da agenda no link “/agenda.apw”, que por sua vez retornará a página ainda em construção wAgenda.aph.

Web Agenda.png

Revisão de Conceitos

  1. Qualquer página com link .apw  será processada pela função U_ASPCONN(), que recebe o nome da página no alias virtual HTTPHEADIN->MAIN. No nosso caso, existe um “DO CASE” com uma lista de páginas e as suas respectivas funções para chamada.
  2. Qualquer requisição feita do Browse via URL é do tipo GET, e pode passar parâmetros via URL.
  3. Um formulário HTML, ao ser submetido, pode fazer um GET ou um POST, dependendo do método configurado no form.
  4. Um formulário do tipo POST também pode passar parâmetros via URL, colocados no action do formulário.
  5. O Web Browse apenas estabelece uma conexão HTTP com o Application Server para fazer uma requisição, encerrando a conexão automaticamente após o retorno — ou antes do retorno em caso de time-out ou cancelamento da requisição pelo usuário.
  6. Mesmo que você use alguma validação no Client — por exemplo funções JavaScript — para evitar que dados inconsistentes sejam enviados ao Servidor, não deixe de fazer as consistências da recepção dos dados no Advpl ASP. Pessoas mal intencionadas podem tentar submeter conteúdos inválidos para tentar burlar comportamentos, causar danos ou mesmo indisponibilidade de serviço.
  7. Um processamento AdvPL ASP dentro de um arquivo APH deve ser usado para montar conteúdo dinâmico para ser apresentado e/ou processado no Web Browse. Tudo o que estiver entre as tags <% , <%= e %> será processado somente no momento em que o servidor receber a requisição, cujo retorno será enviado ao Web Browse como uma página HTML.

Conclusão

Os posts anteriores sobre AdvPL ASP são a base para esta nova etapa do Crud, recomendo a leitura deles para uma melhor compreensão dos conceitos aqui apresentados, bem como uma leitura extra sobre o protocolo HTTP. A página da agenda ainda não foi publicada, pois está em construção, aguardem que eu ainda não coloquei todos os ingredientes na cumbuca … risos …

Agradeço a todos os comentários, compartilhamentos e likes, e desejo a todos(as) TERABYTES DE SUCESSO !!! 

Referências

 

 

Protheus e AdvPL ASP – Parte 03

Introdução

No post anterior, Protheus e AdvPL ASP – Parte 02, vimos dois alias virtuais, usados para receber parâmetros do Browse, a partir de requisições GET e POST — são eles o alias virtual HTTPGET e HTTPPOST, respectivamente. Agora vamos os demais alias virtuais disponíveis no AdvPL, começando pelo HTTPSESSION.

Alias virtual HTTPSESSION

É possível criar dinamicamente variáveis em um container global do AdvPL ASP, cujo escopo seja a instância do navegador ou Web Browse utilizado — ou em outras palavras,  “sessions de usuário”. Para isso, usamos o alias virtual HTTPSESSION.

O armazenamento no alias virtual HTTPSESSION é feito dinamicamente, na forma de tuplas chave=valor, onde damos o nome a um campo virtual, e atribuímos a ele uma informação. Por exemplo:

HTTPSESSION->USERNAME := "José"

Para consultarmos se um determinado campo virtual existe e/ou têm conteúdo, fazemos referência a ele usando da mesma forma o alias virtual HTTPSESSION, vide exemplo:

IF Empty(HTTPSESSION->USERNAME)
  conout("Session USERNAME vazia ou inexistente")
Else
  conout("Session USERNAME = " + cValToChaR(HTTPSESSION->USERNAME) )
Endif

Como o AdvPL ASP identifica o usuário?

Uma vez que um usuário abra um navegador, e solicite ao Protheus Server uma página qualquer com link .apw, que fará um processamento de AdvPL ASP, um Cookie de Memória (recurso do Web Browse para armazenar uma tupla chave=valor durante a navegação) é usado para identificar a seção (usuário) atual.

Quando você acabou de abrir o Browse, e fez a primeira requisição de link .apw para o AdvPL ASP, o Protheus Server não vai receber este identificador, então ele cria um identificador novo para a seção atual — aquela instância de Web Browse acessando o site — e retorna este identificador ao Web Browse como um “Cookie de Memória”. O Web Browse, por sua vez, a partir deste momento, e enquanto ele estiver aberto, envia de volta esse identificador como uma informação no cabeçalho HTTP de cada requisição GET ou POST que ele fizer para o Protheus Server.

Escopo e Tempo de Vida das Sessions

Uma vez que você atribua um conteúdo para uma variável de session, este conteúdo é gravado na memória da instância atual do Protheus Server, e somente será possível recuperá-lo através de um código AdvPL executado dentro de uma Working Thread do AdvPL ASP, que foi feita a partir da mesma instância de Web Browser, que fez a gravação da informação e respectivo identificador.

Todas as informações (identificadores e conteúdos) gravados para um determinado e distinto usuário, permanecerão na memória do servidor por tempo indeterminado, desde que este usuário não deixe de fazer uma requisição ao Protheus Server em até 10 minutos. Após 10 minutos sem atividade em um conjunto de dados de HTTPSESSION atrelado a um usuário, os identificadores e conteúdos serão descartados — apagados da memória. Isto não muda o identificador interno da seção daquele usuário.

Este tempo de 10 minutos — ou 600 segundos — é o valor DEFAULT da configuração SESSIONTIMEOUT, que permite definir o tempo de permanência máximo por inatividade do conjunto de variáveis de session por usuário — vide links de referência no final do post.

Onde eu uso variáveis de SESSION?

O uso mais comum são propriedades e parâmetros exclusivos que a aplicação permite definir para um ou mais usuários distintos que estão navegando no Web Site ou Aplicação WEB em questão. Por exemplo, um uso muito comum é a identificação de acesso de usuário, ou Login”.

Imagine que várias páginas dinâmicas da aplicação escrita em AdvPL ASP pode ser acessada por qualquer pessoa — acesso público e irrestrito. Porém, determinadas operações feitas através de determinados programas deste Web Site possuem acesso restrito, onde o usuário que estiver navegando deve fornecer algum tipo de informação para identificar-se na aplicação, e tentar garantir que ele “é quem diz ser”.

Nas páginas ou funções onde esta autenticação ou Login é necessária, podemos verificar se uma determinada SESSION — por exemplo HTTPSESSION->LOGIN possui conteúdo. Esta SESSION somente será criada se o usuário passar pelo processo de Login da Aplicação Web, normalmente usando uma página exclusiva na aplicação para esta finalidade. E, em cada função ou página que requer identificação ou é de acesso restrito, caso a SESSION de LOGIN não esteja definida, podemos lhe informar uma mensagem de “Acesso restrito a usuários inscritos”, e direcioná-lo a uma tela de cadastro ou a uma tela de Login.

O Que eu posso guardar em SESSION ?

Nobre desenvolvedor, você armazenar em campos do alias virtual HTTPSESSION qualquer valor básico do AdvPL, EXCETO “B” (Blocos de Código ou Code-Blocks) e “O” (Objetos ou Instâncias de Classe). O resto, inclusive Array, pode.

Agora, preste a atenção no seguinte: Um usuário ou internauta navegando no seu Web Site em AdvPL ASP, pode simplesmente parar de navegar por qualquer razão. E, durante a navegação, cada requisição de URL vinda do Web Browse é atendida por uma conexão estabelecida entre o Web Browse e o Protheus Server, que é encerrada após o processamento e envio dos dados ao Browse. Trata-se de uma conexão não-persistente.

Logo, se você coloca um botão ou link de “LOGOFF” no seu site, e o usuário realmente clica neste botão, você pode disparar uma função dentro do AdvPL para limpar manualmente todas as variáveis de SESSION deste usuário (HTTPFreeSession). Porém, se o usuário não clicar neste botão e simplesmente fechar o Web Browse, toda a memória consumida por aquele usuário, atrelada a um identificador exclusivo da seção que ele estava navegando, ficarão na memória do Protheus Server até que passe os dez minutos de INACTIVETIMEOUT, ou o tempo de inatividade configurado.

Se você, para um determinado usuário, usou 1 MB de memória para armazenar informações de SESSION, esta memória será ocupada por até 10 minutos a mais do que o usuário está realmente usando no Web Site. Ao aumentar o INACTIVETIMEOUT para valores maiores, aumentamos o tempo de retenção dessa memória. Aproveitando este exemplo, de 1 MB de consumo por usuário, e INACTIVETIMEOUT de 30 minutos. Das 12:00 às 12:10, 500 usuários navegaram no site, dos quais 100 entraram na área restrita e usaram SESSIONs. Em 10 minutos, 100 MB de uso de memória. Entre 12:10 e 12:20, entraram mais 50 usuários na área restrita, e 50 usuários que entraram às 12:00 fecharam o browse e foram almoçar. Logo, eu tenho agora (12:20) 100 usuários acessando a área restrita, mas estou mantendo na memória um total de 150 sessions, de todos os usuários que entraram desde às 12:00. Das 12:20 até 12:30 saíram e entraram mais 50 usuários, às 12:30 eu tenho o mesmo volume de 100 usuários online acessando páginas restritas, mas estou usando 200 MB para armazenar 200 variáveis de SESSION, 100 dos usuários ativos no momento, e as outras 100 que foram criadas desde o meio dia por usuários que já saíram do site.

Boas práticas de Sessions

Só existe uma boa prática de sessions: Evite usar sessions para guardar valores para qualquer pessoa navegando na aplicação WEB ou Web Site. Procure usar somente para guardar o que realmente é imprescindível, apenas para os usuários que precisam disso, como por exemplo uma informação de login ou alguma preferência diferenciada entre usuários.

Se você pretende criar uma aplicação WEB em AdvPL ASP, algo cujo tamanho e quantidade de acessos simultâneos não seja suportado por apenas uma instância única do Protheus como servidor WEB, então monte sua aplicação para não usar variáveis de SESSION, ou na verdade até pode usar, mas prefira utilizar uma abordagem que possibilite por exemplo a execução de requisições não exija “afinidade” — aplicações STATELESS por exemplo. Dessa forma, não importa em qual servidor a sua requisição seja processada, você consegue verificar a sua validade sem depender de um contexto. Se você usa variáveis de SESSION e resolve subir mais de uma instância de Protheus Server, usando um proxy reverso ou NLB (Network Load Balance), e uma requisição cria uma variável de SESSION quando foi processada no Servidor 1, caso a próxima requisição vá consultar a existência dessa variável seja direcionada para o Servidor 2, este servidor não conhece as sessions do Servidor 1, e vai tratar a requisição como se a Session realmente não existisse.

Conclusão

Embora este tópico não tenha visualmente um exemplo palpável, ele é necessário para a implementação em AdvPL ASP de outro tópico em desenvolvimento, sobre o CRUD em ADVPL ASP, onde vamos criar e usar uma SESSION para controle de login de usuário.

Por hora, apenas agradeço a todos(as) pela audiência e desejo a todos(as) TERABYTES DE SUCESSO 😀

Referências

Protheus e FTP Client – Parte 03

Introdução

A ideia era fazer um fonte mais detalhado de exemplo de uso da Classe TFTPClient(), mas o resultado acabou virando um mini WinSCP em AdvPL 😀 Vamos aos prints e aos códigos !!!

O Programa

Existe uma aplicação chamada WINSCP, uma ferramenta de código aberto que permite gerenciamento e sincronização de arquivos, entre a máquina local e um servidor de FTP ou mesmo SFTP (SSH File Transfer Protocol). As operações básicas são realizadas em dois painéis lado-a-lado, onde o lado esquerdo permite navegar na estrutura de pastas da máquina local, e no lado direito é possível conectar em um FTP / SFTP Server, navegar pela estrutura de pastas do servidor FTP, e realizar operações em ambos os lados, como criar pastas, apagar pastas, e copiar pastas e arquivos de um lado para o outro — Download e Upload do FTP.

Este programa de exemplo em AdvPL acabou crescendo, e tornou-se também um exemplo avançado de utilização de interface. Não se trata de um programa muito complexo, ele acaba ficando parecido com o CRUD — Uma máquina de estado de interface onde o disparo das operações dependem do estado de um ou de ambos os painéis de arquivos.

O programa ainda está sendo codificado, a parte mais trabalhosa foi fazer a “área de trabalho” da Interface do programa, onde uma parte as ações são disparadas por um menu superior, e a outra é disparada interativamente dentro dos painéis de navegação. Neste ponto, estou escrevendo as operações para serem feitas entre os painéis, e terminei apenas a cópia (Download/Upload). Porém, com o que já está escrito e operacional, já é possível lançar a versão 1.0 😀

Entrada

Após compilado em um ambiente, o programa deve ser chamado diretamente do SmartClient, através da função U_FTPManager. Ao ser iniciado, o programa abre uma Janela em tela cheia, trazendo os arquivos do RootPath do ambiente “\” no componente de navegação do lado esquerdo. OS detalhes de cada arquivo posicionado podem ser vistos no painel inferior do mesmo lado, tais como Tamanho, Data e Hora de criação (ou última alteração), e atributos do arquivo e/ou pasta.

Uma vez na tela inicial, podemos usar as setas para cima ou para baixo para mudar o arquivo ou pasta em foco, bem como usando o mouse, com um clique no botão esquerdo sobre o arquivo. Caso seja pressionado ENTER ou um Duplo Clique do mouse sobre uma pasta, isto fará com que a aplicação abra esta pasta e mostre os arquivos dentro dela,e permite navegar entre as pastas. Vamos ao fonte inicial do programa:

USER Function FTPManager()
Local oDlg, oFont
Local cTitle := "FTP Client Manager"
Local oSplitter
Local oPLeft , oPRight
Local nOpLeft := 1
Local oLbxLeft,oLbxRight
Local aLList := {} , aLFiles := {}
Local cLPath := "\"
Local nOpRight := 1
Local aRList := {}, aRFiles := {}
Local cRPath := "FTP Client (Não Conectado)"
Local oMenuBar
Local oTMenu1, oTMenu2, oTMenu3, oTMenu4
Local oFtp
Local lConnected := .F.
Local aGetsL := {} , aGetsR := {}
Local cFname := space(50)
Local cFAttr := space(50)
Local cFDate := space(18)
Local nFSize := 0
Local cRFname := space(50)
Local cRFAttr := space(50)
Local cRFDate := space(18)
Local nRFSize := 0
Local aFTPInfo := {}
Local oLSay,oRSay

aadd(aFTPInfo,space(50)) // 1 FTP Addr
aadd(aFTPInfo,21) // 2 FTP Port
aadd(aFTPInfo,5) // 3 FTP TimeOut (sec)
aadd(aFTPInfo,.T.) // 4 Firewall Mode ( passive )
aadd(aFTPInfo,.F.) // 5 Use IP Conn
aadd(aFTPInfo,.T.) // 6 Anonymous Login
aadd(aFTPInfo,"anonymous") // 7 User
aadd(aFTPInfo,"") // 8 Password

// Define Formato de data DD/MM/AAAA
SET DATE BRITISH
SET CENTURY ON

// Usa uma fonte Fixed Size, tamanho 10
oFont := TFont():New('Courier new',,-10,.T.)
SETDEFFONT(oFont)

// Cria o objeto Client de FTP
oFtp := tFtpClient():New()

// Cria a janela principal da Agenda como uma DIALOG
DEFINE WINDOW oDlg FROM 0,0 TO 600,800 PIXEL ;
  TITLE (cTitle) NOSYSMENU

// Permite ESC fechar a janela
oDlg:lEscClose := .T.

// Primeiro cria a barra superior de Menus da Aplicação na Janela Principal
oMenuBar := tMenuBar():New(oDlg)
oMenuBar:SetColor(CLR_BLACK,CLR_WHITE)

// Agora cria a tela inicial
oSplitter := tSplitter():New( 00,00,oDlg,400,280 )
oSplitter:ALIGN := CONTROL_ALIGN_ALLCLIENT

@ 0,0 MSPANEL oPLeft LOWERED SIZE 130, 36 OF oSplitter
@ 0,0 MSPANEL oPRight LOWERED SIZE 130, 36 OF oSplitter

oPLeft:ALIGN := CONTROL_ALIGN_ALLCLIENT
oPRight:ALIGN := CONTROL_ALIGN_ALLCLIENT

@ 0,0 MSPANEL oPLeftUp SIZE 130, 10 OF oPLeft
@ 0,0 MSPANEL oPLeftMid SIZE 130, 15 OF oPLeft
@ 0,0 MSPANEL oPLeftBottom SIZE 130, 65 OF oPLeft

oPLeftUp:ALIGN := CONTROL_ALIGN_TOP
oPLeftBottom:ALIGN := CONTROL_ALIGN_BOTTOM
oPLeftMid:ALIGN := CONTROL_ALIGN_ALLCLIENT

@ 1,1 SAY oLSay PROMPT cLPath SIZE 120,15 COLOR CLR_BLACK,CLR_WHITE ;
  of oPLeftUp PIXEL

@ 0,0 MSPANEL oPRightUp SIZE 130, 10 OF oPRight
@ 0,0 MSPANEL oPRightMid SIZE 130, 15 OF oPRight
@ 0,0 MSPANEL oPRightBottom SIZE 130, 65 OF oPRight

oPRightUp:ALIGN := CONTROL_ALIGN_TOP
oPRightBottom:ALIGN := CONTROL_ALIGN_BOTTOM
oPRightMid:ALIGN := CONTROL_ALIGN_ALLCLIENT

@ 1,1 SAY oRSay PROMPT cRPAth SIZE 120,15 COLOR CLR_BLACK,CLR_WHITE ;
  of oPRightUp PIXEL

// ListBox lado esquerdo
// Arquivos do servidor a partir do RootPath 

aLList := GetLFiles(cLPath,@aLFiles)

@0,0 LISTBOX oLbxLeft VAR nOpLeft;
  ITEMS aLList ;
  ON CHANGE ( doLChange(aGetsL,oLbxLeft,@aLList,@aLFiles) ) ;
  ON DBLCLICK ( EnterLeft(oLbxLeft,@aLList,@aLFiles,@cLPath) ) ;
  OF oPLeftMid

oLbxLeft:ALIGN := CONTROL_ALIGN_ALLCLIENT

aRList := {}

@ 15,15 LISTBOX oLbxRight VAR nOpRight;
  SIZE 300, 300 ;
  OF oPRightMid ;
  ITEMS aRList ;
  ON CHANGE ( doRChange(aGetsR,oLbxRight,@aRList,@aRFiles) ) ;
  ON DBLCLICK ( EnterRight(oFtp,aFtpInfo,oLbxRight,oRSay,@aRList,;
  @aRFiles,@cRPath) ) ;
  PIXEL

oLbxRight:ALIGN := CONTROL_ALIGN_ALLCLIENT

oLSay:ALIGN := CONTROL_ALIGN_TOP
oRSay:ALIGN := CONTROL_ALIGN_TOP

// Insere os gets com os dados do arquivo atual
// do lado esquerdo. Os dados são atualizados conforme
// é feita a navegação na lista

@ 05+3,02 SAY oSay PROMPT "Arquivo" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPLeftBottom PIXEL
@ 05,50 GET oGetFName VAR cFname SIZE CALCSIZEGET(40),10 ;
  OF oPLeftBottom PIXEL
oGetFName:SETENABLE(.F.)
aadd(aGetsL,oGetFName)

@ 20+3,02 SAY oSay PROMPT "Tamanho" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPLeftBottom PIXEL
@ 20,50 GET oGetFSize VAR nFSize PICTURE "999999999999999999" ;
  SIZE CALCSIZEGET(18),10 OF oPLeftBottom PIXEL
oGetFSize:SETENABLE(.F.)
aadd(aGetsL,oGetFSize)

@ 35+3,02 SAY oSay PROMPT "Data" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPLeftBottom PIXEL
@ 35,50 GET oGetFDate VAR cFDate SIZE CALCSIZEGET(18),10 ; 
  OF oPLeftBottom PIXEL
oGetFDate:SETENABLE(.F.)
aadd(aGetsL,oGetFDate)

@ 50+3,02 SAY oSay PROMPT "Atributos" SIZE 40,10 ; 
  COLOR CLR_BLACK,CLR_WHITE OF oPLeftBottom PIXEL
@ 50,50 GET oGetFAttr VAR cFAttr SIZE CALCSIZEGET(5),10 ;
  OF oPLeftBottom PIXEL
oGetFAttr:SETENABLE(.F.)
aadd(aGetsL,oGetFAttr)

// Insere dados e detalhes dos arquivos do FTP
// Os dados são atualizados conforme
// é feita a navegação na lista

@ 05+3,02 SAY oSay PROMPT "Arquivo" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPRightBottom PIXEL
@ 05,50 GET oRFName VAR cRFname SIZE CALCSIZEGET(40),10 ;
  OF oPRightBottom PIXEL
oRFName:SETENABLE(.F.)
aadd(aGetsR,oRFName)

@ 20+3,02 SAY oSay PROMPT "Tamanho" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPRightBottom PIXEL
@ 20,50 GET oRFSize VAR nRFSize PICTURE "999999999999999999" ;
  SIZE CALCSIZEGET(18),10 OF oPRightBottom PIXEL
oRFSize:SETENABLE(.F.)
aadd(aGetsR,oRFSize)

@ 35+3,02 SAY oSay PROMPT "Data" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPRightBottom PIXEL
@ 35,50 GET oRFDate VAR cRFDate SIZE CALCSIZEGET(18),10 ;
  OF oPRightBottom PIXEL
oRFDate:SETENABLE(.F.)
aadd(aGetsR,oRFDate)

@ 50+3,02 SAY oSay PROMPT "Atributos" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPRightBottom PIXEL
@ 50,50 GET oRFAttr VAR cRFAttr SIZE CALCSIZEGET(5),10 ;
  OF oPRightBottom PIXEL
oRFAttr:SETENABLE(.F.)
aadd(aGetsR,oRFAttr)

// Insere as opções de Menu

oTMenu1 := TMenu():New(0,0,0,0,.T.,,oDlg,CLR_BLACK,CLR_WHITE)
oMenuBar:AddItem('&Local' , oTMenu1, .T.)

oTMenu1:Add( TMenuItem():New(oDlg,'&Path',,,,;
  {|| LocalPath(oLbxLeft,@aLList,@aLFiles,oLSay,@cLPath) },,,,,,,,,.T.))
oTMenu1:Add( TMenuItem():New(oDlg,'Sai&r',,,,;
  {||oDlg:End()},,,,,,,,,.T.))

oTMenu2 := TMenu():New(0,0,0,0,.T.,,oDlg,CLR_BLACK,CLR_WHITE)
oMenuBar:AddItem('&FTP' , oTMenu2, .T.)

oTMenu2:Add( TMenuItem():New(oDlg,'&Conectar' ,,,,;
  {|| FTPConn(oDlg,oPRight,oFtp,@aFTPInfo,oLbxRight,@aRList,@aRFiles,oRSay,@cRPath) },,,,,,,,,.T.))
oTMenu2:Add( TMenuItem():New(oDlg,'&Desconectar',,,,;
  {|| FTPDesConn(oDlg,oPRight,oFtp,@aFTPInfo,oLbxRight,@aRList,@aRFiles,oRSay,@cRPath) },,,,,,,,,.T.))

oTMenu4 := TMenu():New(0,0,0,0,.T.,,oDlg,CLR_WHITE,CLR_BLACK)
oMenuBar:AddItem('&Ajuda', oTMenu4, .T.)

oTMenu4:Add(TMenuItem():New(oDlg,'&Sobre',,,,{|| About()},,,,,,,,,.T.))

// Ajusta o tamanho do Menu com o Tamanho da Janela
// PAra nao "suprimir" opções com ">>"
oMenuBar:NWIDTH := oDlg:nWidth

// Posiciona no primeiro arquivo
oLbxLeft:Gotop()

// Copiar Arquivo
SETKEY( VK_F5 , {|| IIF(_KeyRunning , NIL , ;
   ( _KeyRunning := .T. , ;
     CallKey(VK_F5 ,oLbxLeft,oLbxRight,aLFiles,aLList,;
             aRFiles,aRList,cLPath,cRPath,oFtp),;
     _KeyRunning := .F. ))})

ACTIVATE WINDOW oDlg MAXIMIZED ;
  ON INIT DoInit(oDlg) ;
  VALID CanQuit()

// Fecha a conexao com o FTP caso esteja aberta
oFtp:Close()

Return

Estrutura da Interface

A primeira escolha da interface foi usar uma WINDOW para a criação do programa, pois ela permite o redimensionamento da janela. E, para que o redimensionamento mão me obrigasse a fazer cálculos rebuscados de alinhamento de componentes, a grande sacada foi usar um objeto visual tSplitter() para dividir a tela principal em dois painéis, cada um ocupando automaticamente 50% da tela, e dentro de cada painél, mais três painéis de alinhamento automático, para permitir um header e um footer em cada painel, onde a área central contém um tListBox(), que será populado do lado esquerdo com os arquivos locais, e do lado direito com os arquivos da pasta atual do FTP Server conectado.

Para executar o programa, basta iniciar um SmartClient, e chamar diretamente a função U_FTPMANAGER.

FTP Client 1

Ao entrar, o programa ainda não está conectado a nenhum FTP Server, então ele mostra do lado esquerdo a estrutura de pastas a partir do RootPath do Ambiente no Protheus Server, e do lado direito uma lista sem itens.

FTP Client 2

Menu Local

No menu superior chamado “Local”, a opção “Path” permite trocar diretamente a pasta atual do painel do lado esquerdo. Para isso, é aberta uma interface de navegação (função cGetFile) que permite escolher inclusive navegar pela estrutura de pastas do Protheus Server — a partir do Rootpath do ambiente — para escolher um diretório de trabalho.

FTP Client 3

Segue abaixo o fonte da função de menu “LocalPath()

/* ----------------------------------------------------------------------
Permite trocar a pasta atual navegando diretamente
na estrutura de pastas do Servidor a partir do RootPath
---------------------------------------------------------------------- */

STATIC Function LocalPath(oLbxLeft,aLList,aLFiles,oLSay,cLPath)
Local cRet

cRet := cGetFile( 'Todos os Arquivos|*.*' , ;
   'Path', 1, cLPath, .F., GETF_RETDIRECTORY ,.T., .T. )

If !empty(cRet)
  // Troca o path e atualiza a lista de arquivos na tela
  cLPath := cRet
  aLList := GetLFiles(cLPath,aLFiles)
  oLbxLeft:SetArray(aLList)
  oLbxLeft:GoTop()
  oLSay:SetText(cLPath)
Endif

Return

Também temos a opção de Sair, que também pode ser acionada com a tecla ESC.

Menu FTP

No menu superior chamado “FTP”, a opção “Conectar” permite especificar as informações de conexão com um FTP Server, através de uma caixa de diálogo. Normalmente as informações mínimas necessárias — pelo menos para acessar um servidor publico de download — são apenas HOST ou IP do FTP Server, e o número da porta. As demais informações já foram determinadas no programa como DEFAULT.

FTP Client 4

Segue abaixo o fonte da função de conexão com o FTP — “FTPConn()” — Esta função acaba fazendo um pouco mais do que apenas conectar, mas também atualizar o endereço atual do FTP no painel superior, e os arquivos da pasta atual no tListBox() do lado direito.

/* ----------------------------------------------------------------------
Diálogo de Conexão com FTP
Armazema parametros de conexao, e em caso de sucesso,
já alimenta a lista de arquivos do lado direito
---------------------------------------------------------------------- */

STATIC Function FTPConn(oDlg,oPRight,oFtp,aFTPInfo,oLbxRight,aRList,aRFiles,oRSay,cRPath)
Local cTitle := 'Conexão com FTP'
Local oSay1,oSay2
Local lGo := .F.
Local cFTPAddr := padr(aFTPInfo[1],40)
Local nFtpPort := aFTPInfo[2]
Local nTimeOut := aFTPInfo[3]
Local bPasv := aFTPInfo[4]
Local bUseIP := aFTPInfo[5]
Local bAnonymous := aFTPInfo[6]
Local cUser := padr(aFTPInfo[7],40)
Local cPass := padr(aFTPInfo[8],40)
Local nStat

DEFINE DIALOG oDlgConn TITLE (cTitle) ;
  FROM 0,0 TO 220,450 PIXEL;
  OF oDlg ;
  COLOR CLR_WHITE, CLR_BROWN

@ 05+3,05 SAY oSay1 PROMPT "FTP" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 05,50 GET oGetFTP VAR cFtpAddr SIZE CALCSIZEGET(40) ,12 OF oDlgConn PIXEL

@ 20+3,05 SAY oSay2 PROMPT "Porta" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 20,50 GET oGetPorta VAR nFtpPort PICTURE "99999" ;
  SIZE CALCSIZEGET(5) ,12 OF oDlgConn PIXEL

@ 20+3,100 SAY oSay3 PROMPT "TimeOut" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 20,145 GET oGetTimeOut VAR nTimeOut PICTURE "999" ;
  SIZE CALCSIZEGET(3) ,12 OF oDlgConn PIXEL

@ 35,50 CHECKBOX oCkh1 VAR bPasv PROMPT "Passive Mode" ;
  SIZE 80,12 OF oDlgConn PIXEL

@ 45,50 CHECKBOX oCkh2 VAR bUseIP PROMPT "Use IP Conn" ;
  SIZE 80,12 OF oDlgConn PIXEL

@ 55,50 CHECKBOX oCkh3 VAR bAnonymous PROMPT "Anonymous Login" ;
  SIZE 80,12 OF oDlgConn PIXEL

@ 65+3,05 SAY oSay1 PROMPT "Usuário" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 65,50 GET oGetUsr VAR cUser SIZE CALCSIZEGET(40) ,12 ;
  WHEN !bAnonymous OF oDlgConn PIXEL

@ 80+3,05 SAY oSay2 PROMPT "Senha" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 80,50 GET oGetPsw VAR cPass SIZE CALCSIZEGET(40) ,12 ;
  WHEN !bAnonymous OF oDlgConn PIXEL
oGetPsw:LPASSWORD := .T.

@ 95, CALCSIZEGET(40) - 10 BUTTON oBtnOk PROMPT "Ok" SIZE 60,15 ;
  ACTION (lGo := .T. , oDlgConn:End()) OF oDlgConn PIXEL

ACTIVATE DIALOG oDlgConn CENTER

If lGo

  // Fecha qqer conexão existente anteriormente
  oFTP:Close()

  // Ajusta os parametros
  cFTPAddr := alltrim(cFTPAddr)
  cUser := alltrim(cUser)
  cPass := alltrim(cPass)

  // Guarda os parâmetros utilizados
  aFTPInfo[1] := cFTPAddr
  aFTPInfo[2] := nFtpPort
  aFTPInfo[3] := nTimeOut
  aFTPInfo[4] := bPasv
  aFTPInfo[5] := bUseIP
  aFTPInfo[6] := bAnonymous
  aFTPInfo[7] := cUser
  aFTPInfo[8] := cPass

  // Seta parâmetros na classe antes de conectar
  oFtp:BFIREWALLMODE := bPasv
  oFtp:NCONNECTTIMEOUT := nTimeOut
  oFtp:BUSESIPCONNECTION := bUseIP
 
  // Conecta no FTP
  If !bAnonymous
    MsgRun("FTP Connect",cFtpAddr,;
      {|| nStat := oFtp:FtpConnect(cFtpAddr,nFTPPort,cUser,cPass) })
  Else
    MsgRun("FTP Connect",cFtpAddr,;
      {|| nStat := oFtp:FtpConnect(cFtpAddr,nFTPPort,"anonymous","anonymous") })
  Endif

  If nStat == 0
    cCurrDir := ''
    nStat := oFtp:GETCURDIR(@cCurrDir)
    If nStat <> 0
      cRPath := "ftp://"+cFtpAddr+"/"
      oRSay:SetText(cRPath)
      oRSay:Refresh()
      MsgStop("Falha ao recuperar executar GetCurDir() - Erro "+;
        cValtoChar(nStat),oFtp:CERRORSTRING)
    Else
      // Atualiza pasta atual do FTP
      cRPath := "ftp://"+cFtpAddr+cCurrDir
      oRSay:SetText(cRPath)
      oRSay:Refresh()
    Endif
    // Limpa lado direito
    aSize(aRFiles,0)
    aSize(aRList,0)
    oLbxRight:SetArray(aRList)

    // Conectou com sucesso, recupera pasta atual e lista de arquivos
    MsgRun("Obtendo lista de arquivos",cRPath,;
      {|| aRFiles := oFtp:Directory("*",.T.) })
    aSize(aRList,0)

    aEval(aRFiles,{|x| aadd( aRList , alltrim(x[1]) )})
    oLbxRight:SetArray(aRList)

  Else

    aSize(aRFiles,0)
    aSize(aRList,0)
    oLbxRight:SetArray(aRList)

    MsgStop("Falha de Conexão -- Erro "+cValToChar(nStat),;
      oFtp:CERRORSTRING)
    cRPath := "FTP Client (Não Conectado)"
    oRSay:SetText(cRPath)
  Endif

  oLbxRight:GoTop()
  oPRight:Refresh()

Endif

Return

Reparem que algumas chamadas do FTP foram encapsuladas pela função MsgRun() — isto foi proposital, pois se você está conectando com um servidor de FTP mais “longe”, com latência de rede, as operações podem demorar um pouco, e dessa forma sabemos que função está sendo executada — enquanto ela está sendo executada.

Navegação e Funções

Para navegar entre as pastas, tanto de arquivos locais como do FTP, utilize ENTER ou Duplo Clique nas pastas. Para voltar para a pasta anterior, utilize o primeiro arquivo da lista, chamado “..”. Estas regras de navegação valem para ambos os painéis de arquivos – lado direito e esquerdo.

Por hora, as únicas funções disponíveis — além da navegação nas pastas — é a tecla F5. Caso você esteja com o foco em um arquivo local, e pressionar F5, a aplicação permite fazer UPLOAD deste arquivo na pasta atual do FTP mostrada no painel direito. E, caso você esteja com o foco em um arquivo do FTP, no painel direito, e pressionar F5, a aplicação permite fazer o Download do arquivo para a pasta local sendo mostrada no painel esquerdo.

A tecla ENTER ou o Duplo Clique em um arquivo ou pasta do painel esquerdo dispara a função EnterLeft() — vista abaixo:

/* ----------------------------------------------------------------------
Funcao disparada em caso de [ENTER] ou Duplo Click em um arquivo
na lista de arquivos locais -- lado esquerdo. Permite a navegação
entre os diretorios.
---------------------------------------------------------------------- */

STATIC Function EnterLeft(oLbxLeft,aLList,aLFiles,cLPath)
Local cFile
Local aTmp, nI
Local nOp := oLbxLeft:GetPos()

If nOp > 0
  cFile := alltrim(aLList[nOp])
  If cFile == '..'
    // Tenta ir para o nivel anterior
    aTmp := StrTokarr(cLPath,'\')
    cLPath := ''
    For nI := 1 to len(aTmp)-1
      cLPath += ( aTmp[nI] + '\')
    Next
    if empty(cLPath)
      cLPath := '\'
    Endif
    aLList := GetLFiles(cLPath,aLFiles)
    oLbxLeft:SetArray(aLList)
    oLbxLeft:GoTop()
  Else
    // SE for um diretorio, entra nele
    aTmp := aLFiles[nOp]
    if 'D' $ aTmp[5]
      // Se for um diretorio , entra
      cLPath += ( cFile+'\' )
      aLList := GetLFiles(cLPath,aLFiles)
      oLbxLeft:SetArray(aLList)
      oLbxLeft:GoTop()
    Endif
  Endif
Endif
Return

Esta função usa algumas funções auxiliares, que serão vistas no código completo — a ser disponibilizado no GITHUB. Por hora, vamos dar uma olhada também na função de navegação do lado direito — pasta do FTP Server conectado.

/* ----------------------------------------------------------------------
Função disparada em caso de [ENTER] ou Duplo Click em um arquivo
na lista de arquivos de FTP - Lado direito -- Permite navegar
entre os diretórios.
---------------------------------------------------------------------- */
STATIC Function EnterRight(oFTP,aFtpInfo,oLbxRight,oRSay,aRList,aRFiles,cRPath)
Local cFile
Local aTmp, nI
Local nOp := oLbxRight:GetPos()
Local cCurrDir

If nOp > 0
  cFile := alltrim(aRList[nOp])
    If cFile == '..'
    // Volta ao nivel anterior
    nStat := oFTP:CDUP()
    If nStat != 0
      MsgStop("Falha ao mudar de Diretorio - Erro "+cValToChar(nStat),oFtp:CERRORSTRING)
    Else
      cCurrDir := ''
      nStat := oFtp:GETCURDIR(@cCurrDir)
      cRPath := "ftp://"+aFtpInfo[1]+cCurrDir
      oRSay:SetText(cRPath)
      oRSay:Refresh()
      // Pega os arquivos do diretorio atual
      MsgRun("Obtendo lista de arquivos",cRPath,{|| aRFiles := oFtp:Directory("*",.T.) })
      aSize(aRList,0)
      // Acrescenta um atalho para voltar para o nivel anterior
      // SE eu nao estiver no niver RAIZ ...
      IF !(cCurrDir == '/')
        aadd(aRFiles,{"..",0,ctod(""),"",""})
        aSort(aRFiles,,,{|x1,x2| lower(x1[1]) < lower(x2[1]) })
      Endif
      aEval(aRFiles,{|x| aadd( aRList , alltrim(x[1]) )})
      oLbxRight:SetArray(aRList)
      oLbxRight:GoTop()
    Endif
  Else
    // SE for um diretorio, entra nele
    aTmp := aRFiles[nOp]
    if 'D' $ aTmp[5]
      // Se for um diretorio , entra
      // Troca o diretorio atual
      nStat := oFTP:CHDIR(cFile)
      If nStat != 0
        MsgStop("Falha ao mudar de Diretorio - Erro "+cValToChar(nStat),oFtp:CERRORSTRING)
      Else
        cRPath += ( cFile+'/' )
        oRSay:SetText(cRPath)
        // Pega os arquivos do diretorio atual
        MsgRun("Obtendo lista de arquivos",cRPath,{|| aRFiles := oFtp:Directory("*",.T.) })
        aSize(aRList,0)
        // Acrescenta um atalho para voltar para o nivel anterior
        aadd(aRFiles,{"..",0,ctod(""),"",""})
        aSort(aRFiles,,,{|x1,x2| lower(x1[1]) < lower(x2[1]) })
        aEval(aRFiles,{|x| aadd( aRList , alltrim(x[1]) )})
        oLbxRight:SetArray(aRList)
        oLbxRight:GoTop()
      Endif
    Endif
  Endif
Endif
Return

Pulos do Gato

Alguns pulos do gato neste fonte, além dos alinhamentos, foram a escolha dos arrays. Para cada tListBox, existem 2 arrays que trabalham em “paralelo”, um deles apenas com o nome dos arquivos, para ser mostrado na tela, e o outro array com 5 colunas, contendo o nome, tamanho, atributos e detalhes do arquivo, tanto da pasta local como do FTP. Trabalhar com estes arrays de forma sincronizada permite as validações para a navegação entre pastas, por exemplo, para ignorar um Enter ou Duplo Clique em um arquivo — ao invés de uma pasta.

Outra sacada está no controle do disparo da tecla de atalho F5, para a cópia dos arquivos. Primeiro, a forma de setar o bloco de código usando uma variável STATIC, inicializada com o valor .F. :

SETKEY( VK_F5 , {|| IIF(_KeyRunning , NIL , ;
  ( _KeyRunning := .T. , ;
    CallKey(VK_F5 ,oLbxLeft,oLbxRight,aLFiles,aLList,;
    aRFiles,aRList,cLPath,cRPath,oFtp) , ;
    _KeyRunning := .F. ))})

Na prática, isso evita o duplo disparo da tecla F5,  e ainda mais, quando forem acrescentadas novas teclas de função ou atalho, esta proteção vai fazer com que o primeiro atalho disparado não permita nenhuma outra tecla de atalho ser executada. O tratamento da ação da tecla será determinado internamente dentro da função CallKey(), que recebe todos os parâmetros necessários para ela obter os dados que ela precisa, e fazer uma atualização da interface se ou quando necessário.

A outra grande sacada é descobrir em qual componente eu estou com o FOCO, para eu saber se, quando eu pressionar F5, eu devo copiar o arquivo Local para o FTP, ou baixar o arquivo do FTP para a pasta local ? Vamos ao fonte:

/* ----------------------------------------------------------------------
Teclas de Atalho de funcionalidades do FTP
F5 = Copiar Arquivo ( Download ou Upload ) 
---------------------------------------------------------------------- */

STATIC Function CallKey(nKey,oLbxLeft,oLbxRight,aLFiles,aLList,aRFiles,aRList,cLPath,cRPath,oFtp)
Local hHndFocus
Local nPos
Local cFile
Local cSource
Local cTarget
Local cCurrDir
Local lExist
Local lRun
// Pega Handle do componente de interface que estava com o foco
// quando a tecla de atalho foi pressionada
hHndFocus := GETFOCUS()
If hHndFocus == oLbxLeft:HWND
  // Caso o foco esteja na lista de arquivos locais
  // E exista um arquivo posicionado ... 
  nPos := oLbxLeft:GetPos()
  If nPos > 0
    cFile := alltrim(aLFiles[nPos][1])
    cAttr := aLFiles[nPos][5]
    If cFile == '.' .or. cFile == '..'
      MsgStop("Operação com pasta não implementada. Selecione um arquivo.","Atenção")
      return
    ElseIf 'D'$cAttr
      MsgStop("Operação com pasta não implementada. Selecione um arquivo.","Atenção")
      return
    Endif
    If nKey == VK_F5
      // Copia de arquivo Local para o FTP
      cSource := cLPath+cFile
      cTarget := cRPath+cFile
      If MsgYEsNo("Seseja copiar o arquivo local ["+cSource+"] para o FTP ["+cTarget+"] ?")
        MsgRun("FTP Upload",cFile,{|| nStat := oFTP:SENDFILE(cSource,cFile) })
        If nStat <> 0
          MsgStop("Falha no UPLOAD de Arquivo - Erro "+cValToChar(nStat),oFtp:CERRORSTRING)
        Else
          MsgInfo("Upload realizado com sucesso.")
          cCurrDir := ''
          oFtp:GETCURDIR(@cCurrDir)
          // Pega os arquivos do diretorio atual
          MsgRun("Obtendo lista de arquivos",cRPath,{|| aRFiles := oFtp:Directory("*",.T.) })
          aSize(aRList,0)
          // Acrescenta um atalho para voltar para o nivel anterior
          // SE eu nao estiver no nivel RAIZ ...
          IF !(cCurrDir == '/')
            aadd(aRFiles,{"..",0,ctod(""),"",""})
            aSort(aRFiles,,,{|x1,x2| lower(x1[1]) < lower(x2[1]) })
          Endif
          aEval(aRFiles,{|x| aadd( aRList , alltrim(x[1]) )})
          oLbxRight:SetArray(aRList)
        Endif
      Endif
    Else
      MsgInfo("Operação com Arquivo Local ainda não implementada.")
    Endif
  Endif
ElseIf hHndFocus == oLbxRight:HWND
  // Copia arquivo do FTP para pasta Local
  // e exista algum arquivo posicionado
  nPos := oLbxRight:GetPos()
  IF nPos > 0
    cFile := alltrim(aRFiles[nPos][1])
    cAttr := aRFiles[nPos][5]
    If cFile == '.' .or. cFile == '..'
      MsgStop("Operação com pasta não implementada. Selecione um arquivo.","Atenção")
      return
    ElseIf 'D'$cAttr
      MsgStop("Operação com pasta não implementada. Selecione um arquivo.","Atenção")
      return
    Endif
    // Ajusta o nome vindo do FTP 
    AdjustFTP(@cFile)
    If nKey == VK_F5
      // Copia de arquivo do FTP para a pasta local 
      cSource := cRPath+cFile
      cTarget := cLPath+cFile
      lExist := File(cLPath+cFile)
      lRun := .F. 
      IF lExist
        If MsgYesNo("O Arquivo local já existe. Deseja continuar o Download ? ")
          lRun := .T.
          MsgRun("FTP Resume Download",cFile,{|| nStat := oFTP:RESUMERECEIVEFILE(cFile,cTarget) })
        ElseIf MsgYesNo("Apaga o arquivo local e reinicia o Download ?")
          lRun := .T.
          Ferase(cLPath+cFile)
          MsgRun("FTP Download",cFile,{|| nStat := oFTP:RECEIVEFILE(cFile,cTarget) })
        Endif
      Else
        If MsgYEsNo("Deseja baixar o arquivo do FTP ["+cSource+"] para a pasta local ["+cTarget+"] ?")
          lRun := .T.
          MsgRun("FTP Download",cFile,{|| nStat := oFTP:RECEIVEFILE(cFile,cTarget) })
        Endif
      Endif
      If lRun
        If nStat <> 0
          MsgStop("Falha no DOWNLOAD de Arquivo - Erro "+cValToChar(nStat),oFtp:CERRORSTRING)
        Else
          MsgInfo("Download realizado com sucesso.")
          // Atualiza lista de arquivos 
          aLList := GetLFiles(cLPath,aLFiles)
          oLbxLeft:SetArray(aLList)
        Endif
      Endif
    Else
      MsgInfo("Operação com Arquivo do FTP ainda não implementada.")
    Endif
  Endif
Endif

Return

A mágica do foco é feita inicialmente usando a função GetFocus(), que retorna o ID ou “Handler” do componente de interface que está em foco no momento. Como eu estou iniciando o processamento de uma tecla de atalho que não está relacionada a nenhum componente de interface, pressionar F5 não muda o foco do componente de interface atual.

hHndFocus := GETFOCUS()

A segunda parte da mágica, é eu verificar SE o componente em foco é a lista de arquivos do lado direito ou do lado esquerdo. No momento, estes são os únicos componentes da minha interface — fora o Menu — que permitem foco. Logo, eu devo estar posicionado em um deles. Cada componente da interface visual possui um ID ou “Handler”, dado no momento da criação deste componente, e você pode consultá-lo através da propriedade HWND.

If hHndFocus == oLbxLeft:HWND

Desta forma, eu sei se o foco está no tListBox do lado esquerdo, e realizando o mesmo teste com o objeto oLbxRight, eu sei se ele está com foco no lado direito. Se nenhuma das alternativas for a correta, eu assumo que foi pressionado F5 quando o foco não estava em nenhum componente válido para realizar a execução desta funcionalidade.

Detalhes dos Arquivos em Foco

Cada tListBox() foi parametrizado para disparar um evento ONCHANGE, na mudança da seleção ou foco em um item. Este evento é usado pelas funções  doLChange() e doRChange() para atualizar os detalhes dos arquivos em foco nos painéis, vide fonte abaixo:

/* ----------------------------------------------------------------------
Função disparada na troca de posição da lista de arquivos
do lado esquerdo -- arquivos locais
Atualiza as informações do arquivo selecionado no painel inferior
---------------------------------------------------------------------- */

STATIC Function doLChange(aGetsL,oLbxLeft,aLList,aLFiles)
Local cFname , cFDate, nFSize , cFAttr
Local nOp := oLbxLeft:GetPos()
If nOp > 0 .and. nOp <= Len(aLList)
  cFname := aLFiles[nOp][1]
  nFSize := aLFiles[nOp][2]
  cFDate := dtoc(aLFiles[nOp][3])+' ' +aLFiles[nOp][4]
  cFattr := aLFiles[nOp][5]
  Eval(aGetsL[1]:bSetGet,cFname)
  Eval(aGetsL[2]:bSetGet,nFSize)
  Eval(aGetsL[3]:bSetGet,cFDate)
  Eval(aGetsL[4]:bSetGet,cFattr)
Else
  Eval(aGetsL[1]:bSetGet,"")
  Eval(aGetsL[2]:bSetGet,0)
  Eval(aGetsL[3]:bSetGet,"")
  Eval(aGetsL[4]:bSetGet,"")
Endif
aGetsL[1]:Refresh()
aGetsL[2]:Refresh()
aGetsL[3]:Refresh()
aGetsL[4]:Refresh()
return


/* ----------------------------------------------------------------------
Função disparada na troca de posição da lista de arquivos FTP
do lado direito.
Atualiza as informações do arquivo selecionado no painel inferior
---------------------------------------------------------------------- */
STATIC Function doRChange(aGetsR,oLbxRight,aRList,aRFiles)
Local cFname , cFDate, nFSize , cFAttr
Local nOp := oLbxRight:GetPos()
If nOp > 0 .and. nOp <= Len(arList)
  cFname := aRFiles[nOp][1]
  nFSize := aRFiles[nOp][2]
  cFDate := dtoc(aRFiles[nOp][3])+' ' +aRFiles[nOp][4]
  cFattr := aRFiles[nOp][5]
  Eval(aGetsR[1]:bSetGet,cFname)
  Eval(aGetsR[2]:bSetGet,nFSize)
  Eval(aGetsR[3]:bSetGet,cFDate)
  Eval(aGetsR[4]:bSetGet,cFattr)
Else
  Eval(aGetsR[1]:bSetGet,"")
  Eval(aGetsR[2]:bSetGet,0)
  Eval(aGetsR[3]:bSetGet,"")
  Eval(aGetsR[4]:bSetGet,"")
Endif
aGetsR[1]:Refresh()
aGetsR[2]:Refresh()
aGetsR[3]:Refresh()
aGetsR[4]:Refresh()
return

Conclusão

Muitas vezes o fonte cresce em trabalho, não necessariamente em complexidade — desde que ele já tenha nascido usando um mínimo de boas práticas de programação. Se você olhar o fonte com uma lupa, vai ver que ainda existe códigos duplicados e algumas “pontas soltas”, que serão vistas na continuação desse post! O Fonte completo está no link do GITHUB logo abaixo, nas referências.

Agradeço novamente a todos pela audiência e lhes desejo TERABYTES DE SUCESSO !!! 

Referências

 

Protheus e FTP Client – Parte 02

Introdução

No post anterior (Protheus e FTP Client), vimos um exemplo básico de identificação da existência de um arquivo, e como fazer para baixar o arquivo do FTP em uma pasta local a partir do RootPath do ambiente Protheus. Agora, vamos ver com mais detalhes algumas propriedades interessantes da classe TFTPClient.

Propriedades da classe TFTPClient

Todas as propriedades e métodos da classe estão documentadas na TDN, vide link nas referências, no final do post, porém vamos ver algumas delas uma riqueza maior de detalhes, para entender onde é preciso utilizá-las.

bFireWallMode

Esta propriedade indica se a conexão com o FTP será Ativa ou Passiva. Para esta propriedade ter efeito, ela deve ser setada antes de estabelecermos a conexão com o FTP Server.

Em poucas palavras, o FTP usa duas conexões entre o Cliente e o Servidor FTP, uma conexão de dados e outra de controle. Quando usado o modo Ativo (bFireWallMode = .F.  — DEFAULT) , o FTP Server estabelece a conexão de dados em uma porta aberta e indicada pelo Cliente, que abre primeiro a conexão de controle na porta 21 do FTP Server. Pela perspectiva do Firewall do Cliente FTP, uma sistema externo está tentando conectar-se com um cliente interno, o que normalmente é restrito.

Quando habilitamos a propriedade bFireWallMode para .T., indicamos ao Cliente de FTP que ele deve estabelecer a conexão em “Modo Passivo”. Uma vez que o FTP Server seja capaz de trabalhar com a conexão em modo passivo, o Cliente de FTP abre as duas conexões — controle e dados — no FTP Server, na seguinte sequência: Após abrir a conexão de controle no FTP Server, o cliente informa ao FTP Server que a conexão deve ser passiva, então o FTP Server retorna um número de uma segunda porta — acima de 1024 — para o Cliente abrir a conexão de dados, sem precisar abrir um range de portas no Cliente FTP.

— Observação – O Protheus como FTP Server não suporta o modo passivo.  —

bUsesIPConnection

Esta configuração pode ser necessária quando o servidor onde o Protheus Server está sendo executado possua mais de uma interface de rede. Quando o FTP Client não está em modo passivo, e deve receber a conexão de dados de “volta”do FTP Server, normalmente o FTP Client busca o IP da máquina atual para enviar ao FTP Server, porém quando a máquina têm mais de uma interface de rede, não é garantido que o IP retornado seja por exemplo o IP “Externo”, que aceite a conexão. Quando habilitamos a propriedade bUsesIPConnection para .T., o FTP Client pega o IP da Interface de rede que estabeleceu a conexão de controle com o FTP, para informar ao FTP Server onde ele deve fazer a conexão de dados.

cErrorString

Esta é uma propriedade de consulta. Normalmente os métodos da classe cliente de FTP retornam “0” (zero) em caso de sucesso, e em caso de falha um código de erro. A lista de código de erros está documentada neste link da TDN, porém quando um método retorna erro, a propriedade cErrorString é alimentada com a descrição do erro retornado. Isto facilita a montagem de uma mensagem ou LOG de erro com mais detalhes.

nConnectTimeout

Por default, o time-out de tentativa de conexão com o FTP Server é de 5 segundos. Para alterar este tempo (em segundos) antes de estabelecer a conexão, defina o valor desejado nesta propriedade. Alguns servidores — dependendo da velocidade e latência de rede — podem precisar de um tempo um pouco maior.

nControlPort

Caso já exista uma conexão estabelecida com o FTP Server, esta propriedade informa a porta que foi usada para a conexão de controle (DEFAULT=21). Se esta propriedade for setada antes da conexão estabelecida, ela define a porta default de conexão com o FTP Server. O método FTPConnect(), usado para estabelecer a conexão, permite opcionalmente receber a porta de conexão no segundo parâmetro. Caso este não seja informado, será usada a porta definida na propriedade nControlPort.

nDataPort

Caso já exista uma conexão estabelecida com o FTP Server, esta propriedade informa qual é a porta TCP do FTP Client usada para estabelecer a conexão de dados. Quando não usamos o modo passivo, o FTP Client usa uma porta randomicamente sorteada entre 10 e 30 mil.

nDirInfo

Esta propriedade, quando consultada, realiza uma busca da lista de arquivos da pasta atual setada na conexão com o FTP Server. retornando em caso de sucesso o valor 0 (zero), caso contrário retorna o código do erro ocorrido. Normalmente realizamos a busca dos arquivos disponíveis na pasta atual usando o método Directory(), que internamente também realiza a busca de arquivos da pasta atual do FTP Server, permitindo inclusive um retorno filtrado por um arquivo específico ou o uso de máscaras (* e ?).

nDirInfoCount

Após uma consulta à propriedade nDirInfo,  a propriedade nDirInfoCount informa quantos arquivos foram encontrados na pasta atual do FTP Server.

nTransferMode

Esta propriedade indica e permite alterar o modo interno de transferência de dados entre o FTP Client e o FTP Server. Este valor deve ser setado após estabelecida a conexão com o FTP Server. Existem três modos de transferência: 0=Stream, 1=Block e 2=Compressed. O modo Stream é o DEFAULT. A RFC 959 explica em detalhes cada um dos modos, porém vejamos uma síntese de cada um.

Stream significa que os dados do arquivo são transmitidos em um modo contínuo, usando algumas sequencias de escape internas de controle, é o modo mais comum de transmissão; Block indica o uso de uma sequência de blocos de dados formatados em cabeçalho e conteúdo; e Compressed indica o uso de um algoritmo simples de controle para transmissão de bytes repetidos — economizando no tráfego de rede quando existem sequências do mesmo byte repetidas no arquivo a ser transmitido — veja mais detalhes sobre Run-length Encoding.

nTransferStruct

Permite alterar o modo como os dados são tratados na transferência. Os modos disponíveis são: 0=File (DEFAULT), 1=Record e 2=Page. A implementação de arquivo (File) é normalmente a mais utilizada, pois não interfere em seu conteúdo. Já as estruturas de transferência orientadas a registro (Record) e página (Page) podem ter interferências e sofrerem ajustes em uma das pontas da conexão, dependendo da plataforma em uso. Recomenda-se o uso da opção default (0=File), salvo em necessidades de integrações específicas.

nTransferType

Permite definir o tipo de transferência de dados usada na conexão. Por padrão, a transferência é do tipo 1=Image. Isto significa que os dados do arquivo são transmotidos em modo binário, isto é, a sqeuência de Bytes que compõe o arquivo, independente de seu conteúdo, é transmitida e recebida sem alteração.

Quando usado o tipo 0=ASCII, feito exclusivamente para a transmissão de arquivos texto (Texto Simples, sem UTF-8 ou caracteres especiais), podem haver alterações do conteúdo do arquivo entre plataformas, inclusive em casos onde isto seja desejável. Por exemplo, um arquivo texto puro no Windows usa dois bytes (CRLF) para indicar final de linha. Ao ser transmitido por FTP para uma estação Linux, usando o modo 0=ascii, as quebras de linha serão salvas no linux apenas com LF. Porém, se você setar o modo ascii e acidentalmente transmitir um arquivo de imagem ou outro arquivo de conteúdo binário, fatalmente ele vai ser “corrompido” no processo de gravação de quem estiver recebendo o arquivo.

Conclusão

Por hora, este post fica apenas como referência destas propriedades. No próximo sobre este assunto, vamos montar algo um pouco “maior” com a classe client de FTP.

Agradeço novamente a audiência, e desejo a todos TERABYTES DE SUCESSO !!! 

Referências

Protheus e FTP Client

Introdução

No post Protheus como Servidor de FTP, vimos como configurar um Servidor Protheus como FTP Server. Agora, vamos ver uma classe AdvPL que permite estabelecer uma conexão com um servidor FTP, e fazer operações como Download e Upload de arquivos — a classe tFtpClient.

Protocolos FTP, FTPS e SFTP

FTP, acrônimo de File Transfer Protocol, é um protocolo criado com a finalidade de transferência de arquivos entre estações. Para isso, uma estação atua como Cliente, estabelecendo uma conexão TCP/IP com a estação servidora. A porta padrão para os servidores de FTP é a porta 21.

O FTPS nada mais é do que uma extensão do protocolo FTP, que acrescenta o suporte a conexão criptografada usando TLS e/ou SSL.

O SFTP normalmente é confundido com o FTPS.  Na verdade seu nome vêm apenas da finalidade de transferência de arquivos, porém sua origem e natureza de implementação é bem diferente do FTP. Acrônimo de SSH File Transfer Protocol, na verdade é uma extensão do protocolo SSH (Secure Shell Protocol)  versão 2.0, concebido para transferência segura de arquivos.

Classe TFTPClient

Através da classe TFTPClient() do AdvPL, podemos criar uma conexão com um servidor FTP, bastando ter em mãos no mínimo o IP — ou nome do host — e a porta do servidor. Caso o servidor exija autenticação, precisamos ter um nome de usuário e uma senha para ser autenticada pelo servidor e realizar as operações.

Vale mencionar que, por hora, a classe TFTPClient() suporta apenas o protocolo FTP. Não há (ainda) suporte nativo em AdvPL para conexão cliente com FTPS e/ou SFTP. Quando existe a necessidade de uma integração automatizada com um servidor de arquivos implementado sobre um destes protocolos, a alternativa atual é executar uma aplicação externa mediante script ou similar, que faça a conexão e as tarefas necessárias.

Funcionalidades

Basicamente, as operações de um Cliente de FTP são as equivalentes a uma navegação em uma estrutura de pastas ou diretórios. Ao estabelecer a conexão com um servidor de FTP, normalmente o nosso diretório de trabalho remoto é a pasta “raiz” de publicação do FTP Server. A partir desse ponto, podemos executar operações como “listar arquivos da pasta atual”, “entrar em uma pasta”, “voltar para a pasta anterior”, “copiar um arquivo da pasta do FTP para a estação atual”, “copiar um arquivo da estação atual para a pasta atual do FTP”, “apagar um arquivo” e assim por diante.

Através das propriedades e métodos da classe TFTPClient(), podemos consultar ou parametrizar — de acordo com as capacidades do Servidor de FTP — o modo de transferência de arquivos, o tipo de transferência, o modo de conexão (Ativo ou Passivo), entre outras particularidades. Será mais fácil entender o funcionamento da classe partindo de um exemplo.

Exemplo AdvPL

O primeiro exemplo de uso será bem simples. Sua função é identificar a existência de um determinado arquivo na pasta Raiz de um servidor de FTP, e caso o arquivo exista, a aplicação AdvPL fará o download deste arquivo para uma pasta local do ambiente (environment) a partir do rootpath, chamada “downloads”. O Servidor de FTP utilizado foi um IIS em um Windows 10, com um site de FTP configurado para permitir acesso anônimo.

#include 'protheus.ch'

User Function TSTFTP()
Local oFtp, nStat
Local aFtpFile 
Local cFtpSrv := 'note-juliow-ssd'
Local nFTPPort := 21

SET DATE BRITISH
SET CENTURY ON

// Cria o objeto Client
oFtp := tFtpClient():New()

// Estabelece a conexão com o FTP Server 
nStat := oFtp:FtpConnect(cFtpSrv,nFTPPort)
If nStat != 0
  conout("FTPClient - Erro de Conexao "+cValToChar(nStat))
  QUIT
Endif

// Procura pelo arquivo leiame.txt
aFtpFile := oFtp:Directory( "leiame.txt", .T. )

if len(aFtpFile) > 0
  // Arquivo encontrado - Mostra os detalhes do arquivo 
  conout( cValToChar(aFtpFile[1][1])+" | "+; // nome
    cValToChar(aFtpFile[1][2])+" | "+; // tamanho
    cValToChar(aFtpFile[1][3])+" | "+; // data
    cValToChar(aFtpFile[1][4])+" | "+; // horario
    cValToChar(aFtpFile[1][5]) ) // Atributo . D = Diretorio
  // Faz o download do arquivo para a pasta local de downloads
  nStat := oFtp:ReceiveFile('leiame.txt','\downloads\leiame.txt' ) 
  If nStat != 0 
    conout("*** Falha no recebimento do arquivo - Erro "+cValToChar(nStat))
  Else
    conout("Arquivo recebido com sucesso.")
  Endif
Else
  conout("*** Arquivo nao encontrado.")
Endif
oFtp:Close()
Return

Caso ocorra falha de conexão, o programa não continua. Ao determinar a existência do arquivo no Servidor de FTP — através do método ::Directory() — fazemos o download do arquivo usando o método ::ReceiveFile(). Caso o arquivo na pasta local já exista, ele será sobrescrito.

Nas referências deste post, verifiquem todas as propriedades e métodos disponíveis na classe TFTPClient() na documentação dela na TDN. Para uma primeira versão, nosso exemplo será bem “arroz com feijão” mesmo, acredito que somente com um programa mais extenso, ou com mais programas de tamanho reduzido, será possível exemplificar as demais funcionalidades da classe TFTPClient()

Observações

Os primeiros testes das funcionalidades básicas foram feitos configurando o programa Cliente de exemplo usando o Protheus como FTP Server. E, para meu espanto, o método ::Directory()  não encontrava o arquivo, na verdade mesmo que a máscara de busca informada fosse  “*”  — para identificar todos os arquivos e sub-pastas a partir da pasta atual, não localizavam nada. Em um dos testes, eu resolvi acessar — para consulta — a propriedade chamada nDirInfo da engine Client de FTP, e para minha surpresa, após acessar esta propriedade, o método ::Directory(“leiame.txt”) localizou o arquivo, e o download / recebimento foi feito com sucesso. Como houveram falhas também em outras funcionalidades da API client, porém somente quando usado o Protheus como FTP Server, por hora os exemplos usados para demonstração das funcionalidades da classe TFTPClient serão testados com o FTP do Microsoft IIS, e posteriormente com um FTP Server no Linux,

Conclusão

Ainda vamos explorar mais esta classe, inclusive acessando um FTP Server na Internet. Porém, esta abordagem fica para o próximo post.

Agradeço novamente a audiência, curtidas, compartilhamentos, comentários e sugestões, e desejo a todos TERABYTES DE SUCESSO 😀

Referências

 

 

Protheus e AdvPL ASP – Parte 02

Introdução

No post anterior, começamos a entrar no mundo do AdvPL ASP. Dando continuação a esta jornada, agora vamos ver alguns recursos deveras interessantes desse universo.

Recebendo parâmetros do Browse em AdvPL ASP

Como mencionado no post anterior, a utilização dos arquivos APH é uma parte fundamental para criarmos com facilidade páginas dinâmicas usando AdvPL ASP. E, como também vimos, as páginas são compiladas no RPO, e sua execução está atrelada a uma configuração de um pool de threads de processamento — indicado na seção HTTP ou no host pela configuração RESPONSEJOB, e da passagem pela função configurada neste job para atender uma conexão — configuração ONCONNECT. Um ponto interessante sobre o Pool de Processos configurado é o seu tipo — TYPE=WEBEX.

Esta configuração habilita um recurso chamado de “Alias Virtuais” para receber valores do Browse, via URL (GET) ou via POST de formulário, bem como criar e utilizar variáveis de SESSION por usuário, verificar parâmetros recebidos no cabeçalho (header) da conexão HTTP, Cookies de memória e até criar parâmetros de retorno no cabeçalho HTTP que será enviado junto da string de retorno ao Browser.

*** Existem alguns métodos no protocolo HTTP com outras funcionalidades, porém os mais usados para recuperação e envio de informação são os métodos GET e POST, que são tratados pelo Protheus como HTTP Server. Os demais métodos são ignorados ***

A definição de “Alias Virtual” é exclusiva do AdvPL, o “Alias” é um apelido que nós podemos dar a uma tabela ou query aberta em um programa, que permite acessar o valor de um campo da tabela usando a sintaxe ALIAS->CAMPO. Os Alias Virtuais do AdvPL ASP são nomes reservados de componentes que na verdade não são tabelas, mas permitem o uso da mesma sintaxe, com algumas vantagens.

Todos os Alias Virtuais mencionados nesta documentação são criados automaticamente e dinamicamente dentro do contexto do processamento de uma requisição via link .apw, e o escopo do alias é público, isto é, ele pode ser acessado de qualquer ponto da pilha de chamadas durante o processamento da requisição. Neste post, vamos ver o alias virtual HTTPGET e o HTTPPOST

Alias Virtual HTTPGET

Através dele podemos receber parâmetros via URL do Browse. Esta é uma das formas de passar parâmetros do Browse para a aplicação AdvPL. Quando informamos uma URL para ser aberta por um Browser de Internet, ele submete uma requisição HTTP do tipo “GET” para o servidor web indicado na URL.

A sintaxe para esta passagem de parâmetros segue a especificação da montagem de uma URL, que segue o padrão http://host-ou-dominio/pasta/pagina.apw?p1=x&p2=y&p3=0&&#8230;

A primeira interrogação indica o final da identificação da página na URL e o início dos parâmetros. Cada parâmetro é composto de uma tupla chave=valor, onde cada tupla é separada pelo símbolo & (“e” comercial).

Quando vamos informar um valor na tupla, precisamos tomar cuidado para não colocar caracteres reservados diretamente como valor, senão eles podem ser interpretados  de outra forma, como por exemplo p1=C&A não vai ser interpretado desta forma … na verdade quem receber esta requisição vai entender que foi recebido um parâmetro chamado p1, com o conteúdo “C”, e um segundo parâmetro chamado “A”, sem valor. A forma correta de se passar C&A neste caso seria p1=C%26A, onde o “&” foi codificado no formato %HH — sinal percentual seguido de dois dígitos hexadecimais correspondentes ao caractere a ser enviado como valor na tabela ASCII.

Por padrão, no AdvPL ASP, o Protheus Server faz a identificação dos valores informados pela URL e cria os “campos” correspondentes dentro do alias virtual. Porém, devemos lembrar que, para efeitos da Linguagem AdvPL, um nome de campo segue as mesmas regras do nome de uma variável do AdvPL: Deve começar com uma letra, e conter apenas símbolos alfanuméricos (letras de A a Z, números de 0 a 9). Passar um parâmetro via URL sem atentar-se a este formato torna impeditivo o uso do Alias Virtual HTTPGET para buscar o valor de um parâmetro. A vantagem é que o tamanho deste identificador não está limitado a 10 posições, como é o caso das variáveis e nomes de campos de tabelas do AdvPL.

HTTPGET – Exemplo de Uso

Vamos aproveitar os programas do exemplo anterior, e apenas estender algumas funcionalidades. Por exemplo, vamos criar uma chamada de link .apw para chamar um APH, que vai montar uma tela HTML mostrando dois parâmetros recebidos pela URL : ID e NOME.

do case
case cAspPage == 'index'
  // Execura a página INDEX.APH compilada no RPO 
  // A String retornada deve retornar ao Browser
  cReturn := H_INDEX()
case cAspPage == 'getinfo'
  // Executa a pagina GetInfo.APH 
  cReturn := H_GETINFO()
otherwise
  // retorna HTML para informar 
  // a condição de página desconhecida
  cReturn := "<html><body><center><b>Página AdvPL ASP não encontrada.</b></body></html>"
Endcase

Agora, vamos criar e compilar o arquivo GETINFO.APH com o conteúdo abaixo:

<html><body>
<p>Informações por GET</p>
<p>ID = <%=HTTPGET->ID%></p>
<p>NOME = <%=HTTPGET->NOME%></p>
</body></html>

Desta forma, se nós utilizarmos no Web Browse a seguinte URL: http://localhost/getinfo.apw?ID=0&NOME=Seu+Nome , o resultado esperado na tela do browse é:

Advpl ASP - GetInfo

Detalhes e Observações

  1. Os conteúdos dos campos recebidos via alias virtual HTTPGET serão sempre recebidos como String em AdvPL, mesmo que o conteúdo seja um número — como vemos no caso do campo ID.
  2. Para uma URL, os nomes dos identificadores podem ser informados em letras maiúsculas, minúsculas ou misturadas, isto não importa para o AdvPL. Porém, se você passar dois valores com o mesmo nome de identificador, por exemplo um “id”  e um “ID”, os dois valores informados serão retornados para o AdvPL, separados por vírgula.
  3. Como vimos no nosso exemplo, ao informar o NOME da URL, foi passado o valor do “espaço em branco” no formato para URL (sinal de “+” soma). Ao recuperar o valor em AdvPL, ele já é recuperado devidamente “decodificado”. Se, por exemplo, a URL utilizada for http://localhost/getinfo.apw?ID=0&NOME=Jo%E3o , o valor de NOME recebido no AdvPL será “João
  4. Vários Browses possuem a capacidade de codificar URLs digitadas manualmente, porém normalmente eles vão codificar os caracteres acentuados e especiais (ASCII acima do código 128) usando UTF-8. Por exemplo, ao informar o nome “João” na URL, sem fazer a codificação, o Protheus vai receber o valor “João” no nome, que corresponde a representação dele usando UTF-8. Neste caso, você somente conseguiria obter o nome correto no CP1252 (Default do Protheus) caso você use a função DecodeUTF8().
  5. Não há limite de tamanho de URL segundo a especificação, porém entre os navegadores de intermet, foi convencionado um limite próximo de 2 KB. Para todos os efeitos, eu não recomendaria passagens de parâmetro por URL com mais de 512 bytes. Existem formas mais eficientes e projetadas para transferir conteúdos maiores, como veremos adiante.
  6. Embora o nome do recurso de recuperação de valores foi chamado de “Alias Virtual”, ele não possui as capacidades de um ALIAS de uma Tabela no AdvPL. Desse modo, as únicas formas de recuperação de conteúdo são: Especificando de forma literal o identificador desejado (cValor := HTTPGET->ID) ou criando uma string com este formato e usando macro-execução — por exemplo :
cIdName := 'ID'
cValor := &('HTTPGET->'+cIdName)

Caso um determinado argumento não seja informado na URL, mas seja recuperado em AdvPL, não ocorre erro de execução, porém o retorno será do tipo “U” (NIL). Caso o identificador seja informado sem valor, ele será do tipo “C” caractere, e vai conter uma string em branco (LEN=0).

Alias Virtual HTTPPOST

Quando utilizamos um formulário de dados em HTML, podemos criar campos para entrada de dados na tela do Browse (campos do tipo INPUT), e quando submetemos o formulário para uma URL com link apw no AdvPL ASP, podemos recuperar o conteúdo dos campos informados através do alias virtual HTTPPOST.

De forma similar ao HTTPGET, usamos o identificador do campo INPUT do formulário HTML para recuperar o valor informado no campo. Embora seja possível criar um formulário que passe o valor dos campos via URL (Formulário submetido com o método GET), é praxe criarmos o formulário no HTML com o método de submissão “POST”. Vamos pro exemplo que fica mais fácil.

Primeiro, precisamos de dois novos arquivos APH: Um deles vai ser o formulário HTML com os campos a serem preenchidos, e a outra página será responsável por receber os dados do formulário e mostrá-los no Browser. Para isso, primeiro acrescentamos o tratamento adequado no tratamento da URL recebida pelo Protheus Server:

case cAspPage == 'formpost'
  // Executa a pagina FormPost
  cReturn := H_FORMPOST()
case cAspPage == 'postinfo'
  // Executa a pagina PostInfo
  cReturn := H_POSTINFO()

Agora, vamos criar o arquivo FormPost.aph, que vai conter o formulário HTML com os campos para serem preenchidos.

<html><body>
<p>Formulário de POST</p>
<form action="/postinfo.apw" method="post">
First name:<br>
<input type="text" name="firstname"><br>
Last name:<br>
<input type="text" name="lastname">
<hr>
<input type="submit" value="Enviar">
</form>
</body></html>

Agora, vamos criar o arquivo PostInfo.aph, que vai mostrar os conteúdos recebidos.

<html><body>
<p>Dados Sumbetidos</p>
<p>First name: <%=HTTPPOST->FIRSTNAME%><br>
Last name: <%=HTTPPOST->LASTNAME%></p>
</body></html>

Com tudo salvo, acrescentado no projeto e devidamente compilado, vamos ao teste abrindo a URL http://localhost/formpost.apw, e preencher os campos do formulário.

Advpl ASP - Post 1

Após preencher os campos — neste exemplo eu preenchi com “nome” e “sobrenome” — e clicar no botão “Enviar”, o resultado esperado é:

Advpl ASP - Post 2

Detalhes e Observações

Como eu estou em um Browse, eu posso usar caracteres de outros idiomas, ou símbolos que não necessariamente sejam do Code Page default do Protheus — CP1252. Neste caso, qualquer caractere que não tenha representação no CP1252 será enviado ao AdvPL usando a notação decimal do caractere Unicode para HTML, no seguinte formato: &#nnnn; , onde nnnn é o número decimal do caractere Unicode. Por exemplo, usando o Google Translate, eu traduzi a palavra “teste” para russo (“тест”), e colei no formulário como o sobrenome do cidadão.

Teste - Cirilico

Em AdvPL, a string que eu recebi ao consultar o valor de HTTPPOST->LASTNAME está sendo mostrada logo acima. — Tive que tirar um print e gerar uma imagem, o blog não permitiu salvar o texto com a representação Unocode HTML — a visualização do post mostrava os caracteres já convertidos, e não os códigos. O caractere 1090 (decimal) da tabela UNICODE é a letra “t” minúscula em cirílico. — vide link http://www.codetable.net/decimal/1090 — bem como os caracteres subsequentes que também fazem parte do alfabeto cirílico.

Quando criamos o arquivo formpost.aph, colocamos que a ação (action) do formulário era “/postinfo.apw”, certo? Eu posso colocar parâmetros de URL (GET) em um formulário de POST? Sim, pode, e em AdvPL você recupera eles usando o alias virtual HTTPGET, como vimos no exemplo acima. Se trocarmos o action para “/postinfo.apw?OP=1”, ao consultar o conteúdo de HTTPGET->OP, devemos recuperar o valor “1” como Caractere.

Conclusão

Com tratamentos de GET e POST, aliado a páginas estáticas e dinâmicas, construímos desde uma aplicação WEB de exemplo, até um portal inteiro. Porém, para isso, vamos ver nos próximos posts mais alguns alias virtuais, e mais algumas funcionalidades interessantes do AdvPL ASP. Com o que já temos até aqui, dá pra fazer até uma Agenda … opa, CRUD em AdvPL ASP? Aguardem os próximos capítulos.

Agradeço a todos a audiência, e lhes desejo TERABYTES DE SUCESSO !!! 😀

Referências