/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program 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. * * Copyright (c) 2002-2016 Pentaho Corporation.. All rights reserved. */ package org.pentaho.platform.web.http.api.resources; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.codehaus.enunciate.Facet; import org.pentaho.platform.api.engine.IAuthorizationPolicy; import org.pentaho.platform.api.engine.PentahoAccessControlException; import org.pentaho.platform.api.mimetype.IPlatformMimeResolver; import org.pentaho.platform.api.repository2.unified.IPlatformImportBundle; import org.pentaho.platform.engine.core.system.PentahoSessionHolder; import org.pentaho.platform.engine.core.system.PentahoSystem; import org.pentaho.platform.plugin.action.mondrian.catalog.IMondrianCatalogService; import org.pentaho.platform.plugin.services.importer.IPlatformImporter; import org.pentaho.platform.plugin.services.importer.RepositoryFileImportBundle; import org.pentaho.platform.plugin.services.importexport.IRepositoryImportLogger; import org.pentaho.platform.plugin.services.importexport.ImportSession; import org.pentaho.platform.repository2.unified.ServerRepositoryPaths; import org.pentaho.platform.repository2.unified.jcr.JcrTenantUtils; import org.pentaho.platform.security.policy.rolebased.actions.AdministerSecurityAction; import org.pentaho.platform.security.policy.rolebased.actions.PublishAction; import org.pentaho.platform.security.policy.rolebased.actions.RepositoryCreateAction; import org.pentaho.platform.security.policy.rolebased.actions.RepositoryReadAction; import org.pentaho.platform.web.http.api.resources.services.FileService; import com.sun.jersey.core.header.FormDataContentDisposition; import com.sun.jersey.multipart.FormDataParam; @Path ( "/repo/files/import" ) public class RepositoryImportResource { private static final Logger LOGGER = Logger.getLogger( RepositoryImportResource.class ); private static final String DEFAULT_CHAR_SET = "UTF-8"; /** * Attempts to import all files from the zip archive or single file. A log file is produced at the end of import. * * <p><b>Example Request:</b><br /> * POST pentaho/api/repo/files/import * <br /><b>POST data:</b> * <pre function="syntax.xml"> * ------WebKitFormBoundaryB9hzsGp4wR5SGAZD * Content-Disposition: form-data; name="importDir" * * /public * ------WebKitFormBoundaryB9hzsGp4wR5SGAZD * Content-Disposition: form-data; name="fileUpload"; filename="test.csv" * Content-Type: application/vnd.ms-excel * * ------WebKitFormBoundaryB9hzsGp4wR5SGAZD * Content-Disposition: form-data; name="overwriteFile" * * true * ------WebKitFormBoundaryB9hzsGp4wR5SGAZD * Content-Disposition: form-data; name="overwriteAclPermissions" * * true * ------WebKitFormBoundaryB9hzsGp4wR5SGAZD * Content-Disposition: form-data; name="applyAclPermissions" * * true * ------WebKitFormBoundaryB9hzsGp4wR5SGAZD * Content-Disposition: form-data; name="retainOwnership" * * true * ------WebKitFormBoundaryB9hzsGp4wR5SGAZD * Content-Disposition: form-data; name="charSet" * * UTF-8 * ------WebKitFormBoundaryB9hzsGp4wR5SGAZD * Content-Disposition: form-data; name="logLevel" * * INFO * ------WebKitFormBoundaryd1z6iZhXyx12RYxV * Content-Disposition: form-data; name="fileNameOverride" * * fileNameOverriden.csv * ------WebKitFormBoundaryd1z6iZhXyx12RYxV-- * </pre> * </p> * * @param importDir JCR Directory to which the zip structure or single file will be uploaded to. * @param fileUpload Input stream for the file. * @param overwriteFile The flag indicates ability to overwrite existing file. * @param overwriteAclPermissions The flag indicates ability to overwrite Acl permissions. * @param applyAclPermissions The flag indicates ability to apply Acl permissions. * @param retainOwnership The flag indicates ability to retain ownership. * @param charSet The charset for imported file. * @param logLevel The level of logging. * @param fileNameOverride If present and the content represents a single file, this parameter contains the filename to use * when storing the file in the repository. If not present, the fileInfo.getFileName will be used. * Note that the later cannot reliably handle foreign character sets. * * @return A jax-rs Response object with the appropriate header and body. * * <p><b>Example Response:</b></p> * <pre function="syntax.xml"> * <html> * <head> * <title>Repository Import Log</title> * </head> * <body bgcolor="#FFFFFF" topmargin="6" leftmargin="6" style="font-family: arial,sans-serif; font-size: x-small"> * <hr size="1" noshade> * Log session start time Thu Feb 26 11:04:19 BRT 2015<br> * <br> * <table cellspacing="0" cellpadding="4" border="1" bordercolor="#224466" width="100%"> * <tr style="background: #336699; color: #FFFFFF; text-align: left"> * <th>Import File</th> * <th>Level</th> * <th>Message</th> * </tr> * <td title="importFile">/public</td> * <td title="Level">INFO</td> * <td title="Message">Start Import Job</td> * </tr> * <td title="importFile">/public/fileNameOverriden.csv</td> * <td title="Level">INFO</td> * <td title="Message">Start File Import</td> * </tr> * <td title="importFile">/public/fileNameOverriden.csv</td> * <td title="Level"><font color="#993300"><strong>WARN</strong></font></td> * <td title="Message">fileNameOverriden.csv</td> * </tr> * <td title="importFile">/public</td> * <td title="Level">INFO</td> * <td title="Message">End Import Job</td> * </tr> * </table> * <br> * </body></html> * </pre> */ @POST @Consumes ( MediaType.MULTIPART_FORM_DATA ) @Produces ( MediaType.TEXT_HTML ) @Facet( name = "Unsupported" ) public Response doPostImport( @FormDataParam ( "importDir" ) String importDir, @FormDataParam ( "fileUpload" ) InputStream fileUpload, @FormDataParam ( "overwriteFile" ) String overwriteFile, @FormDataParam ( "overwriteAclPermissions" ) String overwriteAclPermissions, @FormDataParam ( "applyAclPermissions" ) String applyAclPermission, @FormDataParam ( "retainOwnership" ) String retainOwnership, @FormDataParam ( "charSet" ) String charSet, @FormDataParam ( "logLevel" ) String logLevel, @FormDataParam ( "fileUpload" ) FormDataContentDisposition fileInfo, @FormDataParam ( "fileNameOverride" ) String fileNameOverride ) { IRepositoryImportLogger importLogger = null; ByteArrayOutputStream importLoggerStream = new ByteArrayOutputStream(); boolean logJobStarted = false; if ( StringUtils.isBlank( charSet ) ) { charSet = DEFAULT_CHAR_SET; } try { validateAccess( importDir ); boolean overwriteFileFlag = ( "false".equals( overwriteFile ) ? false : true ); boolean overwriteAclSettingsFlag = ( "true".equals( overwriteAclPermissions ) ? true : false ); boolean applyAclSettingsFlag = ( "true".equals( applyAclPermission ) ? true : false ); boolean retainOwnershipFlag = ( "true".equals( retainOwnership ) ? true : false ); // If logLevel is null then we will default to ERROR if ( logLevel == null || logLevel.length() <= 0 ) { logLevel = "ERROR"; } //Non-admins cannot process a manifest FileService fileService = new FileService(); if ( !fileService.doCanAdminister() ) { applyAclSettingsFlag = false; retainOwnershipFlag = true; } Level level = Level.toLevel( logLevel ); ImportSession.getSession().setAclProperties( applyAclSettingsFlag, retainOwnershipFlag, overwriteAclSettingsFlag ); //The fileNameOverride was added because the formDataContentDispositionfile object cannot reliable //contain non US-ASCII characters. See RFC283 section 2.3 for details String fileName = fileNameOverride != null ? fileNameOverride : fileInfo.getFileName(); RepositoryFileImportBundle.Builder bundleBuilder = new RepositoryFileImportBundle.Builder(); bundleBuilder.input( fileUpload ); bundleBuilder.charSet( charSet ); bundleBuilder.path( importDir ); bundleBuilder.overwriteFile( overwriteFileFlag ); bundleBuilder.applyAclSettings( applyAclSettingsFlag ); bundleBuilder.overwriteAclSettings( overwriteAclSettingsFlag ); bundleBuilder.retainOwnership( retainOwnershipFlag ); bundleBuilder.name( fileName ); IPlatformImportBundle bundle = bundleBuilder.build(); IPlatformMimeResolver mimeResolver = PentahoSystem.get( IPlatformMimeResolver.class ); String mimeTypeFromFile = mimeResolver.resolveMimeForFileName( fileName ); if ( mimeTypeFromFile == null ) { return Response.ok( "INVALID_MIME_TYPE", MediaType.TEXT_HTML ).build(); } bundleBuilder.mime( mimeTypeFromFile ); IPlatformImporter importer = PentahoSystem.get( IPlatformImporter.class ); importLogger = importer.getRepositoryImportLogger(); final String mimeType = bundle.getMimeType() != null ? bundle.getMimeType() : mimeResolver.resolveMimeForBundle( bundle ); if ( mimeType == null ) { return Response.ok( "INVALID_MIME_TYPE", MediaType.TEXT_HTML ).build(); } logJobStarted = true; importLogger.startJob( importLoggerStream, importDir, level ); importer.importFile( bundle ); // Flush the Mondrian cache to show imported data-sources. IMondrianCatalogService mondrianCatalogService = PentahoSystem.get( IMondrianCatalogService.class, "IMondrianCatalogService", PentahoSessionHolder .getSession() ); mondrianCatalogService.reInit( PentahoSessionHolder.getSession() ); } catch ( PentahoAccessControlException e ) { return Response.serverError().entity( e.toString() ).build(); } catch ( Exception e ) { return Response.serverError().entity( e.toString() ).build(); } finally { ImportSession.clearSession(); if ( logJobStarted == true ) { importLogger.endJob(); } } String responseBody; try { responseBody = importLoggerStream.toString( charSet ); } catch ( UnsupportedEncodingException e ) { LOGGER.error( "Encoding of response body is failed. (charSet=" + charSet + ")", e ); responseBody = importLoggerStream.toString(); } return Response.ok( responseBody, MediaType.TEXT_HTML ).build(); } protected void validateAccess( String importDir ) throws PentahoAccessControlException { IAuthorizationPolicy policy = PentahoSystem.get( IAuthorizationPolicy.class ); //check if we are admin or have publish permisson boolean isAdmin = policy.isAllowed( RepositoryReadAction.NAME ) && policy.isAllowed( RepositoryCreateAction.NAME ) && ( policy.isAllowed( AdministerSecurityAction.NAME ) || policy.isAllowed( PublishAction.NAME ) ); if ( !isAdmin ) { //the user does not have admin or publish permisson, so we will check if the user imports to their home folder boolean importingToHomeFolder = false; String tenatedUserName = PentahoSessionHolder.getSession().getName(); //get user home home folder path String userHomeFolderPath = ServerRepositoryPaths.getUserHomeFolderPath( JcrTenantUtils.getUserNameUtils().getTenant( tenatedUserName ), JcrTenantUtils.getUserNameUtils().getPrincipleName( tenatedUserName ) ); if ( userHomeFolderPath != null && userHomeFolderPath.length() > 0 ) { //we pass the relative path so add serverside root folder for every home folder importingToHomeFolder = ( ServerRepositoryPaths.getTenantRootFolderPath() + importDir ) .contains( userHomeFolderPath ); } if ( !( importingToHomeFolder && policy.isAllowed( RepositoryCreateAction.NAME ) && policy.isAllowed( RepositoryReadAction.NAME ) ) ) { throw new PentahoAccessControlException( "User is not authorized to perform this operation" ); } } } }