Archivo de la categoría: programación

Sigue la lucha contra el Spam

[blog] [programación]

Ya comenté en un post anterior los problemas que estaba teniendo con el Spam.

Como tuve los problemas de indexación en Google, dejaron de bonbardearme, pero ahora que está resuelto, vuelvo a ver una buena cantidad de comentarios Spam nuevos cada vez que me paso por el blog. Dije la última vez que WordPress no traía un sistema anti-spam integrado, y era mentira. Tras investigar un poco veo que trae Akismet, que parece ser una solución muy robusta anti-spam. El problema es que al igual que me pasaba con ReCaptcha, el plugin de Akismet intenta hacer una conexión externa y parece ser el servidor gratuito de FreeHostia no lo permite, por lo que sigo sin poder usar esa opción.

Algo que veo que tienen en común todos los comentarios spam es que no tienen especificado un correo electrónico, cuando es indispensable para mandar un comentario. Por más pruebas que hago no consigo replicar la creación de un comentarios sin correo, lo que me hace pensar que de alguna manera deben estar saltándose alguna de las comprobaciones de wordpress, aunque no intuyo como.

Buscando el momento exacto en el que se guardan los comentarios en la base de datos, parece ser que es en el fichero /wp-includes/comment.php, en el método wp_insert_comment. No se siquiera si es posible que estén llegando hasta este punto evitando algún paso intermedio, pero por si acaso he añadido aquí una comprobación adicional para verificar que el correo del comentario existe y es correcto:

if(!is_email($comment_author_email)){
  return -1;
}

Ahora toca esperar y ver si ha sido efectivo…

ACTUALIZACIÓN 12/12/2011

Aunque el código anterior parecía efectivo, al impedir por completo la creación del nuevo comentario, no había manera de comprobarlo. Si tras unas semanas no aparecía nuevo spam, se podría decir que ha funcionado, pero como quería algo más definitivo, he modificado el código por lo siguiente:

if(!is_email($comment_author_email)){
  $comment_approved = 'spam';
}

Así, lo que hago es permitir que se inserte en la base de datos, pero marcado directamente como spam, y ya ha dado sus frutos, acabo de ver un nuevo comentario de spam etiquetado automáticamente como tal. Se confirma entonces que de alguna manera están evitando el método normal de añadir un comentario en wordpress, y que este código es efectivo en contrarrestarlo mientras no rellenen el campo de correo.

Leer la última linea de un fichero con Java

[programación]

Tras buscar un poco por internet, parece que no hay una forma sencilla y eficiente de lograr esto. La forma más simple es leer todas las líneas del fichero hasta llegar a la última. Sencillo, pero si el fichero es muy grande, no es una opción viable. La otra opción es utilizar la clase RandomAccessFile para situarnos en un lugar cercano al final del fichero y leer las líneas a partir de ahí, pero en este caso debes estimar la longitud máxima de las líneas, por lo que tampoco es perfecto. En mi caso, la longitud es constante, por lo que no supone un problema. Este es el método que he construido:

public static String getFileLastLine(String filePath, long maxLineLength){
    String lastLine = "";

    RandomAccessFile raf = null;

    try{
        raf = new RandomAccessFile(filePath, "r");

        long fileLength = raf.length();
        if(fileLength > maxLineLength) {
            raf.seek(fileLength - maxLineLength);
        }
        String readLine = "";
        while((readLine = raf.readLine()) != null){
            lastLine = readLine;
        }                     
    }
    catch (Exception e){
        e.printStackTrace();
    }
    finally{
        if(raf != null){
            try{
                raf.close();
            }
            catch(Exception e){
                // Nada
            }
        }
    }

    return lastLine;
}

Usando Fancybox y corrigiendo error en CKEditor usando typeof

[fotografía] [programación]

Quiero empezar a poner más imágenes ilustrando los artículos del blog, así que hace unos días me puse a buscar algún plugin que permitiera ampliar las imágenes de los artículos. Encontré unos cuantos, pero todos eran bastante similares y tampoco encontré muchas demos de funcionamiento. Al final, como el que usa Baratijas Blog me gustaba, pues me he puesto el mismo:

Fancybox plugin para Wordpress

Al instalarlo, me he puesto a hacer pruebas, y en la página principal del blog funcionaba sin problemas, pero al pinchar en alguno de los artículos dejaba de funcionar. Pensaba que podía ser problema del tema, pero al mirar la consola de errores de Javascript, he visto que lo que fallaba era un script del plugin para integrar el CKEditor. El error que me daba era “getUserSetting is not defined” en el fichero ckeditor.utils.js. Es posible que este error ya esté solucionado en las nuevas versiones del plugin, pero como era una chorrada he realizado yo la corrección.

La modificación es sustituir esta línea:

if(ckeditorSettings.autostart && getUserSetting('editor') == 'tinymce'){

Por esta otra:

if(ckeditorSettings.autostart && typeof getUserSetting == 'function' && getUserSetting('editor') == 'tinymce'){

Lo que hacemos con la instrucción typeof es comprobar que el método getUserSetting está definido antes de intentar invocarlo.

Y con esto, ya no salta el error y el lightbox vuelve a funcionar. La prueba de concepto:

Una rosa

[ACTUALIZACIÓN] Al abandonar Wordpress, Fancybox ya no está instalado.

Java y caracteres japoneses

[japonés] [programación]

El programa que uso para repasar japonés está hecho en Java. Según voy avanzando en el número de Kanji aprendidos, voy descubriendo que algunas de las palabras clave están incompletas o tienen faltas de ortografía. Hace un tiempo ya implementé una solución consistente en un fichero con las correcciones, lo cual fue bastante sencillo. El problema ha sido el último error que he encontrado, que no afecta a la palabra clave, sí no a el Kanji mostrado.

Cuando salía la palabra clave “parcela” debería aparecer su kanji correspondiente (壌) pero en su lugar aparecía el kanji de “deferencia” (譲). Me puse a investigar como usar caracteres japoneses en Java y en realidad es bastante sencillo. Java trabaja con Unicode, por lo que se pueden incluir estos caracteres tranquilamente en el código. Por ejemplo, este código sería correcto:

char kanji = '譲';

Como la idea es seguir usando un fichero de texto para tener todas las correcciones, intenté guardar el kanji en el fichero y después recuperarlo, pero por algún motivo no conseguí hacerlo funcionar. Pasé entonces a otra solución. En vez de guardar el kanji, guardar su código de Unicode, que en este caso sería u58CC.

Por lo tanto necesitaba dos cosas, obtener el código Unicode de un carácter japonés, y obtener el carácter japonés a partir de su código Unicode.

StackOverflow, como casi siempre, tiene la solución.

Pasar de kanji a unicode

char kanji = '譲';
String unicode = "\u" + Integer.toHexString(kanji | 0x10000).substring(1);

Pasar de unicode a kanji

String unicode = "\u58CC";
char kanji = (char) Integer.parseInt( s.substring(2), 16 );

¡Mucho más fácil de lo que yo pensaba!

Y por último, una página muy útil para obtener el valor Unicode de cualquier caracter y viceversa.

Obtener datos de un Google Docs Spreadsheet mediante Javascript

[programación]

Aunque parezca mentira, google no ofrece una manera sencilla de leer datos de una hora de cálculo pública mediante Javascript. La única manera que he encontrado es publicar la hoja de cálculo como una página web y utilizar Javascript para parsear los datos. Hasta ahora estaba utilizando una API de Yahoo, que funcionaba bien pero con un ligero problema…. SÓLO funcionaba en Firefox. Ni Chrome, ni Safari, ni (sin sorprender a nadie) Explorer.

Pero bueno, en mi público objetivo todos tenían instalado Firefox, así que tan simple como decir “solo funciona en Firefox!!!” 😛

El problema fue cuando un amigo me dijo “NO FUNCIONA EN FIREFOX!!!”. ¿Tal vez era problema de la nueva versión? Efectivamente, en Firefox 4 ya no funcionaba. Siendo esto la gota que colmaba el vaso, me puse a buscar un nuevo script que me permitiera acceder a los datos, encontrando este.

Aquí se puede ver un sencillo ejemplo en funcionamiento y probar tu propia hoja de cálculo.

El ejemplo no es muy intuitivo en cuanto a como obtener los datos (al menos a mi no me lo pareció :P), pero básicamente el objeto “result” es un objeto de JSON, por lo que se puede recorrer el objeto “data” como si fuese un vector. Por ejemplo este código mostraría todos los datos de la hoja de cálculo:

googleSpreadsheet.load(function(result) {
    for(var i = 0; i < result.data.length; i++){
        alert(result.data[i]);
    }
});

El problema es que no hay manera de saber la fila o columna en la que te encuentras, por lo que hay que añadir algo de lógica para tener eso controlado. También me queda comprobar como se comporta cuando la hoja de cálculo tiene varias páginas, pero eso para más adelante 😛

Guardar artículo como borrador en eggBlog

[programación]

Sigo con la ampliación de funcionalidades de eggBlog. En este caso he añadido la posibilidad de guardar borradores de artículos. Hacía tiempo que quería hacer esta modificación, sobre todo porque tiendo a escribir artículos kilométricos que tardo varios días en terminar, y en ocasiones tengo alguna idea buena para un artículo que quiero empezar a escribir y finalizar más adelante.

Pensaba que esta modificación sería sencilla, pero no lo ha sido porque hay que modificar muchas pequeñas cosas en muchos sitios distintos. Incluso ahora que estoy un par de días haciendo pruebas, sigo tenido la sensación de que me estoy dejando algo.

Las módificaciones a realizar son las siguientes:

Primero añadimos una nueva traducción a langs/es.php:

$lang['draft'] = "Borrador";

He añadido un par de funciones útiles a _lib/util.php. La primera comprueba si el usuario actual es el administración:

function userIsAdmin() {
    return !empty( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) &&
    eb_checkadmin( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] );
}

La segunda comprueba si un articulo debe ser visible para el usuario. El administrador debe tener acceso a todos los artículos, pero el resto de usuarios no deben poder ver los borradores:

function articleIsVisibleToUser( $articleId ) {
    settype( $articleId, "integer" );

    if ( userIsAdmin() ) {
        return true;
    }

    $sql = "SELECT article_flag FROM eb_articles"
    ." WHERE article_id = " . $articleId;

    if( $row = mysql_fetch_array( mysql_query( $sql ) ) ) {
        $articleFlag = $row['article_flag'];

        if( $articleFlag == 1 || $articleFlag == 2 ) {
            return true;
        }
    }

    return false;
}

En _lib/news.php tenemos un montón de cambios desperdigados. Esto se debe al nuevo estado que puede tener un artículo. Los estados, incluido el nuevo son:

  • Artículo eliminado (Es decir, los artículos no se eliminan, solo se ocultan)
  • Artículo normal
  • Artículo destacado (Aparecería al inicio de todas las páginas)
  • Borrador de artículo

Para los ficheros con muchos cambios de código, he diseñado una nueva forma para mostrar las diferencias. A ver que tal queda:

Archivo: _lib/news.php - Método: eb_articlelist($n)

$showDrafts = userIsAdmin();

$sql = "SELECT article_id, article_title, article_date, article_flag"
." FROM eb_articles WHERE article_flag = 1";
if( $showDrafts == true ){
    $sql .= " OR article_flag = 3";
}
$sql .= " ORDER BY article_date DESC";

$query = mysql_query( $sql );

Archivo: _lib/news.php – Método: eb_articlelist($n)

.eb_links_article( $row['article_title'], $row['article_id'] ) . '"';
if( $row['article_flag'] == 3 ) {
    $output .= ' style="color:red"';
}
$output .= '>' . $row['article_title'] . '</a></li>';

Archivo: _lib/news.php – Método: eb_articles($n)

$showDrafts = userIsAdmin();

$output=array();
$sql = "SELECT article_id FROM eb_articles WHERE article_flag=1";
if( $showDrafts == true ) {
    $sql .= " OR article_flag = 3";
}    
$sql .= " ORDER BY article_date DESC LIMIT " . $n;

Archivo: _lib/news.php – Método: getArticlesIdsForPage( $page, $n )

$showDrafts = userIsAdmin();

$articleIds = array();
$sql = "SELECT article_id FROM eb_articles WHERE article_flag = 1";
if( $showDrafts == true ) {
    $sql .= " OR article_flag = 3";
}
$sql .= " ORDER BY article_date DESC";

Archivo: _lib/news.php – Método: eb_article( $comments, $id )

$sql = "SELECT article_id, article_date, article_comments, article_title"
.", article_body, user_name, article_flag"
." FROM eb_articles AS a LEFT JOIN eb_users AS u ON a.author_id = u.user_id"
." WHERE article_id = ".$id." ORDER BY article_date DESC";

Archivo: _lib/news.php – Método: eb_article( $comments, $id )


$output .= " <div class="article"> ";

$draftLabel = "";
if( $row['article_flag'] == 3 ) {
    $draftLabel = "<span style="color: red;">(Borrador)</span>";
}

Archivo: _lib/news.php – Método: eb_article( $comments, $id )

$output.=" <h3>" . $draftLabel . "<a href=""
.eb_links_article( $row['article_title'], $row['article_id'] ) . "">"
.$row['article_title'] . "</a></h3>";

Archivo: _lib/news.php – Método: eb_article( $comments, $id )

if(userIsAdmin()){
    $output.=" <a href="admin.php?editarticle=" . $row[0]."">"
    .$lang['edit']." | ."">".$lang['delete']."</a>";
}

Archivo: _lib/news.php – Método: eb_article($comments,$id)

if(userIsAdmin()) {
    $output .= " <a href="admin.php?editcomment=" . $comment[3]
    ."&article=" . $row[0] . "">" . $lang['edit']
    ."</a> | <a href="admin.php?deletecomment=" . $comment[3]
    ."&article=" . $row[0] . "">" . $lang['delete'] . "</a>";
}

Archivo: _lib/news.php – Método: eb_tags($n)

$sql="SELECT tag_name, COUNT(eb_tags_links.article_id) as cnt"
." FROM eb_tags_links LEFT JOIN eb_tags"
." ON eb_tags.tag_id = eb_tags_links.tag_id LEFT JOIN eb_articles"
." ON eb_articles.article_id = eb_tags_links.article_id"
." WHERE article_flag = 1 OR article_flag = 2"
." GROUP BY eb_tags.tag_id ORDER BY cnt";

Archivo: _lib/news.php – Método: eb_tag($n)

$sql="SELECT eb_articles.article_id, article_title"
." FROM eb_articles LEFT JOIN eb_tags_links"
." ON eb_articles.article_id = eb_tags_links.article_id"
." LEFT JOIN eb_tags ON eb_tags.tag_id = eb_tags_links.tag_id"
." WHERE tag_name="" . $tag . "" AND article_flag = 1 OR article_flag = 2"
." ORDER BY article_title";

Como podéis ver no mentía, hay muchos cambios en _lib/news.php. Espero que el nuevo formato permita ver mejor las diferencias entre el código original y el modificado.

Sigamos con los cambios. Ahora le toca a _lib/admin.php. Aquí los cambios están concentrados en los métodos de crear y editar un artículo, por lo que mejor pongo los métodos enteros:

function eb_admin_editarticle( $id ) {
    global $lang;
    $output=" <h3>" . ucwords( $lang['admin'] ) . "</h3> ";
    if( !empty( $_POST['text'] ) ) {
        foreach( $_POST as $key => $value ) {
            $_POST[$key] = str_replace( "&Acirc;", "", $value );
        }
        if( strlen( $_POST['tags'] ) > 1 ) {
            mysql_query( "DELETE FROM eb_tags_links WHERE article_id=" . $id );
            $tags = explode( ",", $_POST['tags'] );
            foreach( $tags as $tag ) {
                $sql = "INSERT INTO eb_tags SET tag_name="" . $tag . """;
                mysql_query( $sql );
                if( mysql_affected_rows() == 1 ) {
                    $tag_ids[] = mysql_insert_id();
                }
                else{
                    $tag_ids[] = mysql_result(
                        mysql_query( "SELECT tag_id FROM eb_tags"
                                  ." WHERE tag_name="" . $tag . """), 0 );
                }
            }
            foreach( $tag_ids as $tag_id ) {
                mysql_query( "INSERT INTO eb_tags_links"
                          ." SET tag_id=" . $tag_id . ", article_id=" . $id );
            }
        }

        $creationDateIfDraft = "";

        $sql = "SELECT article_flag FROM eb_articles"
        ." WHERE article_id = " . $id;

        if( $row = mysql_fetch_array( mysql_query( $sql ) ) ) {
            $articleFlag = $row['article_flag'];
            if( $articleFlag == 3 ) {
                $creationDateIfDraft = "", article_date="" . time();
            }
        }

        $articleStatus = $_POST['sticky'];
        if( !empty( $_POST['saveAsDraft'] ) && $_POST['saveAsDraft'] == "yes" ) {
            $articleStatus = 3;
            $creationDateIfDraft = "", article_date="" . time();
        }

        $sql = "UPDATE eb_articles SET article_flag=" . $articleStatus
        .", article_title="" . str_replace( " ", "", $_POST['title'] )
        ."", article_body="" . $_POST['text']
        ."", article_comments=" . $_POST['comments']
        .", mod_date="" . time()
        .$creationDateIfDraft
        ."" WHERE article_id=" . $id;

        mysql_query( $sql );
        header( 'Location: news.php?id=' . $id );
    }
    else {
        $sql="SELECT article_title, article_body, article_comments, article_flag"
        ." FROM eb_articles WHERE article_id=" . $id;
        $query = mysql_query( $sql );
        $row = mysql_fetch_row( $query );

        $tags = "SELECT tag_name FROM eb_tags LEFT JOIN eb_tags_links"
        ." ON eb_tags_links.tag_id = eb_tags.tag_id WHERE article_id=" . $id;
        $tags = mysql_query( $tags );
        if( mysql_num_rows( $tags ) > 0 ) {
            while( $tags_row = mysql_fetch_array( $tags ) ) $tag_array[] = $tags_row[0];
            $tags = implode( ",", $tag_array );
        }
        else $tags="";

        $output .= '<script type="text/javascript"'
        .' src="_lib/scripts/ckeditor/ckeditor.js"></script>' . " ";

        $output .= " <form action="admin.php?editarticle="
        .$id . "" method="post"> ";
        $output .= " <p><b>" . ucwords( $lang['title'] )
        ."</b><br /><input class="inputtext" type="text" name="title""
        ." value="" . $row[0] . "" /></p> ";
        $output .= " <textarea id="text" name="text"></textarea> ";
        $data = str_replace( '"', '"', $row[1] );
        $data = str_replace( " ", "" +"", $data);
        $output .= '<script type="text/javascript">' . " "
        .'var editor = CKEDITOR.replace( "text" );' . " "
        .'editor.setData( "'.$data.'" )' . " "
        .'</script>';
        $output .= " <p><b>" . ucwords( $lang['tags'] ) . "</b><br />"
        .$lang['tag_seperate']."<br /><input type="text" class="inputtext""
        ." name="tags" value="" . $tags . "" /></p> ";

        // Sticky radio button
        $output .= " <p><b>" . ucwords( $lang['sticky'] )
        ."</b><br /><input type="radio" name="sticky" value="1" ";
        if( $row[3] == 1 || $row[3] == 3 ) {
            $output .= " checked="checked"";
        }
        $output .= "/> " . ucwords( $lang['no'] )
        ."<br /><input type="radio" name="sticky" value="2" ";
        if( $row[3] == 2 ) {
            $output .= " checked="checked"";
        }
        $output .= "/> " . ucwords( $lang['yes'] ) . "<br /></p> ";

        // Comments radio button
        $output .= " <p><b>" . ucwords( $lang['comments'] )
        ."</b><br /><input type="radio" name="comments" value="0" ";
        if( $row[2] == 0 ) $output .= " checked="checked"";
        $output .= "/> " . ucwords( $lang['no'] )
        ."<br /><input type="radio" name="comments" value="1" ";
        if( $row[2] == 1 ) $output .= " checked="checked"";
        $output .= "/> " . ucwords( $lang['yes'] ) . "<br /></p> ";
        $output .= " <input type="hidden" id="saveAsDraft""
        ." name="saveAsDraft" value="no" /> ";
        $output .= " <p><input type="submit" name="submit" value=""
        .ucwords( $lang['save'] ) . "" /> ";
        $output .= " <input type="submit" name="submitDraft" value=""
        .$lang['draft'] . "" onclick="setSaveAsDraftTrue();" /></p> ";
        $output .= " </form> ";
        $output .= '<script type="text/javascript">'
        .'function setSaveAsDraftTrue(){'
        .'var saveAsDraft = document.getElementById( "saveAsDraft" );'
        .'saveAsDraft.value = "yes";'
        .'}'
        .'</script>';


        eb_head('');
        echo $output;
        eb_foot();
    }
}

function eb_admin_newarticle() {
    global $lang;
    $output = " <h3>" . ucwords( $lang['new_article'] ) . "</h3> ";
    if( !empty( $_POST['text'] ) ) {
        foreach( $_POST as $key => $value ) {
            $_POST[$key] = str_replace( "&Acirc;", "", $value);
        }
        $articleStatus = $_POST['sticky'];
        if( !empty( $_POST['saveAsDraft'] ) && $_POST['saveAsDraft'] == "yes" ) {
            $articleStatus = 3;
        }
        $sql = "INSERT INTO eb_articles SET article_flag=" . $articleStatus
        .",article_date = "" . time() . "",mod_date="" . time()
        ."",author_id = "" . $_SESSION['user_id-' . $_SERVER['SERVER_NAME']]
        ."", article_title = "" . str_replace( """, " ", $_POST['title'] )
        ."", article_body = "" . $_POST['text']
        ."", article_comments = " . $_POST['comments'];

        mysql_query( $sql );

        $article_id = mysql_insert_id();

        if( strlen( $_POST['tags'] ) > 1 ) {
            $tags = explode( ",", $_POST['tags'] );
            foreach( $tags as $tag ) {
                $sql = "INSERT INTO eb_tags SET tag_name="" . $tag . """;
                mysql_query( $sql );
                if( mysql_affected_rows() == 1 ) {
                    $tag_ids[] = mysql_insert_id();
                }
                else{
                    $tag_ids[] = mysql_result(
                        mysql_query( "SELECT tag_id FROM eb_tags"
                                  . " WHERE tag_name="" . $tag . """), 0 );
                }
            }
            foreach( $tag_ids as $tag_id ) {
                mysql_query( "INSERT INTO eb_tags_links".
                           " SET tag_id=" . $tag_id . ",article_id=" . $article_id);
            }
        }

        header( 'Location: news.php?id=' . $article_id );
    }
    else {
        $output .= '<script type="text/javascript"'
        .' src="_lib/scripts/ckeditor/ckeditor.js"></script>' . " "
        ." <form action="admin.php?id=newarticle" method="post"> "
        ." <p><b>" . ucwords( $lang['title'] ) . "</b><br />"
        ."<input class="inputtext" type="text" name="title" /></p> "
        ." <textarea class="ckeditor" id="text" name="text">"
        ."</textarea> "
        ." <p><b>".ucwords( $lang['tags'] ) . " " . $lang['tag_seperate']
        ."<br /><input type="text" class="inputtext" name="tags" /></p> "
        ." <p><b>" . ucwords( $lang['sticky'] ) . "</b><br />"
        ."<input type="radio" name="sticky" value="1" checked="checked" /> "
        .ucwords( $lang['no'] ) . "<br />"
        ."<input type="radio" name="sticky" value="2" /> "
        .ucwords( $lang['yes'] ) . "</p> "
        ." <p><b>" . ucwords( $lang['comments'] )
        ."</b><br /><input type="radio" name="comments" value="0" /> "
        .ucwords( $lang['no'] ) . "<br />"
        ."<input type="radio" name="comments" value="1""
        ." checked="checked" /> " . ucwords( $lang['yes'] ) . "</p> "
        ." <input type="hidden" id="saveAsDraft" name="saveAsDraft""
        ." value="no" /> "
        ." <p><input type="submit" name="submit" value=""
        .ucwords( $lang['save'] ) . "" /> "
        ." <input type="submit" name="submitDraft" value=""
        .$lang['draft'] . "" onclick="setSaveAsDraftTrue();" /></p> "
        ." </form> "
        .'<script type="text/javascript">'
        .'function setSaveAsDraftTrue() {'
        .'var saveAsDraft = document.getElementById( "saveAsDraft" );'
        .'saveAsDraft.value = "yes";'
        .'}'
        .'</script>';

        return $output;
    }
}

El último cambio se realiza en sitemap.php, para evitar enviar a Google las URLs de los borradores:

Archivo: sitemap.php

$sql = "SELECT article_id, mod_date, article_title, article_date"
." FROM eb_articles"
." WHERE article_flag = 1 OR article_flag = 2"
." ORDER BY article_date DESC";

Me gustaría destacar la falta de indentación en el código original. Entiendo que en PHP es importante el rendimiento, pero el código spaghetti y los métodos de 200 líneas no facilitan la tarea de modificar el código. Si EggBlog fuese un producto acabado, entendería la optimización del código, pero no es así, está falto de muchas funcionaliades y tiene aspectos muy mejorables, por lo que considero que la legibilidad del código debería ser una prioridad.

También he sacado varias conclusiones con esta modificación. La primera es que compartir el código en los artículos no es la mejor opción, por lo que me estoy planteando subir el código a algún repositorio para poder compartirlo con todos.

La otra es que no me gusta el código actual de eggBlog. Es buen código en cuanto a que funciona muy bien y tiene muy pocos fallos (aunque he encontrado algunos que comentaré en un artículo posterior), pero el problema de la legibilidad hace que cada vez que toco algo, me entren ganas de reordenarlo todo.

Supongo que esto se debe a que principalmente he programado en Java, donde me gusta mantener el código extremadamente limpio, y que soy un gran fan del refactoring, donde la legibilidad es mucho más importante que el rendimiento.

Por ello he descubierto que aún se muy poco sobre las mejores técnicas de programación en PHP, por lo que he dedicado un tiempo en buscar artículos de buenas prácticas. Más adelante haré un artículo recopilatorio con las que me han parecido más interesantes.

Bueno, lo dejo ya con el que ha sido sin duda mi artículo más largo. Por suerte he podido usar la función de borradores para escribirlo! 😀

Añadir seguimiento de nuevos comentarios en eggBlog

[programación]

Otro de los pequeños fallos de EggBlog es su sistema de comentarios. El problema no es como se deja un nuevo comentario, que por lo que he podido probar funciona bastante bien. El problema es que no hay manera de saber si alguien ha dejado un nuevo comentario en un artículo antiguo.

Los artículos de portada no tienen tanto problema, ya que puede que te des cuenta al moverte por la página, pero si alguien deja un comentario en un artículo de hace unos meses, no hay un buen sistema para enterarse. O compruebas todos los artículos para ver si hay un comentario nuevo o lo miras en la base de datos, que es más rápido, pero tampoco es una buena solución.

Mi primera idea fue que cuando alguien deja un nuevo comentario, se enviara un correo al administrador que le avisara. Esta idea es bastante buena, ya que te mantiene informado rápidamente sobre comentarios nuevos. El problema es el posible aluvión de correos en un futuro, aunque siempre se puede poner un filtro o algo.

Me puse a investigar como enviar correos desde PHP y resulta ser bastante sencillo. En la página oficial de PHP está toda la información. Sin embargo, al realizar algunas pruebas descubrí que mi servidor actual, al ser gratuito, no soporta el envío de correos desde PHP, por lo que no me quedó otra opción que buscar una alternativa.

Empecé a pensar en otra solución. Si no podía realizar un aviso externo, tendría que añadir algún elemento visual a la página que me indicara que hay comentarios nuevos. Decidía añadir una nueva entrada en la barra lateral que indique el número de comentarios nuevos y te lleve a una página desde donde poder revisarlos.

Menu con el número de comentarios pendientes en EggBlogEl número a la derecha de “Moderar Comentarios” indica el número de comentarios nuevos desde la última vez que se limpió el listado.

Si pulsamos el enlace llegamos a una nueva página donde aparece un listado con todos los comentarios nuevos, y donde cada comentario es un enlace al artículo donde ha sido publicado. De esta manera tenemos una forma sencilla de revisarlos todos, y que no se quede alguno perdido entre artículos viejos. Por último tenemos un botón que limpia el listado y devuelve el contador a cero.

Las modificaciones al código para implementar esto son las siguientes:

Primero añadimos las traducciones a es.php:

$lang['author'] = "Autor/a";
$lang['clear_list'] = "Limpiar Listado";
$lang['comment'] = "Comentario";
$lang['date'] = "Fecha";
$lang['hiding_comments'] = "Ocultando comentarios anteriores a la fecha";

Lo siguiente es crear una función que devuelva una fecha formateada. Podríamos añadir esta función a _lib/global.php, pero yo he decidido crear un fichero PHP con funciones útiles llamado _lib/util.php. La función devuelve la fecha en el mismo formato que se ve en los artículos, y su código es:

function getNiceDate($date){
    global $lang;

    return date("g:ia",$date) . ", " . ucwords( $lang['day' . date( "w", $date )] ) ." "
    .date( "j", $date ) . " " . ucwords( $lang['month' . date( "n", $date )] )
    ." " . date( "Y", $date );    
}

Si creáis el fichero nuevo, habrá que incluirlo en _lib/global.php:

require_once '_lib/util.php';

A continuación ya podemos crear la página desde donde moderar los comentarios. La he llamado moderate-comments.php y su código es:

<?php

require_once '_lib/global.php';
global $config;

eb_pre();

if( empty( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) ) {
    header( "Location: index.php" );
}
elseif( !eb_checkadmin( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) ) {
    header( "Location: index.php" );
}
else {
    eb_head('');

    $commentsList = "";

    $currentTime = time();
    $showForm = true;

    if( !empty( $_POST['viewTime'] ) ) {
        $viewTime = $_POST['viewTime'];
        $showForm = false;

        $sql = "UPDATE eb_comments SET comment_date = " . $viewTime
        ." WHERE comment_id = 0";

        mysql_query( $sql );

        $commentsList = "<p>" . $lang['hiding_comments'] . ": "
        .getNiceDate( $viewTime ) . "</p>";
    }
    else{
        $sql = "SELECT u.user_name, c.comment_id, c.article_id,"
        ." c.comment_body, c.comment_date, a.article_title"
        ." FROM eb_comments c, eb_users u, eb_articles a"
        ." WHERE c.comment_date > ("
        ."    SELECT comment_date FROM eb_comments"
        ."    WHERE comment_id = 0 )"
        ." AND u.user_id = c.author_id"
        ." AND a.article_id = c.article_id"
        ." AND c.author_id <> " . $_SESSION["user_id-" . $_SERVER['SERVER_NAME']]
        ." ORDER BY c.comment_id";
        $query = mysql_query( $sql );

        while( $row = mysql_fetch_array( $query ) ) {
            $commentBody = $row[3];
            if( strlen( $commentBody ) > 70 ) {
                $commentBody = substr( $commentBody, 0, 70 ) . "...";
            }

            $commentsList .= "<p><b>" . $lang['author'] . ": " . $row[0] . "<br />"
            ."<b>" . $lang['date'] . ": " . getNiceDate( $row[4] ) . "<br />"
            ."<b>" . $lang['comment'] . ":</b> <a href=""
            .eb_links_article( $row[5], $row[2] )
            ."#comment" . $row[1] . "">" . $commentBody . "</a></p> ";
        }
    }
?>

<h3><?php echo ucwords( $lang['moderate_comments'] ) ?></h3>

<?php
    echo $commentsList;
    if( $showForm == true ) {
?>

<form name="resetCommentsTime" action="moderate-comments.php" method="post">
    <input type="hidden" name="viewTime" value="<?php echo $currentTime; ?>" />
    <input type="submit" name="submit" value="<?php echo $lang['clear_list']; ?>" />
</form>

<?php
    }
    eb_foot();
}
?>

Como se pude observar en el código, los enlaces no solo llevan al artículo, sí no también al propio comentario. Para ello debemos añadir el ancla correspondiente para cada artículo. Esto se realiza en el método eb_article() del fichero _lib/news.php, donde:

<p class="comment">

Pasa a ser:

<p class="comment"><a name="comment" . $comment[3] . ""></a>"

Ahora necesitamos añadir en la barra lateral el enlace a la página y mostrar el número de comentarios nuevos. Para ello, en _lib/global.php, en el método eb_foot(), sustituimos el if que comprueba si el usuario es el administrador, por esto:

if( eb_checkadmin( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) ) {
    echo " <li><a href="admin.php">" . ucwords( $lang['admin'] ) . "</a></li> ";

    $sql = "SELECT u.user_name, c.comment_id, c.article_id,"
    ." c.comment_body, a.article_title, c.comment_date"
    ." FROM eb_comments c, eb_users u, eb_articles a"
    ." WHERE c.comment_date > ( SELECT comment_date FROM eb_comments"
    ." WHERE comment_id = 0 ) AND u.user_id = c.author_id"
    ." AND a.article_id = c.article_id"
    ." AND c.author_id <> " . $_SESSION["user_id-" . $_SERVER['SERVER_NAME']];
    $numOfComments = mysql_num_rows( mysql_query( $sql ) );

    echo " <li><a href="moderate-comments.php">"
    .ucwords( $lang['moderate_comments'] ) . " (" . $numOfComments . ")</a></li> ";
}

Por último necesitamos introducir en la base de datos un comentario de referencia. Este comentario no será visto, ni pertenece a ningún artículo. Simplemente utilizaremos su campo de fecha como el momento a partir del cual un comentario se considera nuevo, y será este comentario el que se actualizará al pulsar el botón en la página de moderar comentarios. Podemos añadir el comentario con la siguiente sentencia SQL:

INSERT INTO eb_comments ( comment_id, author_id, article_id, comment_ip, comment_date, comment_body )
VALUES ( 0, 0, 0, 0, 0, 'comentario de referencia' );

Los únicos valores importantes son que el comment_id, article_id y comment_date sean cero.

Y con esto ya tenemos en marcha el sistema de seguimiento de nuevos comentarios. Para cualquier duda podéis usar los comentarios, que ahora estoy seguro de no dejarme ninguno sin leer 😀

Nueva cabecera y cambio de nombre

[blog] [japonés] [programación]

Desde que empecé a usar EggBlog, quería cambiar la cabecera que trae por defecto, pero no teniendo una imagen clara de lo que quería, la aguanté durante estos dos meses. Este fin de semana se me ocurrió una idea sencilla, y resultona, que consistía en mostrar el nombre del blog parte en japonés y parte en español, intentando mostrar un poco la fusión de los temas que trato.

Primero intenté escribir yo mismo los caracteres, pero eran poco menos que horribles, así que me pongo a buscar por internet algunos ya dibujados, y cual es mi sorpresa al descubrir durante la búsqueda que el nombre del blog estaba mal escrito!!!

Yo, que estoy intentando aprender japonés, y en dos meses no me había dado cuenta que me faltaba una ene en el título…

La forma correcta es Konnichi wa (こんにちは) que viene a significar “Buenos días/Buenas tardes”. Tampoco a primera hora de la mañana, donde se diría Ohayo Gozaimasu (おはようございます) ni muy tarde por la tarde, donde ya pasa a ser Konban wa (こんばんは).

Además de renovar la cabecera, he aprovechado para cambiar el favicon, homenajeando a la ene que dejé olvidada. En la Wikipedia hay buenas instrucciones de como cambiar el favicon y su utilidad.

Por suerte, el cambio de dominio y la configuración en el servidor han sido triviales y apenas me ha llevado unos minutos. El problema son las búsquedas de Google o los enlaces directos a alguno de los artículos, que ahora darán un error. Estoy por duplicar el contenido durante un tiempo hasta que encuentre una solución alternativa.

Si alguien tiene algún enlace al blog o a algún artículo, que le añada una ene porfavor! 😀

[ACTUALIZACIÓN 11/04/2011]

He encontrado una solución provisional que evita la página de error. Conseguir que un enlace a uno de los artículos en la vieja dirección lleve al artículo en la nueva se me antoja complicado, por lo que he decido que por lo menos todos los enlaces antiguos lleven a la nueva página principal.

Para ello he modificado el .htaccess, quedando de la siguiente manera:

Options +FollowSymLinks
RewriteEngine on
RewriteBase /
RewriteRule .*$ index.php [L]

Con esto consigo que cualquier enlace antiguo lleve a la vieja página principal, cuyo contenido pasa a ser el siguiente:

<?php
   header( 'Location: http://www.konnichiwamundo.co.cc' ) ;
?>

Y con esto nos encontramos en la nueva página principal. Algo es algo 😀

Aprendiendo Japonés #1 – Mi historia

[japonés] [programación] [software]

Amo Japón. No es nada nuevo. La primera vez que estuve allí, me pareció el mejor viaje de mi vida, y volví con una cosa clara, volvería a ir a Japón algún día. También saqué algo más en claro, quería saber japonés. Eso fue hace 6 años.

Investigué cuales eran los mejores métodos para aprender japonés, y finalmente me decanté por “Kanji para recordar” de James W. Heisig.

Portada del libro Kanji para recordar IEl método de Heisig se basa en asociar una imagen o un cuento a cada uno de los Kanji, por lo que se evita tener que realizar una memorización visual de como se dibuja cada Kanji.

Este sistema hace que dibujar los Kanji al principio sea lento, pues tienes que recordar un cuento para cada uno. Pero poco a poco, según los vas practicando, acaban pasando a memoria visual, mejorando la velocidad, y sigues teniendo la ventaja de poder memorizar muchos más Kanji rápidamente.

La primera vez empecé con mucha ilusión. Aprendí unos 150. El problema fue que no seguí al pie de la letra las instrucciones de Heisig. Intenté memorizar por fuerza bruta, y acabé frustrado. El próximo viaje a Japón estaba muy lejos, y lo dejé de lado.

Pasaron los años. Se empezó a rumorear un nuevo viaje a Japón, y la motivación volvió. Empecé desde cero, y me ceñí a las instrucciones del libro. Llegué a los 300 caracteres, y aquí me topé con otro muro.

Y es que desde mi punto de vista, este método tiene un fallo fundamental: No ofrece un buen sistema de repaso.

Por cada nuevo Kanji que aprendes, tienes que crear una ficha de cartulina. Por delante tienes la imagen del Kanji, por detrás su palabra clave en Español y el cuento. Yo repaso 100 Kanji cada día, por lo que cuando tengo 100 fichas, no hay problema. Con 200, bueno, la cosa sigue siendo manejable. Con 300 empieza el agobio, y entonces piensas “¿Que pasará cuando tenga 1000 o 1500 fichas?”.

Y lo volví a dejar de lado. Ya sobreviví a un viaje sin saber Japonés, podría sobrevivir a otro.

El año pasado volví a Japón. Un viaje nuevamente maravilloso, pero me volvió a quedar una espinita clavada con el idioma. Esto no se podía repetir. Si volvía a Japón, lo cual esta claro que va a suceder :P, tengo que saber japonés sí o sí.

Montaña de fichas de kanjiAsí inicié mi tercer intento de aprender el idioma. Otra vez desde cero, de nuevo llegando a los 300 kanji, y estrellándome contra el muro. Lo que veis a la derecha son las 300 fichas que hice. Para mi son inmanejables. Difíciles de barajar para que lo que repaso sea aleatorio. Difíciles de transportar. Difíciles de gestionar en general.

¡Y solo son 300! Una pila de unos 8 centímetros. Cuando sean 2042, ¡¡¡serán una montaña de unos 50 centímetros!!!

Así que empecé a darle vueltas a un sistema alternativo. Evidentemente tiré hacía lo tecnológico. Lo que necesitaba era un programa que me permitiera realizar mi repaso diario. Como siempre, mi primer impulso es ponerme a programar algo en Java. Después me tranquilizo, y me pongo a buscar a ver si alguien ha tenido la misma idea y me ahorro el trabajo. Y por suerte así fue, más o menos…

El programa que encontré se llama KanjiGym y es el programa oficial del libro. Las grandes ventajas de esto, es que tiene todas las palabras clave en Español, tiene una interfaz gráfica bastante práctica y una biblioteca de Kanji con todos los que aparecen en el libro, además de animaciones de escritura de los trazos.

¿Desventaja? Que no es práctico para repasar, lo cual es un fallo bastante gordo en un programa de repaso… La única opción que ofrecen es un repaso aleatorio, pero eso no es práctico. Si yo se 200 kanji y repaso 100 al día, en dos días quiero haberlos repasado todos al menos una vez.

Pero la interfaz gráfica está bastante bien, y la biblioteca de kanji está completa… ¿y si lo decompilo y modifico para adecuarlo a mis necesidades? ¡Manos a la obra!

Decompiladores de Java hay unos cuantos, pero tras probar muchos, me quedo con el JD-GUI. Decompilo, meto las clases en el Eclipse, soluciono unos cuantos fallos tontos que siempre salen al decompilar un programa complejo, y voilà! el programa funcionando. Solo falla una cosa, las animaciones de dibujado de los trazos, pero como no me resulta imprescindible, lo descarto.

Tras muchas modificaciones y un uso intensivo obtengo el programa final.

Captura del KanjiGymEl funcionamiento es sencillo, y se controla mayormente con la tecla de espacio. Aparece el Kanji, lo escribo en papel y presiono espacio. Compruebo si está bien escrito, y nuevamente espacio para confirmar que está bien y que aparezca el siguiente Kanji.

La gracia del programa es que comprueba cuanto tiempo me cuesta recordar y escribir cada kanji, y según el tiempo lo cataloga como fácil, intermedio o difícil. De esta manera, los dificiles los repaso cada día, mientras que los intermedios se repasan menos y los fáciles poco.

Me inspiré en un método que leí en internet, pero no consigo encontrar el enlace…

El caso es que el programa funciona. Ya no tengo que hacer las fichas de los kanji, lo que me da más tiempo para aprender nuevos caracteres. También machaco más los kanji más difíciles, o los que pueden confundirse con otros, por lo que puedo avanzar de manera constante. Y en las estadísticas veo como un mayor número de kanji van a la sección de los fáciles, lo que indica que estoy mejorando mi velocidad de escritura. En este mi tercer intento, ya llevo 1285 kanji aprendidos y sigo sumando!

Bueno, creo que como introducción ya me he pasado de largo. Hay más cosas que quiero comentar, como herramientas y enlaces útiles, pero creo que eso lo dejaré para otro artículo 😛

Sección de enlaces

[programación]

Algo que echaba en falta en el blog era una sección de enlaces, donde poder mostrar enlaces a páginas que me parecen interesantes y que sigo con asiduidad. Al final me decidí por mostrar estos enlaces en la barra lateral. Esto habría sido muy sencillo realizarlo de manera estática añadiendo los enlaces por código, pero la idea era poder modificarlo vía web. Estos son los pasos que he realizado:

1. Añadir una entrada en el fichero de idioma (es.php):

$lang['links'] = "enlaces";

2. Crear una tabla en la base de datos donde se almacenarán los enlaces, y ya de paso, añadir la creación de la tabla a la instalación para una futura migración. Añadimos lo siguiente al método eb_install() en _lib/admin.php:

mysql_query("DROP TABLE links;");
$sql.="CREATE TABLE links (".
    "link_id int NOT NULL AUTO_INCREMENT,".
    "link_name varchar(255) NOT NULL,".
    "link_url varchar(255) NOT NULL,".
    "PRIMARY KEY (link_id)".
    ");";

3. Creamos la página desde donde se podrán editar los enlaces. Como quería algo sencillo, la edición de los enlaces se realiza en un textarea. Cada enlace está formado por dos líneas. La primera es el nombre del enlace, mientras que la segunda es la dirección del enlace. La página de edición es links.php y su contenido es el siguiente:

<?php

require_once '_lib/global.php';
global $config;
eb_pre();

if(empty($_SESSION['user_id-'.$_SERVER['SERVER_NAME']])){
    header("Location: index.php");
}
elseif( !eb_checkadmin( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) ) {
    header("Location: index.php");
}
else {
    eb_head('');

    $textAreaText = "";

    if(!empty($_POST['editLinks'])){
        $textAreaText = $_POST['linksText'];
        if(!empty($textAreaText)){
            $links = explode(" ", $textAreaText);
        }

        $sqlQuery = "DELETE FROM links";
        mysql_query($sqlQuery);

        $count = 0;
        for($i = 0; $i < count($links); $i+=2){
            $sqlQuery = "INSERT INTO links SET link_name = '".$links[$i]."'"
            .", link_url = '".$links[$i+1]."'";
            mysql_query($sqlQuery);
            $count++;
        }
        echo "<p>Se han guardado ".$count." enlaces.</p>";
        $textAreaText = str_replace(" ", "", $textAreaText);
    }
    else{
        $query = mysql_query("SELECT link_name, link_url FROM links");

        while( $row = mysql_fetch_array( $query ) ) {
            if(!empty($textAreaText)){
                $textAreaText.=' ';
            }
            $textAreaText.=$row['link_name'].' '.$row['link_url'];
        }

        $textAreaText = str_replace(" ", "", $textAreaText);
    }

    $saveText = ucwords($lang['save']);
?>

<h3><?php echo ucwords($lang['links'])?></h3>
<form name="linksForm" action="links.php" method="post">
    <textarea name="linksText" id="linksText" rows="10"></textarea>
    <p>
        <input type="submit" name="submit" value="<?php echo $saveText; ?>" />
        <input type="hidden" name="editLinks" value="TRUE" />
    </p>
</form>

<script type="text/javascript">
<!--
var textArea = document.getElementById( "linksText" );
textArea.value = "<?php echo $textAreaText ?>";
//-->
</script>

<?php
    eb_foot();
}
?>

4. Añadir un enlace en la página de administración para acceder a la página de edición de enlaces. Yo lo he puesto tras el enlace a los miembros. En la función eb_admin() añadimos:

echo " <li><a href="links.php">" . ucwords( $lang['links'] ) . "</a></li>";

5. Ahora solo nos falta mostrar los enlaces en la columna lateral. Para ello debemos modificar el método eb_foot() en _lib/global.php, añadiendo lo siguiente donde queramos que se muestren. Yo me he decidido por ponerlos entre las etiquetas y el listado de artículos:

echo showLinks();

La definición del método es esta:

function showLinks(){
    global $lang;

    $count = mysql_num_rows(mysql_query("SELECT link_id FROM links"));

    if($count == 0){
        return "";
    }    

    $output = '<div class="box"><strong>' . ucwords( $lang['links'] ) . '</strong><ul>';

    $query = mysql_query("SELECT link_name, link_url FROM links");

    while( $row = mysql_fetch_array( $query ) ) {
        $output.=" ".'<li><a href="'.$row['link_url'].'" target="_blank">'
        .$row['link_name'].'</a></li>';
    }

    $output.='</ul></div>';

    return $output;
}

Y con esto ya tenemos nuestra sección de enlaces. Ahora solo falta llenarla de enlaces interesantes! 😀