Visibilidades en JavaScript

Una de las cosas que se argumentan en contra de JavaScript cuando se habla de orientación a objetos es que no soporta la visibilidad de métodos o propiedades. Es decir, todo es público por defecto.

Mucha gente hoy en día cuando programa en JavaScript adopta alguna convención tipo “lo que empiece por guión bajo es privado y no debe ser invocado”. Como chapuza para ir tirando, pues bueno, pero en JavaScript hay maneras de simular una visibilidad privada y de que realmente el creador de un objeto no pueda invocar algunos métodos. En este post veremos un método rápido y sencillo. Por supuesto no es el único ni tiene porque ser el mejor…

Empecemos por la declaración de una función constructora que me permite crear objetos de tipo Foo:

  1. var Foo = function () {
  2.     this.count = 0;
  3.     this.inc = function() {
  4.         this._addToCount(1);
  5.     };
  6.  
  7.     this._addToCount = function (a) {
  8.         this.count += a;
  9.     };
  10. }
  11.  
  12. var foo = new Foo();
  13. console.log(foo.count);
  14. foo.inc();
  15. // Esto debera ser privado
  16. foo._addToCount(100);
  17. console.log(foo.count);
  18. // count no debera poder accederse
  19. foo.count = 10;
  20. console.log(foo.count);

Estoy usando la convención de que los métodos privados empiezan por un guión bajo. Pero es esto: una convención. Para el lenguaje no hay diferencia. De hecho si ejecuto este código el resultado es el siguiente:

image

El desarrollador que crea un objeto Foo puede acceder tanto a inc, como a addToCount como a count. Como podemos solucionar eso?

La solución pasa por no devolver a quien crea el objeto Foo entero si no un “subobjeto” que tan solo contenga las funciones publicas:

  1. var Foo = function () {
  2.     this.count = 0;
  3.     this.inc = function() {
  4.         this._addToCount(1);
  5.     };
  6.  
  7.     this._addToCount = function (a) {
  8.         this.count += a;
  9.     };
  10.  
  11.     return {
  12.         inc : this.inc
  13.     };
  14. }
  15.  
  16. var foo = new Foo();
  17. console.log(foo);

Si ejecuto este código parece que vamos por el buen camino:

image

Ahora el objeto foo contiene tan solo el método inc. Pero, que ocurre si ¿lo ejecutamos? Pues eso:

image

JavaScript se queja que el método _addToCount no está definido! Que es lo que ha ocurrido? Lo ocurrido tiene que ver con el contexto de JavaScript o el valor de this. El método inc que invocamos es el método inc del objeto anónimo que devolvemos al final de la función constructora de Foo. Dentro de este método el valor de this es el valor del objeto anónimo que, por supuesto, no tiene definido _addToCount. Parece que estamos en un callejón sin salida, verdad?

Aquí es cuando entra en escena la función bind: bind es un función que se llama sobre una función. El resultado de aplicar bind a una función es otra función pero atada permanentemente al contexto que se pasa como parámetro a bind. Dicho de otra manera cuando devolvemos el objeto anónimo, tenemos que modificar el contexto del método inc para que sea el objeto Foo entero. Así modificamos el return para que quede como:

  1. return {
  2.     inc: this.inc.bind(this)
  3. };

Cuando se ejecuta este return el valor the this es el objeto Foo entero así que lo que estamos devolviendo es un objeto anónimo, con una función inc (que realmente es this.inc es decir la función inc del objeto Foo entero), pero que está bindeada a this (el objeto Foo entero), es decir que cuando se ejecute este método inc del objeto anónimo el valor de this no será el objeto anónimo si no el propio objeto Foo.

Con esto hemos terminado! Ahora cuando llamamos a new Foo(), lo que obtenemos es un objeto solo con el método inc. Cuando invocamos inc todo funciona ahora correctamente. Y ya no podemos invocar el método privado _addToCount ni acceder a la propiedad count.

Esto es tan solo un mecanismo, hay varias maneras distintas de hacer lo mismo pero todas se basan en este mismo principio.

Saludos!

PD: Os dejo el código de un método, que he llamado _publicInterface. Dicho método lo que hace es, a partir de un objeto, crear otro objeto que contenga tan solo aquellas funciones que NO empiezan por guión bajo:

  1. this._publicInterface = function() {
  2.     var keys = Object.keys(this);
  3.     var protocol = {};
  4.     for (var idx = 0; idx < keys.length; idx++) {
  5.         var key = keys[idx];
  6.         if (key.charAt(0) !== ‘_’ && typeof (this[key]) === «function») {
  7.             protocol[key] = this[key].bind(this);
  8.         }
  9.     }
  10.  
  11.     return protocol;
  12. };

Así podéis definir en vuestros objetos funciones públicas y privadas (que empiecen por guión bajo) y en el return final hacer: return this._publicInterface();

ASP.NET MVC, [Authorize] y jQuery.load

Muy buenas! Estreno el blog este 2014… dios a finales de Febrero! A ver, si empezamos a retomar el ritmo…

Este es un post sencillito, por si os encontráis con ello. La situación es la siguiente: Tenéis controladores que devuelven vistas parciales, las cuales desde JavaScript incluís dentro de vuestro DOM a través de una llamada Ajax, usando p. ej. el método load de jQuery.

Todo funciona correctamente, hasta que un día el usuario entra en el site, se va a comer y cuando vuelve pulsa uno de esos enlaces (o botones o lo que sea) que incluyen una de esas llamadas Ajax… Y ocurre que en lugar de aparecer la vista parcial, aparece la página de Login allí incrustada.

La razón, obviamente, es que la acción que devuelve la vista parcial está protegida con [Authorize] y al haber caducado la cookie de autorización, este atributo manda un HTTP 401 (no autorizado). Hasta ahí bien. Entonces entra en juego nuestro amigo FormsAuthentication, que “captura” este 401 y lo convierte en un 302 (redirección) que es lo que recibe el navegador. Desde el punto de vista del navegador, lo que llega es un 302 por lo que este, obendientemente, se redirige a la página de Login. Las peticiones Ajax hacen caso del HTTP 302 y por lo tanto el resultado de la redirección (la página de Login) se muestra.

Una alternativa sencilla y rápida para solucionar esto consiste en modificar la petición modificada por FormsAuthentication, de forma que cambiamos todos los 302 que sean resultado de una petición Ajax por un 401 y así revertir lo que FormsAuthentication hace.

  1. protected void Application_EndRequest()
  2. {
  3.     var context = new HttpContextWrapper(this.Context);
  4.     if (FormsAuthentication.IsEnabled && context.Response.StatusCode == 302
  5.         && context.Request.IsAjaxRequest())
  6.     {
  7.         context.Response.Clear();
  8.         context.Response.StatusCode = 401;
  9.     }
  10. }

Con esto convertimos todos los 302 en 401 cuando sean peticiones Ajax y estemos bajo FormsAuthentication. Ojo, que los convertimos todos, incluso aquellos 302 legítimos que podrían haber.

Ahora ya solo queda actualizar nuestro código JavaScript y comprobar que no recibimos un 402 😉

Postdata 1: .NET Framework 4.5

Si usas .NET Framework 4.5 (VS2012), ya no es necesario que hagas este truco. En su lugar puedes usar la propiedad SuppressFormsAuthenticationRedirect de HttpResponse y ponerla a true. Si el valor de esa propiedad es true, pues FormsAuthentication no convierte los 401 en 302.

Es una propiedad que debes establecer cada vez, por lo que lo puedes hacer de nuevo en el Application_EndRequest de Global.asax si lo deseas.

Si alguien me pregunta porque narices esa propiedad es a nivel de Response (ya me dirás tu porque el objeto Response tiene que “entender” del framework de autorización), pues no lo sé… pero no me termina de gustar, la verdad.

Postdata 2: Katana Cookie Middleware

Ya lo decía el bueno de Andrés Montes: La vida puede ser maravillosa. Si en FormsAuthentication arreglaron esta situación con la propiedad SuppressFormsAuthenticationRequest en el middleware de autenticación por cookies de Katana volvemos a la situación anterior. Y si usas VS2013 o los nuevos templates de ASP.NET en VS2012 no estarás usando FormsAuthentication si no el middleware de Katana.

Por suerte Katana está mejor pensado que FormsAuthentication y podemos configurar mucho mejor el middleware de autenticación basada en cookies.

Buscad donde se configura el middleware basado en cookies de Katana, que por defecto es en el fichero App_Start/StartupAuth.cs y sustutís:

  1. app.UseCookieAuthentication(new CookieAuthenticationOptions
  2. {
  3.     AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
  4.     LoginPath = new PathString("/Account/Login")
  5. });

por:

  1. app.UseCookieAuthentication(new CookieAuthenticationOptions
  2. {
  3.     AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
  4.     LoginPath = new PathString("/Account/Login"),
  5.     Provider = new CookieAuthenticationProvider
  6.     {
  7.         OnApplyRedirect = ctx =>
  8.         {
  9.             if (!IsAjaxRequest(ctx.Request))
  10.             {
  11.                 ctx.Response.Redirect(ctx.RedirectUri);
  12.             }
  13.         }
  14.     }
  15. });

De esta manera tomamos el control del redirect por 401 y lo hacemos solo si la request no es Ajax. Bueno, bonito y barato.

Ah si! El método IsAjaxResponse… Este método no es el método IsAjaxResponse clásico (ctx.Request es una IOwinRequest) así que os lo tendréis que crear vosotros. Aquí os pongo una implementación:

  1. public static bool IsAjaxRequest(IOwinRequest request)
  2. {
  3.     IReadableStringCollection query = request.Query;
  4.     if (query != null)
  5.     {
  6.         if (query["X-Requested-With"] == "XMLHttpRequest")
  7.         {
  8.             return true;
  9.         }
  10.     }
  11.  
  12.     IHeaderDictionary headers = request.Headers;
  13.     if (headers != null)
  14.     {
  15.         if (headers["X-Requested-With"] == "XMLHttpRequest")
  16.         {
  17.             return true;
  18.         }
  19.     }
  20.     return false;
  21. }

No,  no me deis las gracias… Si el método IsAjaxRequest os funciona las dais al equipo de Katana, ya que está copiado del código fuente de Katana (concretamente de aquí). Si, si… yo también me pregunto porque es privado este método y no un método de extensión público.

En fin… eso es todo! Espero que os sea útil!

Saludos!

Fuentes usadas: