001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.relations; 029 030import org.opencms.db.CmsDbContext; 031import org.opencms.db.CmsDriverManager; 032import org.opencms.db.CmsPublishList; 033import org.opencms.file.CmsProject; 034import org.opencms.file.CmsResource; 035import org.opencms.file.CmsResourceFilter; 036import org.opencms.file.CmsVfsResourceNotFoundException; 037import org.opencms.file.types.I_CmsResourceType; 038import org.opencms.main.CmsException; 039import org.opencms.main.CmsIllegalStateException; 040import org.opencms.main.CmsLog; 041import org.opencms.main.OpenCms; 042import org.opencms.report.I_CmsReport; 043import org.opencms.util.CmsStringUtil; 044import org.opencms.workplace.threads.A_CmsProgressThread; 045 046import java.util.ArrayList; 047import java.util.HashMap; 048import java.util.Iterator; 049import java.util.List; 050import java.util.Map; 051 052import org.apache.commons.logging.Log; 053 054/** 055 * Validates relations of resources in the OpenCms VFS.<p> 056 * 057 * Relations are, for instance, href attribs in anchor tags and src attribs in 058 * image tags, as well as OpenCmsVfsFile values in Xml Content.<p> 059 * 060 * External links to targets outside the OpenCms VFS don't get validated.<p> 061 * 062 * Objects using this class are responsible to handle detected broken links.<p> 063 * 064 * @since 6.3.0 065 */ 066public class CmsRelationSystemValidator { 067 068 /** The log object for this class. */ 069 private static final Log LOG = CmsLog.getLog(CmsRelationSystemValidator.class); 070 071 /** The driver manager. */ 072 protected CmsDriverManager m_driverManager; 073 074 /** 075 * Default constructor.<p> 076 * 077 * @param driverManager The Cms driver manager 078 */ 079 public CmsRelationSystemValidator(CmsDriverManager driverManager) { 080 081 m_driverManager = driverManager; 082 } 083 084 /** 085 * Validates the relations against the online project.<p> 086 * 087 * The result is printed to the given report.<p> 088 * 089 * Validating references means to answer the question, whether 090 * we would have broken links in the online project if the given 091 * publish list would get published.<p> 092 * 093 * @param dbc the database context 094 * @param publishList the publish list to validate 095 * @param report a report to print messages 096 * 097 * @return a map with lists of invalid links 098 * (<code>{@link org.opencms.relations.CmsRelation}}</code> objects) 099 * keyed by root paths 100 * 101 * @throws Exception if something goes wrong 102 */ 103 public Map<String, List<CmsRelation>> validateResources( 104 CmsDbContext dbc, 105 CmsPublishList publishList, 106 I_CmsReport report) 107 throws Exception { 108 109 // check if progress should be set in the thread 110 A_CmsProgressThread thread = null; 111 if (Thread.currentThread() instanceof A_CmsProgressThread) { 112 thread = (A_CmsProgressThread)Thread.currentThread(); 113 } 114 115 Map<String, List<CmsRelation>> invalidResources = new HashMap<String, List<CmsRelation>>(); 116 boolean interProject = (publishList != null); 117 if (report != null) { 118 report.println( 119 Messages.get().container(Messages.RPT_HTMLLINK_VALIDATOR_BEGIN_0), 120 I_CmsReport.FORMAT_HEADLINE); 121 } 122 List<CmsResource> resources = new ArrayList<CmsResource>(); 123 if (publishList == null) { 124 CmsResourceFilter filter = CmsResourceFilter.IGNORE_EXPIRATION; 125 List<I_CmsResourceType> resTypes = OpenCms.getResourceManager().getResourceTypes(); 126 Iterator<I_CmsResourceType> itTypes = resTypes.iterator(); 127 int count = 0; 128 while (itTypes.hasNext()) { 129 130 // set progress in thread (first 10 percent) 131 count++; 132 if (thread != null) { 133 134 if (thread.isInterrupted()) { 135 throw new CmsIllegalStateException( 136 org.opencms.workplace.commons.Messages.get().container( 137 org.opencms.workplace.commons.Messages.ERR_PROGRESS_INTERRUPTED_0)); 138 } 139 thread.setProgress((count * 10) / resTypes.size()); 140 } 141 142 I_CmsResourceType type = itTypes.next(); 143 if (type instanceof I_CmsLinkParseable) { 144 filter = filter.addRequireType(type.getTypeId()); 145 try { 146 resources.addAll( 147 m_driverManager.readResources( 148 dbc, 149 m_driverManager.readResource(dbc, "/", filter), 150 filter, 151 true)); 152 } catch (CmsException e) { 153 LOG.error( 154 Messages.get().getBundle().key(Messages.LOG_RETRIEVAL_RESOURCES_1, type.getTypeName()), 155 e); 156 } 157 } 158 } 159 } else { 160 resources.addAll(publishList.getAllResources()); 161 } 162 163 // populate a lookup map with the project resources that 164 // actually get published keyed by their resource names. 165 // second, resources that don't get validated are ignored. 166 Map<String, CmsResource> offlineFilesLookup = new HashMap<String, CmsResource>(); 167 Iterator<CmsResource> itResources = resources.iterator(); 168 int count = 0; 169 while (itResources.hasNext()) { 170 171 // set progress in thread (next 10 percent) 172 count++; 173 if (thread != null) { 174 175 if (thread.isInterrupted()) { 176 throw new CmsIllegalStateException( 177 org.opencms.workplace.commons.Messages.get().container( 178 org.opencms.workplace.commons.Messages.ERR_PROGRESS_INTERRUPTED_0)); 179 } 180 thread.setProgress(((count * 10) / resources.size()) + 10); 181 } 182 183 CmsResource resource = itResources.next(); 184 offlineFilesLookup.put(resource.getRootPath(), resource); 185 offlineFilesLookup.put(resource.getStructureId().toString(), resource); 186 } 187 CmsProject project = dbc.currentProject(); 188 if (interProject) { 189 try { 190 project = m_driverManager.readProject(dbc, CmsProject.ONLINE_PROJECT_ID); 191 } catch (CmsException e) { 192 // should never happen 193 LOG.error(e.getLocalizedMessage(), e); 194 } 195 } 196 197 boolean foundBrokenLinks = false; 198 for (int index = 0, size = resources.size(); index < size; index++) { 199 200 // set progress in thread (next 20 percent; leave rest for creating the list and the html) 201 if (thread != null) { 202 203 if (thread.isInterrupted()) { 204 throw new CmsIllegalStateException( 205 org.opencms.workplace.commons.Messages.get().container( 206 org.opencms.workplace.commons.Messages.ERR_PROGRESS_INTERRUPTED_0)); 207 } 208 thread.setProgress(((index * 20) / resources.size()) + 20); 209 } 210 211 CmsResource resource = resources.get(index); 212 String resourceName = resource.getRootPath(); 213 214 if (report != null) { 215 report.print( 216 org.opencms.report.Messages.get().container( 217 org.opencms.report.Messages.RPT_SUCCESSION_2, 218 Integer.valueOf(index + 1), 219 Integer.valueOf(size)), 220 I_CmsReport.FORMAT_NOTE); 221 report.print(Messages.get().container(Messages.RPT_HTMLLINK_VALIDATING_0), I_CmsReport.FORMAT_NOTE); 222 report.print( 223 org.opencms.report.Messages.get().container( 224 org.opencms.report.Messages.RPT_ARGUMENT_1, 225 dbc.removeSiteRoot(resourceName))); 226 report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0)); 227 } 228 List<CmsRelation> brokenLinks = validateLinks(dbc, resource, offlineFilesLookup, project, report); 229 if (brokenLinks.size() > 0) { 230 // the resource contains broken links 231 invalidResources.put(resourceName, brokenLinks); 232 foundBrokenLinks = true; 233 } else { 234 // the resource contains *NO* broken links 235 if (report != null) { 236 report.println( 237 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 238 I_CmsReport.FORMAT_OK); 239 } 240 } 241 } 242 243 if (foundBrokenLinks) { 244 // print a summary if we found broken links in the validated resources 245 if (report != null) { 246 report.println( 247 Messages.get().container(Messages.RPT_HTMLLINK_VALIDATOR_ERROR_0), 248 I_CmsReport.FORMAT_ERROR); 249 } 250 } 251 if (report != null) { 252 report.println( 253 Messages.get().container(Messages.RPT_HTMLLINK_VALIDATOR_END_0), 254 I_CmsReport.FORMAT_HEADLINE); 255 } 256 return invalidResources; 257 } 258 259 /** 260 * Checks a link to a resource which has been deleted.<p> 261 * @param relation the relation 262 * 263 * @param link the URI of the resource which has a link to the deleted resource 264 * @param fileLookup a lookup table of files to be published 265 * @return true if the resource which has a link to the deleted resource is also going to be deleted 266 */ 267 protected boolean checkLinkForDeletedLinkTarget( 268 CmsRelation relation, 269 String link, 270 Map<String, CmsResource> fileLookup) { 271 272 boolean isValidLink = false; 273 // since we are going to delete the resource 274 // check if the linked resource is also to be deleted 275 if (fileLookup.containsKey(link) || fileLookup.containsKey(relation.getSourceId().toString())) { 276 // Technically, if the relation source is going to be published too and is not deleted, the link is not valid. But in that case, validateLinks will also be called for that resource and detect broken the broken link there. 277 isValidLink = true; 278 } 279 return isValidLink; 280 } 281 282 /** 283 * Checks a link from a resource which has changed.<p> 284 * 285 * @param dbc the current dbc 286 * @param resource the link source 287 * @param relation the relation 288 * @param link the link target 289 * @param project the current project 290 * @param fileLookup a lookup table which contains the files which are going to be published 291 * 292 * @return true if the link will be valid after publishing 293 */ 294 protected boolean checkLinkForNewOrChangedLinkSource( 295 CmsDbContext dbc, 296 CmsResource resource, 297 CmsRelation relation, 298 String link, 299 CmsProject project, 300 Map<String, CmsResource> fileLookup) { 301 302 boolean isValidLink = true; 303 // the link is valid... 304 try { 305 // ... if the linked resource exists in the online project 306 // search the target of link in the online project 307 try { 308 link = m_driverManager.getVfsDriver( 309 dbc).readResource(dbc, project.getUuid(), relation.getTargetId(), true).getRootPath(); 310 } catch (CmsVfsResourceNotFoundException e) { 311 // reading by id failed, this means that the link variable still equals relation.getTargetPath() 312 if (LOG.isDebugEnabled()) { 313 LOG.debug( 314 Messages.get().getBundle().key( 315 Messages.LOG_LINK_VALIDATION_READBYID_FAILED_2, 316 relation.getTargetId().toString(), 317 project.getName()), 318 e); 319 } 320 m_driverManager.getVfsDriver(dbc).readResource(dbc, project.getUuid(), relation.getTargetPath(), true); 321 } 322 } catch (CmsException e) { 323 // ... or if the linked resource is a resource that gets actually published 324 if (LOG.isDebugEnabled()) { 325 LOG.debug( 326 Messages.get().getBundle().key( 327 Messages.LOG_LINK_VALIDATION_READBYPATH_FAILED_2, 328 relation.getTargetPath(), 329 project.getName()), 330 e); 331 } 332 if (!fileLookup.containsKey(link)) { 333 isValidLink = false; 334 } 335 } finally { 336 // ... and if the linked resource to be published get deleted 337 if (fileLookup.containsKey(link)) { 338 CmsResource offlineResource = fileLookup.get(link); 339 if (offlineResource.getState().isDeleted()) { 340 if (LOG.isDebugEnabled()) { 341 LOG.debug(Messages.get().getBundle().key(Messages.LOG_LINK_VALIDATION_RESOURCEDELETED_1, link)); 342 } 343 isValidLink = false; 344 } 345 } 346 } 347 return isValidLink; 348 } 349 350 /** 351 * Validates the links for the specified resource.<p> 352 * 353 * @param dbc the database context 354 * @param resource the resource that will be validated 355 * @param fileLookup a map for faster lookup with all resources keyed by their rootpath 356 * @param project the project to validate 357 * @param report the report to write to 358 * 359 * @return a list with the broken links as {@link CmsRelation} objects for the specified resource, 360 * or an empty list if no broken links were found 361 */ 362 protected List<CmsRelation> validateLinks( 363 CmsDbContext dbc, 364 CmsResource resource, 365 Map<String, CmsResource> fileLookup, 366 CmsProject project, 367 I_CmsReport report) { 368 369 List<CmsRelation> brokenRelations = new ArrayList<CmsRelation>(); 370 Map<String, Boolean> validatedLinks = new HashMap<String, Boolean>(); 371 372 // get the relations 373 List<CmsRelation> incomingRelationsOnline = new ArrayList<CmsRelation>(); 374 List<CmsRelation> outgoingRelationsOffline = new ArrayList<CmsRelation>(); 375 try { 376 if (!resource.getState().isDeleted()) { 377 // search the target of links in the current (offline) project 378 outgoingRelationsOffline = m_driverManager.getRelationsForResource( 379 dbc, 380 resource, 381 CmsRelationFilter.TARGETS); 382 } else { 383 // search the source of links in the online project 384 CmsProject currentProject = dbc.currentProject(); 385 dbc.getRequestContext().setCurrentProject(project); 386 try { 387 incomingRelationsOnline = m_driverManager.getRelationsForResource( 388 dbc, 389 resource, 390 CmsRelationFilter.SOURCES); 391 } finally { 392 dbc.getRequestContext().setCurrentProject(currentProject); 393 } 394 } 395 } catch (CmsException e) { 396 LOG.error(Messages.get().getBundle().key(Messages.LOG_LINK_SEARCH_1, resource), e); 397 if (report != null) { 398 report.println( 399 Messages.get().container(Messages.LOG_LINK_SEARCH_1, dbc.removeSiteRoot(resource.getRootPath())), 400 I_CmsReport.FORMAT_ERROR); 401 } 402 return brokenRelations; 403 } 404 405 List<CmsRelation> relations = new ArrayList<CmsRelation>(); 406 relations.addAll(incomingRelationsOnline); 407 relations.addAll(outgoingRelationsOffline); 408 // check the relations 409 boolean first = true; 410 Iterator<CmsRelation> itRelations = relations.iterator(); 411 while (itRelations.hasNext()) { 412 CmsRelation relation = itRelations.next(); 413 String link; 414 if (!resource.getState().isDeleted()) { 415 link = relation.getTargetPath(); 416 } else { 417 link = relation.getSourcePath(); 418 } 419 if (CmsStringUtil.isEmptyOrWhitespaceOnly(link)) { 420 // skip empty links 421 continue; 422 } 423 if (validatedLinks.keySet().contains(link)) { 424 // skip already validated links 425 if (validatedLinks.get(link).booleanValue()) { 426 // add broken relation of different type 427 brokenRelations.add(relation); 428 } 429 continue; 430 } 431 boolean result; 432 if (resource.getState().isDeleted()) { 433 result = checkLinkForDeletedLinkTarget(relation, link, fileLookup); 434 } else { 435 result = checkLinkForNewOrChangedLinkSource(dbc, resource, relation, link, project, fileLookup); 436 437 } 438 boolean isValidLink = result; 439 if (!isValidLink) { 440 if (first) { 441 if (report != null) { 442 report.println( 443 Messages.get().container(Messages.RPT_HTMLLINK_FOUND_BROKEN_LINKS_0), 444 I_CmsReport.FORMAT_WARNING); 445 } 446 first = false; 447 } 448 brokenRelations.add(relation); 449 if (report != null) { 450 if (!resource.getState().isDeleted()) { 451 report.println( 452 Messages.get().container( 453 Messages.RPT_HTMLLINK_BROKEN_TARGET_2, 454 relation.getSourcePath(), 455 dbc.removeSiteRoot(link)), 456 I_CmsReport.FORMAT_WARNING); 457 } else { 458 report.println( 459 Messages.get().container( 460 Messages.RPT_HTMLLINK_BROKEN_SOURCE_2, 461 dbc.removeSiteRoot(link), 462 relation.getTargetPath()), 463 I_CmsReport.FORMAT_WARNING); 464 } 465 } 466 } 467 validatedLinks.put(link, Boolean.valueOf(!isValidLink)); 468 } 469 return brokenRelations; 470 } 471 472}