001/*
002 *  Copyright 2011 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.web.skin.actions;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.Enumeration;
023import java.util.HashMap;
024import java.util.LinkedHashMap;
025import java.util.Map;
026
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.parameters.Parameters;
031import org.apache.cocoon.Constants;
032import org.apache.cocoon.acting.ServiceableAction;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Redirector;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.environment.SourceResolver;
037import org.apache.cocoon.servlet.multipart.Part;
038import org.apache.cocoon.servlet.multipart.PartOnDisk;
039import org.apache.cocoon.servlet.multipart.RejectedPart;
040import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
041import org.apache.commons.compress.archivers.zip.ZipFile;
042import org.apache.commons.io.FileUtils;
043import org.apache.commons.io.IOUtils;
044
045import org.ametys.core.cocoon.JSonReader;
046import org.ametys.runtime.util.AmetysHomeHelper;
047
048/**
049 * This action receive a form with the "importfile" zip file as an exported skin.
050 * Replace existing skin 
051 */
052public class UploadSkinAction extends ServiceableAction implements Contextualizable
053{
054    /** The cocoon context */
055    protected org.apache.cocoon.environment.Context _cocoonContext;
056
057    @Override
058    public void contextualize(Context context) throws ContextException
059    {
060        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
061    }
062        
063    @Override
064    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
065    {
066        Request request = ObjectModelHelper.getRequest(objectModel);
067        
068        Map<String, Object> result = new LinkedHashMap<>();
069
070        boolean isModel = parameters.getParameterAsBoolean("model", false);
071        try
072        {
073            // Checking file
074            Part part = (Part) request.get("importfile");
075            if (part instanceof RejectedPart || part == null)
076            {
077                result.put("success", false);
078                result.put("error", "rejected");
079            }
080            else
081            {
082                // Getting file
083                PartOnDisk uploadedFilePart = (PartOnDisk) part;
084                File uploadedFile = uploadedFilePart.getFile();
085                
086                String filename = (String) uploadedFilePart.getHeaders().get("filename");
087                String skinName = filename.substring(0, filename.length() - 4);
088                
089                if (getLogger().isDebugEnabled())
090                {
091                    getLogger().debug("Uploading skin " + skinName);
092                }
093    
094                // Unzipping in tmp
095                File dir = _unzip(uploadedFile);
096                
097                // Filter files
098                _filter(dir, dir.getAbsolutePath(), isModel);
099                
100                // Test if the configuration model file is present.
101                File configFile = new File(dir, "stylesheets" + File.separator + "config" + File.separator + "config-model.xml");
102                
103                result.put("skinDir", dir.getName());
104                result.put("tempDir", dir.getParentFile().getName());
105                result.put("hasConfig", configFile.isFile());
106                result.put("skinName", skinName);
107                result.put("success", "true");
108            }
109        }
110        catch (Exception e)
111        {
112            getLogger().error("Unable to add skin", e);
113            result.put("success", false);
114            
115            Map<String, String> ex = new HashMap<>();
116            ex.put("message", e.getMessage());
117
118            result.put("error", ex);
119        }
120        
121        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
122        
123        return result;    
124    }
125
126    /**
127     * Unzip file in temp directory
128     * @param uploadedFile the uploaded file
129     * @return the unzipped file
130     * @throws IOException if an I/O exception occurs
131     */
132    protected File _unzip(File uploadedFile) throws IOException
133    {
134        File tmpFile = new File(AmetysHomeHelper.getAmetysHomeTmp(), Long.toString(Math.round(Math.random() * 1000000.0)));
135        
136        getLogger().debug("Importing skin in tmp dir " + tmpFile.getAbsolutePath());
137        ZipFile zipFile = new ZipFile(uploadedFile, "cp437");
138        
139        try
140        {
141            Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
142            while (entries.hasMoreElements())
143            {
144                ZipArchiveEntry zipEntry = entries.nextElement();
145                
146                String zipName = zipEntry.getName();
147    
148                if (zipEntry.isDirectory())
149                {
150                    File newDir = new File(tmpFile, zipName);
151                    newDir.mkdirs();
152                }
153                else
154                {
155                    try (InputStream is = zipFile.getInputStream(zipEntry))
156                    {
157                        File newFile = new File(tmpFile, zipName);
158                        newFile.getParentFile().mkdirs();
159                        
160                        try (FileOutputStream fos = new FileOutputStream(newFile))
161                        {
162                            IOUtils.copy(is, fos);
163                        }
164                    }
165                }
166            }
167        }
168        finally
169        {
170            ZipFile.closeQuietly(zipFile);
171        }
172        
173        File[] files = tmpFile.listFiles();
174        if (files.length != 1 || !files[0].isDirectory())
175        {
176            throw new IllegalArgumentException("An imported skin or model must be a zip with a single root directory");
177        }
178        
179        return files[0];
180    }
181    
182    /**
183     * Filter skin directory
184     * @param skinDir the skin directory
185     * @param absoluteSkinPath the absolute path of tmp skin directory
186     * @param isModel true if the uploaded skin is the model of skin.
187     * @throws IOException if an error occurred
188     */
189    protected void _filter(File skinDir, String absoluteSkinPath, boolean isModel) throws IOException
190    {
191        File[] files = skinDir.listFiles();
192        for (File file : files)
193        {
194            if (file.isDirectory())
195            {
196                if ("CVS".equals(file.getName())
197                    || ".svn".equals(file.getName())
198                    || !isModel && "model".equals(file.getName()))
199                {
200                    FileUtils.deleteQuietly(file);
201                }
202                else
203                {
204                    _filter(file, absoluteSkinPath, isModel);
205                }
206            }
207        }
208    }
209}