Es triste ver que un navegador tan bueno (por la forma en la que interpreta los javascript, css, etc.) me este presentando muchas fallas últimamente. Me refiero a Firefox, el cual actualicé a la versión 8 la semana pasada y todo parecía ir de maravilla hasta hace 2 días. Al parecer Firefox arrastra una serie de fallas desde versiones anteriores (3.6) que no han podido corregir al 100%. Hablo/escribo acerca de la falla en que el navegador deja de mostrar las páginas y sólo se ve el estado de carga en "Conectando", posteriormente el usuario cierra el navegador pero al tratar de reiniciarlo aparece un cuadro de dialogo indicando que no es posible porque aún no se cierra el anterior proceso. Sólo cerrando el navegador desde al administrador de tareas es posible terminar el proceso (además de tener que cerrar los múltiples "plugin container.exe" que se iniciaron.
Todo pareciera ir bien al tratar de navegar pero luego de un rato vuelven los síntomas y hay que reiniciar el navegador nuevamente. Muchos recomiendan crear un nuevo perfil de Firefox pero no ha resultado aún. Lo único que note es que también actualicé el iTunes a la versión 10 (coincide con el inicio de los síntomas) y al querer desinstalarlo mientras navegaba con Firefox no pude porque requería que el navegador estuviera cerrado (WTF?!).
Todo lo anterior me orilló a instalar el Google Chrome (aún no me acostumbro) y la primera impresión iba bien hasta que decidí visitar un sitio que creé y ¡Sorpresa! Un poco de javascript (a través de jQuery) que escribí no funcionaba, específicamente la función click de un input de tipo file. El evento si es procesado por Chrome pero no abre el cuadro de dialogo para selección de archivo sí el input tiene un estilo display:none; Anteriormente tuve que adaptar el script porque Firefox si abre el dialogo de selección de archivo aunque el input=file tenga un estilo display:none pero IE no. Así que tanto en IE como en Chrome el tipo de "display" no puede ser "none" (algo que no me gusta pero tendré que lidiar con ello hasta la próxima actualización de Firefox) en los input:file en los que queremos ejecutar el evento click. Pero cuidado, el Chrome si lanza el evento y ejecuta el código que escribas en él, más nunca abrirá el dialogo de selección de archivos.
Para finalizar sólo quiero aclarar que la consola, y herramientas para desarrolladores, del Chrome es más completa que la de Firefox pero me gusta la simpleza del último.
jueves, 1 de diciembre de 2011
lunes, 28 de noviembre de 2011
Lua tags en HTML
Bueno, este es un pequeño script medio extraño (no pregunten como funciona) que permitirá ejecutar código lua (con sus respectivos tags <%lua //codigo %> al estilo php) dentro de los html. Ello significa que no requieren de un servidor con soporte php para crear páginas dinámicas que su propio servidor programado en lua puede generar.
Un ejemplo del código es el siguiente:
<%lua usuario='pepe' %>
Hola <%lua echo(usuario) %>
<%lua for x=1, 10 do
echo("lo:"..x.."\n")
end %>
<%lua for x=1, 10 do %>
this is loop: <%lua echo(x); %>;
<%lua end %>
Como verán este código HTML con lua tags usa tanto los tags normales como los que el php nos permite usar en loops y declaraciones condicionales combinando todo.
Un ejemplo de cómo utilizar este script con el anterior código es este:
local s = [[<%lua usuario='pepe' %>
Hola <%lua echo(usuario) %>
como estas
<%lua for x=1, 10 do
echo("lo:"..x.."
")
end %>
<%lua for x=1, 10 do %>
this is loop: <%lua echo(x); %>
<%lua end %>]];
print(ExecuteLuaTags(s,"html.html"));
- El código lo he puesto en una variable de tipo string llamada s
- Enviamos dicha variable a la función ExecuteLuaTags junto con una ruta al archivo final que se generará
- El resultado lo imprimimos y como verán es sólo un valor true en caso satisfactorio o nil + errormsg en caso contrario
- Utilice pcall para evitar broncas con el código mal hecho
Aca una imagen del resultado visto en el navegador web (html resultante) del anterior script:
Y sin mayor preámbulo aca les dejo el link al script en pastebin: http://pastebin.com/VGyjSBV4
Un ejemplo del código es el siguiente:
<%lua usuario='pepe' %>
Hola <%lua echo(usuario) %>
<%lua for x=1, 10 do
echo("lo:"..x.."\n")
end %>
<%lua for x=1, 10 do %>
this is loop: <%lua echo(x); %>;
<%lua end %>
Como verán este código HTML con lua tags usa tanto los tags normales como los que el php nos permite usar en loops y declaraciones condicionales combinando todo.
Un ejemplo de cómo utilizar este script con el anterior código es este:
local s = [[<%lua usuario='pepe' %>
Hola <%lua echo(usuario) %>
como estas
<%lua for x=1, 10 do
echo("lo:"..x.."
")
end %>
<%lua for x=1, 10 do %>
this is loop: <%lua echo(x); %>
<%lua end %>]];
print(ExecuteLuaTags(s,"html.html"));
- El código lo he puesto en una variable de tipo string llamada s
- Enviamos dicha variable a la función ExecuteLuaTags junto con una ruta al archivo final que se generará
- El resultado lo imprimimos y como verán es sólo un valor true en caso satisfactorio o nil + errormsg en caso contrario
- Utilice pcall para evitar broncas con el código mal hecho
Aca una imagen del resultado visto en el navegador web (html resultante) del anterior script:
Y sin mayor preámbulo aca les dejo el link al script en pastebin: http://pastebin.com/VGyjSBV4
jueves, 17 de noviembre de 2011
Redimensionar manteniendo proporción
Este tema es básico en extremo pero he decidido publicarlo porque he notado que siguen existiendo errores al realizar esta operación de redimensión.
Es común ver bastantes fórmulas para redimensionar imágenes (por ejemplo cuando se quiere crear una imagen miniatura/thumbnail), o cualquier otro plano 2d, pero muchas de ellas no calculan la redimensión para mantener la proporción. He visto que los autores de otras fórmulas que sí mantienen la proporción son un tanto complejas y no muy claras (con código innecesario) e incluso llegan a fallar o recortar la imagen/plano resultando en algo no deseado. Así que les dejo la fórmula en Lua (pero fácilmente puede ser traducida a otros lenguajes como php, etc) para obtener los el nuevo tamaño de imagen/plano manteniendo el aspecto:
Como podrán ver la fórmula es simple porque primero debemos determinar cual de las 2 dimensiones (altura ó ancho) del plano es la mayor y de ahi crear un divisor de proporción.
Posteriormente dicho divisor de proporción es utilizado para obtener el nuevo alto y ancho del plano, apoyándonos de un redondeo al entero próximo mayor del resultado.
Con ésta fórmula no tendrán inconveniente alguno que incluso funcionará con dimensiones menores a las del thumbnail/miniatura creciendo el plano. Pero si quieren evitar que en dicho caso se agrande el plano entonces sólo deben editar la formula/código anterior quedando de la siguiente forma:
Es común ver bastantes fórmulas para redimensionar imágenes (por ejemplo cuando se quiere crear una imagen miniatura/thumbnail), o cualquier otro plano 2d, pero muchas de ellas no calculan la redimensión para mantener la proporción. He visto que los autores de otras fórmulas que sí mantienen la proporción son un tanto complejas y no muy claras (con código innecesario) e incluso llegan a fallar o recortar la imagen/plano resultando en algo no deseado. Así que les dejo la fórmula en Lua (pero fácilmente puede ser traducida a otros lenguajes como php, etc) para obtener los el nuevo tamaño de imagen/plano manteniendo el aspecto:
function resize(w,h,t_w,t_h) local divisor_proporcion = (w>h) and w/t_w or h/t_h; local new_width = math.ceil(w/divisor_proporcion); local new_height = math.ceil(h/divisor_proporcion); return new_width, new_height; end
local thumb_width = 100; local thumb_height = 100; local image_width = 350; local image_height = 491;
local new_w,new_h = resize(image_width, image_height, thumb_width, thumb_height); print(new_w,new_h);--imprimirá 72,100
Como podrán ver la fórmula es simple porque primero debemos determinar cual de las 2 dimensiones (altura ó ancho) del plano es la mayor y de ahi crear un divisor de proporción.
Posteriormente dicho divisor de proporción es utilizado para obtener el nuevo alto y ancho del plano, apoyándonos de un redondeo al entero próximo mayor del resultado.
Con ésta fórmula no tendrán inconveniente alguno que incluso funcionará con dimensiones menores a las del thumbnail/miniatura creciendo el plano. Pero si quieren evitar que en dicho caso se agrande el plano entonces sólo deben editar la formula/código anterior quedando de la siguiente forma:
function resize(w,h,t_w,t_h) if w > t_w or h > t_h then local divisor_proporcion = (w>h) and w/t_w or h/t_h; local new_width = math.ceil(w/divisor_proporcion); local new_height = math.ceil(h/divisor_proporcion); return new_width, new_height; else return w,h; end end
lunes, 14 de noviembre de 2011
Ejecutar Javascript desde un iFrame
He visto bastantes scripts donde se complican mucho para ejecutar una función desde un documento desde un iframe. Lo más fácil es:
window.parent.nombredelafuncion();
Pero también funciona con sólo parent.nombredelafuncion();
Hay que recordar probar cada script en los distintos navegadores ó por lo menos en los navegadores más frecuentes como IE, Firefox, Chrome y Safari.
window.parent.nombredelafuncion();
Pero también funciona con sólo parent.nombredelafuncion();
Hay que recordar probar cada script en los distintos navegadores ó por lo menos en los navegadores más frecuentes como IE, Firefox, Chrome y Safari.
sábado, 5 de noviembre de 2011
lua id3 v2.3 tag reader / APIC Extractor & Changer
Bueno, el título esta bastante largo pero ahí vamos. Una parte de este script es una adaptación de otro que hay para linux pero sólo en las funciones que leen los id3tag y extraen la imagen del APIC tag. El resto sno funciones hechas por mí como la que es para calcular el SafeSynch Integer en base a un integer normal y viceversa.
Agradezco a Diaelectronics por su script para convertir de hexadecimal a binario (y viceversa) ya que sin ese script no hubiera podido hacer el resto de las funciones.
El script se puede adaptar para leer y editar el resto de los tags v2.3 ó incluso añadirlos, leer los flag bytes, etc. y es por ello que dejo siempre el código fuente.
Aca el script completo con algunas modificaciones como la de insertar el APIC en caso de no encontrarlo, ó cambiar el APIC en caso de contrario.
http://pastebin.com/TTEaCGME
Adjunto una imagen donde se aprecia un mp3 cualquiera con un nuevo APIC que es un sí una captura de pantalla de un script lua.
Agradezco a Diaelectronics por su script para convertir de hexadecimal a binario (y viceversa) ya que sin ese script no hubiera podido hacer el resto de las funciones.
El script se puede adaptar para leer y editar el resto de los tags v2.3 ó incluso añadirlos, leer los flag bytes, etc. y es por ello que dejo siempre el código fuente.
Aca el script completo con algunas modificaciones como la de insertar el APIC en caso de no encontrarlo, ó cambiar el APIC en caso de contrario.
http://pastebin.com/TTEaCGME
Adjunto una imagen donde se aprecia un mp3 cualquiera con un nuevo APIC que es un sí una captura de pantalla de un script lua.
viernes, 28 de octubre de 2011
lua2html script
Este es un pequeño script para pasar código de un archivo lua a html. El resultado puede ser retornado a un string, escrito en un file handler o en un archivo nuevo.
La marcación funciones son las que esten disponibles en el actual lua state desde donde se ejecuta así que es compatible con cualquier sdk.
Al dar el código fuente pueden modificarlo como quieran (como que sólo retorne cierto html, etc.) pero eso ya va de ustedes.
Link to pastebin: http://pastebin.com/73Hc2PHF
Aca el mismo script analizado por si mismo:
La marcación funciones son las que esten disponibles en el actual lua state desde donde se ejecuta así que es compatible con cualquier sdk.
Al dar el código fuente pueden modificarlo como quieran (como que sólo retorne cierto html, etc.) pero eso ya va de ustedes.
Link to pastebin: http://pastebin.com/73Hc2PHF
Aca el mismo script analizado por si mismo:
1 --[[lua2html by WeBuLtRa v0.9]]-- 2 function lua2html(sFile, sFileOut) 3 local keywords = { "while", "function", "for", "require", "elseif", "if", "then", "else", "do", "repeat", "until", "end", "return", "true", "false", "and", "not", "or", "local", "nil", "break", "in"}; 4 local functions = {":write", ":read", ":close", ":seek", ":gsub", ":find", ":gmatch", ":format", ":lower", ":upper", ":sub", ":byte", ":reverse", ":rep", ":len", ":dump", ":char"}; 5 local operators = {"%+", "%#", "%-", "%*", "%=", "~", "%."}; 6 local t = {}; 7 local sF = io.open(sFile, "rb"); 8 if sF then 9 local function print_r(a,b) 10 if b ~= _G then 11 if type(b) == "function" then 12 table.insert(t,a); 13 print(a); 14 elseif type(b) == "table" then 15 for x, y in pairs(b) do 16 if type(y) == "function" then 17 table.insert(t,a.."%."..x) 18 print(a.."."..x); 19 end 20 end 21 end 22 end 23 end 24 table.foreach(_G, print_r); 25 local function ParseScript(s) 26 local tFs, tVars = {}, {}; 27 for key in string.gmatch(s, "function ([%w%._%:]+)") do 28 table.insert(tFs, #tFs+1, key) 29 end 30 for key in string.gmatch(s, "(%w+%.%w+) = function") do 31 table.insert(tFs, #tFs+1, key) 32 end 33 return tFs; 34 end 35 local function CheckKeywords(line, multi) 36 if not multi then 37 for x,y in pairs(keywords) do 38 line = line:gsub("([%W]+)("..y..")([%c%s%;%,%(%)]+)", 39 function(a,b,c) 40 return a.."<span style='color:blue;'>"..b.."</span>"..c 41 end 42 ); 43 line = line:gsub("^"..y.."%s", 44 function(b) 45 return "<span style='color:blue;'>"..b.."</span>"; 46 end 47 ); 48 end 49 end 50 return line; 51 end 52 local function CheckOperators(line, operators, multi) 53 if not multi then 54 for x,y in pairs(operators) do 55 line = line:gsub(y, function(a) return "<span style='color:red;'>"..a.."</span>" end); 56 end 57 end 58 return line; 59 end 60 local function CheckFunctions(line, t, multi, color) 61 if not multi then 62 for x,y in pairs(t) do 63 line = line:gsub("("..y..")%(", function(a) return "<span style='color:"..color..";'>"..a.."</span>(" end); 64 end 65 end 66 return line; 67 end 68 local function CheckALL(line, multi,t, tFs, functions) 69 line = CheckKeywords(line, multi); 70 line = CheckFunctions(line, t, multi, "darkorange"); 71 line = CheckFunctions(line, functions, multi, "darkorange"); 72 line = CheckFunctions(line, tFs, multi, "red"); 73 line = CheckOperators(line, operators, multi); 74 line = line:gsub("('[%w%d%s]-')", function(a) return "<span style='color:darkmagenta;'>"..a.."</span>" end); 75 return line; 76 end 77 local function CheckStrings(line,t,tFs,functions) 78 line = line:gsub("(.-)([\"].-[\"])", function(b,a) return CheckALL(b, multi,t, tFs, functions).."<span style='color:darkmagenta;'>"..a.."</span>" end); 79 line = line:gsub('(".+")(.+)', function(a,b) return a..CheckALL(b, multi,t, tFs, functions) end); 80 line = line:gsub("('[.]+')", function(a) return "<span style='color:darkmagenta;'>"..a.."</span>" end); 81 return line; 82 end 83 local sText = ""; 84 local sRaw = sF:read("*a"); 85 local tFs = ParseScript(sRaw); 86 sF:seek("set"); 87 local comment, multi, d = false, false, 0; 88 for line in sF:lines() do 89 d = d + 1; 90 local g = "<span style='background-color:yellow;' name='linea' >"..d.."</span><span name='linea' > </span> "; 91 line = line:gsub("[^%w]", { ["<"] = "<", [">"] = ">", ["&"] = "&", ["'"] = "'", ["="] = "=" }) or ""; 92 if not comment then 93 if line:find("%-%-%[%[") then 94 line = line:gsub("%-%-%[%[", function(s) return "<span style='color:green;'>"..s; end); 95 if line:find("%]%]") then 96 line = line:gsub("%]%]", function(s) return s.."</span>"; end); 97 else 98 comment = true; 99 end 100 sText = sText..g..line; 101 else 102 local waka = false; 103 if line:find("%-%-") then 104 local a,b,c,e,f = line:find("(.-)(%-%-)(.+)"); 105 if a then 106 if c:find("[\"].-[\"]") then 107 c = CheckStrings(c,t,tFs,functions); 108 waka = true; 109 else 110 waka = false; 111 end 112 if c:find("%[%[") and not c:find("%]%]") then 113 c = c:gsub("(.+)(%[%[.+)", function(a,s) return CheckALL(a, multi, t, tFs, functions).."<span style='color:darkmagenta;'>"..s; end); 114 multi = true; 115 end 116 if not waka then 117 c = CheckALL(c, multi, t, tFs, functions); 118 end 119 if not c:find("%[%[") and c:find("%]%]") then 120 c = c.."</span>"; 121 multi = false; 122 elseif c:find("%[%[.+%]%]") then 123 c = c:gsub("%[%[.+%]%]", function(s) return "<span style='color:darkmagenta;'>"..s.."</span>"; end); 124 end 125 end 126 line = c.."<span style='color:green;'>"..e..f.."</span>"; 127 else 128 if line:find("[\"].-[\"]") then 129 waka = true; 130 line = CheckStrings(line,t,tFs,functions,d); 131 else 132 waka = false; 133 end 134 if line:find("%[%[") and not line:find("%]%]") then 135 line = line:gsub("(.+)(%[%[.+)", function(a,s) return CheckALL(a, multi, t, tFs, functions).."<span style='color:darkmagenta;'>"..s; end); 136 multi = true; 137 end 138 if not waka then 139 line = CheckALL(line, multi, t, tFs, functions, d); 140 end 141 if not line:find("%[%[") and line:find("%]%]") then 142 line = line.."</span>"; 143 multi = false; 144 elseif line:find("%[%[.+%]%]") then 145 line = line:gsub("%[%[.+%]%]", function(s) return "<span style='color:darkmagenta;'>"..s.."</span>"; end); 146 end 147 end 148 sText = sText..g..line; 149 end 150 else 151 if line:find("%]%]") then 152 sText = sText..g..line.."</span>"; 153 comment = false; 154 else 155 sText = sText..g..line; 156 end 157 end 158 end 159 sF:close(); 160 local sStart = [[<p>Archivo: %s<br/> 161 Total de lineas: %d<br/> 162 Total de funciones propias: %d ( 163 <span style='color:gray;'>%s</span> )<br/> 164 <input type='button' id='b' onclick='expand();' value='Expandir/Contraer codigo'/> 165 <input type='button' onclick='hideLinea();' value='Esconder numero de linea'/> 166 <input type='button' onclick='ShowLinea();' value='Mostrar numero de linea'/></p>]]; 167 sStart = sStart:format(sFile, d,#tFs, table.concat(tFs,', ')); 168 local sFinish = [[<script> 169 var e = document.getElementById('formated'); 170 var b = document.getElementById('b'); 171 function expand(){ 172 e.style.width = 'auto'; 173 e.style.height = 'auto'; 174 b.onclick = contraer; 175 } 176 function contraer(){ 177 e.style.width = '800px'; 178 e.style.height = '300px'; 179 b.onclick = expand 180 } 181 function hideLinea(){ 182 aL = document.getElementsByName('linea'); 183 for(var i=0;i<aL.length;i++){ 184 aL[i].style.display = 'none'; 185 } 186 } 187 function ShowLinea(){ 188 aL = document.getElementsByName('linea'); 189 for(var i=0;i<aL.length;i++){ 190 aL[i].style.display = 'inline'; 191 } 192 } 193 </script>]]; 194 local sRet = sStart.."<pre id='formated' style='text-align:left;width:800px;height:300px; overflow:scroll;'>"..sText.."</pre><h3>Raw code</h3><pre style='text-align:left;width:800px;height:120px; overflow:scroll;'><code>"..sRaw.."</code></pre>"..sFinish; 195 if not sFileOut then 196 return sRet; 197 elseif type(sFileOut) == 'userdata' and string.find(tostring(sFileOut), "file") then 198 sFileOut:write(sRet); 199 elseif type(sFileOut) == 'string' then 200 local sO = io.open(sFileOut, "wb"); 201 if sO then 202 sO:write(sRet); 203 sO:close(); 204 return true; 205 else 206 return nil, "Couldn't open destination file"; 207 end 208 else 209 return nil, 'jeje'; 210 end 211 else 212 return nil, "Couldn't open file"; 213 end 214 end
martes, 25 de octubre de 2011
Direct Download link emuparadise.me
Parece que el día de hoy los sitios se empeñan en ponerme trabas y ahora fue el caso de emuparadise.me (sitio lleno de roms). Según tienes que resolver el captcha para obtener los direct download link pero a juzgar por el código sólo plantan una cookie en tu navegador. Nuevamente usando la consola web del firefox podemos brincarnos esa tediosa cosa del captcha. Sólo escriban esto en la consola y la página se recargará mostrando el link:
document.cookie = "downloadcaptcha=1"; window.location.reload();
Imagen de prueba:
Pueden haber más métodos para esto pero este es el más sencillo.
document.cookie = "downloadcaptcha=1"; window.location.reload();
Imagen de prueba:
Pueden haber más métodos para esto pero este es el más sencillo.
Desbloquear contenido (survey) de freemovierepublic.com
Bueno, el día de hoy entre al sitio freemovierepublic.com ya que dicen que tienen buenas películas online (recientes) pero mi sorpresa fue que al entrar a cada link para ver una película se mostraba un cartel bastante molesto solicitando visitar varios links para poder desbloquear al contenido y entonces ver la película.
La verdad es frustrante ver este tipo de sitio llenos de trackers, contadores y publicidad, y en partícular la publicidad que bloquea contenidos.
Así que pues simplemente ví el código fuente del sitio en lo que correspondía al bloqueador de contenidos y resultó ser un javascript proveniente de dollarade.com (como se ve en la imágen). Dicho javascript esta ofuscado de tal forma que resulta imposible leerlo simplemente pero nótese que usan la función eval de javascript para ejecutarlo. Así que lo único que hice fue copiar el contenido de ese js y cambiar la función eval por document.write en otro html nuevo. El resultado es el javascript des-ofuscado jajajaja.
Ya teniendo el javascript pleno sólo era leerlo para ver como se ejecutaba y aunque creí que iba a ser más difícil resultó no serlo. Ello porque tienen una funcion que te avisa cuando el contenido esta desbloqueado lanzando un dialogo (alert) con dicho mensaje (algo como your content has been unlocked). Lo interesante es que después de dicho dialogo hay una función cuyo nombre es bastante largo y que aparentemente no esta definida. Así es, BINGO. Dicha función es la que desbloquea el contenido y se me hace ridículo que la pongan casi al inicio del script (aunque creo es porque lo tenían ofuscado y no les interesó jajaja).
Para ejecutarla pueden usar la consola web de firefox (como en mi caso) ó si usan IE escribir en la barra de navegación:
javascript : nombredelafuncion(); //(reemplacen el nombredelafuncion por la que aparece en el js).
En mi caso la función se llama:
ivaie3i33mav93a8b8ghh7abnalkm34l35jal64naliy7an96atl5kajo8n69anlamlknja9();
Pero puede cambiar según sea el dominio que este usando el dollarade para bloquear su contenido.
Y listo, como verán en la siguiente imagen el contenido fue desbloqueado fácilmente. La verdad como ejercicio estuvo bueno pero sigo dudando de métodos de ofuscación como el presentado por dollarade que son muy sencillos de imprimir.
Otras formas de deshabilitar estos "surveys" es deshabilitando el javascript globalmente o para ciertos sitios en particular, editando el archivo de hosts colocando el dominio de dollarade.com con ip local 127.0.0.1 y también usando add-ons de firefox que deshabilitan el javascript.
Ah por cierto, indagando un poco en la red dí con un link donde se muestra todo el código des-ofuscado de dollarade.com así que aquí les doy el link por si quieren echarle un vistazo y comprender un poco mejor lo anteriormente expuesto así el javascript que usa dicho sitio: http://pastebin.com/pS8jPKdX
Espero les agrade esta info.
La verdad es frustrante ver este tipo de sitio llenos de trackers, contadores y publicidad, y en partícular la publicidad que bloquea contenidos.
Así que pues simplemente ví el código fuente del sitio en lo que correspondía al bloqueador de contenidos y resultó ser un javascript proveniente de dollarade.com (como se ve en la imágen). Dicho javascript esta ofuscado de tal forma que resulta imposible leerlo simplemente pero nótese que usan la función eval de javascript para ejecutarlo. Así que lo único que hice fue copiar el contenido de ese js y cambiar la función eval por document.write en otro html nuevo. El resultado es el javascript des-ofuscado jajajaja.
Ya teniendo el javascript pleno sólo era leerlo para ver como se ejecutaba y aunque creí que iba a ser más difícil resultó no serlo. Ello porque tienen una funcion que te avisa cuando el contenido esta desbloqueado lanzando un dialogo (alert) con dicho mensaje (algo como your content has been unlocked). Lo interesante es que después de dicho dialogo hay una función cuyo nombre es bastante largo y que aparentemente no esta definida. Así es, BINGO. Dicha función es la que desbloquea el contenido y se me hace ridículo que la pongan casi al inicio del script (aunque creo es porque lo tenían ofuscado y no les interesó jajaja).
Para ejecutarla pueden usar la consola web de firefox (como en mi caso) ó si usan IE escribir en la barra de navegación:
javascript : nombredelafuncion(); //(reemplacen el nombredelafuncion por la que aparece en el js).
En mi caso la función se llama:
ivaie3i33mav93a8b8ghh7abnalkm34l35jal64naliy7an96atl5kajo8n69anlamlknja9();
Pero puede cambiar según sea el dominio que este usando el dollarade para bloquear su contenido.
Y listo, como verán en la siguiente imagen el contenido fue desbloqueado fácilmente. La verdad como ejercicio estuvo bueno pero sigo dudando de métodos de ofuscación como el presentado por dollarade que son muy sencillos de imprimir.
Otras formas de deshabilitar estos "surveys" es deshabilitando el javascript globalmente o para ciertos sitios en particular, editando el archivo de hosts colocando el dominio de dollarade.com con ip local 127.0.0.1 y también usando add-ons de firefox que deshabilitan el javascript.
Ah por cierto, indagando un poco en la red dí con un link donde se muestra todo el código des-ofuscado de dollarade.com así que aquí les doy el link por si quieren echarle un vistazo y comprender un poco mejor lo anteriormente expuesto así el javascript que usa dicho sitio: http://pastebin.com/pS8jPKdX
Espero les agrade esta info.
lunes, 24 de octubre de 2011
jquery nl2br
Aunque en javascript podemos usar una función siguiente cuya finalidad sea cambiar los LF (line feed) por tags br de html:
Puede llegar a suceder que esta función no sirva cuando el valor al que vamos a aplicar el filtro proviene de un método ajax get o post de jquery como $.post() o $.get() y es multilínea. Incluso usar la función de php nl2br retornaría un valor incorrecto. Así que deben aplicar una nueva función/filtro de la siguiente forma:
Nótese que se esta 'escapando' el caracter que representa el new line o line feed. Además se agrega el modificador /m para que afecte al valor dado aunque éste sea multilínea.
function nl2br(value)
return value.replace(/\n/g, ' < b r /> ');
end
Puede llegar a suceder que esta función no sirva cuando el valor al que vamos a aplicar el filtro proviene de un método ajax get o post de jquery como $.post() o $.get() y es multilínea. Incluso usar la función de php nl2br retornaría un valor incorrecto. Así que deben aplicar una nueva función/filtro de la siguiente forma:
function nl2br(value)
return value.replace(/\\n/gm, ' < b r / >' );
end
Nótese que se esta 'escapando' el caracter que representa el new line o line feed. Además se agrega el modificador /m para que afecte al valor dado aunque éste sea multilínea.
lunes, 26 de septiembre de 2011
iDNX demo online
Al esta online el sitio demo del idnx rs online con todas las actualizaciones mencionadas ahi mismo (sólo las escribía más no atualizaba el sitio porque lo tenía en mi localhost).
Les recuerdo que con esto concluye la fase alpha 1 del sitio, ya que faltan algunas cosas que programar pero la estructura principal ya esta hecha y así será más fácil darle continuidad al mismo.
Espero les agrade ya que le metí bastante ajax con jquery y css (1,2,3), por lo que les recomiendo utilizar el Firefox 5.0+.
viernes, 23 de septiembre de 2011
Ahora en el .com
Pues al fin me decidí a semi redireccionar el index del dominio (que aparecía sólo con un letreo de "is back") a lo que es el blog principalmente porque no quise hacer un index por ahora pero tampoco quería ese viejo letrero (fue la solución más rápida jaja).
Bueno, el demo del idnx online lo pospuse por un par de semanas (por una distracción con lua) pero ya estoy de regreso en ello así que este fin de semana debe quedar terminada la primera parte.
Bueno, el demo del idnx online lo pospuse por un par de semanas (por una distracción con lua) pero ya estoy de regreso en ello así que este fin de semana debe quedar terminada la primera parte.
jueves, 22 de septiembre de 2011
Bases de datos (3ra parte)
En el pasado post escribí acerca de la importancia de una buena estructura en una base de datos, así que hoy toca escribir acerca de dicha estructura pero para protegernos de un posible un tanto común pero con escasa información de como resolverlo.
A este error le llamo "Ruptura de interrelación" y se origina cuando en un par de tablas relacionadas, a través del primary key, para accesar a los campos de una de otra se elimina un registro. Un ejemplo sería el siguiente, donde la tabla notas se interrelaciona con la tabla usuarios:
Tabla1: usuarios
Campos: id, nombre, edad
Fila1: 1, juan, 15
Fila2: 2, pedro, 20
Tabla 2: notas
Campos: id, id_usuario, nota
Fila1: 1, 2, hola
Fila2: 2, 1, como
La consulta de selección (select query) empleado para este ejemplo es (ansi):
SELECT DISTINCT notas.id, usuarios.nombre, usuarios.edad, notas.nota FROM notas, usuarios WHERE notas.id_usuario=usuarios.id
Con esta consulta el resultado sería:
1, pedro, 20, hola
2, juan, 15, como
Pero que pasaría sí se eliminara el registro 1 de la tabla usuarios con una consulta como:
Al volver a ejecutar la consulta de selección sólo retornaría un resultado, siendo éste:
1, pedro, 20, hola
Ésto sucede por el error que denominé "Ruptura de interrelación" porque en la consulta sólo se seleccionan los registros cuyo campo notas.id_usuario = usuario.id ignorando aquellos que son notas.usuario = null.
Hay varias soluciones:
1.- Usar una cláusula como ésta: WHERE (notas.id_usuario=usuario.id OR notas.usarios=NULL).
2.- Usar los equivalentes a cada lenguaje sql de IF NOT EXISTS(usuario.id, 'algo',usuario.nombre)
Otra opción más es hacer uso de triggers y funciones para cambiar el campo notas.id_usuario a 0 cuando dicho id de usuario es eliminado y luego usar consultas como IF(notas.id_usuario=0, 'sinregistro', usuario.nombre).
En fin, como podrán leer existen varias formas, aparte de las mencionadas (como el if not null), para evitar esas rupturas de interrelación que pueden finalizar en registros innaccesibles por consultas de selección como la primera expuesta.
Así que les recomiendo por mucho que lean a fondo los manuales de consultas selección, triggers, funciones y reserved keywords propias del sql que esten usando.
A este error le llamo "Ruptura de interrelación" y se origina cuando en un par de tablas relacionadas, a través del primary key, para accesar a los campos de una de otra se elimina un registro. Un ejemplo sería el siguiente, donde la tabla notas se interrelaciona con la tabla usuarios:
Tabla1: usuarios
Campos: id, nombre, edad
Fila1: 1, juan, 15
Fila2: 2, pedro, 20
Tabla 2: notas
Campos: id, id_usuario, nota
Fila1: 1, 2, hola
Fila2: 2, 1, como
La consulta de selección (select query) empleado para este ejemplo es (ansi):
SELECT DISTINCT notas.id, usuarios.nombre, usuarios.edad, notas.nota FROM notas, usuarios WHERE notas.id_usuario=usuarios.id
Con esta consulta el resultado sería:
1, pedro, 20, hola
2, juan, 15, como
Pero que pasaría sí se eliminara el registro 1 de la tabla usuarios con una consulta como:
DELETE FROM usuarios WHERE id=1
Al volver a ejecutar la consulta de selección sólo retornaría un resultado, siendo éste:
1, pedro, 20, hola
Ésto sucede por el error que denominé "Ruptura de interrelación" porque en la consulta sólo se seleccionan los registros cuyo campo notas.id_usuario = usuario.id ignorando aquellos que son notas.usuario = null.
Hay varias soluciones:
1.- Usar una cláusula como ésta: WHERE (notas.id_usuario=usuario.id OR notas.usarios=NULL).
2.- Usar los equivalentes a cada lenguaje sql de IF NOT EXISTS(usuario.id, 'algo',usuario.nombre)
Otra opción más es hacer uso de triggers y funciones para cambiar el campo notas.id_usuario a 0 cuando dicho id de usuario es eliminado y luego usar consultas como IF(notas.id_usuario=0, 'sinregistro', usuario.nombre).
En fin, como podrán leer existen varias formas, aparte de las mencionadas (como el if not null), para evitar esas rupturas de interrelación que pueden finalizar en registros innaccesibles por consultas de selección como la primera expuesta.
Así que les recomiendo por mucho que lean a fondo los manuales de consultas selección, triggers, funciones y reserved keywords propias del sql que esten usando.
Spawner-Ex.dll
Excelente dll para trabajar con lua, y aunque sé que fue creada en un principio para el debugger de SciTE, ya que permite ejecutar otras aplicaciones iniciando un proceso (thread) nuevo, sin bloquear el proceso principal (main thread) y devolviendo el código de cierre de dichas aplicaciones.
Pero no solo eso, sino también permite ejecutar aplicaciones que usualmente mostrarían la ventana del shell pero sin ella. Hay 2 modos de ejecución, donde la primera solo es basicamente para ejecutar y leer el stdout y luego se finaliza (suelen bloquear el main thread). La segunda es para poder incluso interactuar con la aplicación permitiendonos enviar strings al stdin sin bloquear el main thread.
Ya me compile la dll modificando los tamaños de buffer habiéndolos incrementando bastante y con excelentes resultados. Si alguien la ocupa sólo dejen comentario (jojojo).
Para ejecutar programas con argumentos y rutas de archivo con espacios es recomendable que usen los corchetes (square brackets).
Les dejo un link al post donde comencé a leer de esta dll: http://groups.google.com/group/scite-interest/browse_thread/thread/b57760317f6bf688?pli=1
Pero no solo eso, sino también permite ejecutar aplicaciones que usualmente mostrarían la ventana del shell pero sin ella. Hay 2 modos de ejecución, donde la primera solo es basicamente para ejecutar y leer el stdout y luego se finaliza (suelen bloquear el main thread). La segunda es para poder incluso interactuar con la aplicación permitiendonos enviar strings al stdin sin bloquear el main thread.
Ya me compile la dll modificando los tamaños de buffer habiéndolos incrementando bastante y con excelentes resultados. Si alguien la ocupa sólo dejen comentario (jojojo).
Para ejecutar programas con argumentos y rutas de archivo con espacios es recomendable que usen los corchetes (square brackets).
Les dejo un link al post donde comencé a leer de esta dll: http://groups.google.com/group/scite-interest/browse_thread/thread/b57760317f6bf688?pli=1
viernes, 16 de septiembre de 2011
Corutinas en Lua [Parte 2]
En el anterior post escribí una corutina creada con la función "coroutine.wrap()". Ahora toca hacerlo con "coroutine.create()":
Así es como se vería el mismo código pero creado con la función "coroutine.create()", la cual retorna un tipo definido como "thread". Para resumir una corutina creada con éste método debemos usar la función "coroutine.resume()", la cual retornará un valor booleano en el primer argumento y en el resto de los argumentos los valores que la función de la corutina regrese.
La principal diferencia de crearla así (en lugar de hacerlo con coroutine.wrap) es que podemos obtener el estado de ejecución de la misma (suspended, normal, dead) y además podemos manejar los errores sin propagarlos al invocador (con wrap el error se propaga directo al invocador).
Es importante volver a mencionar que todo lo que se escriba dentro del paréntesis de "coroutine.yield()" es lo que se retornará (ó en su defecto un simple return que finalizaría la corutina) al invocador de "coroutine.resume()" o a la función creada con wrap. El contenido del paréntesis no indican los nuevos valores a recibir/evaluar, así que podemos tenerlo vacío y aún así seguir recibiendo valores dentro de la corutina:
Todo esto es algo que, como ya mencioné en la publicación anterior, no esta muy bien documentado en los manuales de lua.
Ahora bien, la principal cualidad de una corutina es la de poder controlar varias corutinas desde 1 principal (resumiendo otras, pausando, etc.) logrando lo que se llama "multitasking" (multitareas) y no una función tan simple como la expuesta en los ejemplos. Pero, la finalidad de estos ejemplos es explicar de forma más detallada el uso básico de las corutinas.
NOTA: Todas éstas funciones pueden ser probadas para verificarlas en el demo online que ofrece el sitio de lua.org (http://www.lua.org/cgi-bin/demo).
function Print(valor1, valor2) while true do valor1, valor2 = coroutine.yield(valor1, valor2); end end PrintCo = coroutine.create(Print);
print(PrintCo);-->thread 0xXXXXXX
local a, b, c = coroutine.resume(PrintCo,"Hola", "mundo!") print(a, b, c);-->true, Hola, mundo!
a, b, c = coroutine.resume(PrintCo,"Cómo", "estas?!");
print(a, b, c);-->true, Cómo, estas?!
Así es como se vería el mismo código pero creado con la función "coroutine.create()", la cual retorna un tipo definido como "thread". Para resumir una corutina creada con éste método debemos usar la función "coroutine.resume()", la cual retornará un valor booleano en el primer argumento y en el resto de los argumentos los valores que la función de la corutina regrese.
La principal diferencia de crearla así (en lugar de hacerlo con coroutine.wrap) es que podemos obtener el estado de ejecución de la misma (suspended, normal, dead) y además podemos manejar los errores sin propagarlos al invocador (con wrap el error se propaga directo al invocador).
Es importante volver a mencionar que todo lo que se escriba dentro del paréntesis de "coroutine.yield()" es lo que se retornará (ó en su defecto un simple return que finalizaría la corutina) al invocador de "coroutine.resume()" o a la función creada con wrap. El contenido del paréntesis no indican los nuevos valores a recibir/evaluar, así que podemos tenerlo vacío y aún así seguir recibiendo valores dentro de la corutina:
function Print(valor1, valor2) while true do
print(valor1, valor2); valor1, valor2 = coroutine.yield(); end end PrintCo = coroutine.create(Print);
print(PrintCo);-->thread 0xXXXXXX
local a = coroutine.resume(PrintCo,"Hola", "mundo!")
---->Hola, mundo! print(a);-->true
a= coroutine.resume(PrintCo,"Cómo", "estas?!");
---->Cómo, estas?!
print(a);-->true
Todo esto es algo que, como ya mencioné en la publicación anterior, no esta muy bien documentado en los manuales de lua.
Ahora bien, la principal cualidad de una corutina es la de poder controlar varias corutinas desde 1 principal (resumiendo otras, pausando, etc.) logrando lo que se llama "multitasking" (multitareas) y no una función tan simple como la expuesta en los ejemplos. Pero, la finalidad de estos ejemplos es explicar de forma más detallada el uso básico de las corutinas.
NOTA: Todas éstas funciones pueden ser probadas para verificarlas en el demo online que ofrece el sitio de lua.org (http://www.lua.org/cgi-bin/demo).
jueves, 15 de septiembre de 2011
Corutinas en Lua
Hoy escribiré acerca de las corutinas de Lua (coroutines en inglés). Sé que la documentación de lua, manuales, etc. no logran explicar bien qué son ni cómo funcionan (y los ejemplos vaya que son difíciles de entender) así que trataré de facilitarlo para todos.
Las corutinas son nuevos procesos (threads) que se ejecutan independientemente de otros procesos. No se pueden ejecutar en paralelo, pero si en forma secuencial.
Muchos dirán: "De que sirven las corutinas si podemos simplemente ejecutar X función". Bueno, la corutinas son más rápidas en ejecución que una función porque ademas de estar cargadas en la memoria tambien se ejecutan en procesos independientes. Es decir, al ejecutar una función simplemente corremos una serie de instrucciones cargadas en X dirección de la memoria (pero sigue siendo el mismo proceso), y al ejecutar una corutina se ejecuta un proceso alterno que ya tiene cargadas la serie de instrucciones a seguir siendo más rápida la respuesta.
Pero no solo esto, sino también dichas corutinas (en procesos distintos) pueden pausarse, resumirse, etc.
En resumen, la ventajas son:
- La ejecución más rapida de funciones
- Poder controlar el flujo de otras corutinas desde dentro, o fuera, de otras.
Pero bueno, para dejarlo más claro escribiré un ejemplo muy básico de cómo crear una corutina:
Con éste código creamos una corutina infinita (nunca se terminará) llamada "PrintEx" basada en la función "Print" que cada vez que la llamemos imprimirá los argumentos que le enviemos (bien podríamos simplemente usar la función print("Hola", "Mundo"); pero el caso es explicar las corutinas).
Bueno, ahora paso por paso.
1.- La función que será nuestra corutina es la función "Print" y recibe 2 argumentos.
2.- Dicha función inicia un loop infinito dónde por cada vuelta pausará la corutina usando "coroutine.yield()";
3.- La función "coroutine.yield(valor1, valor2);" lo que hace es retornar los argumentos suministrados por la función. Algo así como el clásico "return valor1, valor2;" en la funciones normales.
4.- Noten la línea de código "valor1, valor2 = coroutine.yield(valor1, valor2)". Como ya sabemos, la función "coroutine.yield();" retornará al invocante los argumentos que pongamos dentro del paréntesis. Pero si anteponemos "variable1, variable2 = " a dicha función entonces le estamos indicando a la corutina que la próxima vez que sea invocada los argumentos suministrados por el nuevo invocador serán guardados en las variables que antepongamos a "coroutine.yield()". De otro modo cada que la corutina seá invocada sólo retornaría los valores suministrados la primera vez que se le invocó.
5.- Usé como generador de corutina la función "coroutine.wrap()" (porque es más adecuado para este ejemplo), la cual retornará una nueva función que cada vez que sea invocada resumirá o ejecutará la corutina. En otras palabras, es lo mismo que usar "coroutine.resume(corutina, argumentos)" sólo que no retorna el primer valor boleano, sino sólo los argumentos que retorna la corutina.
Espero no sea tan complicada esta explicación, y más adelante escribiré otro ejemplo pero ahora si usando la función generadora "coroutine.create()".
Las corutinas son nuevos procesos (threads) que se ejecutan independientemente de otros procesos. No se pueden ejecutar en paralelo, pero si en forma secuencial.
Muchos dirán: "De que sirven las corutinas si podemos simplemente ejecutar X función". Bueno, la corutinas son más rápidas en ejecución que una función porque ademas de estar cargadas en la memoria tambien se ejecutan en procesos independientes. Es decir, al ejecutar una función simplemente corremos una serie de instrucciones cargadas en X dirección de la memoria (pero sigue siendo el mismo proceso), y al ejecutar una corutina se ejecuta un proceso alterno que ya tiene cargadas la serie de instrucciones a seguir siendo más rápida la respuesta.
Pero no solo esto, sino también dichas corutinas (en procesos distintos) pueden pausarse, resumirse, etc.
En resumen, la ventajas son:
- La ejecución más rapida de funciones
- Poder controlar el flujo de otras corutinas desde dentro, o fuera, de otras.
Pero bueno, para dejarlo más claro escribiré un ejemplo muy básico de cómo crear una corutina:
function Print(valor1, valor2) while true do valor1, valor2 = coroutine.yield(valor1, valor2); end end PrintEx = coroutine.wrap(Print, a, b); print(PrintEx("Hola", "mundo!")); print(PrintEx("Cómo", "estas?"));
Con éste código creamos una corutina infinita (nunca se terminará) llamada "PrintEx" basada en la función "Print" que cada vez que la llamemos imprimirá los argumentos que le enviemos (bien podríamos simplemente usar la función print("Hola", "Mundo"); pero el caso es explicar las corutinas).
Bueno, ahora paso por paso.
1.- La función que será nuestra corutina es la función "Print" y recibe 2 argumentos.
2.- Dicha función inicia un loop infinito dónde por cada vuelta pausará la corutina usando "coroutine.yield()";
3.- La función "coroutine.yield(valor1, valor2);" lo que hace es retornar los argumentos suministrados por la función. Algo así como el clásico "return valor1, valor2;" en la funciones normales.
4.- Noten la línea de código "valor1, valor2 = coroutine.yield(valor1, valor2)". Como ya sabemos, la función "coroutine.yield();" retornará al invocante los argumentos que pongamos dentro del paréntesis. Pero si anteponemos "variable1, variable2 = " a dicha función entonces le estamos indicando a la corutina que la próxima vez que sea invocada los argumentos suministrados por el nuevo invocador serán guardados en las variables que antepongamos a "coroutine.yield()". De otro modo cada que la corutina seá invocada sólo retornaría los valores suministrados la primera vez que se le invocó.
5.- Usé como generador de corutina la función "coroutine.wrap()" (porque es más adecuado para este ejemplo), la cual retornará una nueva función que cada vez que sea invocada resumirá o ejecutará la corutina. En otras palabras, es lo mismo que usar "coroutine.resume(corutina, argumentos)" sólo que no retorna el primer valor boleano, sino sólo los argumentos que retorna la corutina.
Espero no sea tan complicada esta explicación, y más adelante escribiré otro ejemplo pero ahora si usando la función generadora "coroutine.create()".
jueves, 8 de septiembre de 2011
Aprender lenguajes de programación
Para quienes comienzan a aventurarse en el mundo de la programación hay ciertos puntos que les pueden ayudar:
1.- Diagramas de flujo: Si bien es algo latoso hacerlos (tareas, clases, etc.) no significa que sean inútiles. La mayoría de las empresas que se dedican a desarrollar aplicaciones hacen los diagramas de flujo (de X aplicación) por mucho tiempo antes de siquiera sentarse a programar. No significa que uds. lo requieran pero es una buena costumbre. Lo primordial de un diagrama de flujo es precisamente entender paso a paso todas las acciones que se ejcutarán desde que inicia el programa. Al entender este flujo entenderán en realidad como se procesa la información por las computadoras y podrán desarrollar mejores aplicaciones libres de bugs.
2.- Controles de estructura, loops, signos de comparación y palabras reservadas: Son escenciales en todo lenguaje de programación así que son de las primeras cosas que deben aprender y memorizar (en más fácil de lo que creen).
3.- Variables: Aprendan todo lo básico de las variables, desde los distintos tipos hasta la forma correcta de declararlas. Los ámbitos (global ó local) de las variables también son importantes porque les ayudarán a que sus códigos se ejecuten más rápido.
4.- Funciones: Aprender a crear sus propias funciones les ayudará a ahorrar líneas de código y a ejecutar más rápido sus funciones. Eso de escribir series líneas de código idéntico en distintas zonas de su aplicación no es óptimo.
5.- Por último serían las funciones propias del lenguaje de programación.
Conforme más programemos en X lenguaje más rápido memorizaremos lo anteriorente mencionado.
Si ya se conoce por lo menos 1 lenguaje de programación de alto nivel será mucho más fácil aprender el resto.
1.- Diagramas de flujo: Si bien es algo latoso hacerlos (tareas, clases, etc.) no significa que sean inútiles. La mayoría de las empresas que se dedican a desarrollar aplicaciones hacen los diagramas de flujo (de X aplicación) por mucho tiempo antes de siquiera sentarse a programar. No significa que uds. lo requieran pero es una buena costumbre. Lo primordial de un diagrama de flujo es precisamente entender paso a paso todas las acciones que se ejcutarán desde que inicia el programa. Al entender este flujo entenderán en realidad como se procesa la información por las computadoras y podrán desarrollar mejores aplicaciones libres de bugs.
2.- Controles de estructura, loops, signos de comparación y palabras reservadas: Son escenciales en todo lenguaje de programación así que son de las primeras cosas que deben aprender y memorizar (en más fácil de lo que creen).
3.- Variables: Aprendan todo lo básico de las variables, desde los distintos tipos hasta la forma correcta de declararlas. Los ámbitos (global ó local) de las variables también son importantes porque les ayudarán a que sus códigos se ejecuten más rápido.
4.- Funciones: Aprender a crear sus propias funciones les ayudará a ahorrar líneas de código y a ejecutar más rápido sus funciones. Eso de escribir series líneas de código idéntico en distintas zonas de su aplicación no es óptimo.
5.- Por último serían las funciones propias del lenguaje de programación.
Conforme más programemos en X lenguaje más rápido memorizaremos lo anteriorente mencionado.
Si ya se conoce por lo menos 1 lenguaje de programación de alto nivel será mucho más fácil aprender el resto.
Base de datos (2da parte)
Anteriormente ya hablé (escribí) de las bases de datos y su importancia en todo tipo de aplicaciones. Pero ahora toca el turno de las estructuras de éstas bases de datos.
La estructura de una base de datos es igual de importante que el código que usemos para leerla y insertarle datos (independientemente del lenguaje de programación usado). De ella dependerá que nuestra aplicación sea adaptable a futuras versiones sin mayores actualizaciones.
Un ejemplo de ello sería una aplicación de ventas donde podemos controlar el inventario en una tabla de nuestra base de datos. Pero también podemos ver quien realizó la compra. En un futura actualización podriamos incluir quien realizó la venta a través de un campo llamado (siguiendo el ejemplo) "vendedor_id".
Así que si van a usar una bse de datos en su aplicación les recomiendo que creen tantos campos como sean necesarios más los que de momento podrían no serlo, ya que en un futuro podríamos llegar a usarlos.
La estructura de una base de datos es igual de importante que el código que usemos para leerla y insertarle datos (independientemente del lenguaje de programación usado). De ella dependerá que nuestra aplicación sea adaptable a futuras versiones sin mayores actualizaciones.
Un ejemplo de ello sería una aplicación de ventas donde podemos controlar el inventario en una tabla de nuestra base de datos. Pero también podemos ver quien realizó la compra. En un futura actualización podriamos incluir quien realizó la venta a través de un campo llamado (siguiendo el ejemplo) "vendedor_id".
Así que si van a usar una bse de datos en su aplicación les recomiendo que creen tantos campos como sean necesarios más los que de momento podrían no serlo, ya que en un futuro podríamos llegar a usarlos.
martes, 6 de septiembre de 2011
Stupidware: Noob
La verdad que es un tanto irritante y desgastante encontrar a cada rato la misma gata pero revolcada (una disculpa por el lenguaje coloquial figurativo). Pero es molesto que los noobs/newbies/novatos (en su afán de sobresalir) renombren programas o packs de programas/archivos/etc. con nombres que inician con alguno de los siguientes ejemplos:
- Mega
- Súper mega
- Ultimate
- Masters
Entre otros. Incluso llegan a inventar versiones de programas cuando la oficial ni siquiera se ha actualizado en años.
A lo único a lo que conducen es a generar basura de versiones no probadas que pueden resultar en el mal funcionamiento, por mencionar un ejemplo. Me pregunto: ¿Qué tanto les cuesta sólo poner el nombre del programa/pack/etc. tal cual es? En lugar de verse "conocedores" resultan hilarantes ante dicha muestra de complejos.
Otra cosa que también he escuchado es: "No son páginas web, es software". Es verdad que es software, pero si nos vamos a definiciones entonces todo aquello no físico de una computadora es SOFTWARE (llámense juegos, aplicaciones, sitios web, etc.). Pero ¿Para qué molestarse en catalogar/dividir el software según sus características comunes? ¡Es obvio! ¿Para qué molestarse en dividir a los perros según su raza ó a los animales según su especie si al final todos son animales? La respuesta esta intrínseca en la pregunta (¿Retórica?).
- Mega
- Súper mega
- Ultimate
- Masters
Entre otros. Incluso llegan a inventar versiones de programas cuando la oficial ni siquiera se ha actualizado en años.
A lo único a lo que conducen es a generar basura de versiones no probadas que pueden resultar en el mal funcionamiento, por mencionar un ejemplo. Me pregunto: ¿Qué tanto les cuesta sólo poner el nombre del programa/pack/etc. tal cual es? En lugar de verse "conocedores" resultan hilarantes ante dicha muestra de complejos.
Otra cosa que también he escuchado es: "No son páginas web, es software". Es verdad que es software, pero si nos vamos a definiciones entonces todo aquello no físico de una computadora es SOFTWARE (llámense juegos, aplicaciones, sitios web, etc.). Pero ¿Para qué molestarse en catalogar/dividir el software según sus características comunes? ¡Es obvio! ¿Para qué molestarse en dividir a los perros según su raza ó a los animales según su especie si al final todos son animales? La respuesta esta intrínseca en la pregunta (¿Retórica?).
lunes, 5 de septiembre de 2011
Ver código generado por jQuery/Ajax
Bueno, mucho dirían que no es posible ver el código (html) que genera jQuery a través de sus funciones Ajax pero no es así. Hay varias formas siendo las más sencillas las siguientes (con firefox):
1.- Usar addons como Live HTTP Headers que capturarán todo el intercambio de datos entre el navegador y el servidor (pero mostrando incluso cabeceras, etc.).
2.- Viendo la página cuyo contenido fue generado por $.load, $.post, etc. y sólo seleccionar los elementos generados por el jQuery y que no aparecen en el CTRL+U (por ejemplo un DIV con algo de text), dar clic con el botón derecho del mouse y elegir la opción de "Mostrar código fuente seleccionado".
Listo, con esas 2 opciones podrán ver el código generado por jQuery en sus funciones ajax (o algunas otras, no se limiten). A lo mejor no es de mucha ayuda, pero sirve para ver de forma más rápida lo que se esta generando y así poder depurar el código.
1.- Usar addons como Live HTTP Headers que capturarán todo el intercambio de datos entre el navegador y el servidor (pero mostrando incluso cabeceras, etc.).
2.- Viendo la página cuyo contenido fue generado por $.load, $.post, etc. y sólo seleccionar los elementos generados por el jQuery y que no aparecen en el CTRL+U (por ejemplo un DIV con algo de text), dar clic con el botón derecho del mouse y elegir la opción de "Mostrar código fuente seleccionado".
Listo, con esas 2 opciones podrán ver el código generado por jQuery en sus funciones ajax (o algunas otras, no se limiten). A lo mejor no es de mucha ayuda, pero sirve para ver de forma más rápida lo que se esta generando y así poder depurar el código.
martes, 30 de agosto de 2011
Warning: Cannot use a scalar value as an array (solución)
Vaya, en una aventura en PHP me encontré con este error el cual se vuelve un poco fastidioso y a veces difícil de entender porque puede producirse de una forma rara. Aca un ejemplo:
Si creamos un array de la siguiente forma:
$my_table[0] = "A value";
$my_table[1] = "Other value";
El código funcionará pero no siempre lo cual es algo raro, pero así es. La forma correcta será declarando la variable de forma correcta antes de ingresarle los valores:
$my_table = array();
$my_table[0] = "A value";
$my_table[1] = "Other value";
Esto solo conduce a ser más estricto al declarar ese tipo de variables.
Si creamos un array de la siguiente forma:
$my_table[0] = "A value";
$my_table[1] = "Other value";
El código funcionará pero no siempre lo cual es algo raro, pero así es. La forma correcta será declarando la variable de forma correcta antes de ingresarle los valores:
$my_table = array();
$my_table[0] = "A value";
$my_table[1] = "Other value";
Esto solo conduce a ser más estricto al declarar ese tipo de variables.
jueves, 28 de julio de 2011
iDNX RS
Deje pendiente por ahora la programación del administrador de contenidos educativos para hacer una versión en php del iDNX RS la cual traerá muchas ventajas y probablemente alguno que otro cambio en los procesos del mismo (por compatibilidad con la nueva plataforma).
La principal ventaja es que será un servicio online por lo que cualquiera usuario desde cualquier computadora o dispositivo móvil podrá acceder, consultar y demás, la aplicación sin necesidad de tener un programa cliente y un servidor.
Por ahora estoy en el diseño del layout en css 1 y 2 (el 3 por incompatiblidad con ciertos navegadores lo dejaré fuera), en la configuración de algunas rutinas jquery para brindar una interfaz lo más parecida a una aplicación desktop, y en la estructura de la base de datos y del root folder. Las partes que irán en ajax pondré al final ya que sólo las usaré para optimizar las rutinas y reducir la carga del servidor.
Si alguien con conocimientos en php, css, ajax, jquery ó javascript quiere contribuir, contácteme por msn.
La principal ventaja es que será un servicio online por lo que cualquiera usuario desde cualquier computadora o dispositivo móvil podrá acceder, consultar y demás, la aplicación sin necesidad de tener un programa cliente y un servidor.
Por ahora estoy en el diseño del layout en css 1 y 2 (el 3 por incompatiblidad con ciertos navegadores lo dejaré fuera), en la configuración de algunas rutinas jquery para brindar una interfaz lo más parecida a una aplicación desktop, y en la estructura de la base de datos y del root folder. Las partes que irán en ajax pondré al final ya que sólo las usaré para optimizar las rutinas y reducir la carga del servidor.
Si alguien con conocimientos en php, css, ajax, jquery ó javascript quiere contribuir, contácteme por msn.
lunes, 11 de julio de 2011
Un buen programa y su base de datos
Refiriéndonos a programas de administración de contenido, talleres, ptv, etc., se debe ser conciente que una de las características con la cual deben contar dichos programas es con la capacidad de ser adaptable a las necesidades de cada quien.
Me es muy frecuente ver códigos o programas limitados en ese rugro ya que no se pueden agregar datos, categorías, etc. a placer. Así que uno de los consejos para programar sería definitivamente el uso de algún motor de bases de datos (sqlite, access, etc.) siempre estructurándola de una forma eficiente.
Es precisamente en las bases de datos donde reside la adaptabilidad, parte de la velocidad de procesamiento (otra parte en las consultas sql correctas) entre otros beneficios. La clave esta en una buena estructura en la interrelación y dependencia entre tablas.
Uno de los errores más comunes es creer que con un par de tablas en la base datos bastaría para poder almacenar todo lo que queremos, pero es precisamente ahí donde se pueden generar sobresaturaciones que resulten en la ralentización del retorno de datos. Ya con una buena estrutura solo resta obtener los datos mediante consultas sql un tanto complejas que permitan disminuir el número de éstas.
Si se usan bases de datos online es importante preservar la seguridad de los datos como nombre de usuario, password, etc. ya que nunca se esta exento de un ataque a la misma.
Un ejemplo claro de adaptibilidad de contenidos, permisos y otras funciones compartidas, son los sistemas de foros como vbulletin y phpbb, por mencionar algunos, junto con sus deficiencias mencionadas en publicaciones anteriores.
Por ahora estoy desarrollando una aplicación, que luego trasladare a php, para la administración de contenidos educativos así que "stay tuned".
Me es muy frecuente ver códigos o programas limitados en ese rugro ya que no se pueden agregar datos, categorías, etc. a placer. Así que uno de los consejos para programar sería definitivamente el uso de algún motor de bases de datos (sqlite, access, etc.) siempre estructurándola de una forma eficiente.
Es precisamente en las bases de datos donde reside la adaptabilidad, parte de la velocidad de procesamiento (otra parte en las consultas sql correctas) entre otros beneficios. La clave esta en una buena estructura en la interrelación y dependencia entre tablas.
Uno de los errores más comunes es creer que con un par de tablas en la base datos bastaría para poder almacenar todo lo que queremos, pero es precisamente ahí donde se pueden generar sobresaturaciones que resulten en la ralentización del retorno de datos. Ya con una buena estrutura solo resta obtener los datos mediante consultas sql un tanto complejas que permitan disminuir el número de éstas.
Si se usan bases de datos online es importante preservar la seguridad de los datos como nombre de usuario, password, etc. ya que nunca se esta exento de un ataque a la misma.
Un ejemplo claro de adaptibilidad de contenidos, permisos y otras funciones compartidas, son los sistemas de foros como vbulletin y phpbb, por mencionar algunos, junto con sus deficiencias mencionadas en publicaciones anteriores.
Por ahora estoy desarrollando una aplicación, que luego trasladare a php, para la administración de contenidos educativos así que "stay tuned".
martes, 11 de enero de 2011
Lo malo de un foro
Creo que el sistema de foros, ya sean phpbb o cualquier otro, pueden no ser siempre la mejor opción en el renglón del orden de la información.
En los foros abiertos (con los cuales ya no estoy tan de acuerdo) de cualquier temática es muy común encontrarse con los siguientes casos:
1.- Muchos noobs (novatos) en el tema tratado en los foros suelen crear posts (temas) con títulos no muy explicativos como "AYUDAAA" ó "URGENTEEE".
2.- La mayoria de ese tipo de posts contienen dudas anteriormente respondidas en otros posts.
3.- La respuesta clásica por parte de otros usuarios, en ese tipo de circunstancias, suele ser "Busca en el foro" ó "Lee bien la sección de tutoriales".
4.- El post que contiene la respuesta que ocupa el noob tiene fecha muy antigua y no tiene un título descriptivo que ayude a su fácil localización mediante la búsqueda.
Si bien es cierto que en la mayoría de las veces la culpa suele ser del noob (por flojera de no querer buscar ni leer) también debemos de aceptar que parte de esa culpa corresponde al sistema de foros o a la inexperiencia en la configuración del mismo por parte de los administradores (y también a la flojera de responder algo que ya ha sido respondido varias veces).
A falta de un sistema alterno, los administradores o moderadores deben localizar las preguntas más frecuentes y hacer una sección propiamente denominada como tal (o FAQ por sus siglas en inglés). A su vez, siendo los expertos en los temas del foro, deben renombrar los títulos de los posts.
Si la intención de un foro es enseñar o dar soporte, o ambas, se debe entonces crear un temario (índice) donde se enumeren los posts denominados TUTORIALES y donde cada tema lleve el orden correcto, y una descripción del mismo, para que el noob (o cualquier usuario) sepa en que nivel va y hasta donde quiere llegar. Cada tutorial debe mencionar además los posts que deben leer antes de continuar con el proceso descrito en el mismo.
La sección de TUTORIALES no debe estar abierta a respuestas (comentarios) por parte de los usuarios ya que sólo generan páginas y más páginas de comentarios que no llevan a nada. Una mejor opción es actualizar el tutorial en base a las verdaderas discusiones del tema llevadas a cabo una sección de SOPORTE.
Ahora, ya tendríamos 2 secciones las cuales se deben interrelacionar. La sección FAQ debe ligarse a la sección de TUTORIALES.
Los keywords (palabras llave) en ambas secciones son muy importantes para realizar búsquedas más exactas. El uso de un campo en nuestra base de datos que lleve el conteo de las visitas a ciertos posts también ayuda a la hora de mostrar los resultados de las búsquedas (al estilo google).
La constante actualización de los posts ya creados en la sección de FAQ también es importante. El sistema de notificaciones de actualización de facebook es un excelente ejemplo de como organizar la información, ya que permite a los usuarios consultar la nueva información en posts de fechas pasadas sin tener que alterar el orden en que los posts se muestran al ingresar al foro correspondiente (eso es algo malo en los foros cuando estan ordenados por fecha).
El uso de un GLOSARIO también es indispensable, porque quienes no dominan el tema encontrarán términos que no comprendan en los posts.
Todo esto lleva a la simplificación del proceso de enseñanza mediante foros, así como a la reducción de posts innecesarios por parte de noobs (y a una mejor base de datos). Aunque como menciona el dicho: "Haz algo a prueba de idiotas y encontrarás a un idiota mejor" LOL.
Bien se puede crear un sistema nuevo que lleve a una organización como la presentada en líneas anteriores, pero también es cierto que podemos configurar nuestros foros de dicha forma. Sólo bastan 4 secciones para hacerlo: FAQ, TUTORIALES, SOPORTE y GLOSARIO.
Una sección siempre controversial (por políticas de hosting, país, etc.) es el de las descargas. Para ello creo que siempre será mejor un FTP privado. El comprimir los archivos con contraseña y cuyos nombres de archivo no sean TAN descriptivos ayudarán a la privacidad del mismo.
Pero esto es sólo una opinión personal.
En los foros abiertos (con los cuales ya no estoy tan de acuerdo) de cualquier temática es muy común encontrarse con los siguientes casos:
1.- Muchos noobs (novatos) en el tema tratado en los foros suelen crear posts (temas) con títulos no muy explicativos como "AYUDAAA" ó "URGENTEEE".
2.- La mayoria de ese tipo de posts contienen dudas anteriormente respondidas en otros posts.
3.- La respuesta clásica por parte de otros usuarios, en ese tipo de circunstancias, suele ser "Busca en el foro" ó "Lee bien la sección de tutoriales".
4.- El post que contiene la respuesta que ocupa el noob tiene fecha muy antigua y no tiene un título descriptivo que ayude a su fácil localización mediante la búsqueda.
Si bien es cierto que en la mayoría de las veces la culpa suele ser del noob (por flojera de no querer buscar ni leer) también debemos de aceptar que parte de esa culpa corresponde al sistema de foros o a la inexperiencia en la configuración del mismo por parte de los administradores (y también a la flojera de responder algo que ya ha sido respondido varias veces).
A falta de un sistema alterno, los administradores o moderadores deben localizar las preguntas más frecuentes y hacer una sección propiamente denominada como tal (o FAQ por sus siglas en inglés). A su vez, siendo los expertos en los temas del foro, deben renombrar los títulos de los posts.
Si la intención de un foro es enseñar o dar soporte, o ambas, se debe entonces crear un temario (índice) donde se enumeren los posts denominados TUTORIALES y donde cada tema lleve el orden correcto, y una descripción del mismo, para que el noob (o cualquier usuario) sepa en que nivel va y hasta donde quiere llegar. Cada tutorial debe mencionar además los posts que deben leer antes de continuar con el proceso descrito en el mismo.
La sección de TUTORIALES no debe estar abierta a respuestas (comentarios) por parte de los usuarios ya que sólo generan páginas y más páginas de comentarios que no llevan a nada. Una mejor opción es actualizar el tutorial en base a las verdaderas discusiones del tema llevadas a cabo una sección de SOPORTE.
Ahora, ya tendríamos 2 secciones las cuales se deben interrelacionar. La sección FAQ debe ligarse a la sección de TUTORIALES.
Los keywords (palabras llave) en ambas secciones son muy importantes para realizar búsquedas más exactas. El uso de un campo en nuestra base de datos que lleve el conteo de las visitas a ciertos posts también ayuda a la hora de mostrar los resultados de las búsquedas (al estilo google).
La constante actualización de los posts ya creados en la sección de FAQ también es importante. El sistema de notificaciones de actualización de facebook es un excelente ejemplo de como organizar la información, ya que permite a los usuarios consultar la nueva información en posts de fechas pasadas sin tener que alterar el orden en que los posts se muestran al ingresar al foro correspondiente (eso es algo malo en los foros cuando estan ordenados por fecha).
El uso de un GLOSARIO también es indispensable, porque quienes no dominan el tema encontrarán términos que no comprendan en los posts.
Todo esto lleva a la simplificación del proceso de enseñanza mediante foros, así como a la reducción de posts innecesarios por parte de noobs (y a una mejor base de datos). Aunque como menciona el dicho: "Haz algo a prueba de idiotas y encontrarás a un idiota mejor" LOL.
Bien se puede crear un sistema nuevo que lleve a una organización como la presentada en líneas anteriores, pero también es cierto que podemos configurar nuestros foros de dicha forma. Sólo bastan 4 secciones para hacerlo: FAQ, TUTORIALES, SOPORTE y GLOSARIO.
Una sección siempre controversial (por políticas de hosting, país, etc.) es el de las descargas. Para ello creo que siempre será mejor un FTP privado. El comprimir los archivos con contraseña y cuyos nombres de archivo no sean TAN descriptivos ayudarán a la privacidad del mismo.
Pero esto es sólo una opinión personal.
lunes, 10 de enero de 2011
Búsquedas concatenadas en SQLite3
La forma habitual de buscar 1 parámetro en más de 2 campos de una tabla sería por ejemplo:
[code]SELECT * FROM tabla WHERE campo1='parametro' OR campo2='parametro'[/code]
Pero hoy les muestro como hacer una búsqueda concatenada para un mismo parámetro.
Primero comenzaré recordando la forma de concatenar (unir) campos en una consulta. Para concatenar 2 o más campos deben usar este doble signo ||. Ejemplo:
[code]SELECT nombre||apellido FROM tabla[/code]
Esto nos arrojaría un resultado como RamónPérez (noten que no hay espacio entre el apellido y el nombre).
Así que para dejar el espacio entre ambos campos debemos concatenarlo (sin olvidar las tildes):
[code]SELECT nombre||' '||apellido FROM tabla[/code]
Estos nos arrojaría un resultado como Ramón Pérez (noten que el espacio ahora si aparece). Podemos poner lo que sea dentro de ' ' y crear concatenaciones mejores, como por ejemplo: ||'Nombre: '||nombre||' Apellido: '||apellido
Bueno, pues es similar para cuando queremos concatenar campos posterior a la cláusula WHERE en una consulta de selección. Ejemplo:
[code]SELECT * FROM tabla WHERE nombre||apellido='Alonso'[/code]
En esta consulta en lugar de hacer algo como "select * from tabla where nombre='alonso' or apellido='alonso'" estamos concatenando el campo de búsqueda (nombre||apellido). Esto nos ahorra líneas de código y ya en usos un poco más complejos podemos realizar consultas en campos multiples campos keywords con simples scripts.
Espero les agrade esa pequeña info de sqlite.
[code]SELECT * FROM tabla WHERE campo1='parametro' OR campo2='parametro'[/code]
Pero hoy les muestro como hacer una búsqueda concatenada para un mismo parámetro.
Primero comenzaré recordando la forma de concatenar (unir) campos en una consulta. Para concatenar 2 o más campos deben usar este doble signo ||. Ejemplo:
[code]SELECT nombre||apellido FROM tabla[/code]
Esto nos arrojaría un resultado como RamónPérez (noten que no hay espacio entre el apellido y el nombre).
Así que para dejar el espacio entre ambos campos debemos concatenarlo (sin olvidar las tildes):
[code]SELECT nombre||' '||apellido FROM tabla[/code]
Estos nos arrojaría un resultado como Ramón Pérez (noten que el espacio ahora si aparece). Podemos poner lo que sea dentro de ' ' y crear concatenaciones mejores, como por ejemplo: ||'Nombre: '||nombre||' Apellido: '||apellido
Bueno, pues es similar para cuando queremos concatenar campos posterior a la cláusula WHERE en una consulta de selección. Ejemplo:
[code]SELECT * FROM tabla WHERE nombre||apellido='Alonso'[/code]
En esta consulta en lugar de hacer algo como "select * from tabla where nombre='alonso' or apellido='alonso'" estamos concatenando el campo de búsqueda (nombre||apellido). Esto nos ahorra líneas de código y ya en usos un poco más complejos podemos realizar consultas en campos multiples campos keywords con simples scripts.
Espero les agrade esa pequeña info de sqlite.
Una buena estructura de base de datos
El tener una buena estructura en nuestra base de datos es imprescindible no sólo por orden, sino también para reducir el peso de la misma así como poder brindar mejores reportes (con más detalles autocalculables por ejemplo). Ah, y la rápidez con que se hagan las consultas también dependerá de ello.
Aunque el tener una sola tabla donde acumular ciertos registros que luego puedan ser agrupados para mostrar resumenes o concentrados de la misma puede llegar a ser tentador, no en todos los casos es viable.
Un ejemplo sería el sólo contar con una tabla para registros de facturas donde se ingresan todos los artículos de dicha factura y cuando se quiere ver en forma de concentrado (por folio) sólo se agrupasen los registros. El error en este tipo de tablas y usos es que pueden llegarse a repetir datos en ciertos campos de forma innecesaria (como el número de folio, nombre del cliente, RFC, etc.), provocando el incremento en el peso de nuestra base de datos.
Una solución a esto es contar con 2 tablas interrelacionadas donde la primera sólo sea un control del ID de la factura con detalles como folio, fecha, cliente, RFC, etc. Y la segunda tabla con los registros de los artículos y precios de los mismos.
De esta forma evitamos repetir los datos de RFC, cliente, fecha y otros, en el registro de cada artículo que constituye la factura. Esto sin mencionar que a veces se usa un campo para comentarios que suelen extenderse bastante.
Un consejo es que recuerden que múltiples tablas pueden accederse en una sola consulta mendiante el uso de left join, full join o consultas en formato ANSI. Por ello a veces es necesario manejar más de un índice único para cada registro en nuestras tablas.
Aunque el tener una sola tabla donde acumular ciertos registros que luego puedan ser agrupados para mostrar resumenes o concentrados de la misma puede llegar a ser tentador, no en todos los casos es viable.
Un ejemplo sería el sólo contar con una tabla para registros de facturas donde se ingresan todos los artículos de dicha factura y cuando se quiere ver en forma de concentrado (por folio) sólo se agrupasen los registros. El error en este tipo de tablas y usos es que pueden llegarse a repetir datos en ciertos campos de forma innecesaria (como el número de folio, nombre del cliente, RFC, etc.), provocando el incremento en el peso de nuestra base de datos.
Una solución a esto es contar con 2 tablas interrelacionadas donde la primera sólo sea un control del ID de la factura con detalles como folio, fecha, cliente, RFC, etc. Y la segunda tabla con los registros de los artículos y precios de los mismos.
De esta forma evitamos repetir los datos de RFC, cliente, fecha y otros, en el registro de cada artículo que constituye la factura. Esto sin mencionar que a veces se usa un campo para comentarios que suelen extenderse bastante.
Un consejo es que recuerden que múltiples tablas pueden accederse en una sola consulta mendiante el uso de left join, full join o consultas en formato ANSI. Por ello a veces es necesario manejar más de un índice único para cada registro en nuestras tablas.
Suscribirse a:
Entradas (Atom)