Fork me on GitHub
Você está aqui: Home ‣ Dive Into HTML5 ‣
№7.
O PASSADO, PRESENTE & FUTURO
DE ARMAZENAMENTO LOCAL PARA
APLICAÇÕES WEB
exibir índice analítico
❧
MERGULHANDO
Armazenamento local persistente é uma das áreas onde aplicações nativas de cliente têm mantido uma
vantagem sobre aplicações web. Para aplicações nativas, o sistema operacional tipicamente provê uma camada
de abstração para armazenar e recuperar dados específicos do aplicativo como preferências ou estado de
tempo de execução. Estes valores podem ser armazenados no registro, arquivos INI, arquivos XML ou algum
outro lugar de acordo com a convenção da plataforma. Se o seu aplicativo cliente nativo necessita de armazenamento local
diferente dos pares chave/valor, você pode inserir o seu próprio banco de dados, inventar o seu próprio formato de arquivo
ou qualquer número de outras soluções.
Historicamente, aplicações web não tiveram nenhum desses luxos. Cookies foram inventados no início da história da web e,
de fato, eles podem ser usados para armazenamento local persistente de pequenas quantidades de dados. Mas eles têm três
desvantagens potencialmente [potentially dealbreaking downsides]:
Cookies são incluídos em toda requisição HTTP, atrasando assim sua aplicação web por desnecessariamente sempre
transmitir os mesmos dados;
Cookies são incluídos em toda requisição HTTP, enviando assim dados não criptografados através da internet (a menos
que sua aplicação web inteira seja servida sobre SSL);
Cookies são limitados a certa de 4KB de dados — suficiente para atrasar sua aplicação (ver acima), mas não o
suficiente para ser muito útil;
O que nós realmente queremos é
bastante espaço de armazenamento
no cliente
que persista depois da atualização da página
e não seja transmitido para o servidor
Antes da HTML5, todas as tentativas de obtenção foram fundamentalmente insatisfatórias de diferentes maneiras.
❧
UMA BREVE HISTÓRIA DOS HACKS DE
ARMAZENAMENTO LOCAIS ANTES HTML5
No início, existia apenas o Internet Explorer. Ou ao menos, era isso que a Microsoft gostaria que o mundo acreditasse. Para
esse fim, como parte da Primeira Grande Guerra de Browsers, a Microsoft inventou uma grande quantidade de coisas e as
incluiu no seu "vencedor de todas as batalhas", o Internet Explorer. Uma dessas coisas foi conhecida como DHTML
Comportamentos, e um desses comportamentos foi conhecido como userData.
userData possibilita que páginas web armazenem até 64KB de dados por domínio, em uma estrutura hierárquica baseada em
XML. (Domínios confiáveis, tais como sites de intranet, podem armazenar 10 vezes esta quantidade. E, 640 KB deveria ser o
suficiente para qualquer um.) O IE não apresenta qualquer forma de diálogo de permissões e não há previsão para o
aumento da quantidade de armazenamento disponível.
Em 2002, a Adobe introduziu uma funcionalidade no Flash 6 que ganhou o infeliz e enganoso nome de “Flash cookies.”
Dentro do ambiente do Flash, a funcionalidade é propriamente conhecida como Local Shared Objects - Objetos de local
compartilhado. Resumidamente, isso permitia que objetos Flash armazenassem até 100KB de dados por domínio. Brad
Neuberg desenvolveu um pré protótipo de uma ponte Flash-to-JavaScript chamada AMASS (AJAX Massive Storage System),
porém ficou limitado por algumas peculiaridades de design do Flash. Em 2006, com o advento da ExternalInterface presente
no Flash 8, acessar LSOs a partir do JavaScript tornou-se uma coisa de magnitude mais fácil e rápida. Brad reescreveu o
AMASS e o integrou dentro do popular Dojo Toolkit com o nome de [Link]. Flash garante a cada domínio 100 KB
de armazenamento “livre.” Além disso, ele solicita ao usuário para cada ordem de magnitude um aumento no armazenamento
de dados (1 Mb, 10 Mb, e assim por diante).
Em 2007, o Google lançou o Gears, um plugin de código aberto com o objetivo de providenciar capacidades adicionais nos
navegadores. (Nós já tínhamos discutido sobre Gears no contexto de providenciar uma API de geolocalização para o
Internet Explorer.) Gears possibilita uma API para uma base de dados SQL icorporada baseada em SQLite. Após obter a
permissão de um usuário uma vez, Gears pode armazenar ilimitada quantidade de dados por um domínio em uma tabela de
banco de dados SQL.
Entretanto, Brad Neuberg e outros continuaram a trabalhar em cima do [Link] para providenciar uma interface
unificada para todos esses diferentes plugins e APIs. Em 2009, [Link] podia auto-detectar (e providenciar uma
interface unificada em cima de) Adobe Flash, Gears, Adobe AIR, e um pré protótipo da HTML5 storage que era apenas
implementado em antigas versões do Firefox.
Quando você analisa essas soluções, um padrão surge: todos eles são específicos de um único navegador, ou dependentes de
plugins de terceiros. Apesar do esforço heróico para deixar clara as diferenças (no [Link]), eles todos apresentam
interfaces totalmente diferentes, têm diferentes limitações de armazenamento, e apresentam diferentes experiências de
usabilidade. Portanto, esse é o problema que a HTML5 se propõe a resolver: providenciar uma padronizada API,
implementada de forma nativa e consistente em vários navegadores, sem a necessidade e dependência de plugins de
terceiros.
❧
INTRODUÇÃO AO HTML5 STORAGE
O que me refiro como “HTML5 Storage” é a especificação nomeada de Web Storage, que estava na especificação da HTML5
por um tempo, mas era tratado separadamente por desinteresse e razões políticas. Certos navegadores se referenciavam
como “Local Storage” ou “DOM Storage.” A nomeação ficava ainda mais complicada em navegadores relacionados com
padrões emergentes, que vou discutir mais adiante neste capítulo.
Então o que é HTML5 Storage? Simplesmente armazenar, é a forma de páginas da web amazenarem pares de chave/valor
localmente, dentro do navegador do cliente. Semelhante aos cookies, esses dados persistem mesmo depois de você sair do
site, fechar a guia ou o navegador. Diferente dos cookies, esses dados nunca são transmitidos ao servidor (a menos que você
queria enviá-los manualmente). Diferente de todas as tentativas anteriores de fornecimento de armazenamento local
persistente, este é implementado de forma nativa nos navegadores, para que esteja disponível mesmo quando os plugins de
terceiros não estiverem.
Quais navegadores? Bom, as últimas versões de quase todos os navegadores suportam HTML5 Storage… até o Internet
Explorer!
SUPORTE AO HTML5 STORAGE
IE FIREFOX SAFARI CHROME OPERA IPHONE ANDROID
8.0+ 3.5+ 4.0+ 4.0+ 10.5+ 2.0+ 2.0+
Direto do seu código JavaScript, você acessa o HTML5 Storage através do objeto localStorage no objeto global window.
Antes de usá-lo, você deve detectar se o navegador tem suporte.
↶ Verificando suporte HTML5 Storage
function supports_html5_storage() {
try {
return 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
}
Ao invés de escrever essa função, você pode usar Modernizr para detectar o suporte da HTML5 Storage.
if ([Link]) {
// [Link] é suportado pelo seu navegador!
} else {
// não há suporte nativo ao HTML5 storage no seu navegador! :(
// tente talvez [Link] ou alguma solução de terceiros
}
❧
USANDO HTML5 STORAGE
HTML5 Storage é baseado em pares de chave/valor nomeadas. Você pode armazenar dados em uma chave nomeada, e com
essa mesma chave, recuperar os dados armazenados. A chave nomeada é uma string. E seu valor, é qualquer um suportado
pelo JavaScript, incluindo Strings, Booleanos, Inteiros, ou Flutuantes. Conteúdo, os dados são armazenados como uma
sequência de strings. Se você estiver armazenando e recuperando qualquer outro tipo de dado diferente de Strings, você
precisa usar funções como parseInt() ou parseFloat() para recuperar os dados em um tipo esperado pelo JavaScript.
interface Storage {
getter any getItem(in DOMString key);
setter creator void setItem(in DOMString key, in any data);
};
Chamando setItem() com o nome de uma chave que já existe, ela sobrescreve o valor anterior. Chamando getItem() com
o nome de uma chave que não existe, retornará null ao invés de de uma exceção.
Como outros objetos JavaScript, você pode tratar o objeto localStorage como um array associativo. Ao invés de usar os
métodos getItem() e setItem(), você pode usar simplesmentes colchetes. Por exemplo, no trecho seguinte:
var foo = [Link]("bar");
// ...
[Link]("bar", foo);
…pode ser reescrito para usar a sintaxe de colchetes ao invés:
var foo = localStorage["bar"];
// ...
localStorage["bar"] = foo;
Existem métodos para remover um valor já dado a uma chave, e limpar o armazenamento local inteiro (isto é, apagar todas
as chaves/valor existentes de uma só vez).
interface Storage {
deleter void removeItem(in DOMString key);
void clear();
};
Chamando removeItem() com uma chave inexistente, não retornará nada.
Finalmente, há uma propriedade para obter o número total de chaves/valor na área de armazenamento local, e também, para
percorrer todas as chaves pelo seu índice (e retornar o nome de cada uma).
interface Storage {
readonly attribute unsigned long length;
getter DOMString key(in unsigned long index);
};
Se você chamar key() com um índice que não está entre 0–(length-1), a função retornará null.
CONTROLANDO ALTERAÇÕES NA ÁREA DO HTML5 STORAGE
Se quiser manter o controle das constantes mudanças no armazenamento local, você pode capturar o evento storage. O
evento storage é ativado no objeto window sempre que setItem(), removeItem(), ou clear() é chamado e, geralmente,
muda alguma coisa. Por exemplo, se você definir um valor a uma chave existente ou chamar clear() em chaves não
nomeadas, o evento storage não será chamado, porque realmente, nada mudou na área de armazenamento local.
O evento storage é suportado em todos os lugares em que o objeto localStorage funciona, incluindo Internet Explorer 8.
IE 8 não suporta o padrão W3C addEventListener (será adicionado, finalmente, no IE 9). Portanto, para pegar o evento
storage , você vai precisar checar qual mecanismo de evento o navegador suporta. (Se você já fez isso antes, para outros
eventos, pode pular essa seção. Pegando o evento storage , funciona da mesma forma como todos os outros eventos, você
estará sempre preso. Se você preferir usar jQuery ou qualquer outra biblioteca JavaScript para registrar os eventos de
manipulação, você pode fazer isso com o evento storage , também.)
if ([Link]) {
[Link]("storage", handle_storage, false);
} else {
[Link]("onstorage", handle_storage);
};
A função de callback handle_storage é chamada junto ao objeto StorageEvent, exceto no Internet Explorer que os objetos
de evento são armazenados no [Link].
function handle_storage(e) {
if (!e) { e = [Link]; }
}
Nesse ponto, a variável e será o objeto StorageEvent, que tem as seguintes propriedades:
OBJETO STORAGEEVENT
PROPRIEDADE TIPO DESCRIÇÃO
key string o nome da chave que foi adicionada, removida ou alterada
oldValue qualquer o valor antigo (agora atualizado), ou null se for um novo item adicionado
newValue qualquer o novo valor, ou null se um item foi removido
url* string a página que chamou o método que realizou a mudança
* Nota: a propriedadeurl é originalmente chamada de uri. Alguns navegadores lançaram essa propriedade antes das mudanças na especificação. Para uma
compatibilidade máxima, você deve checar se a propriedade url existe, se não, checar se a propriedade uri existe.
O evento storage não é cancelável. Dentro da função callback handle_storage, não há maneira de parar essa mundaça. É
uma simples forma do navegador dizer, “ei, isso já aconteceu. Não há nada que você possa fazer, eu só queria te avisar
sobre isso.”
LIMITAÇÕES NOS NAVEGADORES ATUAIS
Ao falar sobre a história dos hacks no armazenamento local usando plugins de terceiros, fiz questão de mencionar a
limitação de cada técnica, tal como limite de armazenamento. Apenas percebi que não tinha mencionado nada sobre as
limitações do agora padronizado armazenamento da HTML5. Vou dar a vocês as respostas primeiro, em seguida explicarei
elas. As respostas, em ordem de importância, são “5 megabytes,” “QUOTA_EXCEEDED_ERR,” e “Não.”
“5 megabytes” é a quantidade em espaço que cada origem tem por padrão. Isto é surpreendentemente consistente em todos
os navegadores, embora seja formulada como não mais do que uma sugestão na especificação do Armazenamento local da
HTML5. Um aspecto a ter em mente é que você está armazenando strings, não as informações no seu formato original. Se
você está armazenando muitos inteiros ou floats, a diferença de representação pode realmente aumentar. Cada dígito
naquele float está sendo armazenado como um caractere, não na representação usual de um ponto de número flutuante.
“QUOTA_EXCEEDED_ERR” é a exceção que será lançada se você exceder sua cota de armazenamento de 5 megabytes. “Não” é a
resposta para a próxima questão óbvia, “Eu posso pedir permissão do usuário para usar mais espaço de armazenamento?” No
momento em que escrevo (fevereiro 2011), não há navegador que suporte qualquer mecanismo para desenvolvedores web
requisitarem mais espaço de armazenamento. Alguns navegadores (como Opera) permitem que o usuário controle a cota de
armazenamento de cada site, mas isto é uma ação puramente iniciada pelo usuário, não algo que você como desenvolvedor
pode construir na sua aplicação web.
❧
ARMAZENAMENTO HTML5 EM AÇÃO
Vamos ver o Armazenamento da HTML5 em ação. Recorde o jogo Halma que nós construímos no capítulo do canvas. Existe
um pequeno problema com o jogo: se você fechar o navegador no meio do jogo, você perderá seu progresso. Porém, com o
armazenamento da HTML5, podemos salvar o progresso localmente, dentro do próprio navegador. Aqui está uma
demonstração no ar. Faça alguns movimentos, depois feche a aba do navegador, em seguida reabra ela. Se o seu navegador
suporta Armazenamendo da HTML5, a página de demonstração deve magicamente recordar a sua exata posição dentro do
jogo, incluindo o número de movimentos que você fez, a posição de cada uma das peças do tabuleiro, e até mesmo se uma
determinada peça está selecionada.
Como isto funciona? Cada momento que uma mudança acontece dentro do jogo, chamamos esta função:
function saveGameState() {
if (!supportsLocalStorage()) { return false; }
localStorage["[Link]"] = gGameInProgress;
for (var i = 0; i < kNumPieces; i++) {
localStorage["[Link]." + i + ".row"] = gPieces[i].row;
localStorage["[Link]." + i + ".column"] = gPieces[i].column;
}
localStorage["[Link]"] = gSelectedPieceIndex;
localStorage["[Link]"] = gSelectedPieceHasMoved;
localStorage["[Link]"] = gMoveCount;
return true;
}
Como você pode ver, ela usa o objeto localStorage para salvar se há um jogo em progresso (gGameInProgress, um
booleano). Se assim for, percorre as peças (gPieces, um Array JavaScript) e salva o número de linha e coluna de cada uma.
Em seguida ele salva alguns estados adicionais do jogo, incluindo a peça que está selecionada (gSelectedPieceIndex, um
inteiro), se a peça está no meio de uma série potencialmente longa de saltos (gSelectedPieceHasMoved, um booleano), e o
número total de movimentos feito até então (gMoveCount, um inteiro).
No carregamento da página, em vez de chamar automaticamente uma função newGame() que resetaria estas variáveis para
valores "hard-coded", chamamos uma função resumeGame(). Usando Armazenamento HTML5, a função resumeGame() checa
se um estado relacionado ao jogo em progresso está armazenado localmente. Se está, ele recupera esses valores usando o
objeto do localStorage.
function resumeGame() {
if (!supportsLocalStorage()) { return false; }
gGameInProgress = (localStorage["[Link]"] == "true");
if (!gGameInProgress) { return false; }
gPieces = new Array(kNumPieces);
for (var i = 0; i < kNumPieces; i++) {
var row = parseInt(localStorage["[Link]." + i + ".row"]);
var column = parseInt(localStorage["[Link]." + i + ".column"]);
gPieces[i] = new Cell(row, column);
}
gNumPieces = kNumPieces;
gSelectedPieceIndex = parseInt(localStorage["[Link]"]);
gSelectedPieceHasMoved = localStorage["[Link]"] == "true";
gMoveCount = parseInt(localStorage["[Link]"]);
drawBoard();
return true;
}
A parte mais importante desta função é a ressalva que mencionei no início deste capítulo, que vou repetir aqui: Informações
são armazenadas como string. Se você está armazenando algo diferente de string, você vai precisar forçar uma conversão quando
você recuperá-la. Por exemplo, a bandeira para verificar se existe um jogo em progresso (gGameInProgress) é um booleano.
Na função saveGameState(), nós apenas armazenamos e não nos preocupamos em relação ao datatype:
localStorage["[Link]"] = gGameInProgress;
Pórem, na função resumeGame(), precisamos tratar o valor que pegamos da área de armazenamento local como uma string e
construir manualmente o valor booleano apropriado:
gGameInProgress = (localStorage["[Link]"] == "true");
Semelhantemente, o número de movimentos está armazenado no gMoveCount como um inteiro. Na função saveGameState(),
apenas armazenamos:
localStorage["[Link]"] = gMoveCount;
Mas na função resumeGame(), precisamos forçar o valor para um inteiro, usando a função nativa do JavaScript parseInt():
gMoveCount = parseInt(localStorage["[Link]"]);
❧
ALÉM DE PARES DE CHAVES-VALORES
NOMEADOS: VISÕES CONFLITANTES
Enquanto o passado é cheio de truques e soluções alternativas, a condição presente do Armazenamendo da HTML5 é
surpreendentemente otimista. Uma nova API vem sendo padronizada e implementada em todos os principais navegadores,
plataformas e dispositivos. Como um desenvolvedor web, isto não é algo que você vê todo dia, não é verdade? Porém, há
mais vida além dos “5 megabytes de pares de chaves/valores nomeados” e o futuro do armazenamento local persistente é…
como poderei colocá-lo… bom, existem visões conflitantes.
Uma visão é uma sigla que você provavelmente já sabe: SQL. Em 2007, a Google lançou Gears, um plugin de código aberto
que incluía um banco de dados incorporado com base de dados no SQLite. Esse protótipo inicial influenciou a criação da
especificação do Web SQL Database. Web SQL Database (formalmente conhecida como “WebDB”) fornece uma fina camada
em torno de uma base de dados SQL, permitindo você fazer coisas como essa pelo JavaScript:
↶código atual funcionando em 4 navegadores
openDatabase('documents', '1.0', 'Local document storage', 5*1024*1024, function (db) {
[Link]('', '1.0', function (t) {
[Link]('CREATE TABLE docids (id, name)');
}, error);
});
Você pode ver, maior parte da ação reside na sequência que você passa para o método executeSql. Esta string pode ser
qualquer comando suportado pelo SQL, incluindo SELECT, UPDATE, INSERT and DELETE. É como programação de base de
dados backend, exceto o fato de você está fazendo isto via JavaScript! Oh alegria!
A especificação da Base de Dados SQL vem sendo implementada por quatro navegadores e plataformas.
SUPORTE A BASE DE DADOS SQL WEB
IE FIREFOX SAFARI CHROME OPERA IPHONE ANDROID
· · 4.0+ 4.0+ 10.5+ 3.0+ 2.0+
Claro, se você já usou mais de um produto de banco de dados em sua vida, você está ciente que “SQL” é mais um termo de
marketing do que um rígido e rápido padrão. (Alguns podem dizer o mesmo da “HTML5,” mas não se importe.) Claro, existe
uma especificação atual do SQL (ela é chamada de SQL-92), mas não há nenhuma base de dados do servidor no mundo que
está em conformidade com esta e apenas esta especificação. Existe o SQL da Oracle, SQL da Microsoft, SQL do MySQL, SQL
do PostgreSQL e SQL do SQLite. De fato, cada um desses produtos adiciona novas funcionalidades ao SQL no passar do
tempo, então mesmo dizendo “SQL do SQLite” não é suficiente para determinar exatamente o que você está falando. Você
precisa dizer a versão do SQL que acompanha o SQLite versão X.Y.Z.”
Tudo o que nos leva ao seguinte aviso, atualmente residindo no topo da especificação da Base de Dados SQL Web:
Esta especificação chegou a um impasse: todos os implementadores interessados têm utilizado o mesmo backend
do SQL (Sqlite), mas precisamos de múltiplas implementações independentes para prosseguir ao longo de um
caminho de normalização. Até outro implementador estar interessado em implementar esta especificação, a
descrição do dialeto SQL foi deixada como simplesmente uma referência para Sqlite, o que não é aceitável para
um padrão.
Isto é contra esse cenário que vou apresentá-lo para uma outra visão concorrente avançada, persistente, armazenamento
local para aplicações web: A API de "Indexed Database", formalmente conhecida como “WebSimpleDB,” agora
carinhosamente conhecida como “IndexedDB.”
A API do “Indexed Database” expõe o que é chamado um objeto de armazenamento. Um objeto de armazenamento
compartilha vários conceitos com o a base de dados SQL. Existem “base de dados” com “registros,” e cada registro tem um
número definido de “campos.” Cada campo tem um “datatype” específico, que é definido quando a base de dados é criada.
Você pode selecionar um subconjunto de registros, depois enumerá-los com um “índice.” Alterações no armazenamento de
objetos são tratados dentro de “transações”.
Se você já fez alguma programação de banco de dados SQL, esses termos provavelmente serão familiares. A principal
diferença é que o objeto de armazenamento não tem linguagem de consulta estruturada. Você não constrói uma declaração
como "SELECT * from USERS where ACTIVE = 'Y'". Em vez disso, você usa métodos fornecidos pelo objeto de
armazenamento para adicionar um índice na base de dados nomeada “USERS,” enumerar os registros, filtrar os registros de
usuários inativos, e usar métodos de acesso para obter os valores de cada campo nos registros restantes. Uma visão geral do
IndexedDB é um bom tutorial de como IndexedDB funciona, fazendo comparações lado a lado entre o IndexedDB e base
dados SQL.
No momento que escrevo, IndexedDB está implementado apenas na versão beta do Firefox 4. (Em contraste, a Mozilla
afirmou que eles nunca irão implementar Base de Dados SQL Web.) Google afirmou que estão considerando o suporte ao
IndexedDB para o Chromium e o Google Chrome. E até mesmo a Microsoft disse que o IndexedDB “é uma ótima solução
para a web.”
Então o que você, como desenvolvedor web, pode fazer com IndexedDB? Neste momento, praticamente nada além de
algumas demonstrações técnicas. Um ano a frente? Talvez algo mais. Para iniciar, confira os links na seção “Leitura
complementar” com alguns bons tutoriais.
❧
LEITURA COMPLEMENTAR
HTML5 storage:
Especificação de Armazenamento HTML5
Introdução ao DOM Storage no MSDN
Web Storage: armazenamento de dados no lado do cliente mais fácil e mais poderoso powerful em Opera Developer
Community
DOM Storage no Mozilla Developer Center. (Nota: a maior parte dessa página é devota ao protótipo da implementação
do Firefox’ do objeto globalStorage, um não-padrão precursor do localStorage. Mozilla adicionou suporte ao padrão
localStorage no Firefox 3.5.)
Desabilite local storage para aplicações web mobile com HTML5, um tutorial na IBM DeveloperWorks
Trabalho prévio de Brad Neuberg et. al. (pré-HTML5):
Internet Explorer possui suporte nativo para persistência?!?! (sobre o objeto userData do IE)
Dojo Storage, parte de um tutorial maior sobre a (agora não continuada) biblioteca Dojo Offline
[Link] referência da API
[Link] repositório Subversion
Web SQL Database:
Especificação Web SQL Database
Introdução à Web SQL Databases
Demonstração de Web Database
[Link], um “ORM assíncrono em JavaScript” construído sob Web SQL Database e Gears
IndexedDB:
Especificação do Indexed Database API
Além de HTML5: Database APIs e o caminho para IndexedDB
Firefox 4: Uma prévia caminhada por IndexedDB
❧
Isso foi “O Passado, Presente & Futuro de Armazenamento Local para Aplicações Web.” Consulte o Sumário, caso queira
continuar com a leitura.
VOCÊ SABIA?
Em associação a Google Press, O’Reilly está distribuindo este livro em variados
formatos, incluindo papel, ePub, Mobi, DRM-free e PDF. A edição paga é chamada
“HTML5: Up & Running” e está disponível agora. Este capítulo está incluído na versão
paga.
Se você gostou deste capítulo e quer mostrar sua apreciação, basta comprar o livro
“HTML5: Up & Running” com esse link afiliado ou comprar a edição eletrônica
diretamente da O’Reilly. Você vai ganhar um livro, e eu vou ganhar um trocado.
Atualmente, não aceito doações diretas.
Copyright MMIX–MMXI Mark Pilgrim
powered by Google™ Search