Monthly Archives: March 2009

SharePoint Search FullTextSqlQuery not returning not all items

Today I was breaking my head on some stupid problem within SharePoint Search. We created a site with about 50 dummy pages to test some cross webapp news aggregation webpart that I created. For some reason just 4(!) news items were found, each one existing in another subweb. In the SSP the View Scopes page also showed the magic number of ‘4’ items found.

THe problem? I forgot to set the following property to false, which is set to true by default:

query.TrimDuplicates = false;

It makes sure that items that look like each other, won’t be returned as a result. Due to the fact that i had 50 of the same dummy pages, just one item per subweb was returned.

ApplicationPool password stored as plain text withing SharePoint

A few days ago I was reading a blog (And I forgot what blog!!) with information that the ApplicationPool password was stored as plain text. If you don’t believe me: check the screenshot below:

The password is as well accessible via the objectmodel, when runnin under elevatedPrivilges..

Lessons learned: Always try to have a least-privilegd installation for your SharePoint farm!

Using the CrossListQueryInfo and CrossListQueryCache

Lately, I am relatively a lot working with the CrossListQueryInfo. This is a way to query multiple lists at once, crossing multiple SPWebs, if you want to. And, the main reason why I used it, it’s possible to use the cached lists of the sitecollection, to improve performance! The first steps with this CrossListQueryInfo can be quite frustrating, as there isnt too much documentation to find on.

I will start with a code sample, to show how to use this to query multuple webs to retrieve Items with the contentType News Item OR Location News Item from the publishing pages. When you are going to the CrossListQueryInfo, it´s smart to make use of U2U´s Caml Query Builder, to quickly build up thecaml that you want use.
Things to keep in mind while using the CrossListQueryInfo:

  • The results that are returned are ONLY PUBLISHED items. I was breaking my head around this one, because I was sure that my query was good, the CAML Query builder also returned a result, but theCrossListQueryCache didn’t. After publishing, all the results showed up properly
  • ViewFields: Insert the fields that you want to see. If there is a field inside that doesnt exist in the list that you query, your result will be nill, nada, nothing. Make sure that you put in the INTERNAL field names!
  • RowLimit: When filling in value ‘0’, you won’t see any results
  • When using the CrossListQueryCache , make sure not to use the GetSiteData that uses the SPWeb param: GetSiteData(SPWeb web) and GetSiteData(SPWeb web, SPSiteDataQuery query) DO NOT use caching!!
  • and make sure to use Microsoft.SharePoint.WebPartPages.WebPart! This WebPart inherits from System.Web.UI.WebControls.WebParts.WebPart and enhances it with some extra functionality for connected webparts, client-side programming and data caching!! Thanks to Waldek Mastykarz for sharing this info.

Below is an example of a query that queries all the Publishing pages libraries that resides in the current SPWeb and all childWebs.

 

private DataTable ExampleQuery()

        {

            clqi = new CrossListQueryInfo();

 

            // Insert the list types that you want to use. In this case, its the publishing page library (850, see code below)

            clqi.Lists = "<Lists ServerTemplate=\"" + (int)ListServerTemplateCodes.PageLibrary + "\" />";

 

            // Insert the fields that you want to see. If there is a field inside that doesnt exist in the list that you query, your result will be nill, nada, nothing.

            // Make sure that you put in the INTERNAL field names!

            clqi.ViewFields = "<FieldRef Name=\"Title\" /><FieldRef Name=\"FileLeafRef\" /><FieldRef Name=\"Nieuws_x0020_Type\" /><FieldRef Name=\"Nieuws_x0020_Leverancier\" /><FieldRef Name=\"Uitgelicht\" /><FieldRef Name=\"Created\" /><FieldRef Name=\"Comments\" />";

 

            // scop to use. Another possibility is SiteCollection

            clqi.Webs = "<Webs Scope=\"Recursive\" />";

 

            // turn the cache on

            clqi.UseCache = true;

 

            // if row limit == 0, you will get 0 results

            clqi.RowLimit = 100;

 

            // I know a stringbuilder would be better, but i wanted to show the markup of the query

            clqi.Query = "<OrderBy>" +

                            "<FieldRef Name='Title' />" +

                        "</OrderBy>" +

                        "<Where>" +

                            "<Or>" +

                                "<Eq>" +

                                    "<FieldRef Name='ContentType' />" +

                                    "<Value Type='Text'>News Item</Value>" +

                                "</Eq>" +

                                "<Eq>" +

                                    "<FieldRef Name='ContentType' />" +

                                    "<Value Type='Text'>LocationNews Item</Value>" +

                                "</Eq>" +

                            "</Or>" +

                        "</Where>";

 

            // put the CrossListQueryInfo object into the CrossListQueryCache

            CrossListQueryCache clqc = new CrossListQueryCache(clqi);

 

            // and query the data!

            // make sure: the GetSiteData(SPWeb web) and GetSiteData(SPWeb web, SPSiteDataQuery query) DO NOT use caching!!!

            DataTable tbl = clqc.GetSiteData((SPContext.Current.Site, CrossListQueryCache.ContextUrl());

 

            // return the datatable

            return tbl;

        }

The enum below can be used to make life a little bit easier. I got it from:http://www.aspenhorizons.com/devblog/?p=29

public enum ListServerTemplateCodes
    {

        GenericList = 100,

        DocumentLibrary = 101,

        Survey = 102,

        LinksList = 103,

        AnnouncementsList = 104,

        ContactsList = 105,

        EventsList = 106,

        TasksList = 107,

        DiscussionBoard = 108,

        PictureLibrary = 109,

        DataSources = 110,

        SiteTemplateGallery = 111,

        UserInformationList = 112,

        WebPartGallery = 113,

        ListTemplateGallery = 114,

        XMLFormLibrary = 115,

        MasterPagesGallery = 116,

        NoCodeWorkflows = 117,

        CustomWorkflowProcess = 118,

        WikiPageLibrary = 119,

        CustomGridForAList = 120,

        DataConnectionLibrary = 130,

        WorkflowHistory = 140,

        GanttTasksList = 150,

        MeetingSeriesList = 200,

        MeetingAgendaList = 201,

        MeetingAttendeesList = 202,

        MeetingDecisionsList = 204,

        MeetingObjectivesList = 207,

        MeetingTextBox = 210,

        MeetingThingsToBringList = 211,

        MeetingWorkspacePagesList = 212,

        PortalSitesList = 300,

        BlogPostsList = 301,

        BlogCommentsList = 302,

        BlogCategoriesList = 303,

        PageLibrary = 850,

        IssueTracking = 1100,

        AdministratorTasksList = 1200,

        PersonalDocumentLibrary = 2002,

        PrivateDocumentLibrary = 2003

    }

more information about this subject can be found at:

http://sharepoint.nailhead.net/2008/04/musing-on-crosslistquerycache-class.html

SharePoint Logging to the ULS and the eventlogs

Tis weekend I came across a function that makes it easy to log to the ULS, Diagnostic Logging. I even didnt know about the namespaces where the classes reside in.

The first one is a class that can log to the ULS, Diagnostic logging: Microsoft.Office.Server.Diagnostics

when using

PortalLog.LogString(“Exception Occurred: {0} || {1}”, ex.Message, ex.StackTrace);

it’s possible to write tot the diagnostic logging. It contains several other functions like a DebugLogString, and

PortalLog.LaunchWatson(ex);

to launch Dr. Watson

When you want to write to the eventlog, use the following namespace and code: Microsoft.SharePoint.Portal.Diagnostics

SafeEventLog l = new SafeEventLog();

l.WriteEntry(ex, EventLogEntryType.Error);

LDAP Problems updating LDAP information: saving attribute values gives error: 0x8000500C – The Active Directory Datatype Cannot be Converted to/from a Native DS Datatype / LDAPConnection: The LDAP server is unavailable

Our customer had the wish to synchronise some Sharepoint UserProfileProperties of UserProfiles back to LDAP, so that information could be synced with SAP. The attributes that needed to be saved, where inherited from custom schemas in LDAP, which gave us some trouble. When updating attribute values in LDAP, and they inherit from the default scheme, there is really no problem. But at the moment that we tried to update an attribute that inherited from a custom schema, we saw some very weird behaviour:

private void UpdateAttribute(SearchResult sr)

        {

            DirectoryEntry de = sr.GetDirectoryEntry();

            if(de.Properties.Contains("customProperty"))

            {

                de.Properties["customProperty"][0] = "bla";

            }

            de.CommitChanges();

        }

threw a com exception: 0x8000500C – The Active Directory Datatype Cannot be Converted to/from a Native DS Datatype. It even threw the exception at the line:

if(de.Properties.Contains("customProperty"))

When trying to access a property that didn’t exist or a property that inherited from the default schema, no exception was thrown. What is the case? There are 2 possibilities:

1) the schema cache can’t be updated on our machine, and with the lack of a correct schema, the datatype couldn’t be converted.
2) For some reason, LDAP V2 is used instead of V3, which has the problem that custom attributes can’t be loaded

We didn’t find a solution for this, but googled brought me an alternative solution:

the System.DirectoryServices.Protocols namespace!
I tried to make a connection to the LDAP server with the following Code:

LdapDirectoryIdentifier id = new LdapDirectoryIdentifier("LDAP://" + ConfigurationManager.AppSettings["LdapServer"], 389);
LdapConnection _LdapConnection = new LdapConnection(id, AuthType.Basic);
            _LdapConnection.SessionOptions.ProtocolVersion = 3;
            _LdapConnection.Bind(); 

But this threw another error: The LDAP server is unavailable. 2 hours of trial and error brought me to the conclusion: leave the “LDAP://” when specifying the server :S

Using LdapDirectoryIdentifier id = new LdapDirectoryIdentifier(ConfigurationManager.AppSettings[“LdapServer”], 389); everything worked fina, and now we have a 2-way working synchronization between a LDAP store and a SharePoint UserProfileStore!

SharePoint: Add a LDAP import connection using the BDC and a webservice to fill userprofiles with additional information

At the company that I work for at the moment, we have several AD’s and a separate LDAP store with additional user information. For that company we are developing a new Intranet in which we want to add their accounts (account + email) and provision their userProfiles with a lot of extra information like their manager, location, division, telephone numbers etc. Adding a AD import connection and a LDAP import connection isn’t possible, because the accountName isnt stored into the LDAP directory, but they unique key that is used is the email address.

In this post I will explain how to write a webservice to retrieve AD information and how to create a BDC to use this webservice to provision the UserProfiles with data.

At first, we need to create a webservice. Add a reference to System.DirectoryServices and create tweo classes:

one to create a UserProfile that is returned, and on to make a LDAP connection. In my case, they are called “UserProfile and LDAP”

 

[Serializable]
public class UserProfile
    {
        public string Email { get; set; } // Unique for each profile
        public string Location { get; set; }
        public string Manager { get; set; }
 
        // assumption that all LDAP values are strings and single value
        public UserProfile FillUserProfile(SearchResult sr)
        {
            Email = sr.Properties["mail"][0] as string;
            Manager = sr.Properties["manager"][0] as string;
            Location = sr.Properties["location"][0] as string;
            return this;
        }
    }
 
 
public class LDAP
    {
        /// <summary>
        /// The server domain name without any protocol specification.
        /// </summary>
        public string LdapServer { get; private set; }
        /// <summary>
        /// The user name for connecting to LDAP without any attributes
        /// </summary>
        public string LdapUsername { get; private set; }
        /// <summary>
        /// The password for connecting to LDAP
        /// </summary>
        public string LdapPassword { get; private set; }
        /// <summary>
        /// The block size for reading search results from LDAP
        /// </summary>
        public int LdapPageSize { get; private set; }
 
        public DirectoryEntry LdapDirectoryServer { get; private set; }
        public DirectorySearcher LdapDirectorySearcher { get; private set; }
 
        public LDAP()
        {
            LdapServer = string.Format("LDAP://{0}", ConfigurationManager.AppSettings["LdapServer"]);
            LdapUsername = string.Format("uid={0}", ConfigurationManager.AppSettings["LdapUsername"]);
            LdapPassword = ConfigurationManager.AppSettings["LdapPassword"];
            LdapPageSize = Convert.ToInt32(ConfigurationManager.AppSettings["LdapPageSize"]);
            // Connect tro LDAP
            LdapDirectoryServer = new DirectoryEntry(LdapServer, LdapUsername, LdapPassword, AuthenticationTypes.ServerBind);
            LdapDirectorySearcher = new DirectorySearcher(LdapDirectoryServer);
            // Filter, should maybe be qualified further to return only the entries for people           
            LdapDirectorySearcher.PageSize = LdapPageSize;
            AddLdapDirectorySearcherProperties(LdapDirectorySearcher);
        }
 
        private static void AddLdapDirectorySearcherProperties(DirectorySearcher LdapDirectorySearcher)
        {
            LdapDirectorySearcher.PropertiesToLoad.Add("mail");
            LdapDirectorySearcher.PropertiesToLoad.Add("manager");
            LdapDirectorySearcher.PropertiesToLoad.Add("location");
        }
 
        private SearchResult GetSearchResult(string email)
        {
            LdapDirectorySearcher.Filter = "(mail=" + email + ")";
            return LdapDirectorySearcher.FindOne();
        }
 
        public UserProfile FindUser(string email)
        {
            SearchResult sr = GetSearchResult(email);
            if (sr != null)
            {
                return new UserProfile().FillUserProfile(sr);
            }
            return null;
        }
    }

now create the Webservices. Three Methods are required, but we are just going to need one working method, the one that finds a userProfile when a email address is specified:

[WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class BDCWebService : System.Web.Services.WebService
    {
        [WebMethod(Description = "Returns null")]
        public List<string> GetAllUserEmail()
        {
            /*
            * IdEnumerator
            */
            return null;
        }
 
        [WebMethod(Description = "Returns a single User Profile Entity by by specifying an Email address")]
        public UserProfile GetUserProfileByEmail(string email)
        {
            /*
            * Specific Finder
            */
            LDAP ldap = new LDAP();
            return ldap.FindUser(email);
 
        }
 
        [WebMethod(Description = "returns null")]
        public List<UserProfile> GetUserProfiles()
        {
            return null;
        }
    }

You an test your LDAP connection settings, username and password by starting your webservice, select the GetUserProfileByEmail method and specify your own email address. If it works, the next step can be taken: creating the BDC.

To create a BDC resource file, you need to download the SharePoint resource kit and install the ApplicationDefinitionDesigner.

Open up the ApplicationDefinitionDesigner and  click on “Add LOB system -> Connect to webservice”. Add the location of your webservice and click “connect”


Right click on Entities -> Add Entities -> OK -> Add WebMethod.
Drag the three webmethods on the blue canvas one by one.

Click on Entity 0 and modify its name into UserProfile
RightClick on Identifier and add an identifier. Name it “Email” as well its displayname.
Open up methods -> GetAllUserEmail -> return -> return and RightClick on it -> Add TypeDescryptor. Select as identifier: Email[UserProfile]
Open up methods -> GetUserProfileByEmail -> email -> email and set it’s identifier to  Email[UserProfile]
do the same for GetUserProfileByEmail -> return -> return -> email and set it’s identifier to  Email[UserProfile]

Open up methods -> GetUserProfiles -> return -> return -> item -> email and set it’s identifier to Email[UserProfile]

add instances:
1) open up Methods -> GetAllUserEmail -> instances and Add an instance. Choose as type IdEnumerator and set its name as “GetAllUSerEmailInstance”
2) open up Methods -> GetUserProfileByEmail -> instances and add an instance. Choose specificFinder as type and name it: GetUserProfileByEmailInstance
3) open up Methods -> GetUserProfiles -> instances and add an instance. Choose finder as type and name it: GetUserProfilesInstance

Rightclick on the rootnode and choose “export”. Save it to your HDD.

Now we are ready to import the BDC:
open your SharedService Provider and navigate to Import Application Definition. Select your model and import it.
Navigate to manage connections and Create a new connection:

at last you need to map your user profile properties to the BDC mapping.
When you are doing a new (incremental or full) import of your user profiles, the BDC import will start right after the AD import has been finished.

Please keep in mind to give every account enough rights to update the user profiles!
1) give the AppPoolID rights on the BDC connection
2) make sure the account has enough rights on the sharepoint_config and ssp_config db, otherwhise, the userprofiles some exceptions will be thrown.

Setting a default value for a sitecolumn in a pagelayout

Today we came across some weird bug within SharePoint. We needed to provide a default value for configuration options in a pagelayout. We verified everything on a development server and everything worked fine. When we deployed this code to the test environment, we found out that the default values were not being set.

What was the problem? To be able to set default values to a sitecolumn, you need to set the contenttype as default in the pages library, or the contenttype needs to be deployed on siteCollection level.

Page Layout Error: ‘Only Content controls are allowed directly in a content page that contains Content controls’

Today we were working on some pagelayouts and we got some strange error when we wanted to deploy them. After some testing, we found out that some page layouts were working, some were not. The following error message appeared:

Page Layout Error: ‘Only Content controls are allowed directly in a content page that contains Content controls’

When opening the page with SharePoint Designer, we found out that the following tag was added to our page layout:

<html xmlns:mso=”urn:schemas-microsoft-com:office:office”xmlns:msdt=”uuid:C2F41010-65B3-11d1-A29F-00AA00C14882″>

2     <head>

3         <META name=”WebPartPageExpansion” content=”full”>

4             <!–[if gte mso 9]><xml>

5 <mso:CustomDocumentProperties>

6 <mso:PublishingPreviewImage msdt:dt=”string”>/_catalogs/masterpage/Preview Images/GenericPagePreview.png</mso:PublishingPreviewImage>

7 <mso:ContentType msdt:dt=”string”>Pagina-indeling</mso:ContentType>

8 <mso:MasterPageDescription msdt:dt=”string”>Linked with Content Type Welcome Page</mso:MasterPageDescription>

9 <mso:PublishingAssociatedContentType msdt:dt=”string”>;#Welkomstpagina;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4;#</mso:PublishingAssociatedContentType>

10 </mso:CustomDocumentProperties>

11 </xml><![endif]–>

12         <title>Web Parts 3 columns (PL01)</title>

13     </head>

After we compared the working page layouts with the page layouts that didnt work, we saw that the

<asp:Content> </asp:Content>

was all lower case. After changing it back to the normal casing, everything worked correctly

Sharepoint: Rewriting zone url to public url

Today I ran into a problem: Via the SharePoint search I got some results returned, including a pathName to it’s default zone. This zone was used for indexing the sites, but isn’t accessible as a production URL due to authentication issues. One solution was to remove the default zone and replace it by the public url via a replace function, but that method of course isn’t failsafe in the future (what if the public url changes??)

Gladly, SharePoint has a neat little function delivered in the SPUtitlity class:

SPUtility.AlternateServerUrlFromHttpRequestUrl(Uri url)

Life is great when having a Utility class 😉