/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSiteService.java $ * $Id: BaseSiteService.java 130212 2013-10-07 16:16:02Z azeckoski@unicon.net $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.site.impl; import java.io.PrintWriter; import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Properties; import java.util.Set; import java.util.Stack; import java.util.Vector; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.authz.api.AuthzGroup; import org.sakaiproject.authz.api.AuthzGroupService; import org.sakaiproject.authz.api.AuthzPermissionException; import org.sakaiproject.authz.api.FunctionManager; import org.sakaiproject.authz.api.GroupNotDefinedException; import org.sakaiproject.authz.api.Member; import org.sakaiproject.authz.api.Role; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.api.SecurityService; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.entity.api.ContextObserver; import org.sakaiproject.entity.api.Entity; import org.sakaiproject.entity.api.EntityAccessOverloadException; import org.sakaiproject.entity.api.EntityCopyrightException; import org.sakaiproject.entity.api.EntityManager; import org.sakaiproject.entity.api.EntityNotDefinedException; import org.sakaiproject.entity.api.EntityPermissionException; import org.sakaiproject.entity.api.EntityProducer; import org.sakaiproject.entity.api.HttpAccess; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.entity.api.ResourcePropertiesEdit; import org.sakaiproject.event.api.Event; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.exception.IdInvalidException; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.IdUsedException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.id.api.IdManager; import org.sakaiproject.javax.PagingPosition; import org.sakaiproject.memory.api.Cache; import org.sakaiproject.memory.api.MemoryService; import org.sakaiproject.site.api.Group; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SiteAdvisor; import org.sakaiproject.site.api.SitePage; import org.sakaiproject.site.api.SiteService; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.site.api.SiteService.SelectionType; import org.sakaiproject.site.api.SiteService.SortType; import org.sakaiproject.thread_local.api.ThreadLocalManager; import org.sakaiproject.time.api.Time; import org.sakaiproject.time.api.TimeService; import org.sakaiproject.tool.api.ActiveToolManager; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserDirectoryService; import org.sakaiproject.user.api.UserNotDefinedException; import org.sakaiproject.util.BasicConfigItem; import org.sakaiproject.util.Resource; import org.sakaiproject.util.ResourceLoader; import org.sakaiproject.util.StorageUser; import org.sakaiproject.util.StringUtil; import org.sakaiproject.util.Validator; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * <p> * BaseSiteService is a base implementation of the SiteService. * </p> */ public abstract class BaseSiteService implements SiteService, Observer { /** Our logger. */ private static Log M_log = LogFactory.getLog(BaseSiteService.class); /** * Security advisor when updating sites. We only have one so we can check we pop the same one off the stack * that we put on. */ private final static SecurityAdvisor ALLOW_ADVISOR; static { ALLOW_ADVISOR = new SecurityAdvisor(){ public SecurityAdvice isAllowed(String userId, String function, String reference) { return SecurityAdvice.ALLOWED; } }; } /** The layouts in human readable form (localized) */ private static final String DEFAULT_RESOURCECLASS = "org.sakaiproject.localization.util.SiteImplProperties"; private static final String DEFAULT_RESOURCEBUNDLE = "org.sakaiproject.localization.bundle.siteimpl.site-impl"; private static final String RESOURCECLASS = "resource.class.siteimpl"; private static final String RESOURCEBUNDLE = "resource.bundle.siteimpl"; private static final String PORTAL_SKIN_NEOPREFIX_PROPERTY = "portal.neoprefix"; private static final String PORTAL_SKIN_NEOPREFIX_DEFAULT = "neo-"; private static final String ORIGINAL_SITE_ID_PROPERTY = "original-site-id"; private static String portalSkinPrefix; private ResourceLoader rb = null; // protected ResourceLoader rb = new ResourceLoader("site-impl"); /** Storage manager for this service. */ private Storage m_storage = null; /** The initial portion of a relative access point URL. */ protected String m_relativeAccessPoint = null; /** A site cache. */ protected SiteCacheImpl m_siteCache = null; /** The name/bean for the User-Site cache. */ protected static final String USER_SITE_CACHE = "org.sakaiproject.site.api.SiteService.userSiteCache"; /** Cache for sites accessible to a given user. */ protected Cache m_userSiteCache = null; /** A list of observers watching site save events **/ protected List<SiteAdvisor> siteAdvisors; /********************************************************************************************************************************************************************************************************************************************************** * Abstractions, etc. *********************************************************************************************************************************************************************************************************************************************************/ /** * Construct storage for this service. */ protected abstract Storage newStorage(); /** * Access the partial URL that forms the root of resource URLs. * * @param relative * if true, form within the access path only (i.e. starting with /content) * @return the partial URL that forms the root of resource URLs. */ protected String getAccessPoint(boolean relative) { return (relative ? "" : serverConfigurationService().getAccessUrl()) + m_relativeAccessPoint; } /** * Access the site id extracted from a site reference. * * @param ref * The site reference string. * @return The the site id extracted from a site reference. */ protected String siteId(String ref) { String start = getAccessPoint(true) + Entity.SEPARATOR; int i = ref.indexOf(start); if (i == -1) return ref; String id = ref.substring(i + start.length()); return id; } /** * Check security permission. * * @param lock * The lock id string. * @param resource * The resource reference string, or null if no resource is involved. * @return true if allowd, false if not */ protected boolean unlockCheck(String lock, String resource) { if (!securityService().unlock(lock, resource)) { return false; } return true; } /** * Check security permission. * * @param lock * The lock id string. * @param resource * The resource reference string, or null if no resource is involved. * @exception PermissionException * Thrown if the user does not have access */ protected void unlock(String lock, String resource) throws PermissionException { if (!unlockCheck(lock, resource)) { throw new PermissionException(sessionManager().getCurrentSessionUserId(), lock, resource); } } /** * Check security permission. * * @param lock1 * The lock id string. * @param lock2 * The lock id string. * @param resource * The resource reference string, or null if no resource is involved. * @return true if either allowed, false if not */ protected boolean unlockCheck2(String lock1, String lock2, String resource) { if (!securityService().unlock(lock1, resource)) { if (!securityService().unlock(lock2, resource)) { return false; } } return true; } /** * Check security permission. * * @param lock1 * The lock id string. * @param lock2 * The lock id string. * @param resource * The resource reference string, or null if no resource is involved. * @exception PermissionException * Thrown if the user does not have access to either. */ protected void unlock2(String lock1, String lock2, String resource) throws PermissionException { if (!unlockCheck2(lock1, lock2, resource)) { throw new PermissionException(sessionManager().getCurrentSessionUserId(), lock1 + "/" + lock2, resource); } } /** * Update the live properties for a site for when modified. */ protected void addLiveUpdateProperties(BaseSite site) { String current = sessionManager().getCurrentSessionUserId(); site.m_lastModifiedUserId = current; site.m_lastModifiedTime = timeService().newTime(); } /** * Create the live properties for the site. */ protected void addLiveProperties(BaseSite site) { String current = sessionManager().getCurrentSessionUserId(); site.m_createdUserId = current; site.m_lastModifiedUserId = current; Time now = timeService().newTime(); site.m_createdTime = now; site.m_lastModifiedTime = (Time) now.clone(); } /** * Return the url unchanged, unless it's a reference, then return the reference url */ protected String convertReferenceUrl(String url) { // make a reference Reference ref = entityManager().newReference(url); // if it didn't recognize this, return it unchanged if (!ref.isKnownType()) return url; // return the reference's url return ref.getUrl(); } /** * Regenerate the page and tool ids for all sites. */ protected void regenerateAllSiteIds() { List<Site> sites = storage().getAll(); for (Iterator<Site> iSites = sites.iterator(); iSites.hasNext();) { Site site = (Site) iSites.next(); if (site != null) { Site edit = storage().get(site.getId()); edit.regenerateIds(); storage().save(edit); M_log.info("regenerateAllSiteIds: site: " + site.getId()); } else { M_log.warn("regenerateAllSiteIds: null site in list"); } } } /********************************************************************************************************************************************************************************************************************************************************** * Configuration *********************************************************************************************************************************************************************************************************************************************************/ /** If true, run the regenerate ids pass on all sites at startup. */ protected boolean m_regenerateIds = false; /** * Configuration: regenerate all site;'s page and tool ids to assure uniqueness. * * @param value * The regenerate ids value */ public void setRegenerateIds(String value) { m_regenerateIds = Boolean.valueOf(value).booleanValue(); } /** The # seconds to cache the site queries. 0 disables the cache. */ protected int m_cacheSeconds = 3 * 60; /** * Set the # minutes to cache the site queries. * * @param time * The # minutes to cache the site queries (as an integer string). */ public void setCacheMinutes(String time) { m_cacheSeconds = Integer.parseInt(time) * 60; } /** The # seconds to cache gets. 0 disables the cache. */ protected int m_cacheCleanerSeconds = 15 * 60; /** * Set the # minutes between cache cleanings. * * @param time * The # minutes between cache cleanings. (as an integer string). */ public void setCacheCleanerMinutes(String time) { m_cacheCleanerSeconds = Integer.parseInt(time) * 60; } /********************************************************************************************************************************************************************************************************************************************************** * Dependencies *********************************************************************************************************************************************************************************************************************************************************/ /** * @return the ServerConfigurationService collaborator. */ protected abstract ServerConfigurationService serverConfigurationService(); /** * @return the EntityManager collaborator. */ protected abstract EntityManager entityManager(); /** * @return the EventTrackingService collaborator. */ protected abstract EventTrackingService eventTrackingService(); /** * @return the ThreadLocalManager collaborator. */ protected abstract ThreadLocalManager threadLocalManager(); /** * @return the SecurityService collaborator. */ protected abstract SecurityService securityService(); /** * @return the SessionManager collaborator. */ protected abstract SessionManager sessionManager(); /** * @return the TimeService collaborator. */ protected abstract TimeService timeService(); /** * @return the FunctionManager collaborator. */ protected abstract FunctionManager functionManager(); /** * @return the MemoryService collaborator. */ protected abstract MemoryService memoryService(); /** * @return the UserDirectoryService collaborator. */ protected abstract UserDirectoryService userDirectoryService(); /** * @return the AuthzGroupService collaborator. */ protected abstract AuthzGroupService authzGroupService(); /** * @return the ActiveToolManager collaborator. */ protected abstract ActiveToolManager activeToolManager(); /** * @return the IdManager collaborator. */ protected abstract IdManager idManager(); /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ /** * Final initialization, once all dependencies are set. */ public void init() { siteAdvisors = new ArrayList<SiteAdvisor>(); try { // Get resource bundle String resourceClass = serverConfigurationService().getString(RESOURCECLASS, DEFAULT_RESOURCECLASS); String resourceBundle = serverConfigurationService().getString(RESOURCEBUNDLE, DEFAULT_RESOURCEBUNDLE); rb = new Resource().getLoader(resourceClass, resourceBundle); m_relativeAccessPoint = REFERENCE_ROOT; // construct storage and read m_storage = newStorage(); storage().open(); if (m_regenerateIds) { regenerateAllSiteIds(); m_regenerateIds = false; } // <= 0 minutes indicates no caching desired if (m_cacheSeconds > 0) { // build a synchronized map for the call cache, automatiaclly checking for expiration every 15 mins. m_siteCache = new SiteCacheImpl(memoryService(), m_cacheCleanerSeconds, siteReference("")); } // Register our user-site cache property serverConfigurationService().registerConfigItem(BasicConfigItem.makeDefaultedConfigItem(PROP_CACHE_USER_SITES, true, "org.sakaiproject.api.SiteService")); // Get the user-site cache from the MemoryService for now -- maybe directly from cache manager or Spring later. // Also register as an observer so we can catch site updates and invalidate. if (serverConfigurationService().getBoolean(PROP_CACHE_USER_SITES, true)) { m_userSiteCache = memoryService().newCache(USER_SITE_CACHE); eventTrackingService().addObserver(this); } // register as an entity producer entityManager().registerEntityProducer(this, REFERENCE_ROOT); // register functions functionManager().registerFunction(SITE_ROLE_SWAP); functionManager().registerFunction(SITE_VISIT); functionManager().registerFunction(SITE_VISIT_UNPUBLISHED); functionManager().registerFunction(SECURE_ADD_SITE); functionManager().registerFunction(SECURE_ADD_USER_SITE); functionManager().registerFunction(SECURE_ADD_PORTFOLIO_SITE); functionManager().registerFunction(SECURE_REMOVE_SITE); functionManager().registerFunction(SECURE_UPDATE_SITE); functionManager().registerFunction(SECURE_VIEW_ROSTER); functionManager().registerFunction(SECURE_UPDATE_SITE_MEMBERSHIP); functionManager().registerFunction(SECURE_UPDATE_GROUP_MEMBERSHIP); functionManager().registerFunction(SECURE_ADD_COURSE_SITE); functionManager().registerFunction(SITE_VISIT_SOFTLY_DELETED); functionManager().registerFunction(SECURE_REMOVE_SOFTLY_DELETED_SITE); functionManager().registerFunction(SECURE_ADD_PROJECT_SITE); portalSkinPrefix = serverConfigurationService().getString(PORTAL_SKIN_NEOPREFIX_PROPERTY, PORTAL_SKIN_NEOPREFIX_DEFAULT); } catch (Exception t) { M_log.warn(".init(): ", t); } } /** * Returns to uninitialized state. */ public void destroy() { storage().close(); m_storage = null; // Stop listening for site update events eventTrackingService().deleteObserver(this); M_log.info("destroy()"); } /********************************************************************************************************************************************************************************************************************************************************** * SiteService implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * @inheritDoc */ public String[] getLayoutNames() { String[] rv = new String[2]; rv[0] = rb.getString("sitpag.lay_sngl"); rv[1] = rb.getString("sitpag.lay_dbl"); return rv; } /** * @inheritDoc */ public boolean allowAccessSite(String id) { boolean rv = false; try { Site site = getSite(id); allowAccessSite(site); rv = true; } catch (Exception ignore) { // Not needed but makes the code clearer. rv = false; } return rv; } /** * Checks to see if the current user has access to the site and throws an exception if they don't. * This was extracted to keep the code common to getSiteVisit and allowSiteAccess * @throws PermissionException If the user isn't allowed to access the site. */ protected void allowAccessSite(Site site) throws PermissionException { if (site.isSoftlyDeleted()) { unlock(SITE_VISIT_SOFTLY_DELETED, site.getReference()); } else { if (site.isPublished()) { unlock(SITE_VISIT, site.getReference()); } else { String roleswap = securityService().getUserEffectiveRole(site.getReference()); if (roleswap!=null) // if in a swapped mode, treat it as a normal site else do the normal unpublished c unlock(SITE_VISIT, site.getReference()); else unlock(SITE_VISIT_UNPUBLISHED, site.getReference()); } } } /** * Access site object from Cache (if available) * * @param id * The site id string. * @return A site object containing the site information or null * if not found */ protected Site getCachedSite(String id) { if (id == null) return null; Site rv = null; // check the cache String ref = siteReference(id); if (m_siteCache != null) { // some cached things are Booleans (site exists), not sites Object o = m_siteCache.get(ref); if ((o != null) && (o instanceof Site)) { rv = (Site) o; // return a copy of the site from the cache rv = new BaseSite(this,rv, true); return rv; } } return null; } /** * Cache a copy of a site if caching is enabled. * * @param site the Site to cache * @return true when the site was cached, false when the site is null or caching is disabled */ protected boolean cacheSite(Site site) { if (site != null && m_siteCache != null) { String ref = siteReference(site.getId()); Site copy = new BaseSite(this, site, true); m_siteCache.put(ref, copy, m_cacheSeconds); return true; } return false; } /** * Access an already defined site object. * * @param id * The site id string. * @return A site object containing the site information * @exception IdUnusedException * if not found */ protected Site getDefinedSite(String id) throws IdUnusedException { if (id == null) throw new IdUnusedException("<null>"); Site rv = getCachedSite(id); // Return the site from cache only if it is a BaseSite and is fully loaded. // // Note that getCachedSite always returns a BaseSite instance now, so // this instanceof check is not strictly necessary, but paranoid. If // the cast would fail, we have to retrieve the site. This is slightly // kludgy because the caching and lazy-loading are somewhat bolted on. if ( rv != null && rv instanceof BaseSite && ((BaseSite)rv).isFullyLoaded()) return rv; // Get the whole site, including the description. rv = storage().get(id); // if not found if (rv == null) throw new IdUnusedException(id); // get all of the site loaded rv.loadAll(); // track it - we don't track site access -ggolden // EventTrackingService.post(EventTrackingService.newEvent(SECURE_ACCESS_SITE, site.getReference())); // cache a copy cacheSite(rv); return rv; } /** * @inheritDoc */ public boolean siteExists(String id) { if (id != null) { // check the cache String ref = siteReference(id); if (m_siteCache != null) { Object o = m_siteCache.get(ref); if (o != null) { if (o instanceof Site) { return true; } if (o instanceof Boolean) { // misses are cached, too return o == Boolean.TRUE; } } } // check the exists cache if (storage().check(id)) { // cache it if (m_siteCache != null) { m_siteCache.put(siteReference(id), Boolean.TRUE, m_cacheSeconds); } return true; } else { // cache the miss if (m_siteCache != null) { m_siteCache.put(siteReference(id), Boolean.FALSE, m_cacheSeconds); } } } return false; } /** * @inheritDoc */ public Site getSite(String id) throws IdUnusedException { if (id == null) { throw new IdUnusedException("null"); } try { return getDefinedSite(id); } catch (IdUnusedException e) { // if this is the current user's site, we can create it if (isUserSite(id) && id.substring(1).equals(sessionManager().getCurrentSessionUserId())) { // pick a template, type based, to clone it exactly but set this as the id BaseSite template = null; try { User user = userDirectoryService().getUser(sessionManager().getCurrentSessionUserId()); template = (BaseSite) getDefinedSite(USER_SITE_TEMPLATE + "." + user.getType()); } catch (Exception t) { } // if a type based template was not found, use the generic one // will throw IdUnusedException all the way out of this method if that's not defined if (template == null) { template = (BaseSite) getDefinedSite(USER_SITE_TEMPLATE); } // reserve a site with this id from the info store - if it's in use, this will return null try { // check security (throws if not permitted) unlock(SECURE_ADD_USER_SITE, siteReference(id)); // reserve a site with this id from the info store - if it's in use, this will return null BaseSite site = (BaseSite) storage().put(id); if (site == null) { throw new IdUsedException(id); } site.setEvent(SECURE_ADD_SITE); // copy in the template site.set(template, false); // Localize the page & tool titles for ( Iterator it=site.getPages().iterator(); it.hasNext(); ) { SitePage page = (SitePage)it.next(); if (!page.getTitleCustom()) { page.localizePage(); } } doSave(site, true); return site; } catch (IdUsedException ee) { throw e; } catch (PermissionException ee) { throw e; } } else { throw e; } } } /** * @inheritDoc */ public Site getSiteVisit(String id) throws IdUnusedException, PermissionException { // get the site Site rv = getSite(id); // Check is user has access, throws PermissionException if the user doesn't allowAccessSite(rv); return rv; } /** * @inheritDoc */ public boolean allowUpdateSite(String id) { return unlockCheck(SECURE_UPDATE_SITE, siteReference(id)); } /** * @inheritDoc */ public boolean allowUpdateSiteMembership(String id) { return unlockCheck(SECURE_UPDATE_SITE_MEMBERSHIP, siteReference(id)); } /** * @inheritDoc */ public boolean allowUpdateGroupMembership(String id) { return unlockCheck(SECURE_UPDATE_GROUP_MEMBERSHIP, siteReference(id)); } /** * @inheritDoc */ public boolean allowRoleSwap(String id) { return unlockCheck(SITE_ROLE_SWAP, siteReference(id)); } /** * @inheritDoc */ public void save(Site site) throws IdUnusedException, PermissionException { if (site.getId() == null) throw new IdUnusedException("<null>"); String siteRef = site.getReference(); if (!unlockCheck(SECURE_UPDATE_GROUP_MEMBERSHIP, siteRef) && !unlockCheck(SECURE_UPDATE_SITE_MEMBERSHIP, siteRef)) { // check security (throws if not permitted) unlock(SECURE_UPDATE_SITE, siteRef); } // check for existance if (!storage().check(site.getId())) { throw new IdUnusedException(site.getId()); } // Save the site doSave((BaseSite) site, false); } /** * @inheritDoc */ public void saveSiteMembership(Site site) throws IdUnusedException, PermissionException { if (site.getId() == null) throw new IdUnusedException("<null>"); // check security (throws if not permitted) unlock2(SECURE_UPDATE_SITE_MEMBERSHIP, SECURE_UPDATE_SITE, site.getReference()); // check for existance if (!storage().check(site.getId())) { throw new IdUnusedException(site.getId()); } try { enableAzgSecurityAdvisor(); saveSiteAzg(site); } finally { disableAzgSecurityAdvisor(); } // track it eventTrackingService().post(eventTrackingService().newEvent(SECURE_UPDATE_SITE_MEMBERSHIP, site.getReference(), true)); } /** * @inheritDoc */ public void saveGroupMembership(Site site) throws IdUnusedException, PermissionException { if (site.getId() == null) throw new IdUnusedException("<null>"); // check security (throws if not permitted) unlock2(SECURE_UPDATE_GROUP_MEMBERSHIP, SECURE_UPDATE_SITE, site.getReference()); // check for existance if (!storage().check(site.getId())) { throw new IdUnusedException(site.getId()); } try { enableAzgSecurityAdvisor(); saveGroupAzgs(site); } finally { disableAzgSecurityAdvisor(); } // track it eventTrackingService().post(eventTrackingService().newEvent(SECURE_UPDATE_GROUP_MEMBERSHIP, site.getReference(), true)); } /** * Comlete the save process. * * @param site * The site to save. */ protected void doSave(BaseSite site, boolean isNew) { if (isNew) { addLiveProperties(site); } // update the properties addLiveUpdateProperties(site); // Give the site advisors, if any, a chance to make last minute changes to the site for(Iterator<SiteAdvisor> iter = siteAdvisors.iterator(); iter.hasNext();) { iter.next().update(site); } site.setFullyLoaded(true); // complete the edit storage().save(site); // Check to see if an this is an interesting enough change to invalidate the user-site cache. // For now, we just check if the title changed because that persists in the portal navigation. // As with other areas, if the main and user-site caches were more integrated (keeping references // for users rather than copies), we would not have to synchronize explicitly here. Site cached = getCachedSite(site.getId()); if (cached != null && site.getTitle() != null && !site.getTitle().equals(cached.getTitle())) { clearUserCacheForSite(site); } cacheSite(site); // save any modified azgs try { enableAzgSecurityAdvisor(); saveSiteAzg(site); saveGroupAzgs(site); } finally { disableAzgSecurityAdvisor(); } // sync up with all other services // TODO: do this under the security advisor, too, so we don't need all the various service security on site creation? -ggolden enableRelated(site, isNew); // track it String event = site.getEvent(); if (event == null) event = SECURE_UPDATE_SITE; eventTrackingService().post(eventTrackingService().newEvent(event, site.getReference(), true)); // clear the event for next time site.setEvent(null); } /** * Establish a security advisor to allow the "embedded" azg work to occur with no need for additional security permissions. */ protected void enableAzgSecurityAdvisor() { // put in a security advisor so we can do our azg work without need of further permissions // TODO: could make this more specific to the AuthzGroupService.SECURE_UPDATE_AUTHZ_GROUP permission -ggolden securityService().pushAdvisor(ALLOW_ADVISOR); } /** * Disabled the security advisor. */ protected void disableAzgSecurityAdvisor() { SecurityAdvisor popped = securityService().popAdvisor(); if (!ALLOW_ADVISOR.equals(popped)) { if (popped == null) { M_log.warn("Someone has removed our advisor."); } else { M_log.warn("Removed someone elses advisor, adding it back."); securityService().pushAdvisor(popped); } } } /** * Save the site's azg if modified. * * @param site * The site to save. */ protected void saveSiteAzg(Site site) { if (((BaseSite) site).m_azgChanged) { try { authzGroupService().save(((BaseSite) site).m_azg); } catch (Exception t) { M_log.warn(".saveAzgs - site: " + t); } ((BaseSite) site).m_azgChanged = false; } } /** * Save group azgs that are modified. * * @param site * The site to save. */ protected void saveGroupAzgs(Site site) { for (Iterator i = site.getGroups().iterator(); i.hasNext();) { BaseGroup group = (BaseGroup) i.next(); if (group.m_azgChanged) { try { authzGroupService().save(group.m_azg); } catch (Exception t) { M_log.warn(".saveAzgs - group: " + group.getTitle() + " : " + t); } group.m_azgChanged = false; } } } /** * @inheritDoc */ public void saveSiteInfo(String id, String description, String infoUrl) throws IdUnusedException, PermissionException { Site site = getSite(id); site.setDescription(description); site.setInfoUrl(infoUrl); save(site); } /** * @inheritDoc */ public boolean allowAddSite(String id) { // check security (throws if not permitted) if (id != null && isUserSite(id)) { return unlockCheck(SECURE_ADD_USER_SITE, siteReference(id)); } else if (id != null && isCourseSite(id)) { return unlockCheck(SECURE_ADD_COURSE_SITE, siteReference(id)); } else if (id != null && isPortfolioSite(id)) { return unlockCheck(SECURE_ADD_PORTFOLIO_SITE, siteReference(id)); } else if (id != null && isProjectSite(id)) { return unlockCheck(SECURE_ADD_PROJECT_SITE, siteReference(id)); } else { return unlockCheck(SECURE_ADD_SITE, siteReference(id)); } } /** * read the site Type definition from configuration files */ public List<String> getSiteTypeStrings(String type) { String[] siteTypes = serverConfigurationService().getStrings(type + "SiteType"); if (siteTypes == null || siteTypes.length == 0) { siteTypes = new String[] {type}; } return Arrays.asList(siteTypes); } private boolean isCourseSite(String siteId) { boolean rv = false; try { Site s = getSite(siteId); List<String> courseSiteTypes = getSiteTypeStrings("course"); if (courseSiteTypes.contains(s.getType())) return true; } catch (IdUnusedException e) { M_log.warn("isCourseSite(): no site with id: " + siteId); } return rv; } private boolean isPortfolioSite(String siteId) { boolean rv = false; try { Site s = getSite(siteId); List<String> portfolioSiteTypes = getSiteTypeStrings("portfolio"); if (portfolioSiteTypes.contains(s.getType())) return true; } catch (IdUnusedException e) { M_log.warn("isPortfolioSite(): no site with id: " + siteId); } return rv; } private boolean isProjectSite(String siteId) { boolean rv = false; try { Site s = getSite(siteId); List<String> projectSiteTypes = getSiteTypeStrings("project"); if (projectSiteTypes.contains(s.getType())) return true; } catch (IdUnusedException e) { M_log.warn("isProjectSite(): no site with id: " + siteId); } return rv; } public boolean allowAddCourseSite() { return unlockCheck(SECURE_ADD_COURSE_SITE, siteReference(null)); } public boolean allowAddPortfolioSite() { return unlockCheck(SECURE_ADD_PORTFOLIO_SITE, siteReference(null)); } public boolean allowAddProjectSite() { return unlockCheck(SECURE_ADD_PROJECT_SITE, siteReference(null)); } /** * @inheritDoc */ public Site addSite(String id, String type) throws IdInvalidException, IdUsedException, PermissionException { // check for a valid site id if (!Validator.checkResourceId(id)) { throw new IdInvalidException("Id " + id + " is not a valid id format"); } id = Validator.escapeResourceName(id); // check for a valid site type if (!Validator.checkSiteType(type)) { throw new IdInvalidException("Type " + type + " is not a valid type format"); } // check security (throws if not permitted) unlock(SECURE_ADD_SITE, siteReference(id)); // SAK-12631 if (getSiteTypeStrings("course").contains(type)) { unlock(SECURE_ADD_COURSE_SITE, siteReference(id)); } // KNL-703 if (getSiteTypeStrings("portfolio").contains(type)) { unlock(SECURE_ADD_PORTFOLIO_SITE, siteReference(id)); } // KNL-952 if (getSiteTypeStrings("project").contains(type)) { unlock(SECURE_ADD_PROJECT_SITE, siteReference(id)); } // reserve a site with this id from the info store - if it's in use, this will return null Site site = storage().put(id); if (site == null) { throw new IdUsedException(id); } // set the type before we enable related, since the azg template for the site depends on type if (type != null) { site.setType(type); } ((BaseSite) site).setEvent(SECURE_ADD_SITE); doSave((BaseSite) site, true); return site; } /** * @inheritDoc */ public Site addSite(String id, Site other) throws IdInvalidException, IdUsedException, PermissionException { // check for a valid site id if (!Validator.checkResourceId(id)) { throw new IdInvalidException("Id " + id + " is not valid"); } id = Validator.escapeResourceName(id); // check security (throws if not permitted) if (isUserSite(id)) { unlock(SECURE_ADD_USER_SITE, siteReference(id)); } else { unlock(SECURE_ADD_SITE, siteReference(id)); } // SAK=12631 if ( isCourseSite(other.getId()) ) { unlock(SECURE_ADD_COURSE_SITE, siteReference(id)); } // KNL-703 if ( isPortfolioSite(other.getId()) ) { unlock(SECURE_ADD_PORTFOLIO_SITE, siteReference(id)); } // KNL-952 if ( isProjectSite(other.getId()) ) { unlock(SECURE_ADD_PROJECT_SITE, siteReference(id)); } // reserve a site with this id from the info store - if it's in use, this will return null Site site = storage().put(id); if (site == null) { throw new IdUsedException(id); } // make this site a copy of other, but with new ids (not an exact copy) ((BaseSite) site).set((BaseSite) other, false); // copy the realm (to get permissions settings) try { AuthzGroup realm = authzGroupService().getAuthzGroup(other.getReference()); AuthzGroup re = authzGroupService().addAuthzGroup(site.getReference(), realm, userDirectoryService().getCurrentUser().getId()); // clear the users from the copied realm, adding in the current user as a maintainer re.removeMembers(); re.addMember(userDirectoryService().getCurrentUser().getId(), re.getMaintainRole(), true, false); authzGroupService().save(re); } catch (Exception e) { M_log.warn(".addSite(): error copying realm", e); } // clear the site's notification id in properties site.getPropertiesEdit().removeProperty(ResourceProperties.PROP_SITE_EMAIL_NOTIFICATION_ID); // KNL-1103, store the site we are copying from site.getPropertiesEdit().addProperty(ORIGINAL_SITE_ID_PROPERTY, other.getId()); ((BaseSite) site).setEvent(SECURE_ADD_SITE); doSave((BaseSite) site, true); return site; } /** * @inheritDoc */ public boolean allowRemoveSite(String id) { return unlockCheck(SECURE_REMOVE_SITE, siteReference(id)); } /** * @inheritDoc */ public void removeSite(Site site) throws PermissionException, IdUnusedException { // check security (throws if not permitted) unlock(SECURE_REMOVE_SITE, site.getReference()); // if soft site deletes are active if(serverConfigurationService().getBoolean("site.soft.deletion", false)) { M_log.debug("Soft site deletes are enabled."); //KNL-983 only soft delete if not user site //made it verbose for logging purposes if(isUserSite(site.getId())) { M_log.debug("Site: " + site.getId() + " is user site and will be hard deleted."); } else if (isSpecialSite(site.getId())) { M_log.debug("Site: " + site.getId() + " is special site and will be hard deleted."); } else { M_log.debug("Site: " + site.getId() + " is not user or special site and will be soft deleted."); // if site is not already softly deleted, softly delete it // if already marked for deletion, check permission to hard delete, if ok, let continue. if(!site.isSoftlyDeleted()) { site.setSoftlyDeleted(true); save(site); return; } else { unlock(SECURE_REMOVE_SOFTLY_DELETED_SITE, site.getReference()); } } } // complete the edit storage().remove(site); // track it eventTrackingService().post(eventTrackingService().newEvent(SECURE_REMOVE_SITE, site.getReference(), true)); // get the services related to this site setup for the site's removal disableRelated(site); } /** * @inheritDoc */ public String siteReference(String id) { return getAccessPoint(true) + Entity.SEPARATOR + id; } /** * @inheritDoc */ public String sitePageReference(String siteId, String pageId) { return getAccessPoint(true) + Entity.SEPARATOR + siteId + Entity.SEPARATOR + PAGE_SUBTYPE + Entity.SEPARATOR + pageId; } /** * @inheritDoc */ public String siteToolReference(String siteId, String toolId) { return getAccessPoint(true) + Entity.SEPARATOR + siteId + Entity.SEPARATOR + TOOL_SUBTYPE + Entity.SEPARATOR + toolId; } /** * @inheritDoc */ public String siteGroupReference(String siteId, String groupId) { return getAccessPoint(true) + Entity.SEPARATOR + siteId + Entity.SEPARATOR + GROUP_SUBTYPE + Entity.SEPARATOR + groupId; } /** * @inheritDoc */ public boolean isUserSite(String site) { if (site == null) return false; // deal with a reference if (site.startsWith(siteReference("~")) && (!site.equals(siteReference("~")))) return true; // deal with an id return (site.startsWith("~") && (!site.equals("~"))); } /** * @inheritDoc */ public String getSiteUserId(String site) { // deal with a reference String ref = siteReference("~"); if (site.startsWith(ref)) { return site.substring(ref.length()); } else if (site.startsWith("~")) { return site.substring(1); } return null; } /** * @inheritDoc */ public String getUserSiteId(String userId) { return "~" + userId; } /** * @inheritDoc */ public boolean isSpecialSite(String site) { if (site == null) return false; // Note: ! is special except if it's !admin, not considered special // deal with a reference if (site.startsWith(siteReference("!")) && !site.equals(siteReference("!admin"))) return true; // TODO: legacy code - we don't use the "~" site anymore (!user.template*) -ggolden if (site.equals(siteReference("~"))) return true; // deal with an id if (site.startsWith("!") && !site.equals("!admin")) return true; // TODO: legacy code - we don't use the "~" site anymore (!user.template*) -ggolden if (site.equals("~")) return true; return false; } /** * @inheritDoc */ public String getSiteSpecialId(String site) { // deal with a reference String ref = siteReference("!"); if (site.startsWith(ref)) { return site.substring(ref.length()); } else if (site.startsWith("!")) { return site.substring(1); } return null; } /** * @inheritDoc */ public String getSpecialSiteId(String special) { return "!" + special; } /** * @inheritDoc */ public String getSiteDisplay(String id) { String rv = "(" + id + ")"; if (isUserSite(id)) { String userName = id; try { User user = userDirectoryService().getUser(getSiteUserId(id)); userName = user.getDisplayName(); } catch (UserNotDefinedException ignore) { } rv = "\"" + rb.getFormattedMessage("sitdis.usr", new Object[]{userName}) + "\" " + rv; } else { Site site = null; try { site = getSite(id); rv = "\"" + site.getTitle() + "\" " + rv; } catch (IdUnusedException ignore) { } } return rv; } /** * @inheritDoc */ public ToolConfiguration findTool(String id) { if (id == null) return null; ToolConfiguration rv = null; // check the site cache if (m_siteCache != null) { rv = m_siteCache.getTool(id); if (rv != null) { // return a copy from the cache rv = new BaseToolConfiguration(this, rv, rv.getContainingPage(), true); return rv; } // if not, get the tool's site id, cache the site, and try again String siteId = storage().findToolSiteId(id); if (siteId != null) { // read and cache the site, pages, tools, etc. try { Site site = getDefinedSite(siteId); // return what we find from the copy we got from the cache rv = site.getTool(id); return rv; } catch (IdUnusedException e) { } } return null; } rv = storage().findTool(id); return rv; } /** * {@inheritDoc} */ public SitePage findPage(String id) { if (id == null) return null; SitePage rv = null; // check the site cache if (m_siteCache != null) { rv = m_siteCache.getPage(id); if (rv != null) { rv = new BaseSitePage(this,rv, rv.getContainingSite(), true); return rv; } // if not, get the page's site id, cache the site, and try again String siteId = storage().findPageSiteId(id); if (siteId != null) { // read and cache the site, pages, tools try { Site site = getDefinedSite(siteId); // return what we find from the site copy from the cache rv = site.getPage(id); return rv; } catch (IdUnusedException e) { } } return null; } rv = storage().findPage(id); return rv; } /** * @inheritDoc */ public boolean allowViewRoster(String id) { return unlockCheck(SECURE_VIEW_ROSTER, siteReference(id)); } /** * @inheritDoc */ public void join(String id) throws IdUnusedException, PermissionException { String user = sessionManager().getCurrentSessionUserId(); if (user == null) { throw new PermissionException(null, AuthzGroupService.SECURE_UPDATE_OWN_AUTHZ_GROUP, siteReference(id)); } // get the site Site site = getDefinedSite(id); // must be joinable if (!site.isJoinable()) { throw new PermissionException(user, AuthzGroupService.SECURE_UPDATE_OWN_AUTHZ_GROUP, siteReference(id)); } // the role to assign String roleId = site.getJoinerRole(); if (roleId == null) { M_log.warn(".join(): null site joiner role for site: " + id); throw new PermissionException(user, AuthzGroupService.SECURE_UPDATE_OWN_AUTHZ_GROUP, siteReference(id)); } // do the join try { authzGroupService().joinGroup(siteReference(id), roleId); } catch (GroupNotDefinedException e) { throw new IdUnusedException(e.getId()); } catch (AuthzPermissionException e) { throw new PermissionException(e.getUser(), e.getFunction(), e.getResource()); } } /** * @inheritDoc */ public void unjoin(String id) throws IdUnusedException, PermissionException { try { authzGroupService().unjoinGroup(siteReference(id)); } catch (GroupNotDefinedException e) { throw new IdUnusedException(e.getId()); } catch (AuthzPermissionException e) { throw new PermissionException(e.getUser(), e.getFunction(), e.getResource()); } } /** * @inheritDoc */ public boolean allowUnjoinSite(String id) { // basic unjoin AuthzGroup test if (!authzGroupService().allowUnjoinGroup(siteReference(id))) return false; // one more check - don't let a maintain role user unjoin a non-joinable site, or // a joinable site that does not have the maintain role as the joiner role. try { // get the site Site site = getDefinedSite(id); // get the AuthGroup AuthzGroup azg = authzGroupService().getAuthzGroup(siteReference(id)); String user = sessionManager().getCurrentSessionUserId(); if (user == null) return false; if ((StringUtil.different(site.getJoinerRole(), azg.getMaintainRole())) || (!site.isJoinable())) { Role role = azg.getUserRole(user); if (role == null) { return false; } if (role.getId().equals(azg.getMaintainRole())) { return false; } } } catch (IdUnusedException e) { return false; } catch (GroupNotDefinedException e) { return false; } return true; } /** * @inheritDoc */ public String getSiteSkin(String id) { // check the site cache if (m_siteCache != null) { try { // this gets the site from the cache, or reads the site / pages / tools and caches it Site s = getDefinedSite(id); return adjustSkin(s.getSkin(), s.isPublished()); } catch (IdUnusedException e) { return adjustSkin(null,true); } } //No site cache. Check the db. return adjustSkin(storage().getSiteSkin(id),true); } /** * {@inheritDoc} */ public List<String> getSiteTypes() { return storage().getSiteTypes(); } /** * @inheritDoc */ public List<Site> getUserSites() { return getUserSites(true); } /** * @inheritDoc */ public List<Site> getUserSites(boolean requireDescription) { String userId = sessionManager().getCurrentSessionUserId(); List<Site> userSites = getCachedUserSites(userId); // Retrieve sites on cache miss or anonymous user if (userSites == null) { userSites = getSites( org.sakaiproject.site.api.SiteService.SelectionType.ACCESS, null, null, null, org.sakaiproject.site.api.SiteService.SortType.TITLE_ASC, null, requireDescription); // Cache the results setCachedUserSites(userId, userSites); } return userSites; } /** * Cache the list of accessible Sites for a user. * * @param userId the (internal) user ID for whom to cache sites; null will result in a no-op * @param sites the list of sites that are accessible for the user; may be null to remove the user from the cache */ protected void setCachedUserSites(String userId, List<Site> sites) { if (m_userSiteCache != null && userId != null) { if (sites == null) { clearUserCacheForUser(userId); } else { m_userSiteCache.put(userId, sites); } } } /** * Remove the list of cached sites for a specified user. * * @param userId the (internal) user ID for whom to purge sites; null will result in a no-op */ protected void clearUserCacheForUser(String userId) { if (m_userSiteCache != null && userId != null) { m_userSiteCache.remove(userId); } } /** * Clear the user-site cache for all the members of this site. * * This is provided to force retrieval of the user-site list for all members of an updated site. * * If the site and user-site cache were more tightly integrated, we could update, but membership * updates are relatively rare and the retrieval is relatively cheap when done occasionally. * * @param site The site for which all members' site cache should be cleared. * */ protected void clearUserCacheForSite(Site site) { if (m_userSiteCache != null && site != null) { for (Member member : site.getMembers()) { clearUserCacheForUser(member.getUserId()); } } } /** * Get the list of sites that are accessible to a given user from the cache. * * @param the internal user ID to check in the cache; null results in a null return * @return a List of Sites that are accessible to the user, null on cache miss */ @SuppressWarnings("unchecked") protected List<Site> getCachedUserSites(String userId) { List<Site> userSites = null; if (m_userSiteCache != null && userId != null) { userSites = (List<Site>) m_userSiteCache.get(userId); } return userSites; } /** * @inheritDoc */ public List<Site> getSites(SelectionType type, Object ofType, String criteria, Map propertyCriteria, SortType sort, PagingPosition page) { return getSites(type, ofType, criteria, propertyCriteria, sort, page, true); } /** * @inheritDoc */ public List<Site> getSites(SelectionType type, Object ofType, String criteria, Map propertyCriteria, SortType sort, PagingPosition page, boolean requireDescription) { return storage().getSites(type, ofType, criteria, propertyCriteria, sort, page, requireDescription); } /* (non-Javadoc) * @see org.sakaiproject.site.api.SiteService#getSiteIds(org.sakaiproject.site.api.SiteService.SelectionType, java.lang.Object, java.lang.String, java.util.Map, org.sakaiproject.site.api.SiteService.SortType, org.sakaiproject.javax.PagingPosition) */ public List<String> getSiteIds(SelectionType type, Object ofType, String criteria, Map<String, String> propertyCriteria, SortType sort, PagingPosition page) { return storage().getSiteIds(type, ofType, criteria, propertyCriteria, sort, page); } /** * @inheritDoc */ public List<Site> getSoftlyDeletedSites() { return storage().getSoftlyDeletedSites(); } /** * @inheritDoc */ public int countSites(SelectionType type, Object ofType, String criteria, Map propertyCriteria) { return storage().countSites(type, ofType, criteria, propertyCriteria); } /** * @inheritDoc */ public void setSiteSecurity(String siteId, Set updateUsers, Set visitUnpUsers, Set visitUsers) { storage().setSiteSecurity(siteId, updateUsers, visitUnpUsers, visitUsers); // the site's azg may have just been updated, so enforce site group subset membership enforceGroupSubMembership(siteId); Event invalidate = eventTrackingService().newEvent(EVENT_SITE_USER_INVALIDATE, siteId, true); eventTrackingService().post(invalidate); } /** * @inheritDoc */ public void setUserSecurity(String userId, Set<String> updateSites, Set<String> visitUnpSites, Set<String> visitSites) { //KNL-512 we need to filter out any non-existent sites from the list List<String> nonExistentIds = new ArrayList<String>(); Iterator<String> updateIt = updateSites.iterator(); while (updateIt.hasNext()) { String id = updateIt.next(); if (!nonExistentIds.contains(id)) { if (!this.siteExists(id)) { M_log.warn("setUserSecurity passed a non existent site Id it will be discarded: " + id); nonExistentIds.add(id); } } } Iterator<String> visitUnpIt = visitUnpSites.iterator(); while (visitUnpIt.hasNext()) { String id = visitUnpIt.next(); if (!nonExistentIds.contains(id)) { if (!this.siteExists(id)) { M_log.warn("setUserSecurity passed a non existent site Id it will be discarded: " + id); nonExistentIds.add(id); } } } Iterator<String> visitIt = visitSites.iterator(); while (visitIt.hasNext()) { String id = visitIt.next(); if (!nonExistentIds.contains(id)) { if (!this.siteExists(id)) { M_log.warn("setUserSecurity passed a non existent site Id it will be discarded: " + id); nonExistentIds.add(id); } } } for (int i = 0; i < nonExistentIds.size(); i++) { String id = nonExistentIds.get(i); updateSites.remove(id); visitUnpSites.remove(id); visitSites.remove(id); } storage().setUserSecurity(userId, updateSites, visitUnpSites, visitSites); } /********************************************************************************************************************************************************************************************************************************************************** * EntityProducer implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * {@inheritDoc} */ public String getLabel() { return "site"; } /** * {@inheritDoc} */ public boolean willArchiveMerge() { return false; } /** * {@inheritDoc} */ public HttpAccess getHttpAccess() { return new HttpAccess() { public void handleAccess(HttpServletRequest req, HttpServletResponse res, Reference ref, Collection copyrightAcceptedRefs) throws EntityPermissionException, EntityNotDefinedException, EntityAccessOverloadException, EntityCopyrightException { try { Site site = (Site) ref.getEntity(); String skin = getSiteSkin(site.getId()); String skinRepo = serverConfigurationService().getString("skin.repo"); String skinDefault = serverConfigurationService().getString("skin.default"); // make sure that it points to the default if there is no skin if (skin == null) { skin = skinDefault; } res.setContentType("text/html; charset=UTF-8"); PrintWriter out = res.getWriter(); out .println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"); out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">"); out.println("<head>"); out.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />"); out.println("<meta http-equiv=\"Content-Style-Type\" content=\"text/css\" />"); out.println("<link href=\"" + skinRepo + "/tool_base.css\" type=\"text/css\" rel=\"stylesheet\" media=\"all\" />"); out.println("<link href=\"" + skinRepo + "/" + skin + "/tool.css\" type=\"text/css\" rel=\"stylesheet\" media=\"all\" />"); out.println("<title>"); out.println(site.getTitle()); out.println("</title>"); out.println("</head><body class=\"siteDescriptionFrame\"><div class=\"portletBody siteDescription\">"); // get the description - if missing, use the site title String description = site.getDescription(); if (description == null) { description = site.getTitle(); } out.println(description); out.println("</div></body></html>"); } catch (Exception t) { throw new EntityNotDefinedException(ref.getReference()); } } }; } /** * {@inheritDoc} */ public boolean parseEntityReference(String reference, Reference ref) { // for site access if (reference.startsWith(REFERENCE_ROOT)) { String id = null; String container = null; String subType = SITE_SUBTYPE; // we will get null, service, siteId, page | group | tool, page/group/tool id String[] parts = StringUtil.split(reference, Entity.SEPARATOR); if (parts.length > 2) { id = parts[2]; container = id; if (parts.length > 4) { subType = parts[3]; id = parts[4]; } } ref.set(APPLICATION_ID, subType, id, container, null); return true; } return false; } /** * {@inheritDoc} */ public String getEntityDescription(Reference ref) { // double check that it's mine if (!APPLICATION_ID.equals(ref.getType())) return null; String rv = rb.getFormattedMessage("entdsc.sit", new Object[]{ref.getReference()}); try { Site site = getSite(ref.getId()); rv = rb.getFormattedMessage("entdsc.sit_usr", new Object[]{ site.getTitle() + " (" + site.getId() + ")", site.getCreatedTime().toStringLocalFull(), site.getCreatedBy().getDisplayName() + " (" + site.getCreatedBy().getDisplayId() + ")", StringUtil.limit((site.getDescription() == null ? "" : site.getDescription()), 30)}); } catch (IdUnusedException e) { } catch (NullPointerException e) { } return rv; } /** * {@inheritDoc} */ public ResourceProperties getEntityResourceProperties(Reference ref) { return null; } /** * {@inheritDoc} */ public Entity getEntity(Reference ref) { // double check that it's mine if (!APPLICATION_ID.equals(ref.getType())) return null; Entity rv = null; try { rv = getSite(ref.getId()); } catch (IdUnusedException e) { M_log.warn("getEntity(): " + e); } catch (NullPointerException e) { M_log.warn("getEntity(): " + e); } return rv; } /** * {@inheritDoc} */ public Collection getEntityAuthzGroups(Reference ref, String userId) { // double check that it's mine if (!APPLICATION_ID.equals(ref.getType())) return null; Collection rv = new Vector(); try { // first, use the reference as an authzGroup (site, group, page or tool) rv.add(ref.getReference()); // do NOT use the site if the reference is a group or other part // // if this is a sub-type, add the site's reference - container is site id // if (!SITE_SUBTYPE.equals(ref.getSubType())) // { // rv.add(siteReference(ref.getContainer())); // } // add the current user's realm ref.addUserAuthzGroup(rv, userId); // site helper rv.add("!site.helper"); } catch (Exception e) { M_log.warn("getEntityRealms(): " + e); } return rv; } /** * {@inheritDoc} */ public String getEntityUrl(Reference ref) { String url = null; if (ref != null) { try { Site site = getSite(ref.getId()); url = site.getUrl(); } catch (IdUnusedException e) { // this could happen if the site reference is invalid if (M_log.isDebugEnabled()) M_log.debug("getEntityUrl(): " + e); } catch (Exception e) { // this is a real failure M_log.error("getEntityUrl(): "+e.getClass().getName()+": " + e, e); } } return url; } /** * {@inheritDoc} */ public String archive(String siteId, Document doc, Stack stack, String archivePath, List attachments) { return ""; } /** * {@inheritDoc} */ public String merge(String siteId, Element root, String archivePath, String fromSiteId, Map attachmentNames, Map userIdTrans, Set userListAllowImport) { return ""; } /********************************************************************************************************************************************************************************************************************************************************** *********************************************************************************************************************************************************************************************************************************************************/ /** * Sync up with all other services for a site that exists. * * @param site * The site. */ protected void enableRelated(BaseSite site, boolean isNew) { // skip if special if (isSpecialSite(site.getId())) { return; } try { // take care of our AuthzGroups enableAzgSecurityAdvisor(); enableAzg(site); } finally { disableAzgSecurityAdvisor(); } // offer to all EntityProducers that are ContexObservers for (Iterator i = entityManager().getEntityProducers().iterator(); i.hasNext();) { EntityProducer ep = (EntityProducer) i.next(); if (ep instanceof ContextObserver) { try { ContextObserver co = (ContextObserver) ep; // is this CO's tools in the site? boolean toolPlacement = !site.getTools(co.myToolIds()).isEmpty(); if (isNew) { co.contextCreated(site.getId(), toolPlacement); } else { co.contextUpdated(site.getId(), toolPlacement); } } catch (Exception t) { M_log.warn("Error encountered while notifying ContextObserver of Site Change", t); } } } } /** * Sync up with all other services for a site that is going away. * * @param site * The site. */ protected void disableRelated(Site site) { // skip if special if (isSpecialSite(site.getId())) { return; } // send to all EntityProducers that are ContextObservers for (Iterator i = entityManager().getEntityProducers().iterator(); i.hasNext();) { EntityProducer ep = (EntityProducer) i.next(); if (ep instanceof ContextObserver) { try { ContextObserver co = (ContextObserver) ep; // is this CO's tools in the site? boolean toolPlacement = !site.getTools(co.myToolIds()).isEmpty(); co.contextDeleted(site.getId(), toolPlacement); } catch (Exception t) { M_log.warn("Error encountered while notifying ContextObserver of Site Change", t); } } } // disable the azgs last, so permissions were in place for the above try { enableAzgSecurityAdvisor(); disableAzg(site); } finally { disableAzgSecurityAdvisor(); } } /** * Enable the site and site group AuthzGroups. * * @param site * The site. */ protected void enableAzg(BaseSite site) { // figure the site authorization group template String siteAzgTemplate = siteAzgTemplate(site); // try the site created-by user for the maintain role in the site String userId = site.getCreatedBy().getId(); if (userId != null) { // make sure it's valid try { userDirectoryService().getUser(userId); } catch (UserNotDefinedException e1) { userId = null; } } // use the current user if needed if (userId == null) { User user = userDirectoryService().getCurrentUser(); userId = user.getId(); } enableAuthorizationGroup(site.getReference(), siteAzgTemplate, userId, "!site.template"); // figure the group authorization group template String groupAzgTemplate = groupAzgTemplate(site); // enable a realm for each group: use the same template as for the site, but don't assign a user maintain in the group's azg for (Iterator iGroups = site.getGroups().iterator(); iGroups.hasNext();) { Group group = (Group) iGroups.next(); enableAuthorizationGroup(group.getReference(), groupAzgTemplate, null, "!group.template"); } // disable the authorization groups for any groups deleted in this edit for (Iterator iGroups = site.m_deletedGroups.iterator(); iGroups.hasNext();) { Group group = (Group) iGroups.next(); disableAuthorizationGroup(group.getReference()); } } /** * Disable the site and site group azgs for a site that's being deleted. * * @param site * The site. */ protected void disableAzg(Site site) { // disable a realm for each group for (Iterator iGroups = site.getGroups().iterator(); iGroups.hasNext();) { Group group = (Group) iGroups.next(); disableAuthorizationGroup(group.getReference()); } // disable realm last, to keep those permissions around disableAuthorizationGroup(site.getReference()); } /** * Figure the site's authorization group template, based on type and if it's a user site. * * @param site * The site to figure the realm for. * @return the site's authorization group template, based on type and if it's a user site. */ protected String siteAzgTemplate(Site site) { String azgTemplate = null; if (isUserSite(site.getId())) { azgTemplate = "!site.user"; } else { // use the type's template, if defined azgTemplate = "!site.template"; String type = site.getType(); if (type != null) { azgTemplate = azgTemplate + "." + type; } } return azgTemplate; } /** * Figure the authorization group template for a group of this site, based on type and if it's a user site. * * @param site * The site to figure the authorization group templates for. * @return the authorization group template for a group of this site, based on type and if it's a user site. */ protected String groupAzgTemplate(Site site) { String azgTemplate = null; if (isUserSite(site.getId())) { azgTemplate = "!group.user"; } else { // use the type's template, if defined azgTemplate = "!group.template"; String type = site.getType(); if (type != null) { azgTemplate = azgTemplate + "." + type; } } return azgTemplate; } /** * Setup the realm for an active site. * * @param ref * The reference for which the realm will be created (site, user). * @param templateId * The realm id of a template to use for the new realm. * @param userId * The user to get maintain in this realm. */ protected void enableAuthorizationGroup(String ref, String templateId, String userId, String fallbackTemplate) { // see if it exists already try { AuthzGroup realm = authzGroupService().getAuthzGroup(ref); } catch (GroupNotDefinedException un) { // see if there's a new site AuthzGroup template AuthzGroup template = null; try { template = authzGroupService().getAuthzGroup(templateId); } catch (Exception e) { try { // if the template is not defined, try the fall back template template = authzGroupService().getAuthzGroup(fallbackTemplate); } catch (Exception ee) { } } // add the realm try { AuthzGroup realm = null; if (template == null) { realm = authzGroupService().addAuthzGroup(ref); } else { realm = authzGroupService().addAuthzGroup(ref, template, userId); } } catch (Exception e) { M_log.warn(".enableRealm: AuthzGroup exception: " + e); } } } /** * Remove a site's realm. * * @param site * The site. */ protected void disableAuthorizationGroup(String ref) { try { authzGroupService().removeAuthzGroup(ref); } catch (Exception e) { M_log.warn(".removeSite: AuthzGroup exception: " + e); } } /********************************************************************************************************************************************************************************************************************************************************** * Storage *********************************************************************************************************************************************************************************************************************************************************/ protected interface Storage { /** * Open and be ready to read / write. */ public void open(); /** * Close. */ public void close(); /** * Does the site with this id exist? * * @param id * The site id. * @return true if the site with this id exists, false if not. */ public boolean check(String id); /** * Get the site with this id, or null if not found. * * @param id * The site id. * @return The site with this id, or null if not found. */ public Site get(String id); /** * Get all sites. * * @return The list of all sites. */ public List getAll(); /** * Add a new site with this id. * * @param id * The site id. * @return The site with this id, or null if in use. */ public Site put(String id); /** * Save the changes. * * @param site * The site to commit. */ public void save(Site site); /** * Save the changes to the two info fields (description and infoUrl) only. * * @param siteId * The site to commit. * @param description * The new site description. * @param infoUrl * The new site infoUrl. */ public void saveInfo(String siteId, String description, String infoUrl); /** * Remove this site. * * @param user * The site to remove. */ public void remove(Site site); /** * Count all the sites. * * @return The count of all sites. */ public int count(); /** * Access a unique list of String site types for any site type defined for any site, sorted by type. * * @return A list (String) of all used site types. */ public List getSiteTypes(); /** * Access a list of Site objects that meet specified criteria. * * @param type * The SelectionType specifying what sort of selection is intended. * @param ofType * Site type criteria: null for any type; a String to match a single type; A String[], List or Set to match any type in the collection. * @param criteria * Additional selection criteria: sits returned will match this string somewhere in their id, title, description, or skin. * @param propertyCriteria * Additional selection criteria: sites returned will have a property named to match each key in the map, whose values match (somewhere in their value) the value in the map (may be null or empty). * @param sort * A SortType indicating the desired sort. For no sort, set to SortType.NONE. * @param page * The PagePosition subset of items to return. * @return The List (Site) of Site objects that meet specified criteria. */ public List getSites(SelectionType type, Object ofType, String criteria, Map propertyCriteria, SortType sort, PagingPosition page); /** * Access a list of Site objects that meet specified criteria, with control over description retrieval. * Note that this signature is primarily provided to help with performance when retrieving lists of * sites not for full display, specifically for the list of a user's sites for navigation. Note that * any sites that have their descriptions, pages, or tools cached will be returned completely, so some * or all full descriptions may be present even when requireDescription is passed as false. * * If a fully populated Site is desired from a potentially partially populated Site, call * {@link #getSite(String id) getSite} or {@link Site#loadAll()}. Either method will load and cache * whatever additional data is not yet cached. * * @param type * The SelectionType specifying what sort of selection is intended. * @param ofType * Site type criteria: null for any type; a String to match a single type; A String[], List or Set to match any type in the collection. * @param criteria * Additional selection criteria: sits returned will match this string somewhere in their id, title, description, or skin. * @param propertyCriteria * Additional selection criteria: sites returned will have a property named to match each key in the map, whose values match (somewhere in their value) the value in the map (may be null or empty). * @param sort * A SortType indicating the desired sort. For no sort, set to SortType.NONE. * @param page * The PagePosition subset of items to return. * @param requireDescription * When true, force a full retrieval of each description; when false, return any uncached descriptions as the empty string * @return The List of Site objects that meet specified criteria. */ public List<Site> getSites(SelectionType type, Object ofType, String criteria, Map propertyCriteria, SortType sort, PagingPosition page, boolean requireDescription); /** * Get the Site IDs for all sites matching criteria. * This is useful when you only need the listing of site ids (for other operations) and do not need the actual Site objects. * * All parameters are the same as {@link #getSites(org.sakaiproject.site.api.SiteService.SelectionType, Object, String, Map, org.sakaiproject.site.api.SiteService.SortType, PagingPosition)} * * @param type * The SelectionType specifying what sort of selection is intended. * @param ofType * Site type criteria: null for any type; a String to match a single type; A String[], List or Set to match any type in the collection. * @param criteria * Additional selection criteria: sites returned will match this string somewhere in their id, title, description, or skin. * @param propertyCriteria * Additional selection criteria: sites returned will have a property named to match each key in the map, whose values match (somewhere in their value) the value in the map (may be null or empty). * @param sort * A SortType indicating the desired sort. For no sort, set to SortType.NONE. * @param page * The PagePosition subset of items to return. * @return a List of the Site IDs for the sites matching the criteria. */ List<String> getSiteIds(SelectionType type, Object ofType, String criteria, Map<String, String> propertyCriteria, SortType sort, PagingPosition page); /** * Count the Site objets that meet specified criteria. * * @param type * The SelectionType specifying what sort of selection is intended. * @param ofType * Site type criteria: null for any type; a String to match a single type; A String[], List or Set to match any type in the collection. * @param criteria * Additional selection criteria: sits returned will match this string somewhere in their id, title, description, or skin. * @param propertyCriteria * Additional selection criteria: sites returned will have a property named to match each key in the map, whose values match (somewhere in their value) the value in the map (may be null or empty). * @return The count of Site objets that meet specified criteria. */ public int countSites(SelectionType type, Object ofType, String criteria, Map propertyCriteria); /** * Access the ToolConfiguration that has this id, if one is defined, else return null. The tool may be on any SitePage in any site. * * @param id * The id of the tool. * @return The ToolConfiguration that has this id, if one is defined, else return null. */ public ToolConfiguration findTool(String id); /** * Access the Site id for the tool with this id. * * @param id * The id of the tool. * @return The Site id for the tool with this id, if the tool is found, else null. */ public String findToolSiteId(String id); /** * Access the Page that has this id, if one is defined, else return null. The page may be on any Site. * * @param id * The id of the page. * @return The SitePage that has this id, if one is defined, else return null. */ public SitePage findPage(String id); /** * Access the Site id for the page with this id. * * @param id * The id of the page. * @return The Site id for the page with this id, if the page is found, else null. */ public String findPageSiteId(String id); /** * Read site properties from storage into the site's properties. * * @param site * The site for which properties are desired. */ public void readSiteProperties(Site site, ResourcePropertiesEdit props); /** * Read properties for all pages in the site * * @param site * The site to read properties for. */ public void readSitePageProperties(Site site); /** * Read site properties and all page and tool properties for the site from storage. * * @param site * The site for which properties are desired. */ public void readAllSiteProperties(Site site); /** * Read page properties from storage into the page's properties. * * @param page * The page for which properties are desired. */ public void readPageProperties(SitePage page, ResourcePropertiesEdit props); /** * Read tool configuration from storage into the tool's configuration properties. * * @param tool * The tool for which properties are desired. */ public void readToolProperties(ToolConfiguration tool, Properties props); /** * Read group properties from storage into the group's properties. * * @param groupId * The groupId for which properties are desired. */ public void readGroupProperties(Group groupId, Properties props); /** * Read site pages from storage into the site's pages. * * @param site * The site for which pages are desired. */ public void readSitePages(Site site, ResourceVector pages); /** * Read site page tools from storage into the page's tools. * * @param page * The page for which tools are desired. */ public void readPageTools(SitePage page, ResourceVector tools); /** * Read tools for all pages from storage into the site's page's tools. * * @param site * The site for which tools are desired. */ public void readSiteTools(Site site); /** * Return the skin for this site * * @param siteId * The site id. * @return the skin for this site. */ public String getSiteSkin(String siteId); /** * Establish the internal security for this site. Previous security settings are replaced for this site. Assigning a user with update implies the two reads; assigning a user with unp read implies the other read. * * @param siteId * The id of the site. * @param updateUsers * The set of String User Ids who have update access. * @param visitUnpUsers * The set of String User Ids who have visit unpublished access. * @param visitUsers * The set of String User Ids who have visit access. */ public void setSiteSecurity(String siteId, Set updateUsers, Set visitUnpUsers, Set visitUsers); /** * Establish the internal security for user for all sites. Previous security settings are replaced for this user. Assigning a user with update implies the two reads; assigning a user with unp read implies the other read. * * @param userId * The id of the user. * @param updateSites * The set of String site ids where the user has update access. * @param visitUnpSites * The set of String site ids where the user has visit unpublished access. * @param visitSites * The set of String site ids where the user has visit access. */ public void setUserSecurity(String userId, Set updateSites, Set visitUnpSites, Set visitSites); /** * Write an updated tool configuration to the database. * * @param tool * TooConfiguration to commit. */ public void saveToolConfig(ToolConfiguration tool); /** * Access the Site id for the group with this id. * * @param id * The id of the group. * @return The Site id for the group with this id, if the group is found, else null. */ public String findGroupSiteId(String id); /** * Read site groups from storage into the site's groups. * * @param site * The site for which groups are desired. * @param groups * The Collection to fill in. */ public void readSiteGroups(Site site, Collection groups); /** * Get all sites that have been softly deleted * * @return List of Sites or empty list if none. */ public List<Site> getSoftlyDeletedSites(); } /** * Adjust a skin value to be just a (folder) name, with no extension, and if missing, be null. * * @param skin * The skin value to adjust. * @return A defaulted and adjusted skin value. */ protected String adjustSkin(String skin, boolean published) { // return the skin as just a name, no ".css", and not dependent on the published status, or a null if not defined if (StringUtils.isEmpty(skin)) { skin = serverConfigurationService().getString("skin.default", "default"); } String templates = serverConfigurationService().getString("portal.templates", "neoskin"); if("neoskin".equals(templates)) { if (StringUtils.isNotEmpty(portalSkinPrefix)) { skin = portalSkinPrefix + skin; } } if (!skin.endsWith(".css")) return skin; return skin.substring(0, skin.lastIndexOf(".css")); } /** * @inheritDoc */ public String merge(String siteId, Element el, String creatorId) { StringBuilder msg = new StringBuilder(); try { // if the target site already exists, don't change the site attributes Site s = getSite(siteId); } catch (IdUnusedException e) { try { // reserve a site with this id from the info store - if it's in use, this will return null // check security (throws if not permitted) // TODO: why security on add_user_site? -ggolden unlock(SECURE_ADD_USER_SITE, siteReference(siteId)); // reserve a site with this id from the info store - if it's in use, this will return null BaseSite site = (BaseSite) storage().put(siteId); if (site == null) { msg.append(this + "cannot find site: " + siteId); } else { site.setEvent(SECURE_ADD_SITE); if (creatorId != null) { el.setAttribute("created-id", creatorId); } // assign source site's attributes to the target site ((BaseSite) site).set(new BaseSite(this, el, timeService()), false); try { save(site); } catch (Exception t) { M_log.warn(".merge: " + t); } } } catch (PermissionException ignore) { } } return msg.toString(); } /** * @inheritDoc */ public Group findGroup(String refOrId) { if (refOrId == null) return null; Group rv = null; // parse the reference or id Reference ref = entityManager().newReference(refOrId); // for ref, get the site from the cache, or cache it and get the group from the site if (APPLICATION_ID.equals(ref.getType())) { try { // here we return the group from the site, so the group's containing site is really the site that contains it. Site site = getDefinedSite(ref.getContainer()); rv = site.getGroup(ref.getId()); } // we can ignore a site not found exception, just returning a null Group catch (IdUnusedException e) { } } // for id, check the cache or get the site from storage, then get the group from the site else { // check the site cache if (m_siteCache != null) { // this lets us find the group from an alredy cached site directly, by group id Group group = m_siteCache.getGroup(refOrId); if (group != null) { // Here we need to make a copy of the site, and pull the group from there, // so that the group's containing site really contains the group we return. // The group we get from the siteCache is a group from the actual cached site, so it's containing site is the actual cached site. // get a copy of the site from the cache Site site = new BaseSite(this,group.getContainingSite(), true); // get the group from there rv = site.getGroup(refOrId); } } // if we don't have it yet, get the group's site, and the group from there if (rv == null) { String siteId = storage().findGroupSiteId(refOrId); if (siteId != null) { try { // read (and cache if enabled) the full site Site site = getDefinedSite(siteId); // here we return the group from the site, so the group's containing site is really the site that contains it. rv = site.getGroup(refOrId); } // we can ignore a site not found exception, just returning a null Group catch (IdUnusedException e) { } } } } return rv; } /** * Adjust any site groups for this site so that the group membership is a subset of the site's membership. * * @param siteId * The site to adjust. */ protected void enforceGroupSubMembership(String siteId) { // just being paranoid, but lets make sure we don't get stuck in a loop here -ggolden if (threadLocalManager().get("enforceGroupSubMembership") != null) { M_log.warn(".enforceGroupSubMembership: recursion avoided!: " + siteId); return; } threadLocalManager().set("enforceGroupSubMembership", siteId); try { Site site = getDefinedSite(siteId); for (Iterator i = site.getGroups().iterator(); i.hasNext();) { Group group = (Group) i.next(); group.keepIntersection(site); } try { // save any changed group azg enableAzgSecurityAdvisor(); saveGroupAzgs(site); } finally { disableAzgSecurityAdvisor(); } } catch (IdUnusedException e) { // site not found - will happen with site delete, no problem } threadLocalManager().set("enforceGroupSubMembership", null); } /** * @inheritDoc */ public void addSiteAdvisor(SiteAdvisor siteAdvisor) { siteAdvisors.add(siteAdvisor); } /** * @inheritDoc */ public List<SiteAdvisor> getSiteAdvisors() { return Collections.unmodifiableList(siteAdvisors); } /** * @inheritDoc */ public boolean removeSiteAdvisor(SiteAdvisor siteAdvisor) { return siteAdvisors.remove(siteAdvisor); } /** * Process site update events (from EventTrackingService) * * The only events processed now are "site.usersite.invalidate" and "site.visit.denied", * which were added to encapsulate the peculiarities of real-world events (users being * added and removed from sites) versus the posted site and authz group events. The * interesting actions where we can expect a user's view of membership to update * immediately (add, join, unjoin) flow through setSiteSecurity with some variety of * other events being posted. The invalidate event gets posted at the conclusion of that * method to be picked up here, across the cluster. The visit denied event gets posted * when a known user visits a known site and is denied. This is considered a signal to * regenerate that user's cache since the assumption is generally that the user would have * clicked a site link presented to them before their access was revoked. * * @param _ The Observable, which is effectively nothing with ETS * @param eventObj The event from ETS; will be checked and no-op if null or not an Event */ public void update(Observable _, Object eventObj) { if (eventObj == null || !(eventObj instanceof Event)) { return; } // TODO: Update this dispatching once ETS can register listeners for specific events Event event = (Event) eventObj; // When membership updates come in, we purge the user-site cache for all members. // This could be optimized by integrating the site and user-site caches more closely, but // it is a reasonable cost since the user-site cache will be regenerated for each user on // on their first portal hit. Membership updates are much more rare than visits, so this // allows the cache to have a reasonably high TTL across the cluster. The site will be // cached on every server, so it will not force each user to retrieve it, just recalculate // based on the cache and any other uncached sites on the next portal hit. // // We are catching adds and role updates with the invalidate event. The denied visit event // captures the case where someone visits a site from which they were removed. Drops are // harder to catch (because EVENT_USER_SITE_MEMBERSHIP_REMOVE is not fired consistently), // but this approach is generally acceptable. A user may need to be added to a site and // may communicate with someone who can do so and is logged in on a different server. This // will invalidate immediately. Drops are not typically communicated. The user will not // retain the privilege to view the site and the denied visit will remove the inaccessible // site from the user cache, so having his/her cache persist for the TTL is not problematic. String eventType = event.getEvent(); if (EVENT_SITE_USER_INVALIDATE.equals(eventType)) { try { Site site = getSite(event.getResource()); clearUserCacheForSite(site); } catch (IdUnusedException e) { if (M_log.isDebugEnabled()) { M_log.debug("Site not found when handling an event (" + eventType + "), ID/REF: " + event.getResource()); } } } else if (EVENT_SITE_VISIT_DENIED.equals(eventType) || AuthzGroupService.SECURE_UNJOIN_AUTHZ_GROUP.equals(eventType)) { clearUserCacheForUser(event.getUserId()); } } protected Storage storage() { return m_storage; } }