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}