001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.runtime.config;
020
021import java.io.ByteArrayInputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.jar.Attributes;
027import java.util.jar.Manifest;
028import java.util.zip.ZipEntry;
029import java.util.zip.ZipException;
030import java.util.zip.ZipFile;
031import java.util.zip.ZipInputStream;
032
033import lucee.commons.io.IOUtil;
034import lucee.commons.io.SystemUtil;
035import lucee.commons.io.compress.ZipUtil;
036import lucee.commons.io.log.Log;
037import lucee.commons.io.log.LogUtil;
038import lucee.commons.io.res.Resource;
039import lucee.commons.io.res.filter.ExtensionResourceFilter;
040import lucee.commons.io.res.filter.ResourceFilter;
041import lucee.commons.io.res.util.FileWrapper;
042import lucee.commons.io.res.util.ResourceUtil;
043import lucee.commons.lang.ExceptionUtil;
044import lucee.commons.lang.StringUtil;
045import lucee.commons.lang.SystemOut;
046import lucee.runtime.Info;
047import lucee.runtime.extension.RHExtension;
048import lucee.runtime.op.Caster;
049import lucee.runtime.op.Decision;
050import lucee.runtime.type.util.ListUtil;
051
052public class DeployHandler {
053        
054        
055
056        private static final ResourceFilter ALL_EXT = new ExtensionResourceFilter(new String[]{".lex",".lar"});
057
058
059        public static void deploy(Config config){
060                synchronized (config) {
061                        Resource dir = getDeployDirectory(config);
062                        int ma = Info.getMajorVersion();
063                        int mi = Info.getMinorVersion();
064                        if(!dir.exists()) {
065                                if(ma>4 || ma==4 && mi>1) {// FUTURE remove the if contition
066                                        dir.mkdir(); // TODO we have set this to mkdir to avoid that delted context are recreated (the file structure), but the question is, why is this executed on a deleted context
067                                }
068                                return;
069                        }
070                        
071                        Resource[] children = dir.listResources(ALL_EXT);
072                        Resource child;
073                        String ext;
074                        for(int i=0;i<children.length;i++){
075                                child=children[i];
076                                try {
077                                // Lucee archives
078                                ext=ResourceUtil.getExtension(child, null);
079                                if("lar".equalsIgnoreCase(ext)) {
080                                        deployArchive(config,child);
081                                }
082                                
083                                // Lucee Extensions
084                                else if("lex".equalsIgnoreCase(ext))
085                                        deployExtension(config, child);
086                                }
087                                catch (ZipException e) {
088                                        SystemOut.printDate(config.getErrWriter(),ExceptionUtil.getStacktrace(e, true));
089                                }
090                                catch (IOException e) {
091                                        SystemOut.printDate(config.getErrWriter(),ExceptionUtil.getStacktrace(e, true));
092                                }
093                        }
094                }
095        }
096
097        public static Resource getDeployDirectory(Config config) {
098                return config.getConfigDir().getRealResource("deploy");
099        }
100
101        private static void deployArchive(Config config,Resource archive) throws ZipException, IOException {
102                Log logger = ((ConfigImpl)config).getLog("deploy");
103                String type=null,virtual=null,name=null;
104                boolean readOnly,topLevel,hidden,physicalFirst;
105                short inspect;
106                InputStream is = null;
107                ZipFile file=null;
108                try {
109                file=new ZipFile(FileWrapper.toFile(archive));
110                ZipEntry entry = file.getEntry("META-INF/MANIFEST.MF");
111                
112                // no manifest
113                if(entry==null) {
114                        logger.log(Log.LEVEL_ERROR,"archive","cannot deploy Lucee Archive ["+archive+"], file is to old, the file does not have a MANIFEST.");
115                        moveToFailedFolder(archive);
116                        return;
117                }
118                
119                        is = file.getInputStream(entry);
120                        Manifest manifest = new Manifest(is);
121                    Attributes attr = manifest.getMainAttributes();
122                    
123                    //id = unwrap(attr.getValue("mapping-id"));
124                    type = unwrap(attr.getValue("mapping-type"));
125                    virtual = unwrap(attr.getValue("mapping-virtual-path"));
126                    name = ListUtil.trim(virtual, "/");
127                    readOnly = Caster.toBooleanValue(unwrap(attr.getValue("mapping-readonly")),false);
128                    topLevel = Caster.toBooleanValue(unwrap(attr.getValue("mapping-top-level")),false);
129                    inspect = ConfigWebUtil.inspectTemplate(unwrap(attr.getValue("mapping-inspect")), ConfigImpl.INSPECT_UNDEFINED);
130                    if(inspect==ConfigImpl.INSPECT_UNDEFINED) {
131                        Boolean trusted = Caster.toBoolean(unwrap(attr.getValue("mapping-trusted")),null);
132                        if(trusted!=null) {
133                                if(trusted.booleanValue()) inspect=ConfigImpl.INSPECT_NEVER;
134                                else inspect=ConfigImpl.INSPECT_ALWAYS;
135                        }       
136                    }
137                    hidden = Caster.toBooleanValue(unwrap(attr.getValue("mapping-hidden")),false);
138                    physicalFirst = Caster.toBooleanValue(unwrap(attr.getValue("mapping-physical-first")),false);
139                }
140                finally{
141                        IOUtil.closeEL(is);
142                        ZipUtil.close(file);
143                }
144                Resource trgDir = config.getConfigDir().getRealResource("archives").getRealResource(type).getRealResource(name);
145                Resource trgFile = trgDir.getRealResource(archive.getName());
146                trgDir.mkdirs();
147                
148                // delete existing files
149                
150                try {
151                        ResourceUtil.deleteContent(trgDir, null);
152                        ResourceUtil.moveTo(archive, trgFile,true);
153                        
154                        logger.log(Log.LEVEL_INFO,"archive","add "+type+" mapping ["+virtual+"] with archive ["+trgFile.getAbsolutePath()+"]");
155                        if("regular".equalsIgnoreCase(type))
156                                ConfigWebAdmin.updateMapping((ConfigImpl)config,virtual, null, trgFile.getAbsolutePath(), "archive", inspect, topLevel);
157                        else if("cfc".equalsIgnoreCase(type))
158                                ConfigWebAdmin.updateComponentMapping((ConfigImpl)config,virtual, null, trgFile.getAbsolutePath(), "archive", inspect);
159                        else if("ct".equalsIgnoreCase(type))
160                                ConfigWebAdmin.updateCustomTagMapping((ConfigImpl)config,virtual, null, trgFile.getAbsolutePath(), "archive", inspect);
161                        
162                    
163                }
164                catch (Throwable t) {
165                ExceptionUtil.rethrowIfNecessary(t);
166                        moveToFailedFolder(archive);
167                        LogUtil.log(logger, Log.LEVEL_ERROR,"archive",t);
168                }
169        }
170        
171
172        private static void deployExtension(Config config, Resource ext) {
173                ConfigImpl ci = (ConfigImpl)config;
174                boolean isWeb=config instanceof ConfigWeb;
175                String type=isWeb?"web":"server";
176                Log logger = ((ConfigImpl)config).getLog("deploy");
177                
178                // Manifest
179                Manifest manifest = null;
180                ZipInputStream zis=null;
181        try {
182                zis = new ZipInputStream( IOUtil.toBufferedInputStream(ext.getInputStream()) ) ;     
183                ZipEntry entry;
184                String name;
185                while ( ( entry = zis.getNextEntry()) != null ) {
186                        name=entry.getName();
187                        if(!entry.isDirectory() && name.equalsIgnoreCase("META-INF/MANIFEST.MF")) {
188                                manifest = toManifest(config,zis,null);
189                        }
190                    zis.closeEntry() ;
191                }
192        }
193        catch(Throwable t){
194                ExceptionUtil.rethrowIfNecessary(t);
195                LogUtil.log(logger, Log.LEVEL_ERROR,"extension", t);
196                        moveToFailedFolder(ext);
197                        return;
198        }
199        finally {
200                IOUtil.closeEL(zis);
201        }
202        
203        int minCoreVersion=0;
204        double minLoaderVersion=0;
205        String strMinCoreVersion="",strMinLoaderVersion="",version=null,name=null,id=null;
206        
207        if(manifest!=null) {
208                Attributes attr = manifest.getMainAttributes();
209                // version
210                version=unwrap(attr.getValue("version"));
211
212
213                // id
214                id=unwrap(attr.getValue("id"));
215                
216                // name
217                name=unwrap(attr.getValue("name"));
218
219                // core version
220                strMinCoreVersion=unwrap(attr.getValue("lucee-core-version"));
221                if(StringUtil.isEmpty(strMinCoreVersion))
222                        strMinCoreVersion=unwrap(attr.getValue("railo-core-version"));
223                minCoreVersion=Info.toIntVersion(strMinCoreVersion,minCoreVersion);
224                
225                // loader version
226                strMinLoaderVersion=unwrap(attr.getValue("lucee-loader-version"));
227                if(StringUtil.isEmpty(strMinLoaderVersion))
228                        strMinLoaderVersion=unwrap(attr.getValue("railo-loader-version"));
229                minLoaderVersion=Caster.toDoubleValue(strMinLoaderVersion,minLoaderVersion);
230                
231        }
232        if(StringUtil.isEmpty(name,true)) {
233                name=ext.getName();
234                int index=name.lastIndexOf('.');
235                name=name.substring(0,index-1);
236        }
237        name=name.trim();
238        
239        
240        // check core version
241                if(minCoreVersion>Info.getVersionAsInt()) {
242                        logger.log(Log.LEVEL_ERROR,"extension", "cannot deploy Lucee Extension ["+ext+"], Lucee Version must be at least ["+strMinCoreVersion+"].");
243                        moveToFailedFolder(ext);
244                        return;
245                }
246
247                // check loader version
248                if(minLoaderVersion>SystemUtil.getLoaderVersion()) {
249                        logger.log(Log.LEVEL_ERROR,"extension", "cannot deploy Lucee Extension ["+ext+"], Lucee Loader Version must be at least ["+strMinLoaderVersion+"], update the lucee.jar first.");
250                        moveToFailedFolder(ext);
251                        return;
252                }
253                // check id
254                if(!Decision.isUUId(id)) {
255                        logger.log(Log.LEVEL_ERROR,"extension", "cannot deploy Lucee Extension ["+ext+"], this Extension has no valid id ["+id+"],id must be a valid UUID.");
256                        moveToFailedFolder(ext);
257                        return;
258                }
259                
260                
261                
262                
263                Resource trgFile=null;
264                try{
265                        ConfigWebAdmin.removeRHExtension(ci,id);
266                        
267                        Resource trgDir = config.getConfigDir().getRealResource("extensions").getRealResource(type).getRealResource(name);
268                        trgFile = trgDir.getRealResource(ext.getName());
269                        trgDir.mkdirs();
270                        ResourceUtil.moveTo(ext, trgFile,true);
271                }
272            catch(Throwable t){
273                ExceptionUtil.rethrowIfNecessary(t);
274                LogUtil.log(logger, Log.LEVEL_ERROR,"extension", t);
275                        moveToFailedFolder(ext);
276                        return;
277            }
278            
279                try {
280                zis = new ZipInputStream( IOUtil.toBufferedInputStream(trgFile.getInputStream()) ) ;     
281                ZipEntry entry;
282                String path;
283                String fileName;
284                List<String> jars=new ArrayList<String>(), flds=new ArrayList<String>(), tlds=new ArrayList<String>(), contexts=new ArrayList<String>(), applications=new ArrayList<String>();
285                while ( ( entry = zis.getNextEntry()) != null ) {
286                        path=entry.getName();
287                        fileName=fileName(entry);
288                        // jars
289                        if(!entry.isDirectory() && (startsWith(path,type,"jars") || startsWith(path,type,"jar") || startsWith(path,type,"lib") || startsWith(path,type,"libs")) && StringUtil.endsWithIgnoreCase(path, ".jar")) {
290                                logger.log(Log.LEVEL_INFO,"extension","deploy jar "+fileName);
291                                ConfigWebAdmin.updateJar(config,zis,fileName,false);
292                                jars.add(fileName);
293                        }
294                        
295                        // flds
296                        if(!entry.isDirectory() && startsWith(path,type,"flds") && StringUtil.endsWithIgnoreCase(path, ".fld")) {
297                                logger.log(Log.LEVEL_INFO,"extension","deploy fld "+fileName);
298                                ConfigWebAdmin.updateFLD(config, zis, fileName,false);
299                                flds.add(fileName);
300                        }
301                        
302                        // tlds
303                        if(!entry.isDirectory() && startsWith(path,type,"tlds") && (StringUtil.endsWithIgnoreCase(path, ".tld") || StringUtil.endsWithIgnoreCase(path, ".tldx"))) {
304                                logger.log(Log.LEVEL_INFO,"extension","deploy tld "+fileName);
305                                ConfigWebAdmin.updateTLD(config, zis, fileName,false); 
306                                tlds.add(fileName);
307                        }
308                        
309                        // context
310                        String relpath;
311                        if(!entry.isDirectory() && startsWith(path,type,"context") && !StringUtil.startsWith(fileName(entry), '.')) {
312                                relpath=path.substring(8);
313                                //log.info("extension","deploy context "+relpath);
314                                logger.log(Log.LEVEL_INFO,"extension","deploy context "+relpath);
315                                ConfigWebAdmin.updateContext(ci, zis, relpath,false);
316                                contexts.add(relpath);
317                        }
318                        
319                        // applications
320                        if(!entry.isDirectory() && startsWith(path,type,"applications") && !StringUtil.startsWith(fileName(entry), '.')) {
321                                relpath=path.substring(13);
322                                //log.info("extension","deploy context "+relpath);
323                                logger.log(Log.LEVEL_INFO,"extension","deploy application "+relpath);
324                                ConfigWebAdmin.updateApplication(ci, zis, relpath,false);
325                                applications.add(relpath);
326                        }
327                        
328                        
329                    zis.closeEntry() ;
330                }
331                
332                //installation successfull
333                
334                ConfigWebAdmin.updateRHExtension(ci,
335                                new RHExtension(id,name,version,
336                                jars.toArray(new String[jars.size()]),
337                                flds.toArray(new String[flds.size()]),
338                                tlds.toArray(new String[tlds.size()]),
339                                contexts.toArray(new String[contexts.size()]),
340                                applications.toArray(new String[applications.size()])));
341                
342        }
343            catch(Throwable t){
344                ExceptionUtil.rethrowIfNecessary(t);
345                // installation failed
346                
347                LogUtil.log(logger, Log.LEVEL_ERROR,"extension",t);
348                        moveToFailedFolder(trgFile);
349                        return;
350            }
351        finally {
352                IOUtil.closeEL(zis);
353        }
354        }
355
356        private static Manifest toManifest(Config config,InputStream is, Manifest defaultValue) {
357                try {
358                        String cs = config.getResourceCharset();
359                        String str = IOUtil.toString(is,cs);
360                        if(StringUtil.isEmpty(str,true)) return defaultValue;
361                        str=str.trim()+"\n";
362                        return new Manifest(new ByteArrayInputStream(str.getBytes(cs)));
363                }
364                catch (Throwable t) {
365                ExceptionUtil.rethrowIfNecessary(t);
366                        return defaultValue;
367                }
368        }
369
370        private static boolean startsWith(String path,String type, String name) {
371                return StringUtil.startsWithIgnoreCase(path, name+"/") || StringUtil.startsWithIgnoreCase(path, type+"/"+name+"/");
372        }
373
374        private static String fileName(ZipEntry entry) {
375                String name = entry.getName();
376                int index=name.lastIndexOf('/');
377                if(index==-1) return name;
378                return name.substring(index+1);
379        }
380
381        private static void moveToFailedFolder(Resource archive) {
382                Resource dir = archive.getParentResource().getRealResource("failed-to-deploy");
383                Resource dst = dir.getRealResource(archive.getName());
384                dir.mkdirs();
385                
386                try {
387                        if(dst.exists()) dst.remove(true);
388                        ResourceUtil.moveTo(archive, dst,true);
389                }
390                catch (Throwable t) {
391                ExceptionUtil.rethrowIfNecessary(t);
392        }
393                
394                // TODO Auto-generated method stub
395                
396        }
397
398        private static String unwrap(String value) {
399                if(value==null) return "";
400                String res = unwrap(value,'"');
401                if(res!=null) return res; // was double quote
402                
403                return unwrap(value,'\''); // try single quote unwrap, when there is no double quote.
404        }
405        
406        private static String unwrap(String value, char del) {
407                value=value.trim();
408                if(StringUtil.startsWith(value, del) && StringUtil.endsWith(value, del)) {
409                        return value.substring(1, value.length()-1);
410                }
411                return value;
412        }
413        
414}