Geohide: localización y gestión de contenidos en Drupal

Geohide Just For Drupal
Geohide: localización y gestión de contenidos en Drupal

Hace un tiempo un cliente nos pidió que implementásemos una pequeña solucion software para sus plataformas de negocio basadas en Drupal. Quería - básicamente - que se pudiese ocultar un campo específico de un tipo de contenido concreto sobre algunos nodos existentes dentro de uno de sus sitios web. La razón estaba ligada a los objetivos de marketing de su negocio y de manera específica, al realizar una campaña anunciando algún servicio, como no estaba disponible para un lugar concreto, que el campo asociado donde se promocionaba ese servicio  se ocultase (creo recordar que era un sitio web para Australia, y la ciudad - región para la que ocultar ese campo era el entorno de Sydney), de tal manera que una persona que visitase desde Sydney el sitio web y accediese el nodo específico donde se insertaba esa información la tuviese oculta pero siguiese viendo el resto de campos asociados al tipo de contenido.

La mecánica era bastante sencilla en cuanto a flujo de decisiones, como se puede apreciar:

 

Y nos pusimos a investigar soluciones libres ya creadas. Encontramos varias soluciones libres basadas en PHP y aptas para integración con Drupal. Librerías y módulos como estos:

Módulo SmartIP para Drupal: https://www.drupal.org/project/smart_ip
Este módulo es una herramienta de trabajo para geolocalización de visitantes a través de IP en Drupal que provee información diversa sobre su posicionamiento: long/lat, país, región, ciudad y código postal, almacenándola en una variable de sesión con clave ‘smart_ip’ en el array de $_SESSION. Ok.

El módulo resultaba muy interesante y además cuenta con una interesante versión integrada con Webforms para crear campos de datos geográficos en formularios y añadir el valor de los leads capturados desde un sitio web (https://www.drupal.org/project/webform_smart_ip) sin embargo, en pruebas no nos daba resultados demasiados depurados en cuanto a localización de la ciudad, obteniendo valores poco afinados a la hora de localizarlas, tanto en entornos locales de desarrollo como integrándola en pre-producción. En pruebas realizadas desde Sevilla (España), arrojaba resultados similares a las pruebas con su propio sitio web de testing:

 

Lo que lo hacía poco útil para la pequeña utilidad que teníamos que generar. En cualquier caso, también quedaba pendiente implementar la pequeña lógica de trabajo con los tipos de contenidos y campos, por lo que -de momento - lo descartamos.

Librería GeoIP para PHP: http://php.net/manual/en/book.geoip.php
Extensión PHP para operar con datos geoposicionados en base a IP. Inicialmente interesante pero limitada en cuanto a combinación con GeoIP2 y de cara a extraer ciudades, requiere integración con otras librerías y funcionalidades, además de bases de datos como GeoLiteCity.dat (más de 12 MB). Nos pareció demasiado para una consulta que debería ser lo más rápida posible de cara a un desarrollo también rápido.

También testeamos soluciones libres como IP2country para Drupal:
https://www.drupal.org/project/ip2country Pero no ofrecía la granularidad suficiente como para recibir la información sobre ciudades, así que pronto quedó descartada.

 

¿Qué decidimos?

Construir un pequeño módulo ad-hoc para gestionar las tres diferentes operaciones a realizar por el script, para darle una manera más integrada al script y sus funcionalidades y estructurarlo un poco mejor. Las operaciones a realizar eran esencialmente tres:

1-Obtener IP de la persona visitante

2- Obtener la ciudad a partir de la IP y cotejar si coincide con la ciudad de referencia

3- Realizar la ocultación del campo solicitado para el tipo de contenido creado.

De manera visual, el funcionamiento de lo que empezar a llamar “Geohide” era el siguiente:

 

Para obtener la dirección IP, una simple función para extraerla de varias opciones posibles dentro de la variable global $_SERVER:

function getIP() {
    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
        $ip = $_SERVER['HTTP_CLIENT_IP'];
    }
           elseif (! empty($_SERVER['HTTP_X_FORWARDED_FOR']))
    {
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    }
            else {
        $ip = $_SERVER['REMOTE_ADDR'];
    }
    return $ip;
}

Luego realizábamos la petición a un servicio externo con disponilidad gratuita de hasta 120 peticiones por minuto y posibilidad de ampliar el ratio con pago (el sitio web al que lo teníamos que integrar tiene picos de visitas relativamente alto). El servicio elegido fue geoplugin http://www.geoplugin.com/  de cuya respuesta extrajimos la ciudad del visitante:

            $ip = getIP();
            $xml = simplexml_load_file("http://www.geoplugin.net/xml.gp?ip={$ip}");
            $cityvisitor = $xml->geoplugin_city;

En último lugar, redefinimos la función para mostrar nodos a través del hook de Drupal 7 correspondiente, el hook_node_view(), con una selección rápida dado que la ciudad de referencia para realizar la ocultación era fija y podíamos permitirnos (aunque no recomendado) ponerla “a fuego” en el código, al igual que el nodo de referencia y el tipo de contenido, que eran fijos para esta solicitud. Como conociamos los datos de partida, era sencillo artícularlo dentro de una función geohide_node_view con parámetros $node y $view_mode.

Simplemente con la instrucción dada al nodo:

            $node->content['field']['#access'] = FALSE;

Con lo que los objetivos de la solicitud por parte del cliente quedaban resueltos. Pero empezamos a hacernos preguntas....

 

¿Qué se nos ocurrió a partir de ahí?

El pequeño script organizado como módulo ad-hoc para este desarrollo podía sentar unas buenas bases para articular algo mucho más ambicioso: a fin de cuentas no existía una solución de este tipo creada para Drupal y que permitiese manejar la visibilidad de campos, nodos o tipos de contenido. Empezamos a pensar en la posibilidad de hacer algo reutilizable para otros proyectos y liberarlo para que otras personas en nuestro caso pudieran usarlo como posible herramienta, ya que todo lo que habíamos encontrado eran soluciones parciales entre las fases I, II y III de las funciones de Geohide, pero no existía nada de manera complementaria dentro del mismo módulo.

 

Eh, un momento ¡Podríamos aportar nuestro granito de arena a la comunidad Drupal aportando algo de nuestra propia cosecha! eso nos motivó bastante, así que nos pusimos manos a la obra sacando algo de tiempo de nuestro día a día para poner orden, investigar y construir algo mucho más mejorado.

 

                          Así nació Geohide.

Para empezar con buen pie, había que darle una forma más funcional a lo creado: retirar valores puesto a fuego y dotar al módulo de una serie de capacidades para hacerlo directamente reutilizable en otro proyecto.

Para empezar, creamos enlaces de menú para configuración y ayuda, mediante los Hooks de Drupal, mediante el hook_menu() y el hook_help(), funciones elementales para dotar a un módulo de contexto e información.

/**
 * Implements hook_menu().
 */
function geohide_menu() {
  $items = array();

  $items['admin/config/content/geohide'] = array(
    'title' => 'Geohide',
    'description' => 'Configuration for Geohide module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('geohide_form'),
    'access arguments' => array('access administration pages'),
    'type' => MENU_NORMAL_ITEM,
  );

  return $items;
}

/**
 * Implements hook_help().
 *
 * Displays help and module information.
 *
 * @param path
 *   Which path of the site we're using to display help
 * @param arg
 *   Array that holds the current path as returned from arg() function
 */
function geohide_help($path, $arg) {
  switch ($path) {
    case "admin/help#geohide":
            $output = '';
            $output .= '<h3>' . t('About') . '</h3>';
            $output .= '<p>' . t('The Geohide module allows you to create geo-localized campaigns hidding or showing diverse kinds of information on your website, filtering the users by IP.') . '</p>';

            $output .= '<h3>' . t('Uses') . '</h3>';
            $output .= '<p>' . t("Geolocalize a visitor and hide any given field depending on node's title") . '</p>';
            return $output;
            break;
  }
}

En tercer lugar es muy importante dotar al módulo de un menú de configuración interno para evitar valores a fuego y poder configurarlo a modo de callback para settings, implementando un geohide_form con dos campos configurables que permitiesen reeditar la función geohide_node_view para trabajar con variables, quedando tal que así:

function geohide_node_view($node, $view_mode) {
        

    if ($node->title==variable_get('nodetitle') && variable_get('city')==$cityvisitor){
        if ($view_mode == 'full'){
            $node->content['field_x']['#access'] = FALSE;
           }
         return $node;
    }
}

Bien, pero en realidad no saliamos así de un caso básico en el que el administrador conociese perfectamente el campo a gestionar, o el nombre del nodo, o el tipo de contenido. Para sitios web con pocas opciones esto podría ser viable, pero si queríamos hacer una versión relativamente usable, a modo de MVP de nuestra idea majara, tendríamos que ofrecer algo más, opciones de búsqueda y localización de todo esto para permitir seleccionar de la mejor manera posible. Por suerte, contamos con toda la potencialidad de la API Form de Drupal 7 para montar un menú de configuración del módulo algo más avanzado.

https://api.drupal.org/api/drupal/developer%21topics%21forms_api_reference.html/7.x

Se nos ocurrió generar un triple select dinámico dependiente de la primera opción para que tras seleccionar uno de los tipos de contenidos específicos creados en la plataforma, en los dos siguientes se mostrasen de manera dinámica tanto los nodos creados con ese TdC como los campos disponibles para ese TdC, y devolviendo respuestas del sistema para los casos en los que se eligiese un tipo de contenido con el que no se había creado ningún nodo todavía (en el caso de los fields algo más procesado, ya que al crear un TdC por defecto se genera un campo “body” por defecto en la base de datos).

$form['dropdown_first'] = array(
          '#type' => 'select',
          '#title' => t('Select a Content Type'),
          '#description' => t('Take a content type for use with Geohide'),
          '#options' => $options_first,
          '#default_value' => $value_dropdown_first,
          // Bind an ajax callback to the change event (which is the default for the
          // select form type) of the first dropdown. It will replace the second
          // dropdown when rebuilt
           '#ajax' => array(
          // When 'event' occurs, Drupal will perform an ajax request in the
          // background. Usually the default value is sufficient (eg. change for
          // select elements), but valid values include any jQuery event,
          // most notably are 'mousedown', 'blur', and 'submit'.
            'event' => 'change',
            'callback' => 'geohide_ajax_callback',
          'wrapper' => array('dropdown_second_replace', 'dropdown_thrid_replace'),
          //'wrapper' => 'dropdown_second_replace',
              ),
           );

   $form['dropdown_second'] = array(
      '#type' => 'select',
      '#title' => t('Select all the nodes what you need'),
      '#description' => t('Here you can select all the nodes whose fields you want to hide'),
      // Allow multiple selection of nodes
      '#multiple' => TRUE,
      // The entire enclosing div created here gets replaced when dropdown_first
      // is changed.
     '#prefix' => '<div id="dropdown_second_replace">',
     '#suffix' => '</div>',
     // when the form is rebuilt during ajax processing, the $value_dropdown_first variable
     // will now have the new value and so the options will change

 '#options' => geohide_second_dropdown_options($value_dropdown_first),
     '#default_value' => isset($form_state['values']['dropdown_second']) ? $form_state['values']['dropdown_second'] : '',
    );

    $form['dropdown_third'] = array(
       '#type' => 'select',
       '#title' => t('Select the fields from the selected Content Type'),
       '#description' => t('And now, check all the fields to hide'),
       // Allow multiple selection of fields
       '#multiple' => TRUE,
       // The entire enclosing div created here gets replaced when dropdown_first
       // is changed.
      '#prefix' => '<div id="dropdown_third_replace">',
      '#suffix' => '</div>',
      // when the form is rebuilt during ajax processing, the $value_dropdown_first variable
      // will now have the new value and so the options will change
      '#options' => geohide_third_dropdown_options($value_dropdown_first),
      '#default_value' => isset($form_state['values']['dropdown_third']) ? $form_state['values']['dropdown_third'] : '',

     );

Esta es la estructura de esta sección de la configuración de Geohide, que responde de manera cambiante a un evento ‘change’ específico sobre el select de la primera opción y a partir de ahí sale a buscar los nodos y campos asociados de manera dinámica.
La modificación de la vista se realiza de manera parcial mediante AJAX vía comandos de reemplazamiento de los dos selects últimos:

function geohide_ajax_callback($form, $form_state) {
    return array(
    '#type' => 'ajax',
    '#commands' => array(
      ajax_command_replace("#dropdown_second_replace", render($form['dropdown_second'])),
      ajax_command_replace("#dropdown_third_replace", render($form['dropdown_third']))
          )
  );

}

Y la gestión de las opciones a mostrar en los selects se realizan mediante funciones externas que lanzan consultas, del tipo:

/**
  * Get all the Content types extracting only
  * the names to show in the first dropdown select
  */

    function get_node_type_info(){
         $wizard_plugins = node_type_get_types();
         $opciones = array();
         foreach($wizard_plugins as $key=>$wizard){
                 $opciones[$key] = $wizard->name;
         }
         return $opciones;
    }

 

function get_user_node_by_type($node_type){

 $titles = array();
 $titles = db_query('SELECT title FROM {node} WHERE type = :type', array(':type' => $node_type))->fetchCol();
if (count($titles) == 0) {
 $titles = array("Sorry, but seems there aren't any node created with this Content Type", "Try to select another Content Type", "Greetings");
 }
 return $titles;
 }

 function get_fields_by_type($content_type){

     $fields = array();
     $fields = db_query('SELECT field_name FROM {field_config_instance} WHERE bundle = :type', array(':type' => $content_type))->fetchCol();
    if (count($fields) == 1) {
        // If the array only have a result, is the default field for any content type,
        // called 'body' and then is really empty.
     $fields = array("Sorry, but seems there aren't any field created with this Content Type", "Try to select another Content Type", "Greetings");
     }
     return $fields;
     }

 
Como se puede apreciar, tenemos un problema con asociar el response en caso negativo al array de respuestas a mostrar, lo que genera mensajes dentro del select. Esto es francamente horrible desde varios puntos de vista, pero estamos trabajando en ello y prometemos que cambiará en breve. Estamos en ello.

Esto genera un menú de configuración con posibilidad de select multiple para gestionar varios nodos y varios campos a la vez:

 

¿Cómo organizarlo?

En cuanto a lo comunitario

De cara a una liberación “formal”, es necesario realizar un tránsito cubriendo diversos aspectos de los procedimientos diseñados por Drupal. Actualmente, usamos como repositorio abierto a descargas una de las herramientas proporcionadas por Drupal para testing de proyectos, que permite crear un “sandbox” personal a modo de pruebas de proyecto para subir contenidos con posibilidad de conexión remota al repositorio git del proyecto para commits y demás operaciones.

En primera instancia, digamos que el repositorio actualmente carece de una organización exhaustiva. Necesitamos crear tags de versiones y ramas para desarrollos específicos que luego podamos mergear en lugar de trabajar directamente con la rama master. Hasta ahora y como prueba nos ha servido, pero como en otras cuestiones, estamos saltándonos recomendaciones y buenas prácticas.

Luego para una transición a un repositorio “oficial” de módulo hay N cosas que modificar, sustituir o directamente eliminar.

 

Igualmente para conseguir pasar el  Security Advisory Process de Drupal, https://www.drupal.org/drupal-security-team/security-advisory-process-and-permissions-policy

Los filtros de revisión del módulo arrojan demasiados errores y avisos que hay que corregir. Este es el feedback proporcionado por la herramienta https://pareview.sh

 

 

En cuanto a lo cualitativo

Hasta el momento, Geohide es la suma de pequeños momentos parciales de dedicación entre mil historias diferentes y trabajo directo sobre proyectos. Es más la combinación de ideas majaras y ganas de sacar algo interesante que de un análisis madurado de enfoque funcional, lo que hace que hasta el momento no estemos cumpliendo -desgraciadamente- con una serie de pautas fundamentales, cuestiones importantes como el cumplimiento de estándares en los nombrados, depuración y simplificado de código que realmente puede realizarse en menos líneas, desarrollo TDD, patrones... y algunos aspectos más.

Geohide es más, en el momento de escribir estas líneas, un pequeño caos bienintencionado que un proyecto debidamente estructurado y se hace necesario encontrar el equilibrio entre “libera rápido, libera a menudo” y tener que refactorizar parcialmente que globalmente, cuando todo sea más complejo. Son metas que tenemos que alcanzar.

En cuanto a lo funcional

A grosso modo, la geolocalización IP como recurso fundamental del core del proyecto no es una cuestión exacta en el estado actual para nosotros. Los resultados evidencian errores porque en ocasiones la ubicación de una dirección de manera exacta depende de factores como DNS, los rangos de IP gestionados por cada ISP y el problema de la afinación en resultados en cuanto se pasa del nivel regional (sobre el que los ISP suelen operar en bulk) al nivel poblacional de ciudad/provincia (no todos los ISP tienen centros de gestión distribuidos en una región), las asignaciones cambiantes de IP4 ¿fingerprinting? y otros elementos. Incluso deberíamos asumir que geolocalizar por IP es un asunto viejuno y que nos convendría operar sobre otras fuentes de datos. Nos encontramos con la cuestión fundamental de conseguir resultados mucho más eficientes y esencialmente, tenemos mucho que investigar sobre otros proyectos en otras tecnología y de cómo lo están haciendo. (Less NIH, more PFE).

En términos algo más específicos, tenemos que enfocarnos en la creación de funciones que realmente redunden en la mejora del uso del futuro módulo Geohide.
Manteniendo la estructura de bloques de trabajo del módulo a modo de “fases”, debemos tener en cuenta que Geohide debe ofrecer las siguientes posibilidades:

1- Opción de seleccionar el uso de cualquier servicio de consulta de geolocalización

2- Opción de seleccionar para que ciudades, países o regiones se quiere procesar la visibilidad de los campos.

3- Opción de decidir si se quiere procesar la visibilidad de campos, nodos o tipos de contenido.

4- Opción de configuración mediante sistema de permisos y roles de usuario. Mostrar/no mostrar/cambiar en base a perfiles.

 

Planes, objetivos, hitos

La visión con la que nos manejamos

Desde un punto de vista más abstracto, imaginamos un módulo Drupal que permita configurar con relativa facilidad y de manera intuitiva la visibilidad de Entidades dentro de un sitio web y permita realizar campañas de marketing online más depuradas, pruebas de rendimiento de contenido e incluso pruebas A/B desde la plataforma de manera integrada, intuitiva y bien ejecutadas.

Para ello debemos integrar otros subsistemas que permitan ampliar las funcionalidades y mejorar la experiencia de configuración. Investigar, Desarrollar, Integrar, Probar e Iterar son nuestros verbos fundamentales para este camino.

Para ello nos hemos propuesto una serie de hitos parciales asociados a objetivos que debemos ir cumpliendo progresivamente, articulando así nuestra misión asociada:

1 - Preparar una release estable y funcional que tenga como milestone diciembre de 2017 e integre un corpus de funciones elementales y sea apta para pasar los procesos de validación del tránsito a proyecto publicable.
 

2 - Portar Geohide a Drupal 8 es fundamental para asegurar el futuro de la herramienta. Trabajamos sobre Drupal 7 por ser la versión inicial para la que el script fue configurado y el resto ha sido en cierta manera pura inercia, pero debemos ofrecer una versión para 8. Hay que pasar de hablar de nodos, campos, tipos de contenido, etc a operar con entidades y con las APIs de Drupal 8. Orientación a Objetos y toda la mandanga. Esto es muy importante.

3 -Encontrar alternativas al simple filtrado por IP. ¿Google y Javascript? 

4 - Investigar las posibilidades de integración con Geofield.

El camino tomado está plagado de retos a nivel tecnológico, como puede intuirse. Pero asumimos como cierto el viejo mensaje de “Hasta el camino más largo empieza por un pequeño paso”. Y no dudamos que llegaremos, porque ya hemos empezado y estamos caminando.

 

 

28 Agosto 2017 - 4:46pm
Total de votos: 31
Imagen de David Rodríguez
David Rodríguez

Añadir nuevo comentario

Plain text

  • No se permiten etiquetas HTML.
  • Las direcciones de las páginas web y las de correo se convierten en enlaces automáticamente.
  • Saltos automáticos de líneas y de párrafos.
CAPTCHA
Esta pregunta es para probar si eres o no un visitante humano.