SharePoint pide credenciales al acceder archivos en una biblioteca de documentos

image

Español | English

No he actualizado esta versión. Pero puedes leerla en ingles actualizada 29-SEP-2011. Leerla actualizada pero en inglés.

Hace tiempo que aquí en la Universidad nos enfrentamos a un problema con cierta funcionalidad que traen los productos WSS, SharePoint Portal 2007 y SharePoint Server 2010.

Como sabemos, para acceder a un sitio Web de SharePoint, debemos ingresar nuestras credenciales de la forma dominio\username. Esto esta bien si el sitio no tiene el acceso anónimo habilitado. ¿Pero qué pasa cuando hacemos clic en un documento de Office (digamos un Word o Excel) de nuestra biblioteca de documentos?

Cada vez que tratamos de abrir un documento, Office nos pregunta nuevamente por las credenciales una y otra vez. Esto pasa aún si el usuario es el creador del documento o el acceso anónimo esta habilitado.

¿Y qué tiene que decir al respecto Microsoft?

This is by design.

Referencias de este problema desde sitios de Microsoft:

Multiple Logon while open office Document from SharePoint
http://blogs.technet.com/b/steve_chen/archive/2010/06/25/multiple-logon-while-open-office-document-from-sharepoint.aspx

Office: Authentication prompts when opening Microsoft Office documents
http://support.microsoft.com/kb/2019105

How documents are opened from a Web site in Office 2003
http://support.microsoft.com/kb/838028

Acaso están locos?

Lo que esta pasando es que Office esta tratando de ser muy inteligente y necesita saber quien eres para ofrecerte más funcionalidad para edición de contenido en línea dentro de la suite de Office.

– Yo: Esto esta bien, pero al menos dame la opción de deshabilitar eso desde la Administración de SharePoint.

– Office-SharePoint: No. No puedes.

Explanation of this behaviour

Well, if you try to open an Office file directly from a remote place, Office wants to know who the hell you are. If the server responses with the

  • Microsoft Office Protocol Discovery or a
  • FrontPage Protocol or a
  • WebDav Protocol

then it ask you for credentials. Not only Office does this. If you try to open a library as a HTTP SharePoint Folder View, Windows (something to do with how IE handles downloads) does the same. The request comes from one of these protocols so the Microsoft client app have to ask for credentials. It is by design.

You can create an HttpModule that searchs for the Request.UserAgent string, so if you find one of these protocols, then send a 200 statuscode in the response. The problem here is that you are disabling not only logins prompts for documents, but for the entire client integration feature (opening files from Word, Excel or folder views in libraries). This is not what we want.

We want to be able to use client integration, it’s a nice feature, but we do not want to be prompted inside our document libraries.

Solución WSS & SPS2007

Como he mencionado, no hay una solución a este problema pues este comportamiento es por diseño. Pero eso no va a detenernos, ¿o sí? Aquí el workaround.

Después de buscar en la Web sin una solución que realmente funcionara, decidí enfrentar el problema desde el punto de vista de un desarrollador Web. Si notamos, hay una opción dentro del menú contextual llamada “Enviar a->Descargar una copia”. Esta opción hace exactamente lo que queremos. It calls a file inside layouts folder called download.aspx. This file serves a SP file using:

[js]Response.AddHeader("Content-Disposition", "attachment;filename=" + filename);[/js]

That’s why this option does not open the file directly on the server (the attachment string does it all).

The problem here is that this download.aspx file needs you to be authenticated. So if your site is anonymous, the option “Sent To->Download a Copy” will ask you for credentials. It’s stupid I know, but it does. They fix this on SPS2010 though.

Don’t worry. We are going to handle the anonymous stuff later on.

image

Bueno, ¿y donde esta el código que hace eso?

Exacto. Como lo supones, esta en un archivo de Java Script llamado “CORE.JS” dentro de la carpeta C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\template\layouts\[some numbers].

1. Recuerda hacer un respaldo del archivo core.js.

2. Busca esta línea dentro del archivo CORE.JS:

[js]function DispDocItemEx(ele, fTransformServiceOn, fShouldTransformExtension, fTransformHandleUrl, strProgId)[/js]

3. Agrega este código justo debajo:

[js] // Agregado por Memo Vera para evitar que abra las App de Office.
// 28 SEP 2010
if (ctx.CurrentUserId < 0) {
// Si no vas a usar el http modulo para anonimos, comenta de aqui hasta donde dice Termina parte …
var office_extensions = /(.docx?|.pptx?|xlsx?|md[ab]|mp[pt])$/i; // Modifica las extensiones de Office que quieras interceptar
if (office_extensions.test(ele)) {
STSNavigate(ctx.HttpRoot + "/_layouts/download.aspx?SourceUrl=" + ele);
return false;
}
// Termina parte especial de modulo para anonimos.
if (browseris.ie)
event.cancelBubble = false;
return true;
}
STSNavigate(ctx.HttpRoot + "/_layouts/download.aspx?SourceUrl=" + ele + "&Source=" + GetSource());
return false;
// Termina agregado[/js]

Deja el resto de la función como esta. No necesitamos cambiar nada más.

No te preocupes, no necesitas reiniciar tu servidor de SharePoint u otro servicio. Tampoco necesitas agregar la URL de tu sitio de SharePoint a los Sitios de Confianza de IE (aunque siempre se recomienda hacerlo).

Esto es todo. Actualiza tu página Web y asegúrate de limpiar el cache de tu navegador Web para que bajes la última versión del archivo CORE.JS.

image

Tengo esto funcionando en mi ambiente de WSS 3.0 sin problemas. Todos andan felices, pueden usar la parte de integración de clientes y esas peticiones por credenciales han desaparecido. Si queremos editar un documento en línea usando Word, pues seleccionamos la opción “Editar en Microsoft Word” o abrimos el documento desde la aplicación de Word.

image

Anonymous sites on MOSS 2007 and WSS 3.0

Like I wrote before, we are using the C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\template\layouts\download.aspx file witch is the one that serves files from SP content database. The problem with anonymous sites and this file, is that it needs you to be authenticated. As crazy as it might sound, it is true. Try it yourself on a clean SP box:

Create an anonymous site, upload some Office documents and log off. Visit your site as anonymous and click on “Sent To->Download a Copy”. The box will ask for your credentials. Just crazy.

image

Microsoft fix this on SPS2010 though, so you do not need to do this on 2010 boxes.

We are going to write an HTTP module that catches the authentication request when it comes from this download.aspx file and we will serve the file ourselves.

1. I already created the module for you. You can download the compiled library and the source code here: MemoQuitaMOPD.zip

Here you have the C# code:

[csharp]
using System;
using System.Web;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net;

namespace MemoQuitaMOPD
{
public class MemoQuitaMOPD : IHttpModule
{
public void Init(HttpApplication context)
{
// Vamos a interferir justo después de pedir la autenticacion
context.PostAuthenticateRequest += new EventHandler(context_PostAuthenticateRequest);
}

/// <summary>
/// Esta se ejecuta justo despues de realizada la autenticacion.
/// Entonces ya sabemos quien es y podemos preguntar si esta autenticado.
/// </summary>
/// <param name="sender"></param>
/// <param name="e">Aqui viene la HttpApplication</param>
void context_PostAuthenticateRequest(object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
HttpContext context = app.Context;
// Si no es anonimo, dejamos que SP trabaje normal
if (context.User != null)
if (context.User.Identity.IsAuthenticated)
return;
// Es anonimo y ademas esta pidiendo la pagina download
if (context.Request.RawUrl.ToString().IndexOf("download.aspx?SourceUrl") >= 0)
{
if (String.IsNullOrEmpty(context.Request.QueryString["SourceUrl"]) == false)
{
WebClient client = new WebClient();
try
{
// Obtenemos la url
string url = context.Request.QueryString["SourceUrl"];
if (url.IndexOf("http://") < 0)
url = "http://" + context.Request.Url.Host + "/" + url;
// Obtenemos el nombre del archivo
string filename = Path.GetFileName(context.Request.QueryString["SourceUrl"]);
// SP reemplaza los espacios con guion bajo
filename = filename.Replace(‘ ‘, ‘_’);
// Descargamos nuestro archivo en nuestro arreglo de bytes.
byte[] myDataBuffer = client.DownloadData(url);
context.Response.ClearHeaders();
context.Response.ClearContent();
// Nombre del archivo para que el usuario no vea download.aspx en el cuadro de descarga
context.Response.AddHeader("Content-Disposition", "attachment;filename=" + filename);
context.Response.ContentType = "application/octet-stream";
// Para mis amigos de otros idiomas:
// You only need to especify this line if your installation is in Spanish like mine
// This handles special chars like ñ, á, é, etc.
context.Response.HeaderEncoding = Encoding.GetEncoding("iso-8859-1");
// Mandamos OK al browser y servimos nuestro archivo
context.Response.StatusCode = 200;
context.Response.BinaryWrite(myDataBuffer);
}
catch
{
// Si hay algún error, mandamos un 404 NOT FOUND
context.Response.StatusCode = 404;
}
finally
{
client.Dispose();
}
context.Response.End();
}
}
}

/// <summary>
/// Esta funcion es por si quieres imprimir mensajes en un archivo de texto para depurar la aplicacion.
/// Por ejemplo si quieres ver que trae el URL: Log(context.Request.RawUrl.ToString(),app.Context);
/// </summary>
/// <param name="mensaje">Cadena a imprimir</param>
/// <param name="context">El app.Context que viene en context_AuthenticateRequest</param>
public void Log(string mensaje, HttpContext context)
{
StreamWriter LogFile = null;
try
{
LogFile = new StreamWriter("c:\\Inetpub\\wwwroot\\log.txt", true);
LogFile.WriteLine(System.DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss tt") + " – " + mensaje);
LogFile.Close();
LogFile.Dispose();
}
catch
{
//No hace nada
}
}

public void Dispose()
{
}
}
}
[/csharp]

2. If you trust me, you only need the file MemoQuitaMOPD.dll (It’s already signed).

Optional: If you want to make your it own, create a new Visual Studio 2010 Class Library Project and use the source file MemoQuitaMOPD.cs. Make changes if you like. Not need to put my name or whatever in it. It is for you.

image

3. Be careful here. We are going to add this library into the server GAC. Just log into your server and open the folder C:\WINDOWS\assembly. Drag and drop the MemoQuitaMOPD.dll library here (do not copy-paste, just drag). This way we have installed the library. Easy as cake.

image

4. Let’s make it available for SharePoint. Open your SharePoint Website configuration file. Usually in: C:\Inetpub\wwwroot\web.config and find this lines:

[xml]<httpmodules>
<clear />
[/xml]

Add this line right after the clear tag:

[xml]<add name="MemoQuitaMOPD" type="MemoQuitaMOPD.MemoQuitaMOPD, MemoQuitaMOPD, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9bef5ad97fb29b08" />[/xml]

If you made your own library, change the class name and namespace accordingly. Nice! Save your file.

5. Try it! Visit your site anonymously and click on a file or use the option Sent To->Download a Copy”.

image

Beautiful, isn’t it?

image

At some point if you want to disable the module, just comment the line we added in the web.config and the ones marked in CORE.JS file. No harm done. Risa

image

Solución SharePoint Server 2010

En SPS 2010 un poco diferente.

Busca un archivo JavaScript llamado “init.js” qué está en \Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\1033.

Las funciones dentro de este archivo se refieren al core.js. Este archivo init.js esta minified (viene de init.debug.js) así que no será fácil editarlo. Tienes que tener cuidado.

1. Realiza un respaldo de tu archivo init.js.

2. Encuentra esta línea en tu archivo init.js:

[js]
function DispEx(o,n,e,a,d,i,g,m,k,b,h,j,f,c,l){
[/js]

3. Agrega lo siguiente justo después:

[js]
/*Agregado por Memo Vera para evitar que abra las App de Office. 18 NOV 2010.*/STSNavigate(ctx.HttpRoot + "/_layouts/download.aspx?SourceUrl=" + o);return false;
[/js]

Deja el resto de la función como está. No hay necesidad de cambiar nada más.

Esto aplica para todos los archivos de la suite de Office y también funciona con otros navegadores como FireFox:

Espero que esto te ayude.

Problemas

Bueno, el único problema que puedo pensar ahora mismo, es cuando una actualización o parche de SharePoint reemplace nuestro archivo core.js. No creo que sea mucho problema. Añadimos tan solo un par de lineas de código. Ciertamente podemos ponerlas de vuelta.

Claro que podemos hacer algo más elegante como programar un módulo que haga esto, cambiar nuestras master o layouts pages. Pero, en realidad, ¿para que molestarse tanto? Tan sólo queremos que cuando el usuario haga clic, descargue su documento (como debería de ser). Algo tan simple, merece una solución así de simple.

Encontré un detalle al usar el módulo con los nombres de archivo si usas caracteres como ñ, o acentos. En un rato libre trataré de corregir eso.

Pruébalo y déjame saber si te ha funcionado.