Holy Water – Censura en las stores

Después de haberme peleado sin éxito contra la absurda censura de Itunes Connect y Google Play con tal de publicar Fap Fapp he acabado desarrollando la misma versión para beatos, Holy Water.

Itunes Connect rechazó Fap Fapp varias veces por el motivo de ser simple y no corresponder a los estándares de calidad de Apple:


Lejos de darse por vencido, mejoré la aplicación para pasar cada uno de los rechazos. Al final, después de varios meses, la negación llegó , ya que según los revisores, la aplicación mostraba sexo explícito (sin comentarios…). Era un no tajante.

En cuanto a Google Play, desde el primer momento aseguraron que mostrábamos sexo de manera explícita. De todas formas se puede descargar la aplicación desde la web fapfapp.com a través del link superior de ésta. Cada cual que juzgue la ingente cantidad de sexo mostrada.

Para no tirar el trabajo realizado, se intentó una vez más. Cambio de gráfica y tema. Misma aplicación, pero en lugar de masturbarse, se trataba de bendecir a los paganos. Pasó en ambas stores sin problemas. Impresionante la hipocresía demostrada.

Fap Fapp aplicación que mide la agitación del móvil

Link: Link Google Play Update: Google Play ha cancelado la aplicación.
Diseño : Francesc Moret
Web: http://fapfapp.com




Fap Fapp es una aplicación que calcula las sacudidas que se le da a móvil y obtiene una puntuación para poder participar en un ranking. Los gráficos son sencillos y minimal, en parte para pasar la censura de los revisores de Apple o Google. No olvidemos que, al fin y al cabo, es una referencia a la masturbación. (FAP FAP es el meme o más bien dicho la onomatopeya internacional para tal acto impío).

En un principio, pensamos y diseñamos Fap Fapp para iPhone. Sin embargo, Apple tiene unas políticas bastante estrictas a la hora de publicar aplicaciones. Así que tras intentarlo durante varias revisiones sin éxito, en lugar de desechar la aplicación, cambiamos a Google Play.

Para minimizar el tiempo de desarrollo en Android, la aplicación se ha implementado usando jQuery Mobile y PhoneGap.

Etiquetas |

Reducir una imagen antes de subirla via Canvas

Normalmente a la hora de subir una imagen a un servidor o hosting mediante un formulario se utilizan los input de tipo file.
¿Qué sucede si la imagen que queremos subir no debe superar un cierto tamaño?

En la mayoría de casos lo que se hace es enviar esa imagen a través del input y una vez se ha cargado en el servidor, éste comprueba si tiene un tamaño aceptado y si no lo tiene aborta el proceso y la destruye. Cuantas veces no habéis visto el típico “Subiendo” en la barra del navegador y una vez acaba dice algo como “Archivo demasiado grande”.

Con Canvas podemos reducir la imagen con el fin de reducir el tamaño antes de subirla. Obviamente perderemos calidad, pero nos ahorrará poner un mensaje diciendo que no se admiten imágenes > 1 MB y que luego nos contacten usuarios, que no se molestan en leer, diciendo que intentan subir un TIFF de 50MB y les peta la web (/sight).

La idea es fijar un tamaño fijo de píxeles para el ancho (Nx) y el alto (Ny) de la imagen y que al reducirla esta no pueda sobrepasar nunca esos tamaños. Es más, para asegurarnos, podríamos hacer que Nx y Ny fueran iguales y definir N = Nx = Ny. Esto aseguraría que cualquier imagen que tuviera longitud y altura por debajo de N, ocuparía siempre menos que su imagen de número de píxeles NxN.

Y si por lo que fuera las imágenes tuvieran que ocupar un tamaño considerable siempre se puede prescindir de la subida por defecto por input file y utilizar objetos Blob de Canvas para controlar la transferencia de subida.

Suponiendo que tenemos una input file que utilizamos para coger las imágenes:

<input id="fbBrowseBtn" type="file" name="photo[]" multiple>

Con el siguiente código de Javascript podríamos cojer la imagen, reducirla, enviarla y controlar el progreso una vez haya finalizado la acción:

var files = document.getElementById('fbBrowseBtn').files;
resizeAndUpload(files[0], uploaded, progressBar);
 
function resizeAndUpload(file, callback, progress)
{
	var reader = new FileReader();
	reader.onloadend = function() {
		var tempImg = new Image();
		tempImg.onload = function() {
			// Comprobamos con el aspect cómo será la reducción
			// MAX_IMAGE_SIZE_PROCESS es la N que definimos como máxima
			var MAX_WIDTH = MAX_IMAGE_SIZE_PROCESS;
			var MAX_HEIGHT = MAX_IMAGE_SIZE_PROCESS;
			var tempW = tempImg.width;
			var tempH = tempImg.height;
			if (tempW > tempH) {
				if (tempW > MAX_WIDTH) {
					tempH *= MAX_WIDTH / tempW;
					tempW = MAX_WIDTH;
				}
			} else {
				if (tempH > MAX_HEIGHT) {
					tempW *= MAX_HEIGHT / tempH;
					tempH = MAX_HEIGHT;
				}
			}
			// Creamos un canvas para la imagen reducida y la dibujamos
			var resizedCanvas = document.createElement('canvas');
			resizedCanvas.width = tempW;
			resizedCanvas.height = tempH;
			var ctx = resizedCanvas.getContext("2d");
			ctx.drawImage(this, 0, 0, tempW, tempH);
			var dataURL = resizedCanvas.toDataURL("image/jpeg");
 
			// Pasamos la dataURL que nos devuelve Canvas a objeto Blob
			// Envíamos por Ajax el objeto Blob
			// Cogiendo el valor de photo (nombre del input file)
			var file = dataURLtoBlob(dataURL);
			var fd = new FormData();
			fd.append("photo", file);
 
			$.ajax({
				url: <<url del endpoint que se encarga de la subida>>,
				type: "POST",
				data: fd,
				processData: false,
				contentType: false,
				dataType: 'json',
				xhr: function() {
					var xhr = new window.XMLHttpRequest();
					xhr.upload.addEventListener("progress", function(evt) {
						if (evt.lengthComputable) {
							// Calculando el porcentaje de todo el proceso 
							var percentComplete = evt.loaded / evt.total;
							progress(percentComplete * PERCENT_UPLOAD * 0.01);
						}
					}, false);
					return xhr;
				}
			}).done(function(respond) {
				// Una vez ha acabado la subida
				callback(respond);
			});
		};
		tempImg.src = reader.result;
	}
	reader.readAsDataURL(file);
}
 
function dataURLtoBlob(dataURL)
{
	// Decodifica dataURL
	var binary = atob(dataURL.split(',')[1]);
	// Se transfiere a un array de 8-bit unsigned
	var array = [];
	var length = binary.length;
	for(var i = 0; i < length; i++) {
		array.push(binary.charCodeAt(i));
	}
	// Retorna el objeto Blob
	return new Blob([new Uint8Array(array)], {type: 'image/jpeg'});
}
 
function uploaded(response)
{
	// Código siguiente a la subida
}
 
function progressBar(percent)
{
	// Código durante la subida
}

El método controlador del endpoint sería algo así:

$data = array();
$data['success'] = false;
// Miramos que efectivamente sea un imagen
$size = getimagesize($_FILES['photo']['tmp_name']);
if($size) {
	if(	(
		$_FILES['photo']['type'] == 'image/gif' || 
		$_FILES['photo']['type'] == 'image/jpeg' || 
		$_FILES['photo']['type'] == 'image/jpg' || 
		$_FILES['photo']['type'] == 'image/pjpeg' || 
		$_FILES['photo']['type'] == 'image/png' || 
		$_FILES['photo']['type'] == 'image/x-png')) {
			// Es una imagen, la guardamos cogiendo los datos de la key 'photo' de $_FILES
			$newFile = "path definitiva donde se guardará la imagen";
			if (move_uploaded_file($_FILES['photo']['tmp_name'], $newFile)) {
				// Todo ha ido bien, devolvemos la ubicación de la imagen.
				// Marcamos la respuesta como buena
				$data['success'] = true;
				$data['image'] = $newFile;
				$data['msg'] = lang('upload_success');
			} else {
				// Error transfiriendo la imagen a la ubicación
				$data['msg'] = lang('upload_error');
			}
	} else {
		// No se trata de una imagen habitual
		$data['msg'] = lang('upload_error_extension');
	}
 
} else {
	// No es una imagen
	$data['msg'] = lang('upload_error_noimage');
}
echo json_endode($data);

Ambos casos tuve que aplicarlos en una nueva página que hice por mera diversión: Let’s Pixel.

Etiquetas | 8 comentarios

Twitter: Error leyendo tweets sin autentificar con la API 1.0

Twitter ha actualizado su API a la versión 1.1.

Uno de los cambios de esta API ha provocado que muchas de las páginas en las que había programado una lista de tweets para una cuenta de Twitter determinada dejaran de funcionar.
Por ejemplo en muchos casos programé bloques o muros de tweets utilizando Tweet! (plugin de jQuery).

La conexión que realiza este plugin, a día de hoy, se basa en la vieja API 1.0 en la que no eran necesarias las conexiones autentificadas. No me ha quedado otra que revisar página a página y adaptar el listado de tweets a la nueva API 1.1.

Por suerte, mucha gente se ha encontrado en el mismo problema y ya han encontrado una solución que permite no empezar desde 0.

En este repositorio de GitHub se pueden descargar los nuevos archivos php y js necesarios para que vuelva a funcionar.

Los pasos a seguir son los siguientes:

1) El repositorio utiliza como acceso a la API la dirección /twitter. En mi caso, como la mayoría de mis webs funcionan con CI, he tenido que crear un controlador llamado twitter en cada una de ellas.

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
 
class Twitter extends CI_Controller {
	public function index()
	{
		$this->twitteroauth->init();
		$this->twitteroauth->fetch();
	}
}

2) Sobrescribir el fichero de Tweet! jquery.tweet.js o su minificado, depende de cual hayamos utilizado.

3) Crear una app en Twitter. Entramos en https://dev.twitter.com/apps y creamos una aplicación rellenando el formulario, indicando como Callback URL y Website la dirección de nuestra página web. Una vez creada generará unos campos que son los que utilizaremos en las variables de configuración de la clase ezTweet si estáis siguiendo los archivos del repositorio. Los campos son App Consume Key, App Consumer Secret, App Access Token y App Access Token Secret.

4) Adaptar las librerías PHP y el archivo index.php para realizar la autentificación. De nuevo, en el caso de CI, hay que adaptarlo. Lo que he ido haciendo es crear librerías con el código que viene en index.php adaptado y las llamo desde el controlador de twitter. Es decir, me he cargado la clase ezTweet y he creado una nueva TwitterOauth que lo único diferente que tiene es que inicializa sus variables de configuración en una función init:

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/**
 * Current TwitterOauth Class
 *
 * @package	Twitter oAuth
 * @subpackage	Libraries
 * @category	Utils Functions
 */
class CI_TwitterOauth {
 
	// Your Twitter App Consumer Key
	private $consumer_key = '';
 
	// Your Twitter App Consumer Secret
	private $consumer_secret = '';
 
	// Your Twitter App Access Token
	private $user_token = '';
 
	// Your Twitter App Access Token Secret
	private $user_secret = '';
 
	// Path to tmhOAuth libraries
	private $lib = '';
 
	// Enable caching
	private $cache_enabled = true;
 
	// Cache interval (minutes)
	private $cache_interval = 15;
 
	// Path to writable cache directory
	private $cache_dir = './';
 
	// Enable debugging
	private $debug = true;
 
	/**************************************************************************************/
 
	public function init() {
 
		$this->CI =& get_instance();
 
		// Initialize paths and etc.
		$this->pathify($this->cache_dir);
		$this->pathify($this->lib);
		$this->message = '';
 
		// Set server-side debug params
		if($this->debug === true) {
			error_reporting(-1);
		} else {
			error_reporting(0);
		}
 
		$this->consumer_key = 'CONSUMER KEY AQUÍ';
		$this->consumer_secret = 'CONSUMER SECRET AQUÍ';
		$this->user_token = 'USER TOKEN AQUÍ';
		$this->user_secret = 'USER SECRET AQUÍ';
		$this->lib = APPPATH. 'libraries/lib/';   // En mi caso tengo los archivos tmhOAuth.php y tmhUtilities.php del repositorio en application/libraries/lib
		$this->cache_enabled = true;
		$this->cache_interval = 15;
		$this->cache_dir = './';
		$this->debug = true;
	}
 
	public function fetch() {
		echo json_encode(
			array(
				'response' => json_decode($this->getJSON(), true),
				'message' => ($this->debug) ? $this->message : false
			)
		);
	}
 
	private function getJSON() {
		if($this->cache_enabled === true) {
			$CFID = $this->generateCFID();
			$cache_file = $this->cache_dir.$CFID;
 
			if(file_exists($cache_file) && (filemtime($cache_file) > (time() - 60 * intval($this->cache_interval)))) {
				return file_get_contents($cache_file, FILE_USE_INCLUDE_PATH);
			} else {
 
				$JSONraw = $this->getTwitterJSON();
				$JSON = $JSONraw['response'];
 
				// Don't write a bad cache file if there was a CURL error
				if($JSONraw['errno'] != 0) {
					$this->consoleDebug($JSONraw['error']);
					return $JSON;
				}
 
				if($this->debug === true) {
					// Check for twitter-side errors
					$pj = json_decode($JSON, true);
					if(isset($pj['errors'])) {
						foreach($pj['errors'] as $error) {
							$message = 'Twitter Error: "'.$error['message'].'", Error Code #'.$error['code'];
							$this->consoleDebug($message);
						}
						return false;
					}
				}
 
				if(is_writable($this->cache_dir) && $JSONraw) {
					if(file_put_contents($cache_file, $JSON, LOCK_EX) === false) {
						$this->consoleDebug("Error writing cache file");
					}
				} else {
					$this->consoleDebug("Cache directory is not writable");
				}
				return $JSON;
			}
		} else {
			$JSONraw = $this->getTwitterJSON();
 
			if($this->debug === true) {
				// Check for CURL errors
				if($JSONraw['errno'] != 0) {
					$this->consoleDebug($JSONraw['error']);
				}
 
				// Check for twitter-side errors
				$pj = json_decode($JSONraw['response'], true);
				if(isset($pj['errors'])) {
					foreach($pj['errors'] as $error) {
						$message = 'Twitter Error: "'.$error['message'].'", Error Code #'.$error['code'];
						$this->consoleDebug($message);
					}
					return false;
				}
			}
			return $JSONraw['response'];
		}
	}
 
	private function getTwitterJSON() {
		require $this->lib.'tmhOAuth.php';
		require $this->lib.'tmhUtilities.php';
 
		$tmhOAuth = new tmhOAuth(array(
			'host'                  => $_POST['request']['host'],
			'consumer_key'          => $this->consumer_key,
			'consumer_secret'       => $this->consumer_secret,
			'user_token'            => $this->user_token,
			'user_secret'           => $this->user_secret,
			'curl_ssl_verifypeer'   => false
		));
 
		$url = $_POST['request']['url'];
		$params = $_POST['request']['parameters'];
 
		$tmhOAuth->request('GET', $tmhOAuth->url($url), $params);
		return $tmhOAuth->response;
	}
 
	private function generateCFID() {
		// The unique cached filename ID
		return md5(serialize($_POST)).'.json';
	}
 
	private function pathify(&$path) {
		// Ensures our user-specified paths are up to snuff
		$path = realpath($path).'/';
	}
 
	private function consoleDebug($message) {
		if($this->debug === true) {
			$this->message .= 'tweet.js: '.$message."\n";
		}
	}
}
Etiquetas |

Consejos a la hora de montar tu propia web o tienda virtual

Este post es el resultado de multitud de experiencias como programador que he tenido a la hora de programar y diseñar ya sean webs corporativas, aplicaciones webs o tiendas virtuales.

Me he cruzado con todo tipo de clientes y puedo afirmar que existen unas normas básicas que cuando son incumplidas por alguna de las partes (cliente – diseñador / programador) surgen problemas que o bien merman la calidad o bien incrementan el tiempo y por lo tanto el coste del producto final. Son normas de sentido común, pero que seguramente por comodidad o por negar lo que es evidente no se llegan a hacer.

Las he resumido en la siguiente lista:

1. DEFINIR LOS OBJETIVOS / CARACTERÍSTICAS DEL PRODUCTO

Para evitar contratiempos y derivaciones de presupuesto es de vital importancia dejar todo atado antes de empezar a programar / diseñar. Es de gran ayuda tener definidos los wireframes y las especificaciones y objetivos de cada apartado.

¿Te imaginas ir a un mecánico después de haberle pedido que te cambie las ruedas de tu coche y le dices que además de la ruedas tiene que darle un toque de pintura y que vas a pagarle lo mismo? ¿A que la situación es surrealista?

La solución por muy aburrida que sea es dejar constancia de que hace exactamente cada elemento del producto. Es necesario. Como clientes ahorraremos en sorpresas y como desarrolladores en control del presupuesto.


2. CONFIAR EN LOS PROFESIONALES

A los diseñadores / programadores se nos cuestiona nuestro trabajo continuamente (más a los diseñadores ya que su trabajo es visual). Hay que tener en cuenta que, por lo general, se contrata a un profesional que normalmente sabe mejor que el cliente como comunicar un diseño o usar una aplicación.

Como cliente está bien dar pinceladas y apuntar notas para que el desarrollo vaya encaminado a lo que se quiere, pero es contraproducente rectificar diseños y alterar comportamientos de programación cuando los profesionales que has contratado te desaconsejan hacerlo.

Esto puede intoxicar la relación entre ambos. Y es completamente innecesario ya que cuando mejor salen las cosas es cuando hay mutuo acuerdo y no una obligación. Así que un poco de confianza no viene mal. Nadie quiere destrozar tu idea, los desarrolladores buscan lo mejor para todos. ¿Por qué algunos se empeñan en ver las opiniones de un desarrollador como algo malo si lo único que se pretende es mejorar el producto lo máximo posible? Motivación, reputación, profesionalidad, …


3. PLATAFORMAS DE DESARROLLO

A la hora de producir una web o tienda virtual existen dos caminos. Partir de 0 con programación y diseño a medida o escoger alguna de las plataformas o CMS que existen.

La primera opción, evidentemente, tiene un tiempo de desarrollo y por lo tanto un coste superior. Pero como pro, no tiene las limitaciones que tiene la segunda opción. Y ahí es dónde voy.

Está muy bien elegir el segundo camino porque el presupuesto es reducido o el proyecto es urgente, pero hay que tener claro que existen ciertas funcionalidades que a priori no se pueden realizar en según que CMS o plataformas, y si se pudieran elevarían el coste a niveles incluso superiores que habiendo hecho exactamente lo mismo con la primera opción. Esto es así, porque para poder programar o diseñar esas funcionalidades hace falta modificar el núcleo o plugins instalados de los CMS o plataformas, lo cual implica un conocimiento bastante especializado que, por supuesto, se tiene que pagar.

En definitiva, si se elige un desarrollo por CMS o plataformas hay que tener claro hasta dónde podemos llegar y no buscar cabezas de turco cuando surjan derivaciones extremadamente altas en el presupuesto para según que cambios. Aquí es donde entra en acción otra vez el punto 1. ¡Déjalo todo bien atado antes de empezar y prevé modificaciones!


4. POSICIONAMIENTO – SEO

Alguna gente se piensa que con solo hacer una página web por arte de magia aparece en los primeros resultados de Google. Imaginemos la siguiente situación: un casillero de más de 500 millones de casillas y una de las casillas es tu web. ¿Cómo puedes esperar o pretender que una persona normal encuentre la casilla sin tu haberle dicho antes dónde está?
El posicionamiento natural de las webs lo realizan los robots (crawlers) de los grandes como Google. En base al contenido que encuentren en tu página la posicionaran mejor para determinadas búsquedas. Pero si ese contenido es escaso y además no es acertado a muy duras penas se escalaran posiciones.

A nivel de programación se puede preparar una web para que el contenido sea mejor interpretado por los crawlers, pero no es suficiente. El contenido tiene que ser adecuado y amplio. En este punto es importante que el cliente proporcione toda la información que tenga a su disposición y lo contraste con un equipo de comunicación o directamente con un experto SEO para que se pueda redactar el texto que aparecerá en la web de forma óptima.

Es importante buscar un equilibrio entre las modificaciones que se deban hacer por motivos de SEO y el aspecto visual de una web. Muchas veces he presentado una web impecable visualmente y posteriormente ha quedado destrozada por modificaciones en los titulares, texto redundante, etc. No olvidéis que una buena imagen lo es todo una vez has atraído a un posible cliente a tu web. No sirve de nada juntar clientes y que luego huyan despavoridos cuando vean el monstruo que has engendrado.


5. PRECIO / PRESUPUESTO

Es extremadamente ofensivo cuando se pide un presupuesto y se intenta regatear o menospreciar el trabajo que se va a realizar. Por ejemplo, yo como programador nunca he cuestionado y pedido justificaciones a un cliente sobre lo que cuesta hacer su trabajo, sin embargo sí que he tenido casos en los que tengo que explicar técnicamente cómo voy a tener programar ciertas funcionalidades.

Es absurdo. La mayoría de veces no entienden la explicación (lo cual es normal) y tengo la sensación que aflora un sentimiento de desconfianza. Así que el consejo es que al igual que debes confiar en el profesional que contratas, ten un poco de respeto por su trabajo.


6. SIMPLIFICA

Hay que bajarse de las nubes e ir paso a paso a la hora de desarrollar. Como cliente intenta sacar la esencia de la idea y busca desarrolladores. No añadas más de lo esencial, no ensucies el producto. Cuanto más simple y bien haga lo que ha de hacer más a gusto estarán los usuarios. A la gente no le gusta lo complicado, no le importa que a ti te haga mucha ilusión, tienen sus propias preocupaciones y quieren algo que comunique lo justo sin adornos.

Como programador o diseñador, cíñete a lo que pide el cliente. Consulta antes de tomar iniciativas. No somos mercenarios a sueldo está claro, pero tampoco somos artistas.



Creo que éstos serían los puntos más críticos para mejorar la relación cliente-desarrollador y para que el producto final, que es lo que queremos todos, sea lo mejor posible. Obviamente no hay que tomárselos al pie de la letra pero si tenerlos en cuenta. Para terminar este wall-of-text os dejo un video para reflexionar y echar unas risas:



Etiquetas | 4 comentarios