/* * * Headwind MDM: Open Source Android MDM Software * https://h-mdm.com * * Copyright (C) 2019 Headwind Solutions LLC (http://h-sms.com) * * Licensed under the Apache 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.apache.org/licenses/LICENSE-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 com.hmdm.persistence; import com.google.inject.Inject; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import com.google.inject.Singleton; import javax.inject.Named; import com.hmdm.persistence.domain.ApplicationVersion; import com.hmdm.rest.json.APKFileDetails; import com.hmdm.rest.json.ApplicationConfigurationLink; import com.hmdm.rest.json.ApplicationVersionConfigurationLink; import com.hmdm.rest.json.LinkConfigurationsToAppRequest; import com.hmdm.rest.json.LinkConfigurationsToAppVersionRequest; import com.hmdm.rest.json.LookupItem; import com.hmdm.util.APKFileAnalyzer; import com.hmdm.util.ApplicationUtil; import com.hmdm.util.FileExistsException; import org.apache.commons.codec.digest.DigestUtils; import org.mybatis.guice.transactional.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.hmdm.persistence.domain.Application; import com.hmdm.persistence.domain.Customer; import com.hmdm.persistence.mapper.ApplicationMapper; import com.hmdm.security.SecurityContext; import com.hmdm.security.SecurityException; import com.hmdm.util.FileUtil; import javax.validation.constraints.NotNull; @Singleton public class ApplicationDAO extends AbstractLinkedDAO<Application, ApplicationConfigurationLink> { private static final Logger log = LoggerFactory.getLogger(ApplicationDAO.class); private final ApplicationMapper mapper; private final CustomerDAO customerDAO; private final String filesDirectory; private final String baseUrl; private APKFileAnalyzer apkFileAnalyzer; @Inject public ApplicationDAO(ApplicationMapper mapper, CustomerDAO customerDAO, @Named("files.directory") String filesDirectory, @Named("base.url") String baseUrl, APKFileAnalyzer apkFileAnalyzer) { this.mapper = mapper; this.customerDAO = customerDAO; this.filesDirectory = filesDirectory; this.baseUrl = baseUrl; this.apkFileAnalyzer = apkFileAnalyzer; } public List<Application> getAllApplications() { return getList(this.mapper::getAllApplications); } public List<Application> getAllApplicationsByValue(String value) { return getList(customerId -> this.mapper.getAllApplicationsByValue(customerId, "%" + value + "%")); } public List<Application> getAllApplicationsByUrl(String url) { return getList(customerId -> this.mapper.getAllApplicationsByUrl(customerId, url)); } /** * <p>Creates new application record in DB.</p> * * @param application an application record to be created. * @throws DuplicateApplicationException if another application record with same package ID and version already * exists either for current or master customer account. */ @Transactional public int insertApplication(Application application) { log.debug("Entering #insertApplication: application = {}", application); // If an APK-file was set for new app then make the file available in Files area and parse the app parameters // from it (package ID, version) final String filePath = application.getFilePath(); if (filePath != null && !filePath.trim().isEmpty()) { final int customerId = SecurityContext.get().getCurrentUser().get().getCustomerId(); Customer customer = customerDAO.findById(customerId); File movedFile = null; try { movedFile = FileUtil.moveFile(customer, filesDirectory, null, filePath); } catch (FileExistsException e) { FileUtil.deleteFile(filesDirectory, FileUtil.getNameFromTmpPath(filePath)); movedFile = FileUtil.moveFile(customer, filesDirectory, null, filePath); } if (movedFile != null) { final String fileName = movedFile.getAbsolutePath(); final APKFileDetails apkFileDetails = this.apkFileAnalyzer.analyzeFile(fileName); // If URL is not specified explicitly for new app then set the application URL to reference to that // file if ((application.getUrl() == null || application.getUrl().trim().isEmpty())) { application.setUrl(this.baseUrl + "/files/" + customer.getFilesDir() + "/" + movedFile.getName()); } application.setPkg(apkFileDetails.getPkg()); application.setVersion(apkFileDetails.getVersion()); } else { log.error("Could not move the uploaded .apk-file {}", filePath); throw new DAOException("Could not move the uploaded .apk-file"); } } insertRecord(application, this.mapper::insertApplication); final ApplicationVersion applicationVersion = new ApplicationVersion(application); this.mapper.insertApplicationVersion(applicationVersion); this.mapper.recalculateLatestVersion(application.getId()); return application.getId(); } /** * <p>Creates new application record in DB.</p> * * @param application an application record to be created. */ @Transactional public int insertWebApplication(Application application) { log.debug("Entering #insertWebApplication: application = {}", application); final Optional<Integer> currentCustomerId = SecurityContext.get().getCurrentCustomerId(); if (currentCustomerId.isPresent()) { String pkg; Long count; do { pkg = DigestUtils.sha1Hex(application.getUrl() + System.currentTimeMillis()); count = this.mapper.countByPackageId(currentCustomerId.get(), pkg); } while (count > 0); application.setPkg(pkg); application.setVersion("0"); insertRecord(application, this.mapper::insertApplication); final ApplicationVersion applicationVersion = new ApplicationVersion(application); this.mapper.insertApplicationVersion(applicationVersion); this.mapper.recalculateLatestVersion(application.getId()); return application.getId(); } else { throw SecurityException.onAnonymousAccess(); } } /** * <p>Checks if another application with same package ID and version already exists or not.</p> * * @param application an application to check against duplicates. * @throws DuplicateApplicationException if a duplicated application is found. */ private void guardDuplicateApp(Application application) { if (application.getPkg() != null && application.getVersion() != null) { final List<Application> dbApps = findByPackageIdAndVersion(application.getPkg(), application.getVersion()); if (!dbApps.isEmpty()) { throw new DuplicateApplicationException(application.getPkg(), application.getVersion(), dbApps.get(0).getCustomerId()); } } } /** * <p>Checks if another application version with same version number already exists for specified application or * not.</p> * * @param application an application to check against duplicates. * @param version an application version to check against duplicates. * @throws DuplicateApplicationException if a duplicated application is found. */ private void guardDuplicateAppVersion(Application application, ApplicationVersion version) { if (getDuplicateAppVersion(application, version) != 0) { throw new DuplicateApplicationException(application.getPkg(), version.getVersion(), application.getCustomerId()); } } /** * <p>Checks if another application version with same version number already exists for specified application or * not.</p> * * @param application an application to check against duplicates. * @param version an application version to check against duplicates. * @return id of a duplicated application if found, otherwise 0 */ private int getDuplicateAppVersion(Application application, ApplicationVersion version) { return this.mapper.getDuplicateVersionForApp( application.getId(), version.getId() == null ? -1 : version.getId(), version.getVersion() ); } /** * <p>Checks if another application with same package ID and version already exists or not.</p> * * @param application an application to check against duplicates. * @throws DuplicateApplicationException if a duplicated application is found. */ // private void guardDowngradeAppVersion(Application application) { // if (application.getPkg() != null && application.getVersion() != null) { // final List<Application> dbApps = findByPackageIdAndNewerVersion(application.getPkg(), application.getVersion()); // if (!dbApps.isEmpty()) { // throw new RecentApplicationVersionExistsException(application.getPkg(), application.getVersion(), dbApps.get(0).getCustomerId()); // } // } // } /** * <p>Checks if another application with same package ID and version already exists or not.</p> * * @param application an application to check against duplicates. * @throws DuplicateApplicationException if a duplicated application is found. */ // private void guardDowngradeAppVersion(Application application, ApplicationVersion version) { // if (application.getPkg() != null && version.getVersion() != null) { // final List<Application> dbApps = findByPackageIdAndNewerVersion(application.getPkg(), version.getVersion()); // if (!dbApps.isEmpty()) { // throw new RecentApplicationVersionExistsException(application.getPkg(), version.getVersion(), dbApps.get(0).getCustomerId()); // } // } // } /** * <p>Updates existing application record in DB.</p> * * @param application an application record to be updated. * @throws DuplicateApplicationException if another application record with same package ID and version already * exists either for current or master customer account. */ @Transactional public void updateApplication(Application application) { updateRecord(application, this.mapper::updateApplication, SecurityException::onApplicationAccessViolation); } /** * <p>Updates existing web application record in DB.</p> * * @param application an application record to be updated. * @throws DuplicateApplicationException if another application record with same package ID and version already * exists either for current or master customer account. */ @Transactional public void updateWebApplication(Application application) { final Optional<Integer> currentCustomerId = SecurityContext.get().getCurrentCustomerId(); if (currentCustomerId.isPresent()) { final Application dbDevice = this.mapper.findById(application.getId()); application.setPkg(dbDevice.getPkg()); application.setVersion("0"); updateRecord(application, this.mapper::updateApplication, SecurityException::onApplicationAccessViolation); } else { throw SecurityException.onAnonymousAccess(); } } /** * <p>Updates existing application version record in DB.</p> * * @param applicationVersion an application version record to be updated. * @throws DuplicateApplicationException if another application record with same package ID and version already * exists either for current or master customer account. */ @Transactional public void updateApplicationVersion(ApplicationVersion applicationVersion) { final Application application = getSingleRecord( () -> this.mapper.findById(applicationVersion.getApplicationId()), SecurityException::onApplicationAccessViolation ); guardDuplicateAppVersion(application, applicationVersion); final ApplicationVersion dbApplicationVersion = this.mapper.findVersionById(applicationVersion.getId()); String existingUrl = dbApplicationVersion.getUrl(); if (existingUrl != null && existingUrl.trim().isEmpty()) { existingUrl = null; } if (applicationVersion.getUrl() != null && applicationVersion.getUrl().trim().isEmpty()) { applicationVersion.setUrl(null); } final String newUrl = applicationVersion.getUrl(); boolean urlChanged = newUrl == null && existingUrl != null || newUrl != null && existingUrl == null || newUrl != null && !newUrl.equals(existingUrl); if (urlChanged) { applicationVersion.setApkHash(null); } else { applicationVersion.setApkHash(dbApplicationVersion.getApkHash()); } final Integer currentLatestVersionId = application.getLatestVersion(); this.mapper.updateApplicationVersion(applicationVersion); this.mapper.recalculateLatestVersion(application.getId()); final Integer newLatestVersionId = this.mapper.findById(applicationVersion.getApplicationId()).getLatestVersion(); if (!currentLatestVersionId.equals(newLatestVersionId)) { final ApplicationVersion newLatestVersion = this.mapper.findVersionById(newLatestVersionId); doAutoUpdateToApplicationVersion(newLatestVersion); } } /** * <p>Removes the application referenced by the specified ID. The associated application versions are removed as * well.</p> * * @param id an ID of an application to delete. * @throws SecurityException if current user is not granted a permission to delete the specified application. */ @Transactional public void removeApplicationById(Integer id) { Application dbApplication = this.mapper.findById(id); if (dbApplication != null && dbApplication.isCommonApplication()) { if (!SecurityContext.get().isSuperAdmin()) { throw SecurityException.onAdminDataAccessViolation("delete common application"); } } boolean used = this.mapper.isApplicationUsedInConfigurations(id); if (used) { throw new ApplicationReferenceExistsException(id, "configurations"); } updateById( id, this::findById, (record) -> this.mapper.removeApplicationById(record.getId()), SecurityException::onApplicationAccessViolation ); } public List<ApplicationConfigurationLink> getApplicationConfigurations(Integer id) { return getLinkedList( id, this::findById, customerId -> this.mapper.getApplicationConfigurations(customerId, id), SecurityException::onApplicationAccessViolation ); } public List<ApplicationVersionConfigurationLink> getApplicationVersionConfigurations(Integer versionId) { final ApplicationVersion applicationVersion = findApplicationVersionById(versionId); final Application application = this.mapper.findById(applicationVersion.getApplicationId()); final int userCustomerId = SecurityContext.get().getCurrentUser().get().getCustomerId(); if (application.isCommon() || application.getCustomerId() == userCustomerId) { return this.mapper.getApplicationVersionConfigurationsWithCandidates(userCustomerId, versionId); } else { throw SecurityException.onApplicationAccessViolation(application); } } @Transactional public void updateApplicationConfigurations(LinkConfigurationsToAppRequest request) { final List<ApplicationConfigurationLink> activeLinks = request.getConfigurations() .stream() .filter(c -> c.getAction() == 1) .collect(Collectors.toList()); if (!activeLinks.isEmpty()) { activeLinks.forEach(link -> { final int deletedCount = this.mapper.deleteApplicationConfigurationLinksForSamePkg( link.getApplicationId(), link.getConfigurationId() ); log.debug("Deleted {} links to applications with same package as for application #{} for configuration #{}", deletedCount, link.getApplicationId(), link.getConfigurationId()); }); } final List<ApplicationConfigurationLink> deletedLinks = request.getConfigurations() .stream() .filter(c -> c.getId() != null && c.getAction() == 0) .collect(Collectors.toList()); deletedLinks.forEach(link -> { this.mapper.deleteApplicationConfigurationLink(link.getId()); }); final List<ApplicationConfigurationLink> updatedLinks = request.getConfigurations() .stream() .filter(c -> c.getId() != null && c.getAction() > 0) .collect(Collectors.toList()); updatedLinks.forEach(this.mapper::updateApplicationConfigurationLink); final List<ApplicationConfigurationLink> newLinks = request.getConfigurations() .stream() .filter(c -> c.getId() == null && c.getAction() > 0) .collect(Collectors.toList()); this.insertApplicationConfigurations(request.getApplicationId(), newLinks); SecurityContext.get().getCurrentUser().ifPresent(user -> { this.mapper.recheckConfigurationMainApplications(user.getCustomerId()); this.mapper.recheckConfigurationContentApplications(user.getCustomerId()); this.mapper.recheckConfigurationKioskModes(user.getCustomerId()); }); } @Transactional public void updateApplicationVersionConfigurations(LinkConfigurationsToAppVersionRequest request) { final int applicationVersionId = request.getApplicationVersionId(); this.removeApplicationConfigurationsByVersionId(applicationVersionId); // If this version is set for installation, then other versions of same app must be set for de-installation final List<ApplicationVersionConfigurationLink> configurations = request.getConfigurations(); configurations.forEach(link -> { if (link.getAction() == 1) { final int uninstalledCount = this.mapper.uninstallOtherVersions(applicationVersionId, link.getConfigurationId()); log.debug("Uninstalled {} application versions of application #{} ({}) for configuration #{} ({})", uninstalledCount, link.getApplicationId(), link.getApplicationName(), link.getConfigurationId(), link.getConfigurationName()); // Update the Main App and Content App references to refer to new application version (if necessary) // final int mainAppUpdateCount // = this.mapper.syncConfigurationMainApplication(link.getConfigurationId(), applicationVersionId); // log.debug("Synchronized {} main application versions of application #{} ({}) for configuration #{} ({})", // mainAppUpdateCount, link.getApplicationId(), link.getApplicationName(), // link.getConfigurationId(), link.getConfigurationName()); // final int contentAppUpdateCount // = this.mapper.syncConfigurationContentApplication(link.getConfigurationId(), applicationVersionId); // log.debug("Synchronized {} content application versions of application #{} ({}) for configuration #{} ({})", // contentAppUpdateCount, link.getApplicationId(), link.getApplicationName(), // link.getConfigurationId(), link.getConfigurationName()); } }); this.insertApplicationVersionConfigurations(applicationVersionId, configurations); SecurityContext.get().getCurrentUser().ifPresent(user -> { this.mapper.recheckConfigurationMainApplications(user.getCustomerId()); this.mapper.recheckConfigurationContentApplications(user.getCustomerId()); this.mapper.recheckConfigurationKioskModes(user.getCustomerId()); }); } public void removeApplicationConfigurationsByVersionId(Integer applicationVersionId) { final ApplicationVersion applicationVersion = findApplicationVersionById(applicationVersionId); final Application application = this.mapper.findById(applicationVersion.getApplicationId()); final int userCustomerId = SecurityContext.get().getCurrentUser().get().getCustomerId(); if (application.isCommon() || application.getCustomerId() == userCustomerId) { this.mapper.removeApplicationVersionConfigurationsById(userCustomerId, applicationVersionId); } else { throw SecurityException.onApplicationAccessViolation(application); } } public void insertApplicationVersionConfigurations(Integer applicationVersionId, List<ApplicationVersionConfigurationLink> configurations) { if (configurations != null && !configurations.isEmpty()) { final ApplicationVersion applicationVersion = findApplicationVersionById(applicationVersionId); final Application application = this.mapper.findById(applicationVersion.getApplicationId()); final int userCustomerId = SecurityContext.get().getCurrentUser().get().getCustomerId(); if (application.isCommon() || application.getCustomerId() == userCustomerId) { this.mapper.insertApplicationVersionConfigurations(application.getId(), applicationVersionId, configurations); } else { throw SecurityException.onApplicationAccessViolation(application); } } } // public void removeApplicationConfigurationsById(Integer applicationId) { // updateLinkedData( // applicationId, // this::findById, // app -> this.mapper.removeApplicationConfigurationsById( // SecurityContext.get().getCurrentUser().get().getCustomerId(), app.getId() // ), // SecurityException::onApplicationAccessViolation // ); // } public void insertApplicationConfigurations(Integer applicationId, List<ApplicationConfigurationLink> configurations) { if (configurations != null && !configurations.isEmpty()) { updateLinkedData( applicationId, this::findById, app -> this.mapper.insertApplicationConfigurations(app.getId(), app.getLatestVersion(), configurations), SecurityException::onApplicationAccessViolation ); } } public List<Application> findByPackageIdAndVersion(String pkg, String version) { return getList(customerId -> this.mapper.findByPackageIdAndVersion(customerId, pkg, version)); } public List<Application> findByPackageIdAndNewerVersion(String pkg, String version) { return getList(customerId -> this.mapper.findByPackageIdAndNewerVersion(customerId, pkg, version)); } public List<Application> findByPackageId(String pkg) { return getList(customerId -> this.mapper.findByPackageId(customerId, pkg)); } public Application findById(int id) { return this.mapper.findById(id); } public ApplicationVersion findApplicationVersionById(int id) { return this.mapper.findVersionById(id); } public List<Application> getAllAdminApplications() { if (SecurityContext.get().isSuperAdmin()) { return this.mapper.getAllAdminApplications(); } else { throw SecurityException.onAdminDataAccessViolation("get all applications"); } } public List<Application> getAllAdminApplicationsByValue(String value) { if (SecurityContext.get().isSuperAdmin()) { return getList(customerId -> this.mapper.getAllAdminApplicationsByValue("%" + value + "%")); } else { throw SecurityException.onAdminDataAccessViolation("get all applications"); } } @Transactional public void turnApplicationIntoCommon_Transaction(Integer id, Map<File, File> filesToCopyCollector) { Application application = this.mapper.findById(id); if (application != null) { if (!application.isCommonApplication()) { guardDuplicateApp(application); final int currentUserCustomerId = SecurityContext.get().getCurrentUser().get().getCustomerId(); final Customer newAppCustomer = customerDAO.findById(currentUserCustomerId); // Create new common application record final Application newCommonApplication = new Application(); newCommonApplication.setPkg(application.getPkg()); newCommonApplication.setName(application.getName()); newCommonApplication.setShowIcon(application.getShowIcon()); newCommonApplication.setSystem(application.isSystem()); newCommonApplication.setCustomerId(newAppCustomer.getId()); newCommonApplication.setLatestVersion(null); this.mapper.insertApplication(newCommonApplication); final Integer newAppId = newCommonApplication.getId(); // Find all applications among all customers which have the same package ID and build the list of // all possible version for target application final List<Application> candidateApplications = mapper.findAllByPackageId(application.getPkg()); final List<Application> affectedApplications = new ArrayList<>(); final Map<Integer, Customer> affectedCustomers = new HashMap<>(); final List<ApplicationVersion> affectedAppVersions = new ArrayList<>(); Map<String, ApplicationVersion> candidateApplicationVersions = new HashMap<>(); candidateApplications.forEach(app -> { final List<ApplicationVersion> applicationVersions = this.mapper.getApplicationVersions(app.getId()); applicationVersions.forEach(ver -> { final String normalizedVersion = ApplicationUtil.normalizeVersion(ver.getVersion()); if (!candidateApplicationVersions.containsKey(normalizedVersion)) { candidateApplicationVersions.put(normalizedVersion, ver); } else { log.debug("Will use following substitution for application versions when turning application {} to common: {} -> {}", application.getPkg(), ver, candidateApplicationVersions.get(normalizedVersion)); } affectedAppVersions.add(ver); }); affectedApplications.add(app); affectedCustomers.put(app.getId(), customerDAO.findById(app.getCustomerId())); }); // Re-create the collected application versions and link them to new application. At the same time // collect the files to copy to master-customer account final Map<String, Integer> versionIdMapping = new HashMap<>(); candidateApplicationVersions.forEach((normalizedVersionText, appVersionObject) -> { final String newUrl = translateAppVersionUrl( appVersionObject, affectedCustomers.get(appVersionObject.getApplicationId()), newAppCustomer, filesToCopyCollector ); ApplicationVersion newAppVersion = new ApplicationVersion(); newAppVersion.setApplicationId(newAppId); newAppVersion.setVersion(appVersionObject.getVersion()); newAppVersion.setUrl(newUrl); this.mapper.insertApplicationVersion(newAppVersion); versionIdMapping.put(normalizedVersionText, newAppVersion.getId()); }); // Replace the references to existing applications and application versions to new one affectedAppVersions.forEach(appVer -> { final String normalizedVersionText = ApplicationUtil.normalizeVersion(appVer.getVersion()); final Integer newAppVersionId = versionIdMapping.get(normalizedVersionText); mapper.changeConfigurationsApplication(appVer.getApplicationId(), appVer.getId(), newAppId, newAppVersionId); mapper.changeConfigurationsMainApplication(appVer.getId(), newAppVersionId); mapper.changeConfigurationsContentApplication(appVer.getId(), newAppVersionId); }); // Remove migrated applications affectedApplications.forEach(app -> { mapper.removeApplicationById(app.getId()); }); // Evaluate the most recent version for new common app this.mapper.recalculateLatestVersion(newCommonApplication.getId()); } } } public void turnApplicationIntoCommon(Integer id) { if (SecurityContext.get().isSuperAdmin()) { final Map<File, File> filesToCopy = new HashMap<>(); turnApplicationIntoCommon_Transaction(id, filesToCopy); // Move the files from affected versions filesToCopy.forEach((currentAppFile, newAppFile) -> { if (newAppFile.exists()) { log.warn("Skip copying file: {} -> {} since the target file already exists", currentAppFile.getAbsolutePath(), newAppFile.getAbsolutePath()); } else if (!currentAppFile.exists()) { log.warn("Skip copying file: {} -> {} since the source file does not exist", currentAppFile.getAbsolutePath(), newAppFile.getAbsolutePath()); } else if (!currentAppFile.isFile()) { log.warn("Skip copying file: {} -> {} since the source file is not a regular file", currentAppFile.getAbsolutePath(), newAppFile.getAbsolutePath()); } else { log.debug("Copying file: {} -> {}...", currentAppFile.getAbsolutePath(), newAppFile.getAbsolutePath()); try { Path newAppFileDir = newAppFile.toPath().getParent(); newAppFileDir = Files.createDirectories(newAppFileDir); if (!Files.exists(newAppFileDir)) { log.error("Couldn't create a directory '{}' in files area for Master-customer account", newAppFileDir.toAbsolutePath()); } else { Files.copy(currentAppFile.toPath(), newAppFile.toPath()); deleteAppFile(currentAppFile); } } catch (IOException e) { log.error("Failed to copy file: {} -> {} due to unexpected error. The process continues.", currentAppFile.getAbsolutePath(), newAppFile.getAbsolutePath()); } } }); } else { throw SecurityException.onAdminDataAccessViolation("turn application into common"); } } private void deleteAppFile(File appFile) { final boolean deleted = appFile.delete(); if (deleted) { log.info("Deleted the file {} when turning application to common", appFile.getAbsolutePath()); } else { log.error("Failed to delete the file {} when turning application to common", appFile.getAbsolutePath()); } } /** * <p>Gets the list of versions for specified application.</p> * * @param id an ID of an application to get versions for. * @return a list of versions for requested application. */ public List<ApplicationVersion> getApplicationVersions(Integer id) { return SecurityContext.get().getCurrentUser() .map(currentUser -> { Application dbApplication = this.mapper.findById(id); if (dbApplication != null) { if (dbApplication.getCustomerId() == currentUser.getCustomerId() || dbApplication.isCommonApplication()) { return this.mapper.getApplicationVersions(id); } } throw SecurityException.onApplicationAccessViolation(id); }) .orElseThrow(SecurityException::onAnonymousAccess); } /** * <p>Removes the application version referenced by the specified ID.</p> * * @param id an ID of an application to delete. * @return an URL for the deleted application version. * @throws SecurityException if current user is not granted a permission to delete the specified application. */ @Transactional public String removeApplicationVersionById(@NotNull Integer id) { ApplicationVersion dbApplicationVersion = this.mapper.findVersionById(id); if (dbApplicationVersion != null) { if (dbApplicationVersion.isDeletionProhibited()) { throw SecurityException.onApplicationVersionAccessViolation(id); } if (dbApplicationVersion.isCommonApplication()) { if (!SecurityContext.get().isSuperAdmin()) { throw SecurityException.onAdminDataAccessViolation("delete common application version"); } } final Application dbApplication = this.mapper.findById(dbApplicationVersion.getApplicationId()); boolean used = this.mapper.isApplicationVersionUsedInConfigurations(id); if (used) { throw new ApplicationReferenceExistsException(id, "configurations"); } this.mapper.removeApplicationVersionById(id); // Recalculate latest version for application if necessary if (dbApplication.getLatestVersion() != null && dbApplication.getLatestVersion().equals(id)) { this.mapper.recalculateLatestVersion(dbApplication.getId()); final Application application = this.mapper.findById(dbApplication.getId()); if (application.getLatestVersion() != null) { final ApplicationVersion latestVersion = this.mapper.findVersionById(application.getLatestVersion()); doAutoUpdateToApplicationVersion(latestVersion); } } return dbApplicationVersion.getUrl(); } return null; } /** * <p>Removes the application version referenced by the specified ID and deletes the associated APK-file from local * file system.</p> * * @param id an ID of an application to delete. * @throws SecurityException if current user is not granted a permission to delete the specified application. */ public void removeApplicationVersionByIdWithAPKFile(@NotNull Integer id) { final int customerId = SecurityContext.get().getCurrentUser().get().getCustomerId(); final Customer customer = customerDAO.findById(customerId); final String url = this.removeApplicationVersionById(id); if (url != null && !url.trim().isEmpty()) { final String apkFile = FileUtil.translateURLToLocalFilePath(customer, url, baseUrl); if (apkFile != null) { final boolean deleted = FileUtil.deleteFile(filesDirectory, apkFile); if (!deleted) { log.warn("Could not delete the APK-file {} related to deleted application version #{}", apkFile, id); } } } } /** * <p>Creates new application version record in DB.</p> * * @param applicationVersion an application version record to be created. * @throws DuplicateApplicationException if another application record with same package ID and version already * exists either for current or master customer account. * @throws CommonAppAccessException if target application is common and current user is not a super-admin. */ @Transactional public int insertApplicationVersion(ApplicationVersion applicationVersion) { log.debug("Entering #insertApplicationVersion: application = {}", applicationVersion); // If an APK-file was set for new app then make the file available in Files area and parse the app parameters // from it (package ID, version) final AtomicReference<String> appPkg = new AtomicReference<>(); final String filePath = applicationVersion.getFilePath(); if (filePath != null && !filePath.trim().isEmpty()) { final int customerId = SecurityContext.get().getCurrentUser().get().getCustomerId(); Customer customer = customerDAO.findById(customerId); File movedFile = null; try { movedFile = FileUtil.moveFile(customer, filesDirectory, null, filePath); } catch (FileExistsException e) { FileUtil.deleteFile(filesDirectory, FileUtil.getNameFromTmpPath(filePath)); movedFile = FileUtil.moveFile(customer, filesDirectory, null, filePath); } if (movedFile != null) { final String fileName = movedFile.getAbsolutePath(); final APKFileDetails apkFileDetails = this.apkFileAnalyzer.analyzeFile(fileName); // If URL is not specified explicitly for new app then set the application URL to reference to that // file if ((applicationVersion.getUrl() == null || applicationVersion.getUrl().trim().isEmpty())) { applicationVersion.setUrl(this.baseUrl + "/files/" + customer.getFilesDir() + "/" + movedFile.getName()); } applicationVersion.setVersion(apkFileDetails.getVersion()); } else { log.error("Could not move the uploaded .apk-file {}", filePath); throw new DAOException("Could not move the uploaded .apk-file"); } } final Application existingApplication = findById(applicationVersion.getApplicationId()); if (existingApplication == null) { throw new DAOException("The requested application does not exist: #" + applicationVersion.getApplicationId()); } if (existingApplication.isCommonApplication()) { if (!SecurityContext.get().isSuperAdmin()) { throw new CommonAppAccessException( existingApplication.getPkg(), SecurityContext.get().getCurrentCustomerId().get() ); } } // Check the version package id against application's package id - they must be the same if (appPkg.get() != null) { if (!existingApplication.getPkg().equals(appPkg.get())) { throw new ApplicationVersionPackageMismatchException(appPkg.get(), existingApplication.getPkg()); } } // guardDowngradeAppVersion(existingApplication, applicationVersion); // The user may wish to add the same application and version when he moves // the application from h-mdm.com to his own server int duplicateVersionId = getDuplicateAppVersion(existingApplication, applicationVersion); if (duplicateVersionId > 0) { applicationVersion.setId(duplicateVersionId); this.mapper.updateApplicationVersion(applicationVersion); } else { this.mapper.insertApplicationVersion(applicationVersion); this.mapper.recalculateLatestVersion(existingApplication.getId()); } // Auto update the configurations if the created application version becomes the latest version for application final Application refreshedExistingApplication = findById(applicationVersion.getApplicationId()); final Integer latestVersionId = refreshedExistingApplication.getLatestVersion(); if (latestVersionId != null && latestVersionId.equals(applicationVersion.getId())) { doAutoUpdateToApplicationVersion(applicationVersion); } return applicationVersion.getId(); } private String translateAppVersionUrl(ApplicationVersion appVersion, Customer appCustomer, Customer newAppCustomer, Map<File, File> fileToCopyCollector) { // Update application URL and link it to new customer and copy application file to master // customer final String currentApplicationUrl = appVersion.getUrl(); if (currentApplicationUrl != null) { final String currentCustomerFileDirUrlPart = "/" + appCustomer.getFilesDir() + "/"; int pos = currentApplicationUrl.indexOf(currentCustomerFileDirUrlPart); if (pos >= 0) { final String relativeFilePath = currentApplicationUrl.substring(pos + 1); final File newCustomerFilesBaseDir = new File(this.filesDirectory, newAppCustomer.getFilesDir()); final File currentAppFile = new File(this.filesDirectory, relativeFilePath); final File newAppFile = new File(newCustomerFilesBaseDir, relativeFilePath); fileToCopyCollector.put(currentAppFile, newAppFile); return this.baseUrl + "/files/" + newAppCustomer.getFilesDir() + "/" + relativeFilePath; } else { log.warn("Invalid application URL does not contain the base directory for customer files: {}" , currentApplicationUrl); } } return null; } /** * <p>Updates the configurations which have the AUTO-UPDATE flag set to true to refer to newly added application * version.</p> * * @param newApplicationVersion a new application version to update the configuration references to. */ private void doAutoUpdateToApplicationVersion( ApplicationVersion newApplicationVersion) { int autoUpdatedConfigAppsCount = this.mapper.autoUpdateConfigurationsApplication( newApplicationVersion.getApplicationId(), newApplicationVersion.getId() ); int autoUpdatedMainAppsCount = this.mapper.autoUpdateConfigurationsMainApplication( newApplicationVersion.getApplicationId(), newApplicationVersion.getId() ); int autoUpdatedContentAppsCount = this.mapper.autoUpdateConfigurationsContentApplication( newApplicationVersion.getApplicationId(), newApplicationVersion.getId() ); log.debug("Auto-updated {} application links for configurations", autoUpdatedConfigAppsCount); log.debug("Auto-updated main application for {} configurations", autoUpdatedMainAppsCount); log.debug("Auto-updated content application for {} configurations", autoUpdatedContentAppsCount); } /** * <p>Gets the lookup list of applications matching the package ID with specified filter.</p> * * @param filter a filter to be used for filtering the records. * @param resultsCount a maximum number of items to be included to list. * @return a response with list of applications matching the specified filter. */ public List<LookupItem> getApplicationPkgLookup(String filter, int resultsCount) { String searchFilter = '%' + filter.trim() + '%'; return SecurityContext.get().getCurrentUser() .map(u -> this.mapper.findMatchingApplicationPackages(u.getCustomerId(), searchFilter, resultsCount)) .orElse(new ArrayList<>()); } /** * <p>Locates the applications other than specified one which have the same package ID.</p> * * @param application an application to be validated. * @return a list of existing applications with same package ID as set for validated one. */ public List<Application> getApplicationsForPackageID(Application application) { if (application.getPkg() != null) { final List<Application> dbApps = findByPackageId(application.getPkg()) .stream() .filter(dbApp -> !dbApp.getId().equals(application.getId())) .collect(Collectors.toList()); return dbApps; } return new ArrayList<>(); } public String ddd(Customer customer, String fileName) { return this.baseUrl + "/files/" + customer.getFilesDir() + "/" + fileName; } public boolean isFileUsed(Customer customer, String fileDirPath, String fileName) { final String appFileUrl; if (fileDirPath == null || fileDirPath.trim().isEmpty()) { appFileUrl = this.baseUrl + "/files/" + customer.getFilesDir() + "/" + fileName; } else { appFileUrl = this.baseUrl + "/files/" + customer.getFilesDir() + "/" + fileDirPath.replace('\\', '/') + fileName; } final boolean used = this.mapper.countAllApplicationsByUrl(customer.getId(), appFileUrl) > 0; return used; } public List<String> getUsingApps(Customer customer, String fileDirPath, String fileName) { final String appFileUrl; if (fileDirPath == null || fileDirPath.trim().isEmpty()) { appFileUrl = this.baseUrl + "/files/" + customer.getFilesDir() + "/" + fileName; } else { appFileUrl = this.baseUrl + "/files/" + customer.getFilesDir() + "/" + fileDirPath.replace('\\', '/') + fileName; } return this.mapper.getUsingApps(customer.getId(), appFileUrl); } }