001/*
002 *  Copyright 2013 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.plugins.skincommons;
017
018import java.io.IOException;
019import java.nio.file.Files;
020import java.nio.file.Path;
021import java.util.Collections;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.Map;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.lang.StringUtils;
032
033import org.ametys.core.ui.Callable;
034import org.ametys.core.user.CurrentUserProvider;
035import org.ametys.core.user.User;
036import org.ametys.core.user.UserIdentity;
037import org.ametys.core.user.UserManager;
038import org.ametys.core.util.DateUtils;
039import org.ametys.core.util.path.PathUtils;
040import org.ametys.runtime.authentication.AccessDeniedException;
041import org.ametys.runtime.config.Config;
042import org.ametys.runtime.plugin.component.AbstractLogEnabled;
043import org.ametys.web.repository.page.Page;
044import org.ametys.web.repository.site.Site;
045import org.ametys.web.repository.site.SiteManager;
046import org.ametys.web.skin.Skin;
047import org.ametys.web.skin.SkinsManager;
048
049/**
050 * DAO for skin edition.
051 */
052public abstract class AbstractCommonSkinDAO extends AbstractLogEnabled implements Serviceable, Component
053{
054    private static final String __TEMP_MODE = "temp";
055    private static final String __WORK_MODE = "work";
056    
057    /** The site manager */
058    protected SiteManager _siteManager;
059    /** The skin helper */
060    protected SkinEditionHelper _skinHelper;
061    /** The lock manager */
062    protected SkinLockManager _lockManager;
063    /** The current user provider */
064    protected CurrentUserProvider _userProvider;
065    /** The user manager */
066    protected UserManager _userManager;
067    /** The skin manager */
068    protected SkinsManager _skinsManager;
069    
070    @Override
071    public void service(ServiceManager smanager) throws ServiceException
072    {
073        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
074        _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE);
075        _skinHelper = (SkinEditionHelper) smanager.lookup(SkinEditionHelper.ROLE);
076        _lockManager = (SkinLockManager) smanager.lookup(SkinLockManager.ROLE);
077        _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
078        _userManager = (UserManager) smanager.lookup(UserManager.ROLE);
079    }
080    
081    /**
082     * Get the URI to preview a site
083     * @param siteName the site name
084     * @param lang the site langage
085     * @return The uri
086     */
087    @Callable(rights = Callable.NO_CHECK_REQUIRED)
088    public String getPreviewURI(String siteName, String lang)
089    {
090        Site site = _siteManager.getSite(siteName);
091        
092        String siteLangage = !StringUtils.isEmpty(lang) ? lang : site.getSitemaps().iterator().next().getName();
093        
094        if (site.getSitemap(siteLangage).hasChild("index"))
095        {
096            return siteLangage + "/index.html";
097        }
098        else
099        {
100            Iterator<? extends Page> it = site.getSitemap(siteLangage).getChildrenPages().iterator();
101            
102            if (it.hasNext())
103            {
104                String path = it.next().getPathInSitemap();
105                return siteLangage + "/" + path + ".html";
106            }
107        }
108        
109        return null;
110    }
111    
112    /**
113     * Check if there is unsaved or uncomitted changes
114     * @param skinName The skin name
115     * @return The result
116     * @throws IOException if an error occurs while manipulating files
117     */
118    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
119    public Map<String, Object> checkUnsaveModifications (String skinName) throws IOException
120    {
121        Map<String, Object> result = new HashMap<>();
122                
123        checkUserRight(skinName);
124        
125        Path tempDir = _skinHelper.getTempDirectory(skinName);
126        Path workDir = _skinHelper.getWorkDirectory(skinName);
127        Path skinDir = _skinHelper.getSkinDirectory(skinName);
128        
129        if (!_lockManager.canWrite(tempDir))
130        {
131            // Unecessary to check unsave modifications, the user has no right anymore
132            return result;
133        }
134        
135        long lastModifiedLock = _lockManager.lastModified(tempDir).getTime();
136        if (lastModifiedLock <= Files.getLastModifiedTime(workDir).toMillis())
137        {
138            if (Files.getLastModifiedTime(workDir).toMillis() > Files.getLastModifiedTime(skinDir).toMillis())
139            {
140                result.put("hasUncommitChanges", true);
141            }
142            else
143            {
144                PathUtils.deleteDirectory(workDir);
145            }
146        }
147        else if (lastModifiedLock >= Files.getLastModifiedTime(skinDir).toMillis())
148        {
149            // The modifications were not saved
150            result.put("hasUnsaveChanges", true);
151        }
152        
153        return result;
154    }
155    
156    /**
157     * Save the current skin into the skin work folder
158     * @param skinName The name of the skin
159     * @param quit True if the temp directory can be removed
160     * @return The name of the skin
161     * @throws IOException if an error occurs while manipulating files
162     */
163    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
164    public Map<String, Object> saveChanges(String skinName, boolean quit) throws IOException
165    {
166        checkUserRight(skinName);
167        
168        Map<String, Object> lockInfos = _checkLock(skinName);
169        if (!lockInfos.isEmpty())
170        {
171            return lockInfos;
172        }
173        
174        Path tempDir = _skinHelper.getTempDirectory(skinName);
175        Path workDir = _skinHelper.getWorkDirectory(skinName);
176        
177        if (Files.exists(workDir))
178        {
179            // Delete work directory
180            _skinHelper.deleteQuicklyDirectory(workDir);
181        }
182        
183        if (quit)
184        {
185            // Move to work directory
186            PathUtils.moveDirectory(tempDir, workDir);
187            
188            // Remove lock
189            PathUtils.deleteQuietly(workDir.resolve(".lock"));
190        }
191        else
192        {
193            // Do a copy in work directory
194            PathUtils.copyDirectory(tempDir, workDir, file -> !file.getFileName().toString().equals(".lock"), false);
195        }
196        
197        Map<String, Object> result = new HashMap<>();
198        result.put("skinName", skinName);
199        return result;
200    }
201    
202    /**
203     * Check user rights and throws {@link AccessDeniedException} if it is not authorized
204     * @param skinName the skin name
205     */
206    protected abstract void checkUserRight(String skinName);
207    
208    /**
209     * Commit the changes made to the skin
210     * @param skinName the name of the skin
211     * @param quit True to remove the temporary directory
212     * @return A map with information
213     * @throws Exception if an error occurs when committing the skin changes
214     */
215    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
216    public Map<String, Object> commitChanges(String skinName, boolean quit) throws Exception
217    {
218        checkUserRight(skinName);
219        
220        Skin skin = _skinsManager.getSkin(skinName);
221        if (!skin.isModifiable())
222        {
223            throw new IllegalStateException("The skin '" + skinName + "' is not modifiable and thus cannot be modified.");
224        }
225        
226        Map<String, Object> lockInfos = _checkLock(skinName);
227        if (!lockInfos.isEmpty())
228        {
229            return lockInfos;
230        }
231        
232        Path skinDir = _skinHelper.getSkinDirectory(skinName);
233        
234        // Do a backup (move skin directory to backup directory)
235        Path backupDir = _skinHelper.createBackupFile(skinName);
236        
237        // Move temporary version to current skin
238        Path tempDir = _skinHelper.getTempDirectory(skinName);
239        PathUtils.moveDirectory(tempDir, skinDir);
240        
241        Path workDir = _skinHelper.getWorkDirectory(skinName);
242        if (quit)
243        {
244            // Delete work version
245            _skinHelper.deleteQuicklyDirectory(workDir);
246        }
247        else
248        {
249            // Do a copy in work directory
250            PathUtils.copyDirectory(skinDir, workDir, file -> !file.getFileName().toString().equals(".lock"), true);
251            
252            // Do a copy in temp directory
253            PathUtils.copyDirectory(skinDir, tempDir, true);
254        }
255        
256        // Delete lock file
257        PathUtils.deleteQuietly(skinDir.resolve(".lock"));
258        
259        // Invalidate caches
260        _skinHelper.invalidateCaches(skinName);
261        _skinHelper.invalidateSkinCatalogues(skinName);
262        
263        // Remove old backup (keep only the 5 last backup)
264        _skinHelper.deleteOldBackup(skinName, 5);
265        
266        Map<String, Object> result = new HashMap<>();
267        
268        result.put("backupFilename", backupDir.getFileName().toString());
269        
270        String mailSysAdmin = Config.getInstance().getValue("smtp.mail.sysadminto");
271        if (!mailSysAdmin.isEmpty())
272        {
273            result.put("adminEmail", mailSysAdmin);
274        }
275        
276        return result;
277    }
278    
279    /**
280     * Cancel the current modification to the skin
281     * @param skinName The name of the skin
282     * @param workVersion True it is the work version, false for the temp version
283     * @param toolId the id of the tool
284     * @return True if some changes were canceled
285     * @throws IOException if an error occurs while manipulating files
286     */
287    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
288    public Map<String, Object> cancelChanges(String skinName, boolean workVersion, String toolId) throws IOException
289    {
290        checkUserRight(skinName);
291        
292        Map<String, Object> lockInfos = _checkLock(skinName);
293        if (!lockInfos.isEmpty())
294        {
295            return lockInfos;
296        }
297        
298        String modelBeforeCancel = _skinHelper.getTempModel(skinName);
299        
300        Path tempDir = _skinHelper.getTempDirectory(skinName);
301        if (Files.exists(tempDir))
302        {
303            // Delete current temp version
304            _skinHelper.deleteQuicklyDirectory(tempDir);
305        }
306        
307        Path workDir = _skinHelper.getWorkDirectory(skinName);
308        if (workVersion && Files.exists(workDir))
309        {
310            // Back from the last saved work version
311            PathUtils.copyDirectory(workDir, tempDir);
312        }
313        else
314        {
315            if (Files.exists(workDir))
316            {
317                // Delete work version
318                _skinHelper.deleteQuicklyDirectory(workDir);
319            }
320            
321            // Back from current skin
322            Path skinDir = _skinHelper.getSkinDirectory(skinName);
323            PathUtils.copyDirectory(skinDir, tempDir);
324        }
325        
326        String modelAfterCancel = _skinHelper.getTempModel(skinName);
327        
328        _lockManager.updateLockFile(tempDir, !toolId.isEmpty() ? toolId : "uitool-skineditor");
329        
330        // Invalidate i18n.
331        _skinHelper.invalidateTempSkinCatalogues(skinName);
332        
333        Map<String, Object> result = new HashMap<>();
334        result.put("hasChanges", modelAfterCancel == null || !modelAfterCancel.equals(modelBeforeCancel));
335        return result;
336    }
337    
338    private Map<String, Object> _checkLock (String skinName) throws IOException
339    {
340        Path tempDir = _skinHelper.getTempDirectory(skinName);
341        
342        if (!_lockManager.canWrite(tempDir))
343        {
344            Map<String, Object> result = new HashMap<>();
345            
346            UserIdentity lockOwner = _lockManager.getLockOwner(tempDir);
347            User user = _userManager.getUser(lockOwner.getPopulationId(), lockOwner.getLogin());
348
349            result.put("isLocked", true);
350            result.put("lockOwner", user != null ? user.getFullName() + " (" + lockOwner + ")" : lockOwner);
351            result.put("success", false);
352            
353            return result;
354        }
355        
356        // Not lock
357        return Collections.EMPTY_MAP;
358    }
359    
360    /**
361     * Get the model for the skin
362     * @param siteName the site name. Can be null if skinName is not null.
363     * @param skinName the skin name. Can be null if siteName is not null.
364     * @param mode the edition mode. Can be null.
365     * @return the model's name or null if there is no model for this skin
366     */
367    @Callable(rights = Callable.NO_CHECK_REQUIRED)
368    public String getSkinModel(String siteName, String skinName, String mode)
369    {
370        String skinId = _getSkinName (siteName, skinName);
371        
372        if (__TEMP_MODE.equals(mode))
373        {
374            return _skinHelper.getTempModel(skinId);
375        }
376        else if (__WORK_MODE.equals(mode))
377        {
378            return _skinHelper.getWorkModel(skinId);
379        }
380        else
381        {
382            return _skinHelper.getSkinModel(skinId);
383        }
384    }
385    
386    private String _getSkinName (String siteName, String skinName)
387    {
388        if (StringUtils.isEmpty(skinName) && StringUtils.isNotEmpty(siteName))
389        {
390            Site site = _siteManager.getSite(siteName);
391            return site.getSkinId();
392        }
393        
394        return skinName;
395    }
396
397    /**
398     * Get lock informations on a skin
399     * @param siteName the site name. Can be null if skinName is not null.
400     * @param skinName the skin name. Can be null if siteName is not null.
401     * @return Informations about the lock
402     * @throws IOException if an error occurs
403     */
404    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
405    public Map<String, Object> getLock(String siteName, String skinName) throws IOException
406    {
407        String skinId = _getSkinName(siteName, skinName);
408        
409        checkUserRight(skinId);
410        
411        Map<String, Object> result = new HashMap<>();
412        
413        
414        Path tempDir = _skinHelper.getTempDirectory(skinId);
415        if (Files.exists(tempDir) && _lockManager.isLocked(tempDir))
416        {
417            UserIdentity lockOwner = _lockManager.getLockOwner(tempDir);
418            User user = _userManager.getUser(lockOwner.getPopulationId(), lockOwner.getLogin());
419
420            result.put("isLocked", !_userProvider.getUser().equals(lockOwner));
421            result.put("lockOwner", user != null ? user.getFullName() + " (" + lockOwner + ")" : lockOwner);
422            result.put("lastModified", DateUtils.dateToString(_lockManager.lastModified(tempDir)));
423            result.put("toolId", _lockManager.getLockTool(tempDir));
424        }
425        else
426        {
427            result.put("isLocked", false);
428        }
429        
430        Path workDir = _skinHelper.getWorkDirectory(skinId);
431        if (Files.exists(workDir))
432        {
433            result.put("lastSave", DateUtils.dateToString(new Date(Files.getLastModifiedTime(workDir).toMillis())));
434        }
435        
436        return result;
437    }
438    
439    /**
440     * Revert changes and back to last work version or to current skin
441     * @param skinName The skin name
442     * @param workVersion true to get back the work version
443     * @return The skin name in case of success or lock infos if the skin is locked.
444     * @throws IOException if an error occurs
445     */
446    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
447    public Map<String, Object> clearModifications (String skinName, boolean workVersion) throws IOException
448    {
449        checkUserRight(skinName);
450        
451        Map<String, Object> lockInfos = _checkLock(skinName);
452        if (!lockInfos.isEmpty())
453        {
454            return lockInfos;
455        }
456        
457        Path tempDir = _skinHelper.getTempDirectory(skinName);
458        if (Files.exists(tempDir))
459        {
460            // Delete current temp version
461            _skinHelper.deleteQuicklyDirectory(tempDir);
462        }
463        
464        if (workVersion)
465        {
466            Path workDir = _skinHelper.getWorkDirectory(skinName);
467            if (Files.exists(workDir))
468            {
469                // Delete current work version
470                _skinHelper.deleteQuicklyDirectory(workDir);
471            }
472        }
473        
474        Map<String, Object> result = new HashMap<>();
475        result.put("skinName", skinName);
476        return result;
477    }
478}