Aprendendo PHP, Parte 3: Autenticação, Objetos, Exceções e Fluxo

Este tutorial é a Parte 3 de uma série de três partes, "Aprendendo PHP", que ensina como usar PHP por meio da construção de um aplicativo de fluxo de trabalho simples. Neste tutorial, você aprenderá como usar a autenticação HTTP, arquivos de fluxo e como criar objetos e exceções.

Nicholas Chase, Consultant, 自由职业者

Nicholas Chase esteve envolvido no desenvolvimento de Web sites para empresas como a Lucent Technologies, a Sun Microsystems, a Oracle e a Tampa Bay Buccaneers. Ele foi professor de física no ensino médio, gerente de instalação de lixo radioativo de nível baixo, editor de revista on-line de ficção científica, engenheiro de multimídia, instrutor de Oracle e o executivo chefe em tecnologia de uma empresa de comunicações interativas. Ele é o autor de diversos livros, incluindo XML Primer Plus (Sams, 2002).


nível de autor Master do
        developerWorks

Tyler Anderson, Engineer, Backstop Media

Tyler's photoTyler Anderson graduou-se em ciência da computação em 2004 e concluiu seu mestrado em engenharia elétrica e da computação em 2005 na Brigham Young University. Ele trabalhou para a Stexar Corp. como engenheiro de projetos, em pesquisa e desenvolvimento, de maio de 2005 a agosto de 2006. Desde sua descoberta pela Backstop Media LLC no início de 2005, ele tem escrito e codificado muitos artigos e tutoriais para a IBM developerWorks e DevX.com.



12/Jul/2005

Antes de Começar

Sobre Este Tutorial

Este tutorial conclui o aplicativo de fluxo de trabalho simples iniciado na primeira parte desta série sobre como aprender PHP. Você incluirá autenticação HTTP, a capacidade de transmitir documentos de um local não acessível pela Web e manipulação de exceção. Você também organizará alguns dos aplicativos em objetos.

Em geral, incluirá a capacidade de um administrador para aprovar um arquivo, geralmente tornando-o disponível para usuários. No decorrer deste tutorial, serão discutidos os seguintes tópicos:

  • Ativando e Usando a Autenticação HTTP Baseada em Navegador
  • Transmitindo Dados de um Arquivo
  • Criando Classes e Objetos
  • Usando Métodos e Propriedades de Objetos
  • Criando e Manipulando Exceções
  • Usando Atributos do ID XML
  • Validando um Documento XML Usando uma Document Type Definition (DTD)
  • Controlando o Acesso a Dados com Base na Página de Solicitação

Quem Deve Usar Este Tutorial?

Este tutorial é a Parte 3 de uma série de três partes designada a ensinar os princípios básicos de programação em PHP enquanto constrói um aplicativo de fluxo de trabalho simples. Destina-se a desenvolvedores que querem aprender mais sobre tópicos avançados, por exemplo, como usar PHP para programação orientada a objetos. Este tutorial também menciona a autenticação HTTP, fluxo, classes e objetos e manipulação de exceção, além de oferecer uma outra visão geral sobre a manipulação de XML.

Este tutorial assume a familiaridade com os conceitos básicos de PHP, como sintaxe, manipulação de formulários e acesso a um banco de dados. É possível obter todas as informações necessárias consultando Aprendendo PHP, Parte1" e "Aprendendo PHP, Parte2," e consultando Recursos.

Pré-requisitos

Para prosseguir com o código de amostra, é necessário assegurar que as seguintes ferramentas tenham sido instaladas e testadas:

Servidor HTTP -- É possível instalar PHP em vários servidores HTTP, como Apache e Microsoft® IIS e no Windows®, Linux®, UNIX®, Mac OS X e outras plataformas. Em geral, sua opção de servidor não importa, mas este tutorial cobrirá alguns problemas de configuração relacionados à autenticação HTTP usando o Apache 2.X como um exemplo. É possíver fazer o download do servidor Apache HTTP a partir do Apache.

PHP -- É claro que você também precisará de uma distribuição de PHP. Tanto o PHP V4 quanto o PHP V5 são usados no decorrer desta publicação, mas este tutorial concentra-se na V5 devido aos seus aprimoramentos. Faça Download de PHP.

Banco de Dados -- Parte deste projeto envolve o salvamento de dados em um banco de dados, portanto, você também precisará de um. Este tutorial cobre MySQL porque ele é comumente usado com PHP. è possível fazer o download do MySQL em http://dev.mysql.com/downloads/index.html.


A Situação até o Momento

Como Estão as Coisas agora

Você construiu um aplicativo de fluxo de trabalho simples no decorrer destes tutoriais. O aplicativo permite que os usuários façam upload de arquivos para o sistema e vejam esses arquivos, além dos arquivos aprovados por um administrador. Até o momento, você construiu:

  • Uma página de registro que permite que um usuário use um formulário HTML para inscrever-se em uma conta, inserindo um nome de usuário, endereço de e-mail e senha exclusivos. Você construiu a página PHP que analisa os dados submetidos, verifica o banco de dados para assegurar que o nome de usuário seja exclusivo, e salva o registro no banco de dados.
  • Uma página de login que obtém um nome de usuário e senha, verifica-os no banco de dados e, se forem válidos, cria uma sessão no servidor para que o servidor saiba quais arquivos exibir.
  • Elementos de interface simples que detectam se o usuário está com login efetuado para exibir as opções apropriadas.
  • Uma página de upload que permite que os usuários enviem um arquivo para o servidor através de um navegador. Você também construiu a página que obtém este arquivo transferido por upload e salva-o no servidor, em seguida, inclui informações sobre ele em um arquivo XML para recuperação posterior, usando o Document Object Model (DOM).
  • Uma função de exibição que lê o arquivo XML e exibe as informações usando a Simple API for XML (SAX).

é possível fazer o download dos arquivos que representam onde o aplicativo foi deixado em "Aprendendo PHP, Parte2."

O que Você Vai Fazer

Antes de concluir este tutorial, você terá um aplicativo de fluxo de trabalho completo, embora extremamente simples. Neste tutorial, você irá:

  • Incluir autenticação HTTP, controlada pelo servidor da Web. Também integrará seu processo de registro para que ele inclua novos usuários no servidor da Web.
  • Incluir links na função que exibe os arquivos disponíveis para download por usuários. Criará uma função que transmite estes arquivos para o navegador a partir do local não acessível pela Web.
  • Confirmar se os usuários fazem download de arquivos a partir da página apropriada. Você usará o fato de que os arquivos devem ser transmitidos pelo aplicativo, em vez de apenas atendidos pelo servidor HTTP, para ativar o controle sobre as circunstâncias nas quais os usuários fazem download de arquivos.
  • Criar uma classe que representa um documento, e usar os métodos orientados a objetos para acessar e fazer download dele.
  • Criar e usar exceções customizadas para ajudar a identificar problemas.
  • Incluir informações no arquivo XML que permitirá identificar exclusivamente cada arquivo. Esta etapa permitirá incluir caixas de opções no formulário de exibição que os administradores podem usar para determinar quais arquivos aprovar.
  • Gerenciar o processo de aprovação, ajustando seus arquivos XML para poder solicitar elementos fileInfo diretamente.

Vamos começar descrevendo o que você já possui.

A página de boas-vindas

Até o momento, você se concentrou na construção das partes individuais de seu aplicativo. Está na hora de começar a juntar todas elas, portanto, comece com uma página de boas-vindas simples que pode ser usada como um "pista de aterrissagem" para visitantes. Crie um novo arquivo chamado index.php e inclua o seguinte:

<?php
include ("top.txt");
   include ("scripts.txt");

   display_files();

   include ("bottom.txt");

?>

A primeira função include() carrega os principais elementos da interface para a página e configura uma sessão, se aplicável. A segunda carrega todos os scripts criados até o momento, incluindo a função display_files() criada em "Learning PHP, Part 2," que lista todos os arquivos transferidos por upload pelo usuário atual ou aprovados por um administrador. A inclusão final é apenas a parte inferior da página HTML.

Salve o arquivo no mesmo diretório no qual estão os outros arquivos criados. Por exemplo, é possível colocar o arquivo na raiz do documento de seu servidor, portanto, depois de iniciar o servidor HTTP, será possível ver a página, apontando seu navegador para http://localhost/index.php.

Como pode ser visto na Figura 1, a página é muito simples e, de fato, se você estiver na sequência correta, não deverá ver nenhum arquivo, a menos que esteja com login efetuado, porque nenhum deles foi aprovado ainda. Os arquivos aqui foram aprovados para fins de demonstração.

Figura 1. A página de listagem básica
The basic listing page

Se você acabou de iniciar seu navegador, deverá ver os links Register e Login porque não está com login efetuado. Na próxima seção, você vai conhecer outra maneira de lidar com esse processo.


Usando a Autenticação HTTP

Autenticação HTTP

Até o momento, você usou um sistema de login no qual o usuário insere um nome de usuário e senha em um formulário e, quando o usuário submete o formulário, essas informações são verificadas no banco de dados MySQL. Se elas corresponderem, o aplicativo criará uma sessão no PHP e designará um nome de usuário ao array $_SESSION para uso posterior.

Embora este processo funcione bem, pode ocorrer um problema durante a integração com outros sistemas. Por exemplo, se seu aplicativo de fluxo de trabalho fizer parte de uma intranet na qual os usuários podem estar com login efetuado com nomes de usuário de outros sistemas, eles não precisarão efetuar login novamente. Em vez disso, é necessário que eles já estejam com login efetuado quando estiverem nessa intranet, se já tiverem com login efetuado em outro local. Isto é conhecido como uma conexão única .

Para fazer isso aqui, alterne para um sistema no qual o servidor da Web realmente controla o processo de login. Em vez de apenas atender à página, o servidor verifica um nome de usuário e senha no pedido a partir do navegador e, se não os vir, ele instruirá o navegador a exibir uma caixa de nome de usuário e senha para que seja possível inserir essas informações. Depois de inserir as informações, não será necessário fazer isso novamente, porque o navegador as envia com pedidos subsequentes.

Vamos começar configurando o servidor.

Ativando a Autenticação HTTP

Antes de iniciar, saiba que, se você usar um servidor diferente do Apache 2.X, deverá verificar a documentação da autenticação HTTP para saber o que é preciso fazer para configurá-lo. (Como alternativa, você pode simplesmente ignorar esta seção. Você executará as etapas apropriadas para que o aplicativo funcione com qualquer tipo de autenticação.)

Mas como a autenticação HTTP realmente funciona? Primeiro, o servidor sabe o tipo de segurança que ele precisa fornecer para cada diretório. Uma maneira de alterar isso para um diretório específico é organizar todos os itens na configuração principal para o servidor. Outra maneira é usar um arquivo .htaccess, que contém instruções para o diretório no qual ele reside.

Por exemplo, você deseja que o servidor certifique-se de que todos os usuários que acessam seus arquivos específicos do usuário tenham nomes de usuário e senhas válidos, portanto, crie primeiro um diretório chamado loggedin dentro do diretório no qual residem os seus arquivos. Por exemplo, se seus arquivos residirem em /usr/local/apache2/htdocs, crie /usr/local/apache2/htdocs/loggedin.

Agora, é necessário informar o servidor que você irá substituir a segurança geral para esse diretório, portanto, abra o arquivo httpd.conf e inclua o seguinte nele:

<Directory /usr/local/apache2/htdocs/loggedin>
 AllowOverride AuthConfig
</Directory>

(É claro que você deve usar o diretório correto para sua própria configuração.)

Agora é hora de preparar o diretório real.

Configurando a Autenticação

Em seguida, crie um novo arquivo de texto e salve-o no diretório loggedin com o nome .htaccess. Inclua o seguinte nele:

AuthName "Registered Users Only"
AuthType Basic
AuthUserFile /usr/local/apache2/password/.htpasswd
Require valid-user

Vamos examinar isso desde o início. O AuthName é o texto que aparece na parte superior da caixa de nome de usuário e de senha. O AuthType especifica que você está usando a autenticação Basic , que significa que o nome de usuário e senha serão enviados em texto não-criptografado. O AuthUserFile é o arquivo que contém os nomes de usuários e senhas permitidos. (Esse arquivo é criado rapidamente.) Por último, a diretiva Require permite especificar quem realmente pode ver este conteúdo. Aqui, você está afirmando que o mostrará para qualquer usuário válido, mas também tem a opção de requerer usuários ou grupos de usuários específicos.

Reinicie o servidor HTTP para que estas mudanças entrem em vigor.

(Para o Apache V2.0, chame <APACHE_HOME>/bin/apachectl stop, seguido por <APACHE_HOME>/bin/apachectl start.)

Em seguida, você criará o arquivo de senhas.

Criando o Arquivo de Senhas

Para que tudo isto funcione, é necessário ter um arquivo de senhas que pode ser verificado pelo servidor. Em Incluindo Novos Usuários no Arquivo de Senhas, você saberá como manipular este arquivo a partir do PHP mas, por enquanto, se tiver acesso à linha de comando, poderá criar o arquivo diretamente.

Primeiro, escolha um local para seu arquivo .htpasswd. Ele não deve estar em um diretório que seja acessível pela Web. Não é muito seguro se alguém puder simplesmente fazer download dele e analisá-lo. Ele também deve estar em um local onde o PHP possa gravar nele. Por exemplo, você pode criar um diretório de senhas em seu diretório apache2. Qualquer que seja o local escolhido, certifique-se de que tenha as informações corretas em seu arquivo .htaccess.

Para criar o arquivo de senhas, execute o seguinte comando, substituindo seu próprio diretório e nome de usuário:

htpasswd -c /usr/local/apache2/password/.htpasswd roadnick

Quando for solicitado que digite, repita a senha, como em:

htpasswd -c /usr/local/apache2/password/.htpasswd roadnick
New password:
Re-type new password:
Adding password for user roadnick

O comutador -c instrui o servidor a criar um novo arquivo, portanto, depois de incluir o novo usuário, o arquivo ficará semelhante ao seguinte:

roadnick:IpoRzCGnsQv.Y

Observe que esta versão da senha é criptografada e você deve se lembrar disso quando incluir senhas a partir de seu aplicativo.

Agora vamos ver isso em ação.

Efetuando Login

Para ver isto em ação, é necessário acessar um arquivo no diretório protegido. Mova os arquivos uploadfile.php e uploadfile_action.php para o diretório loggedin e copie index.php para o diretório loggedin como display_files.php.

Em cada um dos três arquivos, altere as instruções include() para considerarem o novo local, como em:

<?php
include ("../top.txt");
   include ("../scripts.txt");

   echo "Logged in user is ".$_SERVER['PHP_AUTH_USER'];

   display_files();

   include ("../bottom.txt");

?>

Neste caso, você corrige as referências aos arquivos incluídos, mas também faz referência a uma variável que deve ser configurada quando o navegador enviar o nome de usuário e senha. Aponte seu navegador para http://localhost/loggedin/display_files.php para ver isto em ação. Como é possível ver na Figura 2, deve ser obtida uma caixa de nome de usuário e senha.

Figura 2. Caixa de nome de usuário e senha
Username and password box

Insira o nome de usuário e senha usados na seção Criando o Arquivo de Senhas para ver a página real.

Usando as Informações de Login

Neste ponto, foi inserido o nome de usuário e efetuado login, portanto, a página pode ser vista. Mas, como é possível ver na Figura 3, apesar da mensagem indicar que o usuário efetuou login, o conteúdo real parece não estar de acordo. Ainda é possível ver os links Register e Login , e a lista de arquivos ainda mostra apenas os que foram aprovados por um administrador -- e não os que foram transferidos por upload pelo usuário atual e ainda estão pendentes.

Figura 3. Com login efetuado ... mais ou menos
Logged in ... sort of

Para resolver estes problemas, existem duas opções. A primeira é voltar e recodificar cada instância na qual o aplicativo faz referência ao nome de usuário e procurar $_SERVER['PHP_AUTH_USER'], em vez de $_SESSION["username"]. Bons programadores são inerentemente preguiçosos, no entanto, essa não é uma opção muito atrativa.

A segunda opção é simplesmente configurar $_SESSION["username"] com base em $_SERVER['PHP_AUTH_USER'] , portanto, tudo continuará funcionando como antes. Isto pode ser feito em top.txt, logo após você iniciar uma nova sessão ou entrar na existente:

<?
   session_start();
   if (isset($_SESSION["username"])){
      //Do nothing
   } elseif (isset($_SERVER['PHP_AUTH_USER'])) {
      $_SESSION["username"] = $_SERVER['PHP_AUTH_USER'];
   }
   

?>
<html>
<head>
<title>Workflow System</title>
</head>
<body>

Primeiramente, a única maneira de fazer o navegador "esquecer" o nome de usuário e senha inseridos é fechar o navegador, portanto, você concede a $_SESSION["username"] a precedência de variável. Assim, você tem a opção de permitir que os usuários efetuem login como qualquer outra pessoa. (Você não fará isso aqui, mas tem a opção.)

Em seguida, se a variável $_SESSION ou a variável $_SERVER não estiver configurada, não acontecerá nada e a página continuará ativa, como se o usuário não tivesse efetuado login -- o que parece ser o caso. Fazer esta simples mudança corrige seu problema de login, como pode ser visto na Figura 4.

Figura 4. A página corrigida
The corrected page

Corrigindo a Interface

Antes de prosseguir para incluir um novo usuário, é necessário fazer algumas correções rápidas em top.txt para acomodar a nova estrutura. Em primeiro lugar, é necessário alterar o link Login para que, em vez de apontar para sua antiga página login.php, ele aponte para o arquivo display_files.php recém-protegido. Quando o usuário tentar acessá-lo, o navegador providenciará uma maneira de efetuar login:

...
<tr>
   <td width="30%" valign="top">
      <h3>Navigation</h3>

<?php
   if (isset($_SESSION["username"]) || isset($username)){
?>
      <p>You are logged in as <?=$_SESSION["username"].$username?>. <!--
You can <a href="/logout.php">logout</a> to login as a different user.--></p>

      <p><a href="/loggedin/uploadfile.php">Upload a file</a></p>
      <p><a href="/loggedin/display_files.php">Display files</a></p>

<?php
   } else {
?>
      <p><a href="/registration.php">Register</a></p>
      <p><a href="/loggedin/display_files.php">Login</a></p>
<?php
   }
?>
   </td>

Observe que, além de corrigir a referência de login e de incluir uma nova opção para exibir a lista de arquivos, comentamos a linha da mensagem sobre como efetuar logout, apenas porque esse assunto está além do escopo deste tutorial.

Agora é necessário apenas integrar o processo de registro com o arquivo de senhas.

Incluindo Novos Usuários no Arquivo de Senhas

A última etapa neste processo é integrar seu registro com o arquivo .htpasswd. Para isso, é necessário apenas incluir uma nova entrada depois de salvar o usuário no banco de dados. Abra registration_action.php e inclua o seguinte:

...
           $passwords = $_POST["pword"];
           $sql = "insert into users (username, email, password) values ('"
                                      .$_POST["name"]."', '".$_POST["email"]
                                      ."', '".$passwords[0]."')";
           $result = mysql_query($sql);

           if ($result){
              echo "It's entered!";

              $pwdfile = '/usr/local/apache2/password/.htpasswd';
              if (is_file($pwdfile)){
                 $opencode = "a";
              } else {
                 $opencode = "w";
              }
              $fp = fopen($pwdfile, $opencode);
              $pword_crypt = crypt($passwords[0]);
              fwrite($fp, $_POST['name'].":".$pword_crypt."\n");
              fclose($fp);

           } else {
               echo "There's been a problem: ".mysql_error();
           }
        } else {

           echo "There is already a user with that name.  Please try again. <br />";
...

Antes de iniciar, se você já tiver um arquivo .htpasswd, certifique-se de que o usuário possa gravar nele em seu servidor da Web. Caso contrário, certifique-se de que o usuário possa gravar no diretório apropriado.

Primeiramente, verifique se o arquivo existe e use essa informação para determinar se você irá gravar um novo arquivo ou anexar informações a um arquivo existente. Quando souber o que fazer, prossiga e abra o arquivo.

Como foi visto na seção Criando o Arquivo de Senhas, a senha é armazenada em formato criptografado, portanto, a função crypt() pode ser usada para obter essa cadeia. Por último, grave o nome de usuário e senha no arquivo e feche-o.

Para fazer o teste, encerre o navegador para limpar as senhas em cache, em seguida, abra http://localhost/index.php.

Clique em Register e crie uma nova conta. Quando concluir a criação da conta, encerre o navegador novamente e tente acessar uma página protegida. Seu novo nome de usuário e senha devem funcionar.


Usando Fluxos

O que São Fluxos?

Agora que o sistema já está configurado, você está pronto para permitir que o usuário realmente faça download dos arquivos disponíveis. No entanto, desde o início, estes arquivos foram armazenados em um diretório não acessível pela Web, portanto, um simples link para eles está fora de questão.

Em vez disso, nesta seção, será criada uma função que transmite o arquivo de seu local atual para o navegador.

Agora, a maneira que você realmente acessa um recurso, como um arquivo, depende de onde e de como ele está armazenado. Acessar um arquivo do local é muito diferente de acessar um em um servidor remoto por meio de HTTP ou FTP.

No entanto, felizmente o PHP fornece wrappers de fluxo. Em outras palavras, é feita uma chamada para um recurso, onde quer que ele esteja e, se o PHP tiver um wrapper disponível, ele descobrirá apenas como fazer essa chamada.

É possível saber quais wrappers estão disponíveis, imprimindo o conteúdo do array retornado pela função stream_get_wrappers() , da seguinte forma:

<?php
print_r(stream_get_wrappers());

?>

A função print_r() é extremamente útil para ver o conteúdo de um array. Por exemplo, seu sistema pode fornecer:

Array
(
    [0] => php
    [1] => file
    [2] => http
    [3] => ftp
)

Isto permitiria armazenar facilmente seus arquivos em um servidor da Web remoto ou servidor FTP como uma alternativa para armazená-los como arquivos no servidor local, e o código usado nesta seção ainda funcionará.

Vamos observar.

Fazendo Download do Arquivo

Para que o usuário possa ver um arquivo, o navegador precisa recebê-lo. Ele também precisa saber qual é o arquivo para exibi-lo corretamente. É preciso atenção com esses dois problemas. Crie um novo arquivo chamado download_file.php e salve-o no diretório loggedin. Inclua o seguinte:

<?php
include ("../scripts.txt");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];
   $filepath = UPLOADEDFILES.$filename;

   if($stream = fopen($filepath, "rb")){
      $file_contents = stream_get_contents($stream);
      header("Content-type: ".$filetype);
      print($file_contents);
   }

?>

Apesar de sua força, o processo aqui é realmente muito direto. Primeiro, você abre o arquivo para leitura e armazenamento em buffer. O que você realmente está fazendo com a função fopen() é criar um recurso que represente o arquivo. Esse recurso pode então ser passado para stream_get_contents(), que lê o arquivo inteiro em uma única cadeia.

Agora que você tem o conteúdo, pode enviá-lo ao navegador, mas o navegador não saberá o que fazer com ele e, provavelmente, o exibirá como texto. Isso funciona bem para um arquivo de texto, mas não tão bem para uma imagem, ou até mesmo um arquivo HTML. Portanto, em vez de apenas enviá-lo em formato bruto, primeiro envie um header para o navegador com informações sobre o Content-type do arquivo, por exemplo, image/jpeg.

Por último, apenas exiba o conteúdo do arquivo no navegador. Tendo recebido o cabeçalho Content-type , o navegador saberá tratá-lo não apenas como uma página da Web (a menos que, é claro, ele seja uma página da Web).

Desde que seja decidido qual arquivo e tipo serão usados de fato, você fará a leitura deles a partir do array $_GET , portanto, eles podem ser incluídos diretamente na URL, como em:

http://localhost/loggedin/download_file.php?file=timeone.jpg&filetype=image/jpeg

Insira esta URL (é claro, com um nome e tipo de arquivo apropriados) em seu navegador para ver os resultados mostrados na Figura 5.

Figura 5. Fazendo download de um arquivo
Downloading a file

Incluindo um Link no Arquivo

Como todas as informações de que a página de download precisa podem ser incluídas na URL, é simples incluir um link permitindo que o usuário faça download de um arquivo. Crie a exibição de arquivos disponíveis usando um fluxo SAX, que significa que a saída real é controlada por uma classe do manipulador de conteúdos chamada, neste caso, de Content_Handler. Abra o arquivo scripts.txt e inclua o seguinte código na classe Content_Handler :

class Content_Handler{

  private $available = false;
  private $submittedBy = "";
  private $status = "";

  private $currentElement = "";

  private $fileName = "";
  private $fileSize = "";
  private $fileType = "";

  function start_element($parser, $name, $attrs){
...
  }

  function end_element($parser, $name){

     if ($name == "workflow"){
        echo "</table>";
     }

     if ($name == "fileInfo"){
        echo "<tr><td><a href='download_file.php?file=".$this->fileName."
&filetype=".$this->fileType."'>"
                       .$this->fileName."</a></td>".
                 "<td>".$this->submittedBy."</td>".
                 "<td>".$this->fileSize."</td>".
                 "<td>".$this->status."</td></tr>";

        $this->fileName = "";
        $this->submittedBy = "";
        $this->fileSize = "";
        $this->status = "";
        $this->fileType = "";

        $this->available = false;
     }

     $this->currentElement = "";

  } 

  function chars($parser, $chars){

     if ($this->available){
         if ($this->currentElement == "fileName"){
            $this->fileName = $this->fileName . $chars;
         }
         if ($this->currentElement == "fileType"){
            $this->fileType = $this->fileType . $chars;
         }
         if ($this->currentElement == "size"){
            $this->fileSize = $this->fileSize . $chars;
         }
      }

  } 
}

Além das informações que já estavam sendo controladas para cada elemento fileInfo , agora é necessário controlar o fileType, portanto, é necessário incluir uma propriedade para isso.

Na função chars() , armazene o valor quando o obtiver. Ao chegar ao final do elemento fileInfo e for o momento de exibir as informações, use-as, juntamente com o fileName, para criar um link que aponte para a página de download. Os resultados podem ser vistos na Figura 6.

Figura 6. Vinculando-se ao arquivo
Linking to the file

Clique em um link para verificar o arquivo.

Em seguida, você observará o encapsulamento deste processo em um objeto.


Usando Objetos

Afinal de Contas, o que São Objetos?

Nesta seção, será examinado o uso de objetos. Até o momento, quase tudo o que foi feito tem sido procedural, significando que você tem um script que é executado muito bem do início ao fim. Agora, você vai sair desse assunto.

O conceito central de programação orientada a objetos é a ideia de que é possível representar "coisas" como um pacote configurável autossuficiente. Por exemplo, uma chaleira elétrica tem propriedades, como sua cor e temperatura máxima, e recursos, como aquecer a água e desligar-se.

Se uma chaleira fosse representada como um objeto, ela também teria propriedades, como color e maximumTemperature e recursos -- ou métodos -- tais como, heatWater() e turnOff(). Se você fosse gravar um programa com interface com a chaleira, ele seria simplesmente chamado de método heatWater() do objeto chaleira, em vez de se preocupar sobre como ele realmente é feito.

Para tornar as coisas um pouco mais relevantes, você criará um objeto que representa um arquivo a ser transferido por download. Ele terá propriedades, como o nome e tipo do arquivo, e métodos, como download().

No entanto, depois de mencionar tudo isso, precisamos informar que você realmente não define um objeto. Em vez disso, define uma classe de objetos. Uma classe age como um tipo de "modelo" para objetos desse tipo. Em seguida, você cria uma instância dessa classe, e essa instância é o objeto.

Vamos começar criando a classe real.

Criando a Classe WFDocument

A primeira etapa para lidar com objetos é criar a classe na qual eles são baseados. Esta definição pode ser incluída no arquivo scripts.txt, mas você está tentando tornar o código mais preservável, não menos. Portanto, crie um arquivo separado, WFDocument.php, e salve-o no diretório principal. Inclua o seguinte:

<?php
include_once("scripts.txt");

class WFDocument {

   function download($filename, $filetype) {

      $filepath = UPLOADEDFILES.$filename;

      if($stream = fopen($filepath, "rb")){
        $file_contents = stream_get_contents($stream);
        header("Content-type: ".$filetype);
        print($file_contents);
      }
   }
}

?>

Primeiro, você precisa da constante UPLOADEDFILES , portanto, inclua o arquivo scripts.txt. Em seguida, crie a classe real. A classe WFDocument possui apenas um único método, download(), que é igual ao código em download_file.php, com exceção do recebimento do nome e tipo do arquivo como entradas para a função em vez de extraí-las diretamente do array $_GET .

Agora, vamos observar a instanciação desta classe.

Chamando o Objeto de Tipo WFDocument

De fato, você já instanciou vários objetos quando você estava trabalhando com o DOM em Parte 2 desta série, mas não mencionamos muito sobre por que ou como. Compensaremos isso agora.

Abra a página download_file.php e altere o código para que ele seja lido da seguinte forma:

<?php
include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument();
   $wfdocument->download($filename, $filetype);

?>

Primeiramente, em vez de incluir o arquivo scripts.txt, você está incluindo a definição da classe WFDocument , que é colocada no arquivo WFDocument.php. (Alguns desenvolvedores acham útil apenas criar uma página que inclua todas as suas classes e, em seguida, incluir essa página em vez de incluir classes individuais em todo o local.)

Agora você está pronto para criar um novo objeto, o que é feito usando a palavra-chave new . Esta linha cria um novo objeto do tipo WFDocument e designa-o à variável $wfdocument .

Quando houver uma referência a esse objeto, será possível chamar qualquer um de seus métodos públicos. Neste caso, há apenas um método, download(), e ele é chamado usando o operador -> . Basicamente, este símbolo indica: "Use o método (ou propriedade) que pertence a este objeto."

Salve o arquivo e teste-o clicando em um dos links em sua página. O código é exatamente igual ao de antes. A única diferença é a maneira como ele está sendo chamado.

Criando Propriedades

É claro, os métodos são apenas parte da história. O principal conceito de um objeto é que ele é encapsulado. Em outras palavras, ele deve conter todas as suas próprias informações, portanto, em vez de fornecer o nome e tipo de arquivo para o método download() , é possível configurá-los como propriedades no objeto. Mas primeiro, é necessário criá-los na classe:

<?php
include_once("../scripts.txt");

class WFDocument {

   public $filename;
   public $filetype;

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      if($stream = fopen($filepath, "rb")){
        $file_contents = stream_get_contents($stream);
        header("Content-type: ".$this->filetype);
        print($file_contents);
      }
   }
}

?>

Observe que as variáveis são declaradas fora da função; elas fazem parte da classe e não da função. Elas também estão sendo declaradas como public, o que significa que é possível acessá-las fora da própria classe. Também é possível configurar uma propriedade como private, o que significa que é possível usá-la apenas na própria classe, ou protected, o que significa que é possível usá-la apenas na classe ou em quaisquer classes baseadas nesta. (Se você não estiver familiarizado com esta ideia, aguarde um momento. Falaremos mais sobre este conceito, herança, em Criando uma Exceção Customizada .)

Por último, para fazer referência a uma propriedade de objeto, é necessário saber a qual objeto ela pertence. Dentro do próprio objeto, é possível usar apenas a palavra-chave $this, que se refere ao próprio objeto. Desse modo, é possível usar $this->filename para referir-se à propriedade filename do objeto que está executando este código.

Agora, vamos observar a configuração de valores para estas propriedades.

Configurando Propriedades

Em vez de passar informações para um objeto, você deseja de fato configurar as propriedades do objeto:

<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument();
   $wfdocument->filename = $filename;
   $wfdocument->filetype = $filetype;
   $wfdocument->download();

?>

Observe a notação aqui. Estão sendo usados o nome do objeto, $wfdocument, o operador -> e o nome da propriedade. Quando estas propriedades tiverem sido configuradas, elas estarão disponíveis de dentro do objeto, portanto, não é necessário passá-las para o método download() .

Agora, depois que tudo isso tiver sido feito, haverá de fato uma melhor maneira de lidar com esse tipo de situação, então, vamos examinar uma alternativa.

Ocultando Propriedades

Embora certamente seja possível configurar o valor de uma propriedade diretamente, como foi feito na seção anterior, esta não é a melhor maneira de lidar com as coisas. Em vez disso, a prática geral é ocultar as propriedades reais do público e usar getters e setters para obter e configurar seus valores, da seguinte forma:

<?php
include_once("../scripts.txt");

class WFDocument {

   private $filename;
   private $filetype;

   function setFilename($newFilename){
      $this->filename = $newFilename;
   }
   function getFilename(){
      return $this->filename;
   }

   function setFiletype($newFiletype){
      $this->filetype = $newFiletype;
   }
   function getFiletype(){
      return $this->filetype;
   }

   function download() {

      $filepath = UPLOADEDFILES.$this-> getFilename()

      if($stream = fopen($filepath, "rb")){
        $file_contents = stream_get_contents($stream);
        header("Content-type: ".$this->getFiletype())
        print($file_contents);
      }
   }
}

?>

Primeiro, defina as propriedades como private. Isto significa que, se você tentar configurá-las diretamente, como tem sido feito, ocorrerá um erro. Mas ainda é preciso configurar estes valores, portanto, use então os métodos getFilename(), setFilename(), getFiletype() e setFiletype() . Observe que aqui eles são usados no método download() , da mesma forma que a propriedade original seria usada.

Usar getters e setters é útil porque oferece maior controle sobre o que está acontecendo com seus dados. Por exemplo, você pode querer executar algumas verificações de validação antes de permitir que um determinado valor seja configurado para uma propriedade.

Chamando as Propriedades Ocultas

Agora que as propriedades estão ocultas, é necessário voltar e modificar a página download_file.php para que não seja retornado um erro:

<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument();
   $wfdocument->setFilename($filename);
   $wfdocument->setFiletype($filetype);
   $wfdocument->download();

?>

Assim como esta abordagem é útil, existem maneiras mais fáceis de configurar propriedades em um objeto.

Criando um Construtor

Se um objeto tiver um construtor, ele será chamado sempre que for criada uma nova instância dessa classe específica. Por exemplo, pode ser criado um construtor simples:

...
   function getFiletype(){
      return $this->filetype;
   }

   function __construct(){
      echo "Creating new WFDocument";
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;
...

Se você tentar executar este script como ele está, verá um erro, porque o objeto exibe o texto (Creating new WFDocument) antes de exibir os cabeçalhos, como pode ser visto na Figura 7.

Figura 7. Erro após a execução do script
Error after running script

Portanto, mesmo que o método __construct() nunca tenha sido chamado explicitamente, o aplicativo chamou-o logo após a instanciação do objeto. Isso pode ser usado a seu favor pela inclusão de informações no construtor.

Criando um Objeto com Informações

Um dos usos mais comuns para um construtor é oferecer uma maneira de inicializar vários valores ao criar o objeto. Por exemplo, é possível configurar a classe WFDocument para que seja possível configurar as propriedades filename e filetype ao criar o objeto:

...
   function getFiletype(){
      return $this->filetype;
   }

   function __construct($filename = "", $filetype = ""){
      $this->setFilename($filename);
      $this->setFiletype($filetype);
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

...

Ao criar o objeto, o PHP executa todas as instruções no construtor antes de prosseguir. Neste caso, esse construtor está procurando filename e filetype. Se não forem fornecidos, ainda não será recebido um erro, porque foram especificados valores-padrão para serem usados se não fornecido nenhum valor quando a função for chamada.

Mas como a função __construct() é chamada explicitamente?

Criando o Objeto: Chamando o Construtor

De fato, o método construtor não é chamado explicitamente. Em vez disso, ele é chamado implicitamente sempre que um objeto for criado. Ou seja, esse momento específico é usado para passar informações para o construtor:

<?php
include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument($filename, $filetype);
   $wfdocument->download();

?>

As informações passadas para a classe ao criar o novo objeto são passadas para o construtor. Dessa forma, você apenas cria o objeto e usa-o para fazer download do arquivo.


Manipulando Exceções

Uma Exceção Genérica

Como as exceções ocorrem quando algo não está correto com um aplicativo, elas geralmente são confundidas com erros. No entanto, as exceções são muito mais flexíveis. Nesta seção, você verá como definir diferentes tipos de exceções e usá-las para determinar o que está acontecendo com o aplicativo.

Vamos começar com uma exceção genérica simples na definição da classe WFDocument :

<?php
include_once("../scripts.txt");

class WFDocument {
...
   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {
         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           }
         } else {
           throw new Exception ("File '".$filepath."' does not exist.");
         }

      } catch (Exception $e) {

         echo "<p style='color: red'>".$e->getMessage()."</p>";

      }
   }
}

?>

Em primeiro lugar, as exceções não apenas ocorrem, elas são lançadas. E, é claro, se você lançar alguma coisa, precisará capturá-la, portanto, é criada uma instrução try-catch . Na seção try , coloque o seu código. Se ocorrer algo inesperado, por exemplo, neste caso, um arquivo não existir, e você lançar uma exceção, o PHP se moverá imediatamente para o bloco catch para capturar a exceção.

Uma exceção possui muitas propriedades, como a linha e o arquivo a partir dos quais a exceção foi lançada, e uma mensagem. Geralmente, o aplicativo configura a mensagem quando ele lança a exceção, como pode ser visto aqui. A própria exceção, $e, pode então fornecer esse texto usando o método getMessage() . Por exemplo, se você tentar fazer download de um arquivo que não existe, verá a mensagem exibida, em vermelho, como na Figura 8.

Figura 8. A exceção básica
The basic exception

No entanto, a força real das exceções é proveniente da criação de sua própria exceção.

Criando uma Exceção Customizada

Na última seção, os objetos foram examinados, mas ficou para trás um aspecto muito importante deles: herança. Vamos observar isso agora.

Uma vantagem de usar classes é a capacidade de usar uma classe como a base para outra. Por exemplo, é possível criar um novo tipo de exceção, NoFileExistsException, que estende a classe original Exception :

class NoFileExistsException extends Exception {

   public function informativeMessage(){
      $message = "The file, '".$this->getMessage()."', called on line ".
           $this->getLine()." of ".$this->getFile().", does not exist.";
      return $message;
   }

}

(Para simplificar, incluímos este código no arquivo WFDocument.php, mas é possível incluí-lo sempre que ele estiver acessível quando você precisar dele.)

Aqui, foi criada uma nova classe, NoFileExistsException, com um único método: informativeMessage(). De fato, esta classe também é uma Exception, portanto, todos os métodos e propriedades públicos para um objeto Exception também estão disponíveis.

Por exemplo, observe que, na função informativeMessage() são chamados os métodos getLine() e getFile() , mesmo que eles não estejam definidos aqui. Eles são definidos na classe base, Exception, portanto, podem ser usados.

Agora vamos ver isso em ação.

Capturando uma Exceção Customizada

A maneira mais fácil de usar o novo tipo de exceção é simplesmente lançá-la assim como você lançaria uma Exception:

function download() {

      $filepath = UPLOADEDFILES.$this->filename;

try {
           if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }

      } catch (NoFileExistsException $e) {

         echo "<p style='color: red'>".$e->informativeMessage()."</p>";

      }
   }

Observe que, mesmo que seja passado apenas o $filepath ao criar a exceção, é retornada a mensagem completa, conforme mostrado na Figura 9.

Figura 9. Usando uma exceção customizada
Using a custom exception

Trabalhando com Várias Exceções

Uma razão para criar classes de exceção customizada é a possibilidade de usar a capacidade do PHP para distingui-las. Por exemplo, é possível criar vários blocos catch para um único try:

...
   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

try {
           if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } else {
              throw new Exception ("Cannot open file ".$filepath);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }

      } catch (NoFileExistsException $e) {

         echo "<p style='color: red'>".$e->informativeMessage()."</p>";

      } catch (Exception $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";
      }
   }
}

Neste caso, você tenta capturar problemas antes de eles ocorrerem, verificando a existência do arquivo e lançando uma NoFileExistsException. Se você venceu este obstáculo e alguém mais está impedindo-o de abrir o arquivo, lance uma exceção genérica. O PHP detecta o tipo de exceção emitida e executa o bloco catch apropriado.

Tudo isto pode parecer um pouco demasiado para apenas exibir mensagens, mas não há nada que indique que isso é tudo o que pode ser feito. É possível criar métodos customizados para sua exceção que, por exemplo, enviem notificações para eventos específicos. Também é possível criar blocos catch customizados que executam diferentes ações, dependendo da situação.

É claro que, só porque você definiu todas estas diferentes exceções, isto não significa que seja necessário capturar cada uma individualmente, como será visto a seguir.

Propagando Exceções

Outro recurso útil de herança é a capacidade de tratar um objeto como se ele fosse um membro de sua classe base. Por exemplo, é possível lançar uma NoFileExistsException e capturá-la como uma Exception:

...
   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

try {
           if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } else {
              throw new Exception ("Cannot open file ".$filepath);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }

      } catch (Exception $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";
      }
   }
}

Neste caso, ao lançar a exceção, o PHP consegue acessar a lista de blocos catch , procurando o primeiro que se aplica. Aqui existe apenas um, mas ele capturará qualquerExceção, conforme mostrado na Figura 10.

Figura 10. Propagando Exceções
Propagating exceptions

Juntando Tudo

O que É Preciso Fazer

Agora que o processo de download de arquivo está em operação, é hora de colocar tudo junto e concluir o aplicativo. Nesta seção, você cuidará de algumas tarefas diversas que ainda precisam ser realizadas:

  • Criação de identificadores de arquivos individuais
  • Detecção de administradores
  • Criação do formulário que permite que um administrador aprove arquivos
  • Verificação de downloads para assegurar que eles não estejam sendo chamados a partir de outro servidor

Comece criando uma classe Counter .

Identificando Documentos Individuais

Até o momento, você não se preocupou com a identificação de arquivos específicos, exceto quando está fazendo download deles, mas agora é necessário prestar um pouco mais de atenção. Basicamente, será processado um formulário que permite que um administrador aprove arquivos específicos, portanto, seria útil ter uma maneira fácil de consultá-los.

O que será feito aqui é criar uma classe Counter que permita a geração de uma chave exclusiva para cada arquivo. Essa chave é então incluída no arquivo XML, permitindo solicitar diretamente o elemento fileInfo apropriado. Comece criando a definição de classe Counter . É possível colocá-la, por exemplo, no arquivo scripts.txt:

class Counter{

   function getNextId(){
     $filename = "/usr/local/apache2/htdocs/counter.txt";
     $handle = fopen($filename, "r+");
     $contents = fread($handle, filesize($filename));

     $nextid = $contents + 1;
     echo $nextid;
     rewind($handle);
     fwrite($handle, $nextid);
     fclose($handle);

     return $nextid;
   }

}

Aqui você tem uma única função, getNextId(), que lê um arquivo existente, counter.txt, e incrementa o conteúdo em 1. (Portanto, antes de iniciar, crie o arquivo com a única entrada 0.) Ela volta então ao início do arquivo e grava o novo valor para que ele esteja presente na próxima vez em que a função for chamada.

Use esta classe quando incluir as informações do arquivo no arquivo XML.

Incluindo o Identificador no Arquivo XML

Basicamente, você deseja poder recuperar um único elemento fileInfo por seu atributo do ID , portanto, prossiga e inclua estas informações no arquivo docinfo.xml criado em "Aprendendo PHP, Parte2."

function save_document_info($fileInfo){

   $xmlfile = UPLOADEDFILES."docinfo.xml";
...
   $filename = $fileInfo['name'];
   $filetype = $fileInfo['type'];
   $filesize = $fileInfo['size'];

   $fileInfo = $doc->createElement("fileInfo");

   $counter = new Counter();
   $fileInfo->setAttribute("id", "_".$counter->getNextId());

   $fileInfo->setAttribute("status", "pending");

   $fileInfo->setAttribute("submittedBy", getUsername());
...
   $doc->save($xmlfile);

}

Sempre que as informações de um novo documento forem salvas, crie um objeto Counter e use seu método getNextId() para fornecer um valor exclusivo para o atributo do id . Como você especificará este atributo posteriormente como sendo do tipo ID, o valor está sendo precedido com um sublinhado (_) porque estes valores não podem começar com um número.

Os resultados são semelhantes aos seguintes (incluímos espaçamento para facilitar um pouco a leitura):

<fileInfo id="_13" status="pending" submittedBy="roadnick">
   <approvedBy/>
   <fileName>timeone.jpg</fileName>
   <location>/var/www/hidden/</location>
   <fileType>image/jpeg</fileType>
   <size>2020</size>
</fileInfo>

Observe que este processo não afeta nenhum de seus dados existentes, portanto, é necessário incluir manualmente atributos do id em todos os seus elementos fileInfo ou excluir o arquivo docinfo.xml e começar novamente, fazendo upload dos arquivos com os quais trabalhar.

Agora você está pronto para aprovar arquivos mas, primeiro, precisa configurar os administradores que vão fazer isso.

Detecção de administradores

Quando criou originalmente a tabela de usuários no banco de dados, você não considerou o fato de que precisava distinguir entre usuários regulares e administradores, portanto, é necessário cuidar disso agora. Efetue login no MySQL e execute os seguintes comandos:

alter table users add status varchar(10) default 'USER';
update users set status = 'USER';
update users set status = 'ADMIN' where id=3;

O primeiro comando inclui a nova coluna, status, na tabela de usuários. Não foi especificado o tipo de usuário na página de registro, portanto, basta especificar um valor-padrão de USER para os novos usuários incluídos no sistema. O segundo comando configura este status para os usuários existentes. Por último, escolha um usuário para ser um administrador. (Certifique-se de usar o valor de id apropriado para seus dados.)

Agora que você tem os dados, é possível criar uma função que retorna o status do usuário atual:

function getUserStatus(){
   $username = $_SESSION["username"];
   db_connect();
   $sql = "select * from users where username='".$username."'";

   $result = mysql_query($sql);
   $row = mysql_fetch_array($result);

   $status = "";

   if ($row) {
     $status = $row["status"];
   } else {
     $status = "NONE";
   }

   mysql_close();

   return $status;

}

Vamos rever como este processo funciona. Primeiro, crie uma conexão com o banco de dados apropriado usando o script criado: db_connect(). Em seguida, é possível criar uma instrução SQL usando o nome de usuário, armazenado na variável $_SESSION . Em seguida, execute essa instrução e tente obter a primeira (e, provavelmente, a única) linha de dados.

Se existir uma linha, configure o status igual ao valor da coluna status . Caso contrário, configure o status igual a NONE. Por último, feche a conexão e retorne o valor.

Coloque esta função no arquivo scripts.txt para poder acessá-la quando exibir a lista de arquivos.

Aprovando o Arquivo: O Formulário

Agora você está pronto para incluir recursos de aprovação no formulário. O que você deseja é exibir uma caixa de opção para arquivos pendentes, se o usuário que está visualizando a lista de arquivos for um administrador. A classe Content_Handler manipula esta exibição:

class Content_Handler{

  private $available = false;
  private $submittedBy = "";
  private $status = "";

  private $currentElement = "";

  private $fileId = "";
  private $fileName = "";
  private $fileSize = "";
  private $fileType = "";

  private $userStatus = "";

  function start_element($parser, $name, $attrs){


     if ($name == "workflow"){

        $this->userStatus = getUserStatus($_SESSION["username"]);

        if ($this->userStatus == "ADMIN"){
           echo "<form action='approve_action.php' method='POST'>";
        }

        echo "<h3>Available files</h3>";
        echo "<table width='100%' border='0'><tr>".
                "<th>File Name</th><th>Submitted By</th>".
                "<th>Size</th><th>Status</th>";
        if ($this->userStatus == "ADMIN"){
            echo "<th>Approve</th>";
        }
        echo "</tr>";
     }

     if ($name == "fileInfo"){
        if ($attrs['status'] == "approved" ||
                  $attrs['submittedBy'] == $this->username){
           $this->available = true;
        }
        if ($this->available){
           $this->submittedBy = $attrs['submittedBy'];
           $this->status = $attrs['status'];
           $this->fileId = $attrs['id'];
        }
     }

     $this->currentElement = $name;

  }

  function end_element($parser, $name){

     if ($name == "workflow"){
        echo "</table>";

        if ($this->userStatus == "ADMIN"){

           echo "<input type='submit' value='Approve Checked Files' />";

           echo "</form>";
        }

     }

     if ($name == "fileInfo"){
        echo "<tr>";
        echo "<td><a href='download_file.php?file=".
                       $this->fileName."&filetype=".
                       $this->fileType."'>".
                            $this->fileName."</a></td>".
                 "<td>".$this->submittedBy."</td>".
                 "<td>".$this->fileSize."</td>".
                 "<td>".$this->status."</td><td>";

        if ($this->userStatus == "ADMIN"){
           if ($this->status == "pending") {
              echo "<input type='checkbox' name='toapprove[]' value='".
                         $this->fileId."' checked='checked' />";
           }
        }

        echo "</td></tr>";

        $this->fileId = "";
        $this->fileName = "";
        $this->submittedBy = "";
        $this->fileSize = "";
        $this->status = "";
        $this->fileType = "";

        $this->available = false;
     }

     $this->currentElement = "";

  } 

  function chars($parser, $chars){
...
  }
}

Começando na parte superior, você tem duas novas propriedades para definir: $fileId e $userStatus. A última, você configura uma vez, quando processa o início do elemento workflow e, portanto, o documento. Neste ponto, se o usuário for um administrador, você deseja incluir um elemento form na página e uma coluna Approve na tabela.

Feche o formulário no final do documento, quando o manipulador de conteúdos receber a notificação do final do elemento workflow .

Para as caixas de opções reais, você as exibe quando exibe a linha real de informações no final de cada elemento fileInfo . Como existe a possibilidade de várias entradas, nomeie o campo toapprove[].

O resultado é um formulário com as caixas apropriadas, como pode ser visto na Figura 11.

Figura 11. O formulário de aprovação
The approval form

Configurando IDs

Agora você tem o formulário, mas para acessar os elementos fileInfo por seus atributos do id , é necessário executar mais uma etapa. Diferentemente em um arquivo HTML, apenas nomear um atributo "id" não é suficiente para fazê-lo agir como um identificador. Em um arquivo XML, é necessário fornecer algum tipo de esquema (observe o pequeno "s") que define o atributo. Neste caso, será incluída uma Document Type Definition (DTD). Primeiro, inclua uma referência a ela no documento real:

function save_document_info($fileInfo){

   $xmlfile = UPLOADEDFILES."docinfo.xml";

   if(is_file($xmlfile)){
      $doc = DOMDocument::load($xmlfile);
      $workflowElements = $doc->getElementsByTagName("workflow");
      $root = $workflowElements->item(0);

      $statistics = $root->getElementsByTagName("statistics")->item(0);
      $total = $statistics->getAttribute("total");
      $statistics->setAttribute("total", $total + 1);

   } else{

      $domImp = new DOMImplementation;
      $dtd = $domImp->createDocumentType('workflow', '', 'workflow.dtd');

      $doc = $domImp->createDocument("", "", $dtd);

      $root = $doc->createElement('workflow');
      $doc->appendChild($root);

      $statistics = $doc->createElement("statistics");
      $statistics->setAttribute("total", "1");
      $statistics->setAttribute("approved", "0");
      $root->appendChild($statistics);
   }
...
}

Em vez de criar o documento instanciando diretamente a classe DOMDocument , crie um DOMImplementation, a partir do qual é criado um objeto DTD. Em seguida, designe essa DTD ao novo documento que está sendo criado.

Se remover o arquivo docinfo.xml e fizer upload de um novo documento, você verá as novas informações:

<?xml version="1.0"?>
<!DOCTYPE workflow SYSTEM "workflow.dtd">
<workflow><statistics total="3" approved="0"/>
...

Agora é necessário criar o arquivo workflow.dtd.

A DTD

A explicação de todas as nuances da validação XML vai muito além do escopo deste tutorial, mas é necessário ter uma DTD que descreva a estrutura do arquivo docinfo.xml. Para isso, crie um arquivo e salve-o como workflow.dtd no mesmo diretório no qual está o docinfo.xml. Inclua o seguinte:

<!ELEMENT workflow (statistics, fileInfo*) >
<!ELEMENT statistics EMPTY>
<!ATTLIST statistics total CDATA #IMPLIED
                     approved CDATA #IMPLIED >
<!ELEMENT fileInfo (approvedBy, fileName, location, fileType, size)>
<!ATTLIST fileInfo id ID #IMPLIED>
<!ATTLIST fileInfo status CDATA #IMPLIED>
<!ATTLIST fileInfo submittedBy CDATA #IMPLIED>
<!ELEMENT approvedBy (#PCDATA)>
<!ELEMENT fileName (#PCDATA)>
<!ELEMENT location (#PCDATA)>
<!ELEMENT fileType (#PCDATA)>
<!ELEMENT size (#PCDATA)>

Resumindo, defina cada elemento e seu modelo de "conteúdo". Por exemplo, o elemento workflow deve ter um elemento statistics filho e zero ou mais filhos fileInfo .

Defina também os atributos e seus tipos. Por exemplo, o elemento statistics tem dois atributos opcionais, total e approved, e eles são dados de caractere.

A chave aqui é a definição do valor do elemento fileInfo , id , que foi definido como o tipo ID.

Agora, estas informações podem ser usadas.

Aprovando o Arquivo: Atualizando o XML

A página de formulário real que aceita as caixas de opções de aprovação, approve_action.php, é muito simples:

<?php
include "../scripts.txt";

  $allApprovals = $_POST["toapprove"];
  foreach ($allApprovals as $thisFileId) {
     approveFile($thisFileId);
  }
  echo "Files approved.";

?>

Para cada caixa de opção toapprove , apenas chame a função approveFile() , em scripts.txt:

function approveFile($fileId){

   $xmlfile = UPLOADEDFILES."docinfo.xml";

   $doc = new DOMDocument();
   $doc->validateOnParse = true;
   $doc->load($xmlfile);

   $statisticsElements = $doc->getElementsByTagName("statistics");
   $statistics = $statisticsElements->item(0);

   $approved = $statistics->getAttribute("approved");
   $statistics->setAttribute("approved", $approved+1);

   $thisFile = $doc->getElementById($fileId);
   $thisFile->setAttribute("status", "approved");

   $approvedByElements = $thisFile->getElementsByTagName("approvedBy");
   $approvedByElement = $approvedByElements->item(0);
   $approvedByElement->appendChild($doc->createTextNode($_SESSION["username"]));

   $doc->save($xmlfile);

}

Antes mesmo de carregar o documento, especifique se deseja que o analisador valide-o ou verifique-o na DTD. Isto configura a natureza do atributo do id . Depois de carregar o arquivo, você obtém uma referência ao elemento statistics , portanto, é possível incrementar o número de arquivos aprovados.

Agora você realmente está pronto para aprovar o arquivo. Como o atributo do id foi configurado como um valor de tipo ID, é possível usar getElementById() para solicitar o elemento fileInfo apropriado. Quando tiver esse elemento, você poderá configurar seu status como aprovado.

Também é necessário obter uma referência ao filho approvedBy deste elemento. Quando tiver essa referência, será possível incluir um novo filho do nó Text com o nome de usuário do administrador.

Por último, salve o arquivo.

Observe que, embora você fez tudo dessa maneira para simplificar, em um aplicativo de produção, é mais eficiente abrir e carregar o arquivo apenas uma vez, fazer todas as mudanças, em seguida, salvar o arquivo.

Verificações de Segurança no Download

Como última etapa, inclua uma verificação de segurança no processo de download. Como este processo é totalmente controlado através do aplicativo, é possível usar a verificação que você desejar. Para este exemplo, você verificará para assegurar que o usuário tenha clicado no link para um arquivo em uma página que esteja em seu servidor local, evitando que alguém vincule-se a ela a partir de um site externo, ou até mesmo que indique o link ou envie para outra pessoa um link bruto.

Comece criando uma nova exceção, apenas para esta ocasião, no arquivo WFDocument.php:

<?php
include_once("../scripts.txt");

class NoFileExistsException extends Exception {

   public function informativeMessage(){
      $message = "The file, '".$this->getMessage()."', called on line ".
           $this->getLine()." of ".$this->getFile().", does not exist.";
      return $message;
   }

}

class ImproperRequestException extends Exception {

   public function logDownloadAttempt(){
      //Additional code here
      echo "Notifying administrator ...";
   }

}

class WFDocument {

   private $filename;
   private $filetype;

   function setFilename($newFilename){
      $this->filename = $newFilename;
   }
   function getFilename(){
      return $this->filename;
   }

   function setFiletype($newFiletype){
      $this->filetype = $newFiletype;
   }
   function getFiletype(){
      return $this->filetype;
   }

   function __construct($filename = "", $filetype = ""){
      $this->setFilename($filename);
      $this->setFiletype($filetype);
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

try {
           $referer = $_SERVER['HTTP_REFERER'];
         $noprotocol = substr($referer, 7, strlen($referer));
         $host = substr($noprotocol, 0, strpos($noprotocol, "/"));
         if ( $host != 'boxersrevenge' &&
                                $host != 'localhost'){
            throw new ImproperRequestException("Remote access not allowed.
                        Files must be accessed from the intranet.");
         }

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } else {
              throw new Exception ("Cannot open file ".$filepath);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }
      } catch (ImproperRequestException $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";
         $e->logDownloadAttempt();

      } catch (Exception $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";

      }
   }
}

?>

Primeiramente, no ImproperRequestException, crie um novo método, logDownloadAttempt(), que pode enviar um e-mail ou executar alguma outra ação. Use esse método no bloco catch deste tipo de exceção.

Na função download() real, a primeira coisa a fazer é obter HTTP_REFERER. Este cabeçalho opcional é enviado com um pedido da Web que identifica a página a partir da qual o pedido foi feito. Por exemplo, se você vincular-se a developerWorks a partir de seu blog, e clicar nesse link, os logs da IBM devem mostrar a URL de seu blog como o HTTP_REFERER para esse acesso.

Neste caso, você deseja assegurar que o pedido esteja vindo de seu aplicativo, portanto, remova primeiro a cadeia "http://" no início, em seguida, salve todo o texto até a primeira barra (/). Este é o nome do host no pedido.

Para um pedido externo, este nome do host pode ser algo nas linhas de boxersrevenge.nicholaschase.com, mas você está procurando apenas pedidos internos, portanto, aceite boxersrevenge ou localhost. Se o pedido vier de algum outro lugar, lance a ImproperRequestException, que é capturada pelo bloco apropriado.

Observe que este método não é garantido em relação à segurança. Alguns navegadores não enviam informações do referente corretamente, porque eles não as suportam ou o usuário alterou o que está sendo enviado. Mas este exemplo deve dar uma ideia dos tipos de coisas que podem ser feitas para ajudar a controlar seu conteúdo.


Resumo

Este tutorial envolveu a série de três partes sobre "Aprendendo PHP" enquanto você construiu um aplicativo de fluxo de trabalho simples. As partes anteriores focalizaram as informações básicas, como sintaxe, manipulação de formulários, acesso ao banco de dados, upload de arquivos e XML. Nesta parte, você avançou mais uma etapa e juntou tudo, criando um formulário através do qual um administrador pode aprovar vários arquivos. Discutimos os seguintes tópicos:

  • Usando a Autenticação HTTP
  • Transmitindo Arquivos
  • Criando Classes e Objetos
  • Propriedades e Métodos de Objetos
  • Usando Construtores de Objetos
  • Usando Exceções
  • Criando Exceções Customizadas
  • Usando Atributos do ID XML
  • Executando Verificações de Segurança Adicionais para Downloads

Recursos

Aprender

Obter produtos e tecnologias

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Software livre
ArticleID=392630
ArticleTitle=Aprendendo PHP, Parte 3: Autenticação, Objetos, Exceções e Fluxo
publish-date=07122005