Cambiar el lenguaje de programación de un repositorio de GitHub

A veces GitHub no detecta correctamente el lenguaje principal de un repositorio. Por ejemplo, para Mi Segundo PC como tiene más ficheros con extensión php que js decía que el lenguaje era PHP en vez de JS.

Para solucionarlo creamos un fichero .gitattributes en el directorio raíz del repositorio con este contenido:

* linguist-vendored
*.js linguist-vendored=false

Esto le indica a GitHub que desactive la detección de lenguaje para todos los ficheros y la active para los .js, haciendo así que el lenguaje detectado sea JavaScript.

Eliminando autenticación para phpMyAdmin

¡Atención! Esto solo hay que hacerlo en nuestro equipo de desarrollo, lógicamente es una locura quitar la autenticación al phpMyAdmin en un servidor accesible desde Internet, ahí no solo debemos tener autenticación sino proteger además la carpeta con una contraseña de acceso porque phpMyAdmin no tiene ninguna protección contra ataques de fuerza bruta (no entiendo por qué, no les costaría nada implementar algo como phpLoginBlacklist).

Pero en nuestro equipo es un rollo que cada cierto tiempo caduque la sesión y nos pida que nos identifiquemos otra vez, así que la solución es editar el fichero /etc/phpmyadmin/config.inc.php y en el bloque de datos del primer servidor añadir estas líneas:

// Esta línea ya existirá, cambiamos su valor a config
$cfg['Servers'][$i]['auth_type'] = 'config';

// Estas son las líneas que añadiremos
$cfg['Servers'][$i]['user'] = 'root';
$cfg['Servers'][$i]['password'] = 'pass';
$cfg['Servers'][$i]['AllowNoPassword'] = TRUE;
$cfg['Servers'][$i]['AllowRoot'] = TRUE;

Sublime Text: convertir espacios a tabs al guardar un fichero

No soy un maniático como Richard en Silicon Valley:

Aunque yo también prefiero usar tabulados

pero como llevo un tiempo usando Pug para el HTML en Vue he acabado harto del mensaje de error Invalid indentation, you can use tabs or spaces but not both.

Para solucionar esto crearemos una carpeta UnexpandTabsOnSave en la carpeta Packages de la configuración de Sublime Text y dentro un fichero UnexpandTabsOnSave.py con este contenido:

import sublime, sublime_plugin, os

class UnexpandTabsOnSave( sublime_plugin.EventListener ):
  # Run Sublime's 'unexpand_tabs' command when saving any file
  def on_pre_save( self, view ):
    view.window().run_command( 'unexpand_tabs' )

Así cada vez que guardemos un fichero convertirá los espacios a tabs, Pug no se quejará y el Richard que llevamos dentro estará satisfecho.

Detectar atajos de teclado en una aplicación Vue

Para asociar atajos de teclado globales lo más sencillo es establecerlos en el div principal de nuestra aplicación Vue.

Así que si queremos por ejemplo lanzar nuestro menú de aplicaciones cuando el usuario haga Ctrl + Clic en cualquier parte de la página o pulse la combinación de teclas Meta + Espacio usaríamos este código:

<div id="app"
     v-on:keyup.meta.space="launchAppsMenu"
     v-on:click.ctrl="launchAppsMenu"></div>

Sin embargo al probar esto veríamos que el Ctrl + Clic funciona pero no así el Meta + Espacio. Esto es porque los eventos de teclado solo se generan para elementos que pueden tener el foco (focusables) y los div no son por defecto focusables. Para que lo sean solamente tenemos que añadirle el atributo tabindex, una vez lo tenga podrá recibir eventos de teclado. Así quedaría entonces nuestro código:

<div id="app" tabindex="0" 
     v-on:keyup.meta.space="launchAppsMenu"
     v-on:click.ctrl="launchAppsMenu"></div>

Número de compilación automático en aplicaciones Vue

Para disponer del número de compilación en nuestra aplicación Vue he creado este pequeño script. Para usarlo hay que modificar el fichero package.json y sustituir en “scripts” el valor de “build” por ./buildAndUpload.php

Luego creamos el fichero ./buildAndUpload.php con este contenido:

#!/usr/bin/php
<?php
// Get version number
$version = json_decode(file_get_contents('package.json'), true)['version'];
$version = substr($version, 0, -2);

// Get build number
$build = trim(file_get_contents('.buildNumber')) + 1;
file_put_contents('.buildNumber', $build);

echo "Shipping version $version build $build\n\n";

// Save version file
$js = "var appVersion = $version;var buildNumber = $build;";
file_put_contents('./public/appVersion.js', $js);

exec('vue-cli-service build');

// Upload dist folder to server

El número de compilación inicial lo guardaremos en el fichero .buildNumber. Y con esto ya estaría todo, ahora cada vez que compilemos nuestra aplicación se generará en la carpeta dist un fichero appVersion.js que podremos incluir en nuestra aplicación y que contendrá el número de versión (que se extrae del fichero package.json) y el número de compilación.

Server Sent Events en PHP

Estos días he estado muy atareado con el lanzamiento de PocaPoca, una plataforma de formación que empieza su negocio con varios cursos de electrónica.

La plataforma consta de tres partes: una página de presentación de la plataforma (web), la propia plataforma (front) y un entorno de gestión de la misma (back).

Las tres están desarrolladas con Vue. Para cada proyecto con Vue yo me abro siempre dos pestañas de terminal: uno para lanzar el entorno de desarrollo local (npm run serve) y otro para compilar y subir al servidor remoto el proyecto (npm run build). Así que para este proyecto (que son realmente tres) tenía seis pestañas abiertas en el terminal y era un lío tener que ir pasando de una a otra para compilar y subir ficheros. Incluso usando el interfaz web de Vue-cli serían tres pestañas. Y eso suponiendo que no necesite por algún motivo lanzar otro proyecto… Vamos, que había ocasiones en que aquello era el festival de la pestaña.

Una vez lanzada la página me puse a pensar cómo evitar que esto me vuelva a pasar en el futuro. El primer paso ha sido sustituir Gnome Terminal por Terminator, que permite partir una pantalla en varias bloques horizontales y verticales. Esto ya es un avance, porque me permitiría (siguiendo con el ejemplo de PocaPoca) tener solo tres pestañas, una para cada proyecto, o incluso una única pestaña con todas las ventanas juntas, porque en Terminator es rápido moverse entre los bloques de una misma pestaña.

Pero se me ha ocurrido algo mejor, y es aprovechar un iPad antiquísimo que tengo para que muestre todos los proyectos que tengo activos y me permita con solo pulsar un botón ejecutarlos o compilarlos. Esto es mucho más rápido incluso que cambiar a la ventana de Terminator y localizar el bloque y lanzar el comando.

Para no perder funcionalidad necesito poder ver la salida de los comandos, para ver si hay algún error. El entorno de ejecución (npm run serve) está continuamente mostrando información que tengo que poder ver, y esto es lo que nos lleva al asunto de la entrada de hoy: ¿cómo mostrar en una página web la salida de un comando que se está ejecutando de forma continua?

La respuesta son los Server Sent Events, es como un websocket pero en un solo sentido, del servidor al cliente. El navegador abre una conexión permanente con nuestro fichero PHP, que le va devolviendo datos según los vaya generando, y esto nos permite recibirlos en JS. Es la solución perfecta para lo que necesito, y en este vídeo se ve cómo desde el iPad puedo ver la salida de un ping según se va generando:

Copio aquí el código, cuando tenga un poco de tiempo quiero ponerlo un poco más bonito y subirlo a GitHub.

<?php
$mode = $_REQUEST['mode'] ?? '';
if ($mode == 'event')
{
  header('Content-Type: text/event-stream');
  header('X-Accel-Buffering: no');
  
  echo "event: message\n";
	
  $descriptorspec = [
    0 => ["pipe", "r"],   // stdin
    1 => ["pipe", "w"],   // stdout
    2 => ["pipe", "w"]];  // stderr
  
  $cmd = "ping 127.0.0.1";
  $process = proc_open($cmd, $descriptorspec, $pipes, realpath('./'), array());
  if (is_resource($process))
    while ($s = fgets($pipes[1]))
    {
      $s = nl2br($s);
      echo "data:$s\n\n";
      while (ob_get_level() > 0)
	ob_end_flush();
      flush();
    }
  
  die("data:#CLOSE\n\n");
}

?><!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <meta name="quality" content="Koas' Code Inside" />
  </head>
  <body>
  	<button onclick="run()">Run process</button>
  	<div id="result" style="white-space: pre"></div>

<script>
function run()
{
	document.getElementById("result").innerHTML = "";

	var source = new EventSource("dashboard.php?mode=event");
	source.onmessage = function(event) 
	{
		if (event.data == "#CLOSE")
			source.close();
		else document.getElementById("result").innerHTML += event.data;
	}
}
</script>

  </body>
</html>

Actualización 12-06-2020: usando esto en un proyecto real me he dado cuenta de que la petición que se hace al servidor al llamar al constructor de EventSource no envía cookies ni hay forma de añadir cabeceras ni nada, solamente se puede enviar información en la URL. Esto es importante tenerlo en cuenta, tendremos que buscarnos la vida para autentificar al usuario que hace la petición. Lo más sencillo es pedir al servidor un token temporal y enviarlo en la petición SSE.

Establecer etiquetas al crear un formulario de subida de fichero a S3 en PHP

Para subir ficheros a S3 desde las aplicaciones JavaScript lo que hago es generar un formulario firmado en PHP y enviarlo desde JS, de esta forma el fichero viaja directamente del ordenador del usuario a S3.

En muchos casos es interesante aplicar etiquetas de costo a estos ficheros para poder después desglosar el coste. Esto es imprescindible si nuestra aplicación la usan varios clientes y debemos facturar a cada uno el importe generado por el almacenamiento y transferencia de sus ficheros.

Estas etiquetas deben especificarse al crear el formulario y firmarse para que en el cliente JS no se puedan alterar.

Después de pasar varias horas buscando porque no existe ni un solo ejemplo de cómo hacer esto en la documentación de Amazon por fin he encontrado cómo se hace. Hay que pasar un campo en el formulario llamado Tagging con el nombre y el valor de la etiqueta codificando siguiendo un esquema que indican en esta página.

$formInputs = [
  'acl' => 'private',
  'Content-Type' => $type,
  'key' => "company_{$idCompany}/$s3Filename",
  'Tagging' => "<Tagging><TagSet><Tag><Key>empresa</Key><Value>{$companyName}</Value></Tag></TagSet></Tagging>"];

$options = [
  ['bucket' => self::S3_BUCKET],
  ['acl' => 'private'],
  ['Content-Type' => $type],
  ['starts-with', '$key', "company_{$idCompany}/"],
  ['eq', '$Tagging', "<Tagging><TagSet><Tag><Key>empresa</Key><Value>{$companyName}</Value></Tag></TagSet></Tagging>"]

$expires = '+1 hours';

$postObject = new \Aws\S3\PostObjectV4($s3Client, self::S3_BUCKET, $formInputs, $options, $expires);

Actualización 21 de marzo de 2020: después de tanto sufrimiento con esto resulta que las etiquetas de coste no funcionan a nivel de objeto sino de bucket, así que la única forma de separar gastos por clientes es que cada uno tenga su propio bucket.

SimpleSamlPhp: cómo resolver el error “Could not load state specified by InResponseTo: NOSTATE Processing response as unsolicited.”

Por algún motivo que desconozco si se usa como sistema de almacenamiento en SimpleSamlPhp el que viene por defecto, que son las sesiones de PHP, aparece de forma aleatoria y completamente impredecible este error.

No sé por qué sale, no sé por qué ha empezado a aparecer después de casi dos meses de tener el servicio de autenticación funcionando, no sé nada pero hazme caso, lector: si vas a usar alguna vez SimpleSamlPhp hazte un favor y crea una base de datos MySQL, y en el fichero config.php de SimpleSamlPhp usa estos valores:

'store.type' => 'sql',
'store.sql.dsn' => 'mysql:host=localhost;dbname=NOMBREDB',
'store.sql.username' => 'USUARIO',
'store.sql.password' => 'CONTRASEÑA',
'store.sql.prefix' => 'sso', // Vale cualquiera

Actualización del 27/02/2020: misteriosamente la solución que indico aquí me solucionó a mí el problema pero no a otros usuarios del sistema. Lo que finalmente arregló la situación fue modificar los nombres por defecto de las cookies en el fichero config.php:

'session.phpsession.cookiename' => 'NOMBRE_PROPIO',
'session.authtoken.cookiename' => 'OTRO_NOMBRE_PROPIO',

Resolver error “Invalid host header” al usar ngrok con vue-cli

Si queremos probar la aplicación que estamos desarrollando con vue-cli a través de ngrok puede que nos encontremos con el error “Invalid host header” si tenemos configurado el proyecto con un nombre de dominio que no sea localhost.

En estos casos hay que lanzar ngrok especificando el host header, por ejemplo:

ngrok http 8081 -host-header="dev.customer:8081"

Cambiar el padre de un elemento mediante media queries (más o menos)

En HTML los elementos se muestran en pantalla en el orden en el que aparecen en el código fuente. En este ejemplo:

<div class="grupo1">
<div class="e1g1">Elemento 1 grupo 1</div>
<div class="e2g1">Elemento 2 grupo 1</div>
</div>
<div class="grupo2">
<div class="e1g2">Elemento 1 grupo 2</div>
</div>

el grupo 1 se mostrará antes que el grupo 2.

Antes de que existieran CSS Flex y CSS Grid la única forma de poder alterar ese orden era meter en el código una copia del grupo 2 con otra clase y mediante media queries mostrar la copia y ocultar el original al cambiar de ancho. Funcionaba, pero no era demasiado elegante.

Gracias a la magia de CSS Flex y CSS Grid resulta muy sencillo invertir el orden en el que se muestran los bloques, y podemos incluso definirlo visualmente con las áreas de CSS Grid.

Sin embargo hay una cosa que aún no podemos hacer solo con CSS y para lo que necesitaremos JS (de ahí el más o menos del título). ¿Qué pasa si queremos que a partir de un cierto ancho el div con clase e1g2 pase a estar dentro de grupo1? Que yo sepa no existe forma de cambiar el padre de un elemento mediante CSS.

Para eso viene a nuestro rescate enquire.js. Nos permite definir funciones JS que se ejecutarán cuando se activen / desactiven las media queries que le pasemos. Para nuestro ejemplo el código sería:

enquire.register("screen and (max-width:950px)", {
match: () => {
let nuevoPadre = document.querySelector("grupo1");
let elementoAmover = document.querySelector("e1g2");
nuevoPadre.appendChild(elementoAmover);
},
unmatch: () => {
let nuevoPadre = document.querySelector("grupo2");
let elementoAmover = document.querySelector("e1g2");
nuevoPadre.appendChild(elementoAmover);
}
});

Usando la misma sintaxis de las media queries tenemos una forma sencilla de mover un elemento de su sitio original a otro sitio dentro del DOM. Por supuesto, mediante CSS podremos definir la posición exacta de ese elemento dentro de “su nueva casa”.

Y además conseguimos esta funcionalidad sin engordar demasiado, la librería solo ocupa 0.8kb una vez minimizada y comprimida.