/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.repository.internal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.xwiki.cache.Cache; import org.xwiki.cache.CacheException; import org.xwiki.cache.CacheManager; import org.xwiki.cache.config.CacheConfiguration; import org.xwiki.cache.eviction.LRUEvictionConfiguration; import org.xwiki.component.annotation.Component; import org.xwiki.component.manager.ComponentLifecycleException; import org.xwiki.component.phase.Disposable; import org.xwiki.component.phase.Initializable; import org.xwiki.component.phase.InitializationException; import org.xwiki.extension.DefaultExtensionDependency; import org.xwiki.extension.Extension; import org.xwiki.extension.ExtensionAuthor; import org.xwiki.extension.ExtensionDependency; import org.xwiki.extension.ExtensionId; import org.xwiki.extension.ExtensionIssueManagement; import org.xwiki.extension.ExtensionNotFoundException; import org.xwiki.extension.ExtensionScm; import org.xwiki.extension.ResolveException; import org.xwiki.extension.internal.ExtensionFactory; import org.xwiki.extension.internal.converter.ExtensionIdConverter; import org.xwiki.extension.repository.ExtensionRepository; import org.xwiki.extension.repository.result.IterableResult; import org.xwiki.extension.version.Version; import org.xwiki.extension.version.Version.Type; import org.xwiki.extension.version.internal.DefaultVersion; import org.xwiki.extension.version.internal.DefaultVersionConstraint; import org.xwiki.model.EntityType; import org.xwiki.model.reference.AttachmentReference; import org.xwiki.model.reference.AttachmentReferenceResolver; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.DocumentReferenceResolver; import org.xwiki.model.reference.EntityReference; import org.xwiki.model.reference.EntityReferenceSerializer; import org.xwiki.model.reference.WikiReference; import org.xwiki.observation.EventListener; import org.xwiki.observation.ObservationManager; import org.xwiki.observation.event.Event; import org.xwiki.query.Query; import org.xwiki.query.QueryException; import org.xwiki.query.QueryManager; import org.xwiki.rendering.listener.reference.AttachmentResourceReference; import org.xwiki.rendering.listener.reference.ResourceReference; import org.xwiki.rendering.listener.reference.ResourceType; import org.xwiki.rendering.parser.ResourceReferenceParser; import org.xwiki.rendering.renderer.reference.ResourceReferenceTypeSerializer; import org.xwiki.repository.internal.reference.ExtensionResourceReference; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiAttachment; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.internal.event.XObjectPropertyAddedEvent; import com.xpn.xwiki.internal.event.XObjectPropertyDeletedEvent; import com.xpn.xwiki.internal.event.XObjectPropertyUpdatedEvent; import com.xpn.xwiki.objects.BaseObject; import com.xpn.xwiki.objects.BaseProperty; @Component(roles = RepositoryManager.class) @Singleton public class RepositoryManager implements Initializable, Disposable { /** * The reference to match property {@link XWikiRepositoryModel#PROP_EXTENSION_ID} of class * {@link XWikiRepositoryModel#EXTENSION_CLASSNAME} on whatever wiki. */ private static final EntityReference EXTENSIONID_PROPERTY_REFERENCE = new EntityReference(XWikiRepositoryModel.PROP_EXTENSION_ID, EntityType.OBJECT_PROPERTY, XWikiRepositoryModel.EXTENSION_OBJECTREFERENCE); private static final List<Event> EVENTS = Arrays.<Event>asList(new XObjectPropertyAddedEvent(EXTENSIONID_PROPERTY_REFERENCE), new XObjectPropertyDeletedEvent(EXTENSIONID_PROPERTY_REFERENCE), new XObjectPropertyUpdatedEvent(EXTENSIONID_PROPERTY_REFERENCE)); private static final Pattern PATTERN_NEWLINE = Pattern.compile("[\n\r]"); /** * Get the reference of the class in the current wiki. */ @Inject @Named("default") private DocumentReferenceResolver<EntityReference> referenceResolver; @Inject @Named("current") private DocumentReferenceResolver<EntityReference> currentResolver; @Inject @Named("current") private DocumentReferenceResolver<String> currentStringResolver; /** * Used to validate download reference. */ @Inject @Named("link") private ResourceReferenceParser resourceReferenceParser; @Inject private ResourceReferenceTypeSerializer resourceReferenceSerializer; @Inject private EntityReferenceSerializer<String> entityReferenceSerializer; /** * Used to validate download reference. */ @Inject private AttachmentReferenceResolver<String> attachmentResolver; @Inject private QueryManager queryManager; @Inject private Provider<XWikiContext> xcontextProvider; @Inject private RepositoryConfiguration configuration; @Inject private CacheManager cacheManager; @Inject private ObservationManager observation; @Inject private ExtensionFactory extensionFactory; @Inject private Logger logger; /** * Link extension id to document reference. The tabe contains null if the id link to no extension. */ private Cache<DocumentReference[]> documentReferenceCache; private EventListener listener = new EventListener() { @Override public void onEvent(Event event, Object source, Object data) { // TODO: Improve a bit by removing only what's changed documentReferenceCache.removeAll(); } @Override public String getName() { return "repository.DefaultRepositoryManager"; } @Override public List<Event> getEvents() { return EVENTS; } }; @Override public void initialize() throws InitializationException { // Init cache CacheConfiguration cacheConfiguration = new CacheConfiguration(); cacheConfiguration.setConfigurationId("repository.extensionid.documentreference"); LRUEvictionConfiguration lru = new LRUEvictionConfiguration(); lru.setMaxEntries(10000); cacheConfiguration.put(LRUEvictionConfiguration.CONFIGURATIONID, lru); try { this.documentReferenceCache = this.cacheManager.createNewCache(cacheConfiguration); } catch (CacheException e) { throw new InitializationException("Failed to initialize cache", e); } // Listen to modifications this.observation.addListener(listener); } @Override public void dispose() throws ComponentLifecycleException { this.observation.removeListener(listener.getName()); } public <T> XWikiDocument getDocument(T[] data) throws XWikiException { return getDocument((String) data[0]); } public XWikiDocument getDocument(String fullName) throws XWikiException { XWikiContext xcontext = this.xcontextProvider.get(); return xcontext.getWiki().getDocument(this.currentStringResolver.resolve(fullName), xcontext); } public XWikiDocument getExistingExtensionDocumentById(String extensionId) throws QueryException, XWikiException { XWikiContext xcontext = this.xcontextProvider.get(); DocumentReference[] cachedDocumentReference = this.documentReferenceCache.get(extensionId); if (cachedDocumentReference == null) { Query query = this.queryManager.createQuery( "select doc.fullName from Document doc, doc.object(" + XWikiRepositoryModel.EXTENSION_CLASSNAME + ") as extension where extension." + XWikiRepositoryModel.PROP_EXTENSION_ID + " = :extensionId", Query.XWQL); query.bindValue("extensionId", extensionId); List<String> documentNames = query.execute(); if (!documentNames.isEmpty()) { cachedDocumentReference = new DocumentReference[] { this.currentStringResolver.resolve(documentNames.get(0)) }; } else { cachedDocumentReference = new DocumentReference[1]; } this.documentReferenceCache.set(extensionId, cachedDocumentReference); } return cachedDocumentReference[0] != null ? xcontext.getWiki().getDocument(cachedDocumentReference[0], xcontext) : null; } public BaseObject getExtensionVersion(XWikiDocument document, Version version) { List<BaseObject> objects = document.getXObjects(XWikiRepositoryModel.EXTENSIONVERSION_CLASSREFERENCE); if (objects != null) { for (BaseObject versionObject : objects) { if (versionObject != null) { String versionString = getValue(versionObject, XWikiRepositoryModel.PROP_VERSION_VERSION, (String) null); if (StringUtils.isNotEmpty(versionString) && version.equals(new DefaultVersion(versionString))) { return versionObject; } } } } return null; } public void validateExtension(XWikiDocument document, boolean save) throws XWikiException { BaseObject extensionObject = document.getXObject(XWikiRepositoryModel.EXTENSION_CLASSREFERENCE); if (extensionObject == null) { // Not an extension return; } boolean needSave = false; XWikiContext xcontext = this.xcontextProvider.get(); // Update last version field String lastVersion = findLastVersion(document); if (lastVersion != null && !StringUtils.equals(lastVersion, getValue(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_LASTVERSION, (String) null))) { BaseObject extensionObjectToSave = document.getXObject(extensionObject.getReference()); extensionObjectToSave.set(XWikiRepositoryModel.PROP_EXTENSION_LASTVERSION, lastVersion, xcontext); needSave = true; } // Update valid extension field boolean valid = isValid(document, extensionObject, xcontext); if (valid) { this.logger.debug("The extension in the document [{}] is not valid", document.getDocumentReference()); } else { this.logger.debug("The extension in the document [{}] is valid", document.getDocumentReference()); } int currentValue = getValue(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_VALIDEXTENSION, 0); if ((currentValue == 1) != valid) { BaseObject extensionObjectToSave = document.getXObject(XWikiRepositoryModel.EXTENSION_CLASSREFERENCE); extensionObjectToSave.set(XWikiRepositoryModel.PROP_EXTENSION_VALIDEXTENSION, valid ? "1" : "0", xcontext); needSave = true; } // Save document if (save && needSave) { xcontext.getWiki().saveDocument(document, "Validated extension", true, xcontext); } } /** * Compare all version located in a document to find the last one. * * @param document the extension document * @return the last version */ private String findLastVersion(XWikiDocument document) { DocumentReference versionClassReference = getClassReference(document, XWikiRepositoryModel.EXTENSIONVERSION_CLASSREFERENCE); List<BaseObject> versionObjects = document.getXObjects(versionClassReference); DefaultVersion lastVersion = null; if (versionObjects != null) { for (BaseObject versionObject : versionObjects) { if (versionObject != null) { String versionString = getValue(versionObject, XWikiRepositoryModel.PROP_VERSION_VERSION); if (versionString != null) { DefaultVersion version = new DefaultVersion(versionString); if (lastVersion == null || version.compareTo(lastVersion) > 0) { lastVersion = version; } } } } } return lastVersion != null ? lastVersion.getValue() : null; } private DocumentReference getClassReference(XWikiDocument document, EntityReference localReference) { return this.referenceResolver.resolve(localReference, document.getDocumentReference().getWikiReference()); } /** * @param document the extension document * @param extensionObject the extension object * @param context the XWiki context * @return true if the extension is valid from Extension Manager point of view * @throws XWikiException unknown issue when manipulating the model */ private boolean isValid(XWikiDocument document, BaseObject extensionObject, XWikiContext context) throws XWikiException { String extensionId = getValue(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_ID); boolean valid = !StringUtils.isBlank(extensionId); if (valid) { // Type String type = getValue(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_TYPE); valid = this.configuration.isValidType(type); if (valid) { // Versions valid = false; List<BaseObject> extensionVersions = document.getXObjects(XWikiRepositoryModel.EXTENSIONVERSION_CLASSREFERENCE); if (extensionVersions != null) { for (BaseObject extensionVersionObject : extensionVersions) { if (extensionVersionObject != null) { valid = isVersionValid(document, extensionVersionObject, context); if (!valid) { return false; } } } } } } return valid; } private boolean isVersionValid(XWikiDocument document, BaseObject extensionVersionObject, XWikiContext context) { // Has a version String extensionVersion = getValue(extensionVersionObject, XWikiRepositoryModel.PROP_VERSION_VERSION); if (StringUtils.isBlank(extensionVersion)) { this.logger.debug("No actual version provided for object [{}({})]", XWikiRepositoryModel.EXTENSIONVERSION_CLASSREFERENCE, extensionVersionObject.getNumber()); return false; } boolean valid; ResourceReference resourceReference = getDownloadReference(document, extensionVersionObject); if (resourceReference != null) { if (ResourceType.ATTACHMENT.equals(resourceReference.getType())) { AttachmentReference attachmentReference = this.attachmentResolver.resolve(resourceReference.getReference(), document.getDocumentReference()); XWikiDocument attachmentDocument; try { attachmentDocument = context.getWiki().getDocument(attachmentReference.getDocumentReference(), context); valid = attachmentDocument.getAttachment(attachmentReference.getName()) != null; } catch (XWikiException e) { this.logger.error("Failed to get document [{}]", attachmentReference.getDocumentReference(), e); valid = false; } if (!valid) { this.logger.debug("Attachment [{}] does not exists", attachmentReference); } } else if (ResourceType.URL.equals(resourceReference.getType()) || ExtensionResourceReference.TYPE.equals(resourceReference.getType())) { valid = true; } else { valid = false; this.logger.debug("Unknown resource type [{}]", resourceReference.getType()); } } else { valid = false; this.logger.debug("No actual download provided for object [{}({})]", XWikiRepositoryModel.EXTENSIONVERSION_CLASSREFERENCE, extensionVersionObject.getNumber()); } return valid; } public void validateExtensions() throws QueryException, XWikiException { Query query = this.queryManager.createQuery("select doc.fullName from Document doc, doc.object(" + XWikiRepositoryModel.EXTENSION_CLASSNAME + ") as extension", Query.XWQL); for (Object[] documentName : query.<Object[]>execute()) { validateExtension(getDocument(documentName), true); } } public ResourceReference getDownloadReference(XWikiDocument document, BaseObject extensionVersionObject) { // Has a version String extensionVersion = getValue(extensionVersionObject, XWikiRepositoryModel.PROP_VERSION_VERSION); // The download reference seems ok String download = getValue(extensionVersionObject, XWikiRepositoryModel.PROP_VERSION_DOWNLOAD); ResourceReference resourceReference = null; if (StringUtils.isNotEmpty(download)) { resourceReference = this.resourceReferenceParser.parse(download); } else { BaseObject extensionObject = document.getXObject(XWikiRepositoryModel.EXTENSION_CLASSREFERENCE); String extensionId = getValue(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_ID); String fileName = extensionId + '-' + extensionVersion + '.' + getValue(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_TYPE); XWikiAttachment attachment = document.getAttachment(fileName); if (attachment == null) { // Try without the prefix int index = fileName.indexOf(':'); if (index != -1 && index < extensionId.length()) { fileName = fileName.substring(index + 1); attachment = document.getAttachment(fileName); } } if (attachment != null) { resourceReference = new AttachmentResourceReference( this.entityReferenceSerializer.serialize(attachment.getReference())); } } return resourceReference; } private Version getVersions(String extensionId, ExtensionRepository repository, Type type, Map<Version, String> versions) throws ResolveException { Version lastVersion = null; IterableResult<Version> versionsIterable = repository.resolveVersions(extensionId, 0, -1); for (Version version : versionsIterable) { if (type == null || version.getType() == type) { if (!versions.containsKey(version)) { versions.put(version, extensionId); } } lastVersion = version; } return lastVersion; } public DocumentReference importExtension(String extensionId, ExtensionRepository repository, Type type) throws QueryException, XWikiException, ResolveException { TreeMap<Version, String> versions = new TreeMap<Version, String>(); Version lastVersion = getVersions(extensionId, repository, type, versions); if (lastVersion == null) { throw new ExtensionNotFoundException( "Can't find any version for the extension [" + extensionId + "] on repository [" + repository + "]"); } else if (versions.isEmpty()) { // If no valid version import the last version versions.put(lastVersion, extensionId); } else { // Select the last valid version lastVersion = versions.lastKey(); } Extension extension = repository.resolve(new ExtensionId(extensionId, lastVersion)); // Get former ids versions Collection<ExtensionId> features = extension.getExtensionFeatures(); for (ExtensionId feature : features) { try { getVersions(feature.getId(), repository, type, versions); } catch (ResolveException e) { // Ignore } } XWikiContext xcontext = this.xcontextProvider.get(); boolean needSave = false; XWikiDocument document = getExistingExtensionDocumentById(extensionId); if (document == null) { // Create document document = xcontext.getWiki().getDocument( new DocumentReference(xcontext.getWikiId(), Arrays.asList("Extension", extension.getName()), "WebHome"), xcontext); for (int i = 1; !document.isNew(); ++i) { document = xcontext.getWiki().getDocument(new DocumentReference(xcontext.getWikiId(), Arrays.asList("Extension", extension.getName() + ' ' + i), "WebHome"), xcontext); } document.readFromTemplate(this.currentResolver.resolve(XWikiRepositoryModel.EXTENSION_TEMPLATEREFERENCE), xcontext); needSave = true; } // Update document BaseObject extensionObject = document.getXObject(XWikiRepositoryModel.EXTENSION_CLASSREFERENCE); if (extensionObject == null) { extensionObject = document.newXObject(XWikiRepositoryModel.EXTENSION_CLASSREFERENCE, xcontext); needSave = true; } if (!StringUtils.equals(extensionId, getValue(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_ID, (String) null))) { extensionObject.set(XWikiRepositoryModel.PROP_EXTENSION_ID, extensionId, xcontext); needSave = true; } // Update extension informations needSave |= updateExtension(extension, extensionObject, xcontext); // Remove unexisting versions Set<String> validVersions = new HashSet<String>(); List<BaseObject> versionObjects = document.getXObjects(XWikiRepositoryModel.EXTENSIONVERSION_CLASSREFERENCE); if (versionObjects != null) { for (BaseObject versionObject : versionObjects) { if (versionObject != null) { String version = getValue(versionObject, XWikiRepositoryModel.PROP_VERSION_VERSION); if (StringUtils.isBlank(version)) { // Empty version, it's invalid document.removeXObject(versionObject); needSave = true; } else { if (!versions.containsKey(new DefaultVersion(version))) { // The version does not exist on remote repository if (!isVersionValid(document, versionObject, xcontext)) { // The version is invalid, removing it to not make the whole extension invalid document.removeXObject(versionObject); needSave = true; } else { // The version is valid, lets keep it validVersions.add(version); } } else { // This version exist on remote repository validVersions.add(version); } } } } } List<BaseObject> dependencyObjects = document.getXObjects(XWikiRepositoryModel.EXTENSIONDEPENDENCY_CLASSREFERENCE); if (dependencyObjects != null) { for (BaseObject dependencyObject : dependencyObjects) { if (dependencyObject != null) { String version = getValue(dependencyObject, XWikiRepositoryModel.PROP_DEPENDENCY_EXTENSIONVERSION); if (!validVersions.contains(version)) { // The version is invalid, removing it to not make the whole extension invalid document.removeXObject(dependencyObject); needSave = true; } } } } // Update versions for (Map.Entry<Version, String> entry : versions.entrySet()) { Version version = entry.getKey(); String id = entry.getValue(); try { Extension versionExtension; if (version.equals(extension.getId().getVersion())) { versionExtension = extension; } else { versionExtension = repository.resolve(new ExtensionId(id, version)); } // Update version related informations needSave |= updateExtensionVersion(document, versionExtension); } catch (Exception e) { this.logger.error("Failed to resolve extension with id [" + id + "] and version [" + version + "] on repository [" + repository + "]", e); } } // Proxy marker BaseObject extensionProxyObject = document.getXObject(XWikiRepositoryModel.EXTENSIONPROXY_CLASSREFERENCE); if (extensionProxyObject == null) { extensionProxyObject = document.newXObject(XWikiRepositoryModel.EXTENSIONPROXY_CLASSREFERENCE, xcontext); extensionProxyObject.setIntValue(XWikiRepositoryModel.PROP_PROXY_AUTOUPDATE, 1); needSave = true; } needSave |= update(extensionProxyObject, XWikiRepositoryModel.PROP_PROXY_REPOSITORYID, repository.getDescriptor().getId()); needSave |= update(extensionProxyObject, XWikiRepositoryModel.PROP_PROXY_REPOSITORYTYPE, repository.getDescriptor().getType()); needSave |= update(extensionProxyObject, XWikiRepositoryModel.PROP_PROXY_REPOSITORYURI, repository.getDescriptor().getURI().toString()); if (needSave) { document.setAuthorReference(xcontext.getUserReference()); if (document.isNew()) { document.setContentAuthorReference(xcontext.getUserReference()); document.setCreatorReference(xcontext.getUserReference()); } xcontext.getWiki().saveDocument(document, "Imported extension [" + extensionId + "] from repository [" + repository.getDescriptor() + "]", true, xcontext); } return document.getDocumentReference(); } private boolean updateExtension(Extension extension, BaseObject extensionObject, XWikiContext xcontext) { boolean needSave = false; // Update properties // Type needSave |= update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_TYPE, extension.getType()); // Name needSave |= update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_NAME, extension.getName()); // Summary needSave |= update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_SUMMARY, getSummary(extension)); // Category if (extension.getCategory() != null) { needSave |= update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_CATEGORY, extension.getCategory()); } // Website /* * Don't import website since most of the time we want the new page to be the extension entry point needSave |= * update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_WEBSITE, extension.getWebSite()); */ // Description if (StringUtils .isEmpty(getValue(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_DESCRIPTION, (String) null))) { extensionObject.set(XWikiRepositoryModel.PROP_EXTENSION_DESCRIPTION, getDescription(extension), xcontext); needSave = true; } // License if (!extension.getLicenses().isEmpty() && !StringUtils.equals(extension.getLicenses().iterator().next().getName(), getValue(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_LICENSENAME, (String) null))) { extensionObject.set(XWikiRepositoryModel.PROP_EXTENSION_LICENSENAME, extension.getLicenses().iterator().next().getName(), xcontext); needSave = true; } // SCM ExtensionScm scm = extension.getScm(); if (scm != null) { if (scm.getUrl() != null) { needSave |= update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_SCMURL, scm.getUrl().toString()); } if (scm.getConnection() != null) { needSave |= update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_SCMCONNECTION, scm.getConnection().toString()); } if (scm.getDeveloperConnection() != null) { needSave |= update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_SCMDEVCONNECTION, scm.getDeveloperConnection().toString()); } } // Issue Management ExtensionIssueManagement issueManagement = extension.getIssueManagement(); if (issueManagement != null) { if (issueManagement.getSystem() != null) { needSave |= update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_ISSUEMANAGEMENT_SYSTEM, issueManagement.getSystem()); } if (issueManagement.getURL() != null) { needSave |= update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_ISSUEMANAGEMENT_URL, issueManagement.getURL()); } } // Authors needSave |= updateAuthors(extensionObject, extension.getAuthors()); // Features needSave |= updateFeatures(XWikiRepositoryModel.PROP_EXTENSION_FEATURES, extensionObject, extension.getExtensionFeatures()); // Allowed namespaces needSave |= updateCollection(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_ALLOWEDNAMESPACES, extension.getAllowedNamespaces(), xcontext); // Properties needSave |= updateProperties(extensionObject, extension.getProperties()); return needSave; } private String getSummary(Extension extension) { String summary = extension.getSummary(); if (summary != null) { // Extract first not blank line Matcher matcher = PATTERN_NEWLINE.matcher(summary); int previousIndex = 0; while (matcher.find()) { int index = matcher.start(); String str = summary.substring(previousIndex, index); if (StringUtils.isNotBlank(str)) { summary = str.trim(); break; } } // truncated to 255 in case it's too long, TODO: should probably be handled at a lower level) if (summary.length() > 255) { summary = summary.substring(0, 255); } } else { summary = ""; } return summary; } private String getDescription(Extension extension) { String description; if (extension.getDescription() != null) { description = extension.getDescription(); } else if (extension.getSummary() != null) { description = extension.getSummary(); } else { description = ""; } return description; } private boolean updateAuthors(BaseObject extensionObject, Collection<ExtensionAuthor> authors) { List<String> authorIds = new ArrayList<String>(authors.size()); for (ExtensionAuthor author : authors) { authorIds.add(resolveAuthorId(author.getName())); } return update(extensionObject, XWikiRepositoryModel.PROP_EXTENSION_AUTHORS, authorIds); } private boolean updateFeatures(String fieldName, BaseObject extensionObject, Collection<ExtensionId> features) { List<String> featureStrings = new ArrayList<String>(features.size()); for (ExtensionId feature : features) { featureStrings.add(ExtensionIdConverter.toString(feature)); } return update(extensionObject, fieldName, featureStrings); } private String resolveAuthorId(String authorName) { String[] authorElements = StringUtils.split(authorName, ' '); XWikiContext xcontext = this.xcontextProvider.get(); String authorId = resolveAuthorIdOnWiki(xcontext.getWikiId(), authorName, authorElements, xcontext); if (authorId == null && !xcontext.isMainWiki()) { authorId = resolveAuthorIdOnWiki(xcontext.getMainXWiki(), authorName, authorElements, xcontext); if (authorId != null) { authorId = xcontext.getMainXWiki() + ':' + authorId; } } return authorId != null ? authorId : authorName; } private String resolveAuthorIdOnWiki(String wiki, String authorName, String[] authorElements, XWikiContext xcontext) { Query query; try { query = this.queryManager.createQuery("from doc.object(XWiki.XWikiUsers) as user" + " where user.first_name like :userfirstname OR user.last_name like :userlastname", Query.XWQL); query.bindValue("userfirstname", '%' + authorElements[0] + '%'); query.bindValue("userlastname", '%' + authorElements[authorElements.length - 1] + '%'); query.setWiki(wiki); List<String> documentNames = query.execute(); if (!documentNames.isEmpty()) { WikiReference wikiReference = new WikiReference(wiki); for (String documentName : documentNames) { DocumentReference documentReference = this.currentStringResolver.resolve(documentName, wikiReference); String userDisplayName = xcontext.getWiki().getPlainUserName(documentReference, xcontext); if (userDisplayName.equals(authorName)) { return documentName; } } } } catch (QueryException e) { this.logger.error("Failed to resolve extension author [{}]", authorName, e); } return null; } private boolean updateExtensionVersionDependencies(XWikiDocument document, Extension extension) throws XWikiException { boolean needSave = false; List<ExtensionDependency> dependencies = new ArrayList<>(extension.getDependencies()); int dependencyIndex = 0; // Clean misplaced or bad existing dependencies associated to this extension version List<BaseObject> xobjects = document.getXObjects(XWikiRepositoryModel.EXTENSIONDEPENDENCY_CLASSREFERENCE); if (xobjects != null) { boolean deleteExistingObjects = false; // Clone since we are going to modify and parse it at the same time xobjects = new ArrayList<>(document.getXObjects(XWikiRepositoryModel.EXTENSIONDEPENDENCY_CLASSREFERENCE)); for (int i = 0; i < xobjects.size(); ++i) { BaseObject dependencyObject = xobjects.get(i); if (dependencyObject != null) { String extensionVersion = getValue(dependencyObject, XWikiRepositoryModel.PROP_DEPENDENCY_EXTENSIONVERSION, (String) null); if (StringUtils.isNotEmpty(extensionVersion) && extension.getId().getVersion().equals(new DefaultVersion(extensionVersion))) { if (deleteExistingObjects) { document.removeXObject(dependencyObject); needSave = true; } else { String xobjectId = getValue(dependencyObject, XWikiRepositoryModel.PROP_DEPENDENCY_ID); String xobjectConstraint = getValue(dependencyObject, XWikiRepositoryModel.PROP_DEPENDENCY_CONSTRAINT); List<String> xobjectRepositories = (List<String>) getValue(dependencyObject, XWikiRepositoryModel.PROP_DEPENDENCY_REPOSITORIES); DefaultExtensionDependency xobjectDependency = new DefaultExtensionDependency(xobjectId, new DefaultVersionConstraint(xobjectConstraint)); xobjectDependency.setRepositories(XWikiRepositoryModel .toRepositoryDescriptors(xobjectRepositories, this.extensionFactory)); if (dependencies.size() > dependencyIndex) { ExtensionDependency dependency = dependencies.get(dependencyIndex); if (dependency.equals(xobjectDependency)) { ++dependencyIndex; continue; } } deleteExistingObjects = true; document.removeXObject(dependencyObject); needSave = true; } } } } } // Add missing dependencies if (dependencyIndex < dependencies.size()) { XWikiContext xcontext = this.xcontextProvider.get(); for (; dependencyIndex < dependencies.size(); ++dependencyIndex) { ExtensionDependency dependency = dependencies.get(dependencyIndex); BaseObject dependencyObject = document.newXObject(XWikiRepositoryModel.EXTENSIONDEPENDENCY_CLASSREFERENCE, xcontext); dependencyObject.set(XWikiRepositoryModel.PROP_DEPENDENCY_EXTENSIONVERSION, extension.getId().getVersion().getValue(), xcontext); dependencyObject.set(XWikiRepositoryModel.PROP_DEPENDENCY_ID, dependency.getId(), xcontext); dependencyObject.set(XWikiRepositoryModel.PROP_DEPENDENCY_CONSTRAINT, dependency.getVersionConstraint().getValue(), xcontext); dependencyObject.set(XWikiRepositoryModel.PROP_DEPENDENCY_REPOSITORIES, XWikiRepositoryModel.toStringList(dependency.getRepositories()), xcontext); needSave = true; } } return needSave; } private boolean updateExtensionVersion(XWikiDocument document, Extension extension) throws XWikiException { boolean needSave; XWikiContext xcontext = this.xcontextProvider.get(); // Update version object BaseObject versionObject = getExtensionVersion(document, extension.getId().getVersion()); if (versionObject == null) { versionObject = document.newXObject(XWikiRepositoryModel.EXTENSIONVERSION_CLASSREFERENCE, xcontext); versionObject.set(XWikiRepositoryModel.PROP_VERSION_VERSION, extension.getId().getVersion().getValue(), xcontext); needSave = true; } else { needSave = false; } // Id needSave |= update(versionObject, XWikiRepositoryModel.PROP_VERSION_ID, extension.getId().getId()); // Features needSave |= updateFeatures(XWikiRepositoryModel.PROP_VERSION_FEATURES, versionObject, extension.getExtensionFeatures()); // Repositories List<String> repositories = XWikiRepositoryModel.toStringList(extension.getRepositories()); needSave |= update(versionObject, XWikiRepositoryModel.PROP_VERSION_REPOSITORIES, repositories); // Update dependencies needSave |= updateExtensionVersionDependencies(document, extension); // Download ExtensionResourceReference resource = new ExtensionResourceReference(extension.getId().getId(), extension.getId().getVersion().getValue(), extension.getRepository().getDescriptor().getId()); String download = this.resourceReferenceSerializer.serialize(resource); needSave |= update(versionObject, XWikiRepositoryModel.PROP_VERSION_DOWNLOAD, download); return needSave; } protected boolean updateProperties(BaseObject object, Map<String, ?> map) { List<String> list = new ArrayList<>(map.size()); for (Map.Entry<String, ?> entry : map.entrySet()) { String entryString = entry.getKey() + '=' + entry.getValue(); if (entryString.length() > 255) { // Protect against properties too big entryString = entryString.substring(0, 255); } list.add(entryString); } if (ObjectUtils.notEqual(list, getValue(object, XWikiRepositoryModel.PROP_EXTENSION_PROPERTIES))) { object.set(XWikiRepositoryModel.PROP_EXTENSION_PROPERTIES, list, this.xcontextProvider.get()); return true; } return false; } protected <T> T getValue(BaseObject object, String field) { return getValue(object, field, (T) null); } protected <T> T getValue(BaseObject object, String field, T def) { BaseProperty<?> property = (BaseProperty<?>) object.safeget(field); return property != null && property.getValue() != null ? (T) property.getValue() : def; } protected boolean update(BaseObject object, String fieldName, Object value) { // Make sure collection are lists if (value instanceof Collection) { if (!(value instanceof List)) { value = new ArrayList<>((Collection) value); } } if (ObjectUtils.notEqual(value, getValue(object, fieldName))) { object.set(fieldName, value, this.xcontextProvider.get()); return true; } return false; } private boolean updateCollection(BaseObject extensionObject, String fieldName, Collection<String> allowedNamespaces, XWikiContext xcontext) { boolean needSave = update(extensionObject, fieldName, allowedNamespaces != null ? allowedNamespaces : Collections.emptyList()); String fieldNameEmpty = fieldName + XWikiRepositoryModel.PROPSUFFIX_EMPTYCOLLECTION; if (extensionObject.getXClass(xcontext).get(fieldNameEmpty) != null) { update(extensionObject, fieldNameEmpty, allowedNamespaces != null && allowedNamespaces.isEmpty() ? 1 : 0); } return needSave; } }