Juntando Tudo
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.
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
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 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.
