001/* 002 * Copyright 2016 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.webcontentio; 017 018import java.io.File; 019import java.io.FileNotFoundException; 020import java.io.FileOutputStream; 021import java.io.FilenameFilter; 022import java.io.IOException; 023import java.io.InputStream; 024import java.time.ZonedDateTime; 025import java.util.HashMap; 026import java.util.Map; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.context.ContextException; 030import org.apache.avalon.framework.context.Contextualizable; 031import org.apache.avalon.framework.logger.AbstractLogEnabled; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.cocoon.Constants; 036import org.apache.cocoon.ProcessingException; 037import org.apache.cocoon.environment.Context; 038import org.apache.commons.io.FileUtils; 039import org.apache.excalibur.source.SourceUtil; 040 041import org.ametys.cms.contenttype.ContentType; 042import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 043import org.ametys.cms.repository.Content; 044import org.ametys.cms.repository.WorkflowAwareContent; 045import org.ametys.core.right.RightManager; 046import org.ametys.core.right.RightManager.RightResult; 047import org.ametys.core.right.RightsException; 048import org.ametys.core.ui.Callable; 049import org.ametys.core.user.CurrentUserProvider; 050import org.ametys.core.user.UserIdentity; 051import org.ametys.plugins.repository.AmetysObjectResolver; 052import org.ametys.plugins.repository.AmetysRepositoryException; 053import org.ametys.plugins.repository.ModifiableAmetysObject; 054import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 055import org.ametys.plugins.repository.RepositoryConstants; 056import org.ametys.plugins.repository.jcr.NameHelper; 057import org.ametys.plugins.workflow.support.WorkflowProvider; 058import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 059import org.ametys.web.repository.content.ModifiableWebContent; 060import org.ametys.web.repository.page.ModifiablePage; 061import org.ametys.web.repository.page.ModifiableZone; 062import org.ametys.web.repository.page.ModifiableZoneItem; 063import org.ametys.web.repository.page.Page; 064import org.ametys.web.repository.page.Page.PageType; 065import org.ametys.web.repository.page.SitemapElement; 066import org.ametys.web.repository.page.ZoneItem.ZoneType; 067import org.ametys.web.repository.site.Site; 068import org.ametys.web.repository.sitemap.Sitemap; 069 070import com.opensymphony.workflow.WorkflowException; 071 072/** 073 * Manager for importing contents 074 */ 075public class ContentIOManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 076{ 077 /** Avalon Role */ 078 public static final String ROLE = ContentIOManager.class.getName(); 079 080 private Context _context; 081 082 private ContentImporterExtensionPoint _contentImporterExtensionPoint; 083 private ContentTypeExtensionPoint _contentTypeExtensionPoint; 084 private RightManager _rightsManager; 085 private WorkflowProvider _workflowProvider; 086 087 private CurrentUserProvider _currentUserProvider; 088 089 private AmetysObjectResolver _resolver; 090 091 @Override 092 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 093 { 094 _context = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 095 } 096 097 @Override 098 public void service(ServiceManager manager) throws ServiceException 099 { 100 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 101 _contentImporterExtensionPoint = (ContentImporterExtensionPoint) manager.lookup(ContentImporterExtensionPoint.ROLE); 102 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 103 _rightsManager = (RightManager) manager.lookup(RightManager.ROLE); 104 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 105 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 106 } 107 108 /** 109 * Import recursively documents contained in a directory on the server 110 * Created contents are of type 'article' and pages are also created upon successful content creation. 111 * @param directoryPath The absolute path of directory on the server 112 * @param rootPageId The id of parent page 113 * @return An array containing the number of successful and unsuccessful content created. 114 * @throws AmetysRepositoryException If an error occurred 115 * @throws ProcessingException if an error occurs during processing. 116 */ 117 @Callable 118 public Map<String, Object> importContent (String directoryPath, String rootPageId) throws ProcessingException, AmetysRepositoryException 119 { 120 UserIdentity user = _currentUserProvider.getUser(); 121 122 SitemapElement rootPage = _resolver.resolveById(rootPageId); 123 124 if (!(rootPage instanceof ModifiableAmetysObject)) 125 { 126 throw new ProcessingException("The selected page '" + rootPage.getPath() + "' is not modifiable."); 127 } 128 129 int[] result = importContent(directoryPath, rootPage, user); 130 131 Map<String, Object> mapResult = new HashMap<>(); 132 mapResult.put("success", result[0]); 133 mapResult.put("error", result[1]); 134 135 return mapResult; 136 } 137 138 /** 139 * Import recursively documents contained in a directory on the server 140 * Created contents are of type 'article' and pages are also created upon successful content creation. 141 * @param path The absolute path of directory on the server 142 * @param rootPage The parent page of the new contents 143 * @param user The user responsible for contents import 144 * @return An array containing the number of successful and unsuccessful content created. 145 * @throws ProcessingException if an error occurs during processing. 146 */ 147 public int[] importContent(String path, SitemapElement rootPage, UserIdentity user) throws ProcessingException 148 { 149 int[] result = new int[2]; 150 151 File file = new File(path); 152 if (!file.exists()) 153 { 154 throw new ProcessingException("The file '" + path + "' does not exist."); 155 } 156 157 if (file.isFile()) 158 { 159 Page page = _importFromFile(file, rootPage, user, false); 160 if (page != null) 161 { 162 result[0]++; 163 } 164 else 165 { 166 result[1]++; 167 } 168 } 169 else 170 { 171 result = _importFromDirectory(file, rootPage, user, false); 172 } 173 174 if (rootPage.getSite().needsSave()) 175 { 176 rootPage.getSite().saveChanges(); 177 } 178 179 return result; 180 } 181 182 /** 183 * Import an InputStream as a content into a new page 184 * @param is The input stream to import 185 * @param mimeType The Mime type of the input stream 186 * @param contentName The name associated with the input stream 187 * @param user The creator 188 * @param rootPage The parent page of the newly created page 189 * @param isContextExtern True if the current context is not in a site 190 * @return The created page or null if failed to create the page 191 * @throws IOException if an error occurs with the temporary file. 192 */ 193 public ModifiablePage importContent(InputStream is, String mimeType, String contentName, UserIdentity user, SitemapElement rootPage, boolean isContextExtern) throws IOException 194 { 195 File tmpFile = null; 196 try 197 { 198 tmpFile = new File(System.getProperty("java.io.tmpdir") + File.separator + Long.toString(Math.round(Math.random() * 1000000.0)), contentName); 199 200 tmpFile.getParentFile().mkdirs(); 201 tmpFile.createNewFile(); 202 203 try (FileOutputStream tmpFileOS = new FileOutputStream(tmpFile)) 204 { 205 SourceUtil.copy(is, tmpFileOS); 206 } 207 208 ModifiablePage page = _importFromFile(tmpFile, rootPage, user, isContextExtern); 209 210 if (rootPage.getSite().needsSave()) 211 { 212 rootPage.getSite().saveChanges(); 213 } 214 215 return page; 216 } 217 catch (FileNotFoundException e) 218 { 219 getLogger().warn("Unable to create a temporary file " + tmpFile.getAbsolutePath() + " to import."); 220 } 221 finally 222 { 223 FileUtils.deleteQuietly(tmpFile); 224 } 225 226 return null; 227 } 228 229 private int[] _importFromDirectory(File dir, SitemapElement rootPage, UserIdentity user, boolean isContextExtern) 230 { 231 int[] result = new int[2]; 232 233 File[] children = dir.listFiles(new FilenameFilter() 234 { 235 // Get all children except child the same directory name (if exists) 236 @Override 237 public boolean accept(File directory, String name) 238 { 239 return !name.startsWith(directory.getName() + '.'); 240 } 241 }); 242 243 for (File child : children) 244 { 245 if (child.isFile()) 246 { 247 Page page = _importFromFile(child, rootPage, user, isContextExtern); 248 if (page != null) 249 { 250 result[0]++; 251 } 252 else 253 { 254 result[1]++; 255 } 256 } 257 else if (child.isDirectory()) 258 { 259 File[] subChildren = child.listFiles(new FilenameFilter() 260 { 261 @Override 262 public boolean accept(File directory, String name) 263 { 264 return name.startsWith(directory.getName() + '.'); 265 } 266 }); 267 268 boolean contentImported = false; 269 ModifiablePage childPage = null; 270 271 if (subChildren.length > 0) 272 { 273 childPage = _importFromFile(subChildren[0], rootPage, user, isContextExtern); 274 if (childPage != null) 275 { 276 result[0]++; 277 } 278 else 279 { 280 result[1]++; 281 } 282 283 contentImported = childPage != null; 284 } 285 286 if (!contentImported) 287 { 288 // no child content with the same name 289 290 String pageTitle = child.getName(); 291 292 String originalPageName = NameHelper.filterName(pageTitle); 293 294 String pageName = originalPageName; 295 int index = 2; 296 while (rootPage.hasChild(pageName)) 297 { 298 pageName = originalPageName + "-" + (index++); 299 } 300 301 childPage = ((ModifiableTraversableAmetysObject) rootPage).createChild(pageName, "ametys:defaultPage"); 302 childPage.setType(PageType.NODE); 303 childPage.setSiteName(rootPage.getSiteName()); 304 childPage.setSitemapName(rootPage.getSitemapName()); 305 childPage.setTitle(child.getName()); 306 } 307 308 int[] subResult = _importFromDirectory(child, childPage, user, isContextExtern); 309 result[0] += subResult[0]; 310 result[1] += subResult[1]; 311 } 312 } 313 314 return result; 315 } 316 317 /** 318 * Import a file into a new content and a new page. 319 * @param file The file to import 320 * @param rootPage The parent page of the newly created page 321 * @param user The user 322 * @param isContextExtern True if the current context is not in a site 323 * @return True if the content and page was successfully created. 324 */ 325 private ModifiablePage _importFromFile(File file, SitemapElement rootPage, UserIdentity user, boolean isContextExtern) 326 { 327 String mimeType = _context.getMimeType(file.getName()); 328 ContentImporter importer = _contentImporterExtensionPoint.getContentImporterForMimeType(mimeType); 329 330 if (importer == null) 331 { 332 getLogger().warn("Unable to import file " + file.getAbsolutePath() + ": no importer found."); 333 return null; 334 } 335 336 try 337 { 338 Map<String, String> params = new HashMap<>(); 339 // import content 340 341 Content content = _convertFileToContent(importer, file, rootPage.getSite(), rootPage.getSitemap(), user, params); 342 if (content != null) 343 { 344 if (!hasRight(content.getTypes()[0], rootPage, user, isContextExtern)) 345 { 346 throw new RightsException("insufficient rights to create a new page and content for user '" + UserIdentity.userIdentityToString(user) + "'."); 347 } 348 349 // content has been imported, create the page 350 ModifiablePage page = _createPageAndSetContent(rootPage, content, rootPage.getSite(), rootPage.getSitemap(), params, file); 351 importer.postTreatment(page, content, file); 352 return page; 353 } 354 } 355 catch (AmetysRepositoryException e) 356 { 357 getLogger().error("Unable to import content from file " + file.getAbsolutePath(), e); 358 } 359 catch (IOException e) 360 { 361 getLogger().error("Unable to import content from file " + file.getAbsolutePath(), e); 362 } 363 catch (WorkflowException e) 364 { 365 getLogger().error("Unable to import content from file " + file.getAbsolutePath(), e); 366 } 367 368 return null; 369 } 370 371 private Content _convertFileToContent(ContentImporter importer, File file, Site site, Sitemap sitemap, UserIdentity user, Map<String, String> params) throws WorkflowException, AmetysRepositoryException 372 { 373 // Creates a workflow entry 374 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(); 375 long workflowId = workflow.initialize("content", 0, new HashMap<>()); 376 377 ModifiableTraversableAmetysObject rootContents = site.getRootContents(); 378 379 String fileName = file.getName(); 380 381 // find an available name and creates the content 382 int i = fileName.lastIndexOf('.'); 383 String desiredContentName = i != -1 ? fileName.substring(0, i) : fileName; 384 385 String originalContentName = NameHelper.filterName(desiredContentName); 386 387 String contentName = originalContentName; 388 int index = 2; 389 while (rootContents.hasChild(contentName)) 390 { 391 contentName = originalContentName + "-" + (index++); 392 } 393 394 ModifiableWebContent content = rootContents.createChild(contentName, RepositoryConstants.NAMESPACE_PREFIX + ":defaultWebContent"); 395 396 ((WorkflowAwareContent) content).setWorkflowId(workflowId); 397 398 // Common metadata 399 content.setCreator(user); 400 content.setCreationDate(ZonedDateTime.now()); 401 content.setLastContributor(user); 402 content.setLastModified(ZonedDateTime.now()); 403 content.setLanguage(sitemap.getName()); 404 content.setSiteName(site.getName()); 405 406 try 407 { 408 importer.importContent(file, content, params); 409 } 410 catch (IOException e) 411 { 412 getLogger().error("Unable to import content from file " + file.getAbsolutePath(), e); 413 content.remove(); 414 return null; 415 } 416 417 return content; 418 } 419 420 private ModifiablePage _createPageAndSetContent(SitemapElement rootPage, Content content, Site site, Sitemap sitemap, Map<String, String> params, File file) 421 { 422 String template = params.get("page.template"); 423 424 String fileName = file.getName(); 425 int i = fileName.lastIndexOf('.'); 426 427 String pageTitle = i != -1 ? fileName.substring(0, i) : fileName; 428 429 String originalPageName = NameHelper.filterName(pageTitle); 430 431 String pageName = originalPageName; 432 int index = 2; 433 while (rootPage.hasChild(pageName)) 434 { 435 pageName = originalPageName + "-" + (index++); 436 } 437 438 ModifiablePage page = ((ModifiableTraversableAmetysObject) rootPage).createChild(pageName, "ametys:defaultPage"); 439 page.setType(PageType.CONTAINER); 440 page.setTemplate(template == null ? "page" : template); 441 page.setSiteName(site.getName()); 442 page.setSitemapName(sitemap.getName()); 443 page.setTitle(pageTitle); 444 445 String longTitle = params.get("page.longTitle"); 446 if (longTitle != null) 447 { 448 page.setLongTitle(longTitle); 449 } 450 451 ModifiableZone zone = page.createZone("default"); 452 ModifiableZoneItem zoneItem = zone.addZoneItem(); 453 zoneItem.setType(ZoneType.CONTENT); 454 zoneItem.setContent(content); 455 456 return page; 457 } 458 459 /** 460 * Test if the current user has the right needed by the content type to create a content. 461 * @param contentTypeId the content type ID. 462 * @param page the current page. 463 * @param user The user 464 * @param isContextExtern True if the current context is not in a site 465 * @return true if the user has the right needed, false otherwise. 466 */ 467 protected boolean hasRight(String contentTypeId, SitemapElement page, UserIdentity user, boolean isContextExtern) 468 { 469 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 470 if (contentType != null) 471 { 472 String right = contentType.getRight(); 473 return right == null || _rightsManager.hasRight(user, right, page) == RightResult.RIGHT_ALLOW; 474 } 475 476 return false; 477 } 478}