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.tag;
020
021import java.io.ByteArrayInputStream;
022import java.io.ByteArrayOutputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.util.ArrayList;
026import java.util.Enumeration;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Set;
030import java.util.zip.ZipEntry;
031import java.util.zip.ZipException;
032import java.util.zip.ZipFile;
033import java.util.zip.ZipInputStream;
034import java.util.zip.ZipOutputStream;
035
036import lucee.commons.io.IOUtil;
037import lucee.commons.io.compress.ZipUtil;
038import lucee.commons.io.res.Resource;
039import lucee.commons.io.res.filter.DirectoryResourceFilter;
040import lucee.commons.io.res.filter.FileResourceFilter;
041import lucee.commons.io.res.filter.OrResourceFilter;
042import lucee.commons.io.res.filter.ResourceFilter;
043import lucee.commons.io.res.util.FileWrapper;
044import lucee.commons.io.res.util.ResourceUtil;
045import lucee.commons.io.res.util.UDFFilter;
046import lucee.commons.io.res.util.WildcardPatternFilter;
047import lucee.commons.lang.StringUtil;
048import lucee.runtime.PageContextImpl;
049import lucee.runtime.exp.ApplicationException;
050import lucee.runtime.exp.ExpressionException;
051import lucee.runtime.exp.PageException;
052import lucee.runtime.ext.tag.BodyTagImpl;
053import lucee.runtime.op.Caster;
054import lucee.runtime.op.Decision;
055import lucee.runtime.type.QueryImpl;
056import lucee.runtime.type.UDF;
057import lucee.runtime.type.dt.DateTimeImpl;
058
059public final class Zip extends BodyTagImpl {
060
061        private String action="zip";
062        private String charset;
063        private Resource destination;
064        private String entryPath;
065        private Resource file;
066        private ResourceFilter filter;
067        private String pattern;
068        private String patternDelimiters;
069        private String name;
070        private boolean overwrite;
071        private String prefix;
072        private boolean recurse=true;
073        private boolean showDirectory;
074        private boolean storePath=true;
075        private String variable;
076        private List<ZipParamAbstr> params;
077        private Set<String> alreadyUsed;
078        private Resource source;
079        private static int id=0;
080        
081        
082    @Override
083    public void release()   {
084        super.release();
085        action="zip";
086        charset=null;
087        destination=null;
088        entryPath=null;
089        file=null;
090        filter=null;
091        name=null;
092        overwrite=false;
093        prefix=null;
094        recurse=true;
095        showDirectory=false;
096        source=null;
097        storePath=true;
098        variable=null;
099            pattern = null;
100            patternDelimiters = null;
101
102        if(params!=null)params.clear();
103        if(alreadyUsed!=null)alreadyUsed.clear();
104    }
105        
106    
107    
108    /**
109         * @param action the action to set
110         */
111        public void setAction(String action) {
112                this.action = action.trim().toLowerCase();
113        }
114
115
116
117        /**
118         * @param charset the charset to set
119         */
120        public void setCharset(String charset) {
121                this.charset = charset;
122        }
123
124
125
126        /**
127         * @param strDestination the destination to set
128         * @throws ExpressionException 
129         * @throws PageException 
130         */
131        public void setDestination(String strDestination) throws PageException {
132                this.destination = ResourceUtil.toResourceExistingParent(pageContext, strDestination);
133                if(!destination.exists())destination.mkdirs();
134                
135                if(!destination.isDirectory())
136                        throw new ApplicationException("destination ["+strDestination+"] is not a existing directory");
137                
138                
139        }
140
141
142
143        /**
144         * @param entryPath the entryPath to set
145         */
146        public void setEntrypath(String entryPath) {
147                if(StringUtil.isEmpty(entryPath,true)) return;
148
149                entryPath=entryPath.trim();
150                entryPath=entryPath.replace('\\','/');
151                
152                if(StringUtil.startsWith(entryPath,'/'))entryPath=entryPath.substring(1);
153                if(StringUtil.endsWith(entryPath,'/'))entryPath=entryPath.substring(0,entryPath.length()-1);
154                this.entryPath = entryPath;
155        }
156
157
158
159        /**
160         * @param file the file to set
161         * @throws ExpressionException 
162         */
163        public void setFile(String file) {
164                this.file = ResourceUtil.toResourceNotExisting(pageContext, file);
165        }
166
167        /**
168         * @param filter the filter to set
169         */
170        public void setFilter(Object filter) throws PageException {
171
172                if (filter instanceof UDF)
173                        this.setFilter((UDF)filter);
174                else if (filter instanceof String)
175                        this.setFilter((String)filter);
176        }
177
178        public void setFilter(UDF filter) throws PageException  {
179
180                this.filter = UDFFilter.createResourceAndResourceNameFilter(filter);
181        }
182
183        public void setFilter(String pattern) {
184
185                this.pattern = pattern;
186        }
187
188        public void setFilterdelimiters(String patternDelimiters) {
189
190                this.patternDelimiters = patternDelimiters;
191        }
192
193
194        /**
195         * @param name the name to set
196         */
197        public void setName(String name) {
198                this.name = name;
199        }
200
201
202
203        /**
204         * @param overwrite the overwrite to set
205         */
206        public void setOverwrite(boolean overwrite) {
207                this.overwrite = overwrite;
208        }
209
210
211
212        /**
213         * @param prefix the prefix to set
214         */
215        public void setPrefix(String prefix) {
216                this.prefix = prefix;
217        }
218
219
220
221        /**
222         * @param recurse the recurse to set
223         */
224        public void setRecurse(boolean recurse) {
225                this.recurse = recurse;
226        }
227
228
229
230        /**
231         * @param showDirectory the showDirectory to set
232         */
233        public void setShowdirectory(boolean showDirectory) {
234                this.showDirectory = showDirectory;
235        }
236
237
238
239        /**
240         * @param strSource the source to set
241         * @throws PageException 
242         */
243        public void setSource(String strSource) throws PageException {
244                source = ResourceUtil.toResourceExisting(pageContext, strSource);
245        }
246
247
248
249        /**
250         * @param storePath the storePath to set
251         */
252        public void setStorepath(boolean storePath) {
253                this.storePath = storePath;
254        }
255
256
257
258        /**
259         * @param variable the variable to set
260         */
261        public void setVariable(String variable) {
262                this.variable = variable;
263        }
264
265
266
267        @Override
268        public int doStartTag() throws PageException    {
269                return EVAL_BODY_INCLUDE;
270        }
271
272        private void actionDelete() throws ApplicationException, IOException {
273                required("file",file,true);
274
275                Resource existing = pageContext.getConfig().getTempDirectory().getRealResource(getTempName());
276                IOUtil.copy(file, existing);
277                
278                ZipInputStream zis = null;  
279                ZipOutputStream zos = null;
280                try {
281                        zis = new ZipInputStream( IOUtil.toBufferedInputStream(existing.getInputStream()) );  
282                        zos = new ZipOutputStream(IOUtil.toBufferedOutputStream(file.getOutputStream()));
283                        
284                ZipEntry entry;
285                String path,name;
286                int index;
287                boolean accept;
288                
289                if(filter==null && recurse && entryPath==null)
290                        throw new ApplicationException("define at least one restriction, can't delete all the entries from a zip file");
291                
292                while ( ( entry = zis.getNextEntry()) != null ) {
293                        accept=false;
294                        path = entry.getName().replace('\\', '/');
295                index=path.lastIndexOf('/');
296                
297                        if(!recurse && index>0) accept=true;
298                        
299                        //dir=index==-1?"":path.substring(0,index);
300                name=path.substring(index+1);
301                
302                if(filter!=null && !filter.accept(file.getRealResource(name))) accept=true;
303                if(!entryPathMatch(path)) accept=true;
304                        
305                if(!accept) continue;
306                        
307                        add(zos, entry, zis, false);
308                        zis.closeEntry();
309                }
310                }
311                finally {
312                        IOUtil.closeEL(zis);
313                        IOUtil.closeEL(zos);
314                        existing.delete();
315                }
316                
317        }
318
319
320
321        private void actionList() throws PageException, IOException {
322                required("file",file,true);
323                required("name",name);
324                
325                lucee.runtime.type.Query query=new QueryImpl(
326                new String[]{"name","size","type","dateLastModified","directory","crc","compressedSize","comment"},
327                0,"query");
328                pageContext.setVariable(name, query);
329        
330                ZipFile zip = getZip(file);
331                Enumeration entries = zip.entries();
332                
333        try {
334                String path,name,dir;
335            ZipEntry ze;
336            int row=0,index;
337            while(entries.hasMoreElements()) {
338                ze = (ZipEntry)entries.nextElement();
339                if(!showDirectory && ze.isDirectory()) continue;
340                
341                path = ze.getName().replace('\\', '/');
342                index=path.lastIndexOf('/');
343                if(!recurse && index>0) continue;
344                
345                dir=index==-1?"":path.substring(0,index);
346                name=path.substring(index+1);
347
348                if(filter!=null && !filter.accept( file.getRealResource(name) )) continue;
349                
350                if(!entryPathMatch(dir)) continue;
351                //if(entryPath!=null && !(dir.equalsIgnoreCase(entryPath) || StringUtil.startsWithIgnoreCase(dir,entryPath+"/"))) ;///continue;
352                
353                row++;
354                query.addRow();
355                query.setAt("name", row, path);
356                query.setAt("size", row, Caster.toDouble(ze.getSize()));
357                query.setAt("type", row, ze.isDirectory()?"Directory":"File");
358                query.setAt("dateLastModified", row, new DateTimeImpl(pageContext,ze.getTime(),false));
359                query.setAt("crc", row, Caster.toDouble(ze.getCrc()));
360                query.setAt("compressedSize", row, Caster.toDouble(ze.getCompressedSize()));
361                query.setAt("comment", row, ze.getComment());
362                query.setAt("directory", row, dir);
363                //zis.closeEntry();
364            }
365        }
366        finally {
367                IOUtil.closeEL(zip);
368        }   
369        }
370
371        private boolean entryPathMatch(String dir) {
372                if(entryPath==null) return true;
373                
374                return dir.equalsIgnoreCase(entryPath) || StringUtil.startsWithIgnoreCase(dir,entryPath+"/");
375        }
376
377
378
379        private void actionRead(boolean binary) throws ZipException, IOException, PageException {
380                required("file",file,true);
381                required("variable",variable);
382                required("entrypath",variable);
383                ZipFile zip = getZip(file);
384                
385                try {
386                        ZipEntry ze = zip.getEntry(entryPath);
387                        if(ze==null)ze = zip.getEntry(entryPath+"/");
388                        if(ze==null) throw new ApplicationException("zip file ["+file+"] has no entry with name ["+entryPath+"]");
389                        
390                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
391                        
392                        InputStream is = zip.getInputStream(ze);
393                        IOUtil.copy(is, baos,true,false);
394                        zip.close();
395                        
396                        if(binary)
397                                pageContext.setVariable(variable, baos.toByteArray());
398                        else {
399                                if(charset==null)charset=((PageContextImpl)pageContext).getResourceCharset().name();
400                                pageContext.setVariable(variable, new String(baos.toByteArray(),charset));
401                        }
402                }
403                finally {
404                        IOUtil.closeEL(zip);
405                }
406                
407        }
408
409
410
411        private void actionUnzip() throws ApplicationException, IOException {
412                required("file",file,true);
413                required("destination",destination,false);
414
415                ZipInputStream zis=null;
416                String path;
417                Resource target,parent;
418                int index;
419        try {
420                
421                zis = new ZipInputStream( IOUtil.toBufferedInputStream(file.getInputStream()) ) ;     
422                ZipEntry entry;
423                while ( ( entry = zis.getNextEntry()) != null ) {
424                        
425                        path = entry.getName().replace('\\', '/');
426                index=path.lastIndexOf('/');
427                
428                // recurse
429                if(!recurse && index!=-1) {
430                        zis.closeEntry();
431                        continue;
432                    }
433
434                        target=destination.getRealResource(entry.getName());
435
436                        // filter
437                        if(filter!=null && !filter.accept(target)) {
438                            zis.closeEntry();
439                            continue;
440                    }
441
442                        // entrypath
443                        if(!entryPathMatch(path)) {
444                            zis.closeEntry();
445                            continue;
446                    }
447                        if(!storePath) target=destination.getRealResource(target.getName());
448                    if(entry.isDirectory()) {
449                        target.mkdirs();
450                    }
451                    else {
452                        if(storePath){
453                                parent=target.getParentResource();
454                                if(!parent.exists())parent.mkdirs();
455                        }
456                        if(overwrite || !target.exists())IOUtil.copy(zis,target,false);
457                    }
458                    target.setLastModified(entry.getTime());
459                    zis.closeEntry() ;
460                }
461        }
462        finally {
463                IOUtil.closeEL(zis);
464        }
465    }
466
467
468
469        private void actionZip() throws PageException, IOException {
470                required("file",file,false);
471                Resource dir = file.getParentResource();
472                
473                if(!dir.exists()) {
474                        throw new ApplicationException("directory ["+dir.toString()+"] doesn't exist"); 
475                }
476                
477                
478                
479                if((params==null || params.isEmpty()) && source!=null) {
480                        setParam(new ZipParamSource(source,entryPath,filter,prefix,recurse));
481                }
482
483                if((params==null || params.isEmpty())) {
484                        throw new ApplicationException("No source/content specified");
485                }
486                
487                
488                
489                
490                ZipOutputStream zos=null;
491                Resource existing=null;
492                try {
493                        
494                        // existing
495                        if(!overwrite && file.exists()) {
496                                existing = pageContext.getConfig().getTempDirectory().getRealResource(getTempName());
497                                IOUtil.copy(file, existing);
498                        }
499                        
500                                zos = new ZipOutputStream(IOUtil.toBufferedOutputStream(file.getOutputStream()));
501
502                        Object[] arr = params.toArray();
503                        for(int i=arr.length-1;i>=0;i--) {
504                                if(arr[i] instanceof ZipParamSource)
505                                        actionZip(zos,(ZipParamSource)arr[i]);
506                                else if(arr[i] instanceof ZipParamContent)
507                                        actionZip(zos,(ZipParamContent)arr[i]);
508                        }
509
510                        if(existing!=null) {
511                                ZipInputStream zis = new ZipInputStream( IOUtil.toBufferedInputStream(existing.getInputStream()) );  
512                                try {
513                                ZipEntry entry;
514                                while ( ( entry = zis.getNextEntry()) != null ) {
515                                        add(zos, entry, zis, false);
516                                        zis.closeEntry();
517                                }
518                                }
519                                finally {
520                                        zis.close();
521                                }
522                        }
523                }
524                finally {
525                        ZipUtil.close(zos);
526                        if(existing!=null)existing.delete();
527                        
528                }
529                
530        }
531
532
533
534        private String getTempName() {
535                return "tmp-"+(id++)+".zip";
536        }
537
538
539
540        private void actionZip(ZipOutputStream zos, ZipParamContent zpc) throws PageException, IOException {
541                Object content = zpc.getContent();
542                if(Decision.isBinary(content)) {
543                        add(zos, new ByteArrayInputStream(Caster.toBinary(content)), zpc.getEntryPath(), System.currentTimeMillis(), true);
544                        
545                }
546                else {
547                        String charset=zpc.getCharset();
548                        if(StringUtil.isEmpty(charset))charset=((PageContextImpl)pageContext).getResourceCharset().name();
549                        add(zos, new ByteArrayInputStream(content.toString().getBytes(charset)), zpc.getEntryPath(), System.currentTimeMillis(), true);
550                }
551        }
552
553
554
555        private void actionZip(ZipOutputStream zos, ZipParamSource zps) throws IOException {
556                // prefix
557                String p=zps.getPrefix();
558                if(StringUtil.isEmpty(p))
559                        p=this.prefix;
560                
561                if(!StringUtil.isEmpty(p)){
562                        if(!StringUtil.endsWith(p, '/'))p+="/";
563                }
564                else 
565                        p="";
566                
567                
568                
569                if(zps.getSource().isFile()){
570                        
571                        String ep = zps.getEntryPath();
572                        if(ep==null)ep=zps.getSource().getName();
573                        if(!StringUtil.isEmpty(p)) ep=p+ep;
574                        
575                        add(zos,zps.getSource().getInputStream(),ep,zps.getSource().lastModified(),true);
576                }       
577                else {
578                        
579                        
580                        
581                        // filter
582                        ResourceFilter f = zps.getFilter();
583                        if(f==null)f=this.filter;
584                        if(zps.isRecurse()) {
585                                if(f!=null)f=new OrResourceFilter(new ResourceFilter[]{DirectoryResourceFilter.FILTER,f});
586                        }
587                        else {
588                                if(f==null)f=FileResourceFilter.FILTER;
589                        }
590                        
591                        addDir(zos,zps.getSource(),p,f);
592                }
593        }
594
595
596
597        private void addDir(ZipOutputStream zos, Resource dir, String parent, ResourceFilter filter) throws IOException {
598                Resource[] children = filter==null?dir.listResources():dir.listResources(filter);
599                
600                for(int i=0;i<children.length;i++) {
601                        
602                        
603                        if(children[i].isDirectory()) addDir(zos, children[i], parent+children[i].getName()+"/",filter);
604                        else {
605                                add(zos, children[i].getInputStream(), parent+children[i].getName(), children[i].lastModified(), true);
606                        }
607                }
608        }
609
610
611
612        private void add(ZipOutputStream zos, InputStream is, String path, long lastMod, boolean closeInput) throws IOException {
613                ZipEntry ze=new ZipEntry(path);
614                ze.setTime(lastMod);
615                add(zos, ze, is, closeInput);
616        }
617        
618        private void add(ZipOutputStream zos, ZipEntry entry,InputStream is, boolean closeInput) throws IOException {
619                if(alreadyUsed==null)alreadyUsed=new HashSet<String>();
620                else if(alreadyUsed.contains(entry.getName())) return;
621                zos.putNextEntry(entry);
622        try {
623            IOUtil.copy(is,zos,closeInput,false);
624        } 
625        finally {
626            zos.closeEntry();
627        }
628                alreadyUsed.add(entry.getName());
629        }
630
631
632
633        @Override
634        public void doInitBody()        {
635                
636        }
637
638        @Override
639        public int doAfterBody()        {
640                return SKIP_BODY;
641        }
642    
643    @Override
644        public int doEndTag() throws PageException      {//print.out("doEndTag"+doCaching+"-"+body);
645                try {
646
647                        if (this.filter == null && !StringUtil.isEmpty(this.pattern))
648                                this.filter = new WildcardPatternFilter(pattern, patternDelimiters);
649
650                        if(action.equals("delete")) actionDelete();
651                        else if(action.equals("list")) actionList();
652                        else if(action.equals("read")) actionRead(false);
653                        else if(action.equals("readbinary")) actionRead(true);
654                        else if(action.equals("unzip")) actionUnzip();
655                        else if(action.equals("zip")) actionZip();
656                else 
657                                throw new ApplicationException("invalid value ["+action+"] for attribute action","values for attribute action are:info,move,rename,copy,delete,read,readbinary,write,append,upload");
658                }
659                catch(IOException ioe) {
660                        throw Caster.toPageException(ioe);
661                }
662                
663                return EVAL_PAGE;
664        }
665
666        /**
667     * sets if tag has a body or not
668     * @param hasBody
669     */
670    public void hasBody(boolean hasBody) {
671       ///this.hasBody=hasBody;
672    }
673
674    private ZipFile getZip(Resource file) throws ZipException, IOException {
675                return new ZipFile(FileWrapper.toFile(file));
676        }
677    
678    /**
679         * throw a error if the value is empty (null)
680     * @param attributeName
681     * @param attributValue
682         * @throws ApplicationException
683     */
684    private void required(String attributeName, String attributValue) throws ApplicationException {
685        if(StringUtil.isEmpty(attributValue))
686            throw new ApplicationException(
687                    "invalid attribute constellation for the tag zip", 
688                    "attribute ["+attributeName+"] is required, if action is ["+action+"]");
689    }
690
691    /**
692         * throw a error if the value is empty (null)
693     * @param attributeName
694     * @param attributValue
695         * @throws ApplicationException
696     */
697    private void required(String attributeName, Resource attributValue, boolean exists) throws ApplicationException {
698        if(attributValue==null)
699            throw new ApplicationException(
700                    "invalid attribute constellation for the tag zip", 
701                    "attribute ["+attributeName+"] is required, if action is ["+action+"]");
702        
703        if(exists && !attributValue.exists())
704                throw new ApplicationException(attributeName+" resource ["+attributValue+"] doesn't exist");
705        else if(exists && !attributValue.canRead())
706                        throw new ApplicationException("no access to "+attributeName+" resource ["+attributValue+"]");
707                
708        
709    }
710
711
712
713
714        public void setParam(ZipParamAbstr param) {
715                if(params==null) {
716                        params=new ArrayList<ZipParamAbstr>();
717                        alreadyUsed=new HashSet<String>();
718                }
719                params.add(param);
720        }
721
722
723
724        /**
725         * @return the source
726         */
727        public Resource getSource() {
728                return source;
729        }
730    
731    
732}