Last active
August 29, 2015 13:56
-
-
Save bdw429s/8910641 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<cfscript> | |
/** | |
******************************************************************************** | |
ContentBox - A Modular Content Platform | |
Copyright 2012 by Luis Majano and Ortus Solutions, Corp | |
www.gocontentbox.org | www.luismajano.com | www.ortussolutions.com | |
******************************************************************************** | |
Apache License, Version 2.0 | |
Copyright Since [2012] [Luis Majano and Ortus Solutions,Corp] | |
Licensed under the Apache License, Version 2.0 (the "License"); | |
you may not use this file except in compliance with the License. | |
You may obtain a copy of the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS IS" BASIS, | |
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
See the License for the specific language governing permissions and | |
limitations under the License. | |
******************************************************************************** | |
* A generic content service for content objects | |
*/ | |
/** | |
******************************************************************************** | |
ContentBox - A Modular Content Platform | |
Copyright 2012 by Luis Majano and Ortus Solutions, Corp | |
www.gocontentbox.org | www.luismajano.com | www.ortussolutions.com | |
******************************************************************************** | |
Apache License, Version 2.0 | |
Copyright Since [2012] [Luis Majano and Ortus Solutions,Corp] | |
Licensed under the Apache License, Version 2.0 (the "License"); | |
you may not use this file except in compliance with the License. | |
You may obtain a copy of the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS IS" BASIS, | |
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
See the License for the specific language governing permissions and | |
limitations under the License. | |
******************************************************************************** | |
* A generic content service for content objects | |
*/ | |
component extends="coldbox.system.orm.hibernate.VirtualEntityService" singleton{ | |
// DI | |
property name="settingService" inject="id:settingService@cb"; | |
property name="cacheBox" inject="cachebox"; | |
property name="log" inject="logbox:logger:{this}"; | |
property name="customFieldService" inject="customFieldService@cb"; | |
property name="categoryService" inject="categoryService@cb"; | |
property name="commentService" inject="commentService@cb"; | |
property name="contentVersionService" inject="contentVersionService@cb"; | |
property name="authorService" inject="authorService@cb"; | |
property name="populator" inject="wirebox:populator"; | |
property name="systemUtil" inject="SystemUtil@cb"; | |
/** | |
* Constructor | |
* @entityName.hint The content entity name to bind this service to. | |
*/ | |
ContentService function init(entityName="cbContent"){ | |
// init it | |
super.init(entityName=arguments.entityName, useQueryCaching=true); | |
return this; | |
} | |
/** | |
* Clear all content caches | |
* @async.hint Run it asynchronously or not, defaults to false | |
*/ | |
function clearAllCaches(boolean async=false){ | |
var settings = settingService.getAllSettings(asStruct=true); | |
// Get appropriate cache provider | |
var cache = cacheBox.getCache( settings.cb_content_cacheName ); | |
cache.clearByKeySnippet(keySnippet="cb-content",async=arguments.async); | |
return this; | |
} | |
/** | |
* Clear all page wrapper caches | |
* @async.hint Run it asynchronously or not, defaults to false | |
*/ | |
function clearAllPageWrapperCaches(boolean async=false){ | |
var settings = settingService.getAllSettings(asStruct=true); | |
// Get appropriate cache provider | |
var cache = cacheBox.getCache( settings.cb_content_cacheName ); | |
cache.clearByKeySnippet(keySnippet="cb-content-pagewrapper",async=arguments.async); | |
return this; | |
} | |
/** | |
* Clear all page wrapper caches | |
* @slug.hint The slug partial to clean on | |
* @async.hint Run it asynchronously or not, defaults to false | |
*/ | |
function clearPageWrapperCaches(required any slug, boolean async=false){ | |
var settings = settingService.getAllSettings(asStruct=true); | |
// Get appropriate cache provider | |
var cache = cacheBox.getCache( settings.cb_content_cacheName ); | |
cache.clearByKeySnippet(keySnippet="cb-content-pagewrapper-#arguments.slug#",async=arguments.async); | |
return this; | |
} | |
/** | |
* Clear a page wrapper cache | |
* @slug.hint The slug to clean | |
* @async.hint Run it asynchronously or not, defaults to false | |
*/ | |
function clearPageWrapper(required any slug, boolean async=false){ | |
var settings = settingService.getAllSettings(asStruct=true); | |
// Get appropriate cache provider | |
var cache = cacheBox.getCache( settings.cb_content_cacheName ); | |
cache.clear("cb-content-pagewrapper-#arguments.slug#/"); | |
return this; | |
} | |
/** | |
* Searches published content with cool paramters, remember published content only | |
* @searchTerm.hint The search term to search | |
* @max.hint The maximum number of records to paginate | |
* @offset.hint The offset in the pagination | |
* @asQuery.hint Return as query or array of objects, defaults to array of objects | |
* @sortOrder.hint The sorting of the search results, defaults to publishedDate DESC | |
* @isPublished.hint Search for published, non-published or both content objects [true, false, 'all'] | |
* @searchActiveContent.hint Search only content titles or both title and active content. Defaults to both. | |
*/ | |
function searchContent( | |
any searchTerm="", | |
numeric max=0, | |
numeric offset=0, | |
boolean asQuery=false, | |
any sortOrder="publishedDate DESC", | |
any isPublished=true, | |
boolean searchActiveContent=true){ | |
var results = {}; | |
var c = newCriteria(); | |
// only published content | |
if( isBoolean( arguments.isPublished ) ){ | |
// Published bit | |
c.isEq( "isPublished", javaCast( "Boolean", arguments.isPublished ) ); | |
// Published eq true evaluate other params | |
if( arguments.isPublished ){ | |
c.isLt("publishedDate", now() ) | |
.$or( c.restrictions.isNull("expireDate"), c.restrictions.isGT("expireDate", now() ) ) | |
.isEq("passwordProtection",""); | |
} | |
} | |
// Search Criteria | |
if( len( arguments.searchTerm ) ){ | |
// like disjunctions | |
c.createAlias("activeContent","ac"); | |
// Do we search title and active content or just title? | |
if( arguments.searchActiveContent ){ | |
c.$or( c.restrictions.like("title","%#arguments.searchTerm#%"), | |
c.restrictions.like("ac.content", "%#arguments.searchTerm#%") ); | |
} | |
else{ | |
c.like( "title", "%#arguments.searchTerm#%" ); | |
} | |
} | |
// run criteria query and projections count | |
results.count = c.count( "contentID" ); | |
results.content = c.resultTransformer( c.DISTINCT_ROOT_ENTITY ) | |
.list(offset=arguments.offset, max=arguments.max, sortOrder=arguments.sortOrder, asQuery=arguments.asQuery); | |
return results; | |
} | |
/** | |
* Get an id from a slug of a content object | |
* @slug.hint The slug to search an ID for. | |
*/ | |
function getIDBySlug(required any slug){ | |
var results = newCriteria() | |
.isEq("slug", arguments.slug) | |
.withProjections(property="contentID") | |
.get(); | |
// verify results | |
if( isNull( results ) ){ return "";} | |
return results; | |
} | |
/** | |
* Find a published content object by slug and published unpublished flags, if not found it returns | |
* a new content object | |
* @slug.hint The slug to search | |
* @showUnpublished.hint To also show unpublished content, defaults to false. | |
*/ | |
function findBySlug(required any slug, required boolean showUnpublished=false){ | |
var c = newCriteria(); | |
// Override usually for admins | |
if( !showUnpublished ){ | |
c.isTrue("isPublished") | |
.isLT("publishedDate", now()) | |
.$or( c.restrictions.isNull("expireDate"), c.restrictions.isGT("expireDate", now() ) ); | |
} | |
// By criteria now | |
var content = c.isEq("slug",arguments.slug).get(); | |
// return accordingly | |
return ( isNull( content ) ? new() : content ); | |
} | |
/** | |
* Verify an incoming slug is unique or not | |
* @slug.hint The slug to search for uniqueness | |
* @contentID.hint Limit the search to the passed contentID usually for updates | |
*/ | |
function isSlugUnique(required any slug, any contentID=""){ | |
var c = newCriteria() | |
.isEq( "slug", arguments.slug ); | |
if( len( arguments.contentID ) ){ | |
c.ne( "contentID", javaCast( "int", arguments.contentID ) ); | |
} | |
return ( c.count() gt 0 ? false : true ); | |
} | |
/** | |
* Delete a content object safely via hierarchies | |
* @content.hint the Content object to delete | |
*/ | |
ContentService function deleteContent(required any content){ | |
// Check for dis-associations | |
if( arguments.content.hasParent() ){ | |
arguments.content.getParent().removeChild( arguments.content ); | |
} | |
if( arguments.content.hasCategories() ){ | |
arguments.content.removeAllCategories(); | |
} | |
// now delete it | |
delete( arguments.content ); | |
// return service | |
return this; | |
} | |
/** | |
* Find published content objects | |
* @max.hint The maximum number of records to paginate | |
* @offset.hint The offset in the pagination | |
* @searchTerm.hint The search term to search | |
* @category.hint The category to filter the content on | |
* @asQuery.hint Return as query or array of objects, defaults to array of objects | |
* @parent.hint The parent ID to filter on or not | |
* @showInMenu.hint Whether to filter with the show in menu bit or not | |
*/ | |
function findPublishedContent( | |
numeric max=0, | |
numeric offset=0, | |
any searchTerm="", | |
any category="", | |
boolean asQuery=false, | |
any parent, | |
boolean showInMenu){ | |
var results = {}; | |
var c = newCriteria(); | |
// sorting | |
var sortOrder = "publishedDate DESC"; | |
// only published pages | |
c.isTrue("isPublished") | |
.isLT("publishedDate", Now()) | |
.$or( c.restrictions.isNull("expireDate"), c.restrictions.isGT("expireDate", now() ) ) | |
// only non-password pages | |
.isEq("passwordProtection",""); | |
// Show only pages with showInMenu criteria? | |
if( structKeyExists(arguments,"showInMenu") ){ | |
c.isEq( "showInMenu", javaCast( "boolean", arguments.showInMenu ) ); | |
} | |
// Category Filter | |
if( len(arguments.category) ){ | |
// create association with categories by slug. | |
c.createAlias("categories","cats").isIn( "cats.slug", listToArray( arguments.category ) ); | |
} | |
// Search Criteria | |
if( len(arguments.searchTerm) ){ | |
// like disjunctions | |
c.createAlias("activeContent","ac"); | |
c.or( c.restrictions.like("title","%#arguments.searchTerm#%"), | |
c.restrictions.isEq("ac.content", "%#arguments.searchTerm#%") ); | |
} | |
// parent filter | |
if( structKeyExists(arguments,"parent") ){ | |
if( len( trim(arguments.parent) ) ){ | |
c.eq("parent.contentID", javaCast("int",arguments.parent) ); | |
} | |
else{ | |
c.isNull("parent"); | |
} | |
sortOrder = "order asc"; | |
} | |
// run criteria query and projections count | |
results.count = c.count( "contentID" ); | |
results.content = c.resultTransformer( c.DISTINCT_ROOT_ENTITY ) | |
.list(offset=arguments.offset,max=arguments.max,sortOrder=sortOrder,asQuery=arguments.asQuery); | |
return results; | |
} | |
/** | |
* Bulk Publish Status Updates | |
* @contentID.hint The list or array of ID's to bulk update | |
* @status.hint The status either 'publish' or 'draft' | |
*/ | |
any function bulkPublishStatus(required any contentID, required any status){ | |
var publish = false; | |
// publish flag | |
if( arguments.status eq "publish" ){ | |
publish = true; | |
} | |
// Get all by id | |
var contentObjects = getAll(id=arguments.contentID); | |
for(var x=1; x lte arrayLen( contentObjects ); x++){ | |
contentObjects[x].setpublishedDate( now() ); | |
contentObjects[x].setisPublished( publish ); | |
} | |
// transaction the save of all the content objects | |
saveAll( contentObjects ); | |
return this; | |
} | |
/** | |
* Get the top visited content entries | |
* @max.hint The maximum to retrieve, defaults to 5 entries | |
*/ | |
array function getTopVisitedContent(numeric max=5){ | |
var c = newCriteria() | |
.list(max=arguments.max, sortOrder="hits desc", asQuery=false); | |
return c; | |
} | |
/** | |
* Get the top commented content entries | |
* @max.hint The maximum to retrieve, defaults to 5 entries | |
*/ | |
array function getTopCommentedContent(numeric max=5){ | |
var c = newCriteria() | |
.list(max=arguments.max, sortOrder="numberOfComments desc", asQuery=false); | |
return c; | |
} | |
/** | |
* Get all content for export as flat data | |
* @inData.hint The data to use for exporting, usually concrete implementtions can override this. | |
*/ | |
array function getAllForExport(any inData){ | |
var result = []; | |
if( !structKeyExists( arguments, "inData") ){ | |
// export from the root node, instead of everything. | |
var data = newCriteria().isNull( "parent" ).list(); | |
} | |
else{ | |
data = arguments.inData; | |
} | |
for( var thisItem in data ){ | |
arrayAppend( result, thisItem.getMemento() ); | |
} | |
return result; | |
} | |
/** | |
* Import data from a ContentBox JSON file. Returns the import log | |
* @importFile.hint The absolute file path to use for importing | |
* @override.hint Override records or not | |
*/ | |
string function importFromFile(required importFile, boolean override=false){ | |
var data = fileRead( arguments.importFile ); | |
var importLog = createObject("java", "java.lang.StringBuilder").init("Starting import with override = #arguments.override#...<br>"); | |
if( !isJSON( data ) ){ | |
throw(message="Cannot import file as the contents is not JSON", type="InvalidImportFormat"); | |
} | |
// deserialize packet: Should be array of { settingID, name, value } | |
return importFromData( deserializeJSON( data ), arguments.override, importLog ); | |
} | |
/** | |
* Import data from an array of structures of content or just one structure of a content entry | |
* @importData.hint The data to import | |
* @override.hint Override records or not | |
* @importLog.hint The import log buffer | |
*/ | |
string function importFromData( | |
required any importData, | |
boolean override=false, | |
required any importLog){ | |
var allContent = []; | |
// if struct, inflate into an array | |
if( isStruct( arguments.importData ) ){ | |
arguments.importData = [ arguments.importData ]; | |
} | |
// iterate and import | |
for( var thisContent in arguments.importData ){ | |
// Inflate content from data | |
var inflateResults = inflateFromStruct( thisContent, arguments.importLog ); | |
// continue to next record if author not found | |
if( !inflateResults.authorFound ){ continue; } | |
// if new or persisted with override then save. | |
if( !inflateResults.content.isLoaded() ){ | |
arguments.importLog.append( "New content imported: #thisContent.slug#<br>" ); | |
arrayAppend( allContent, inflateResults.content ); | |
} | |
else if( inflateResults.content.isLoaded() and arguments.override ){ | |
arguments.importLog.append( "Persisted content overriden: #thisContent.slug#<br>" ); | |
arrayAppend( allContent, inflateResults.content ); | |
} | |
else{ | |
arguments.importLog.append( "Skipping persisted content: #thisContent.slug#<br>" ); | |
} | |
} // end import loop | |
// Save content | |
if( arrayLen( allContent ) ){ | |
saveAll( allContent ); | |
arguments.importLog.append( "Saved all imported and overriden content!" ); | |
} | |
else{ | |
arguments.importLog.append( "No content imported as none where found or able to be overriden from the import file." ); | |
} | |
return arguments.importLog.toString(); | |
} | |
/** | |
* Inflate a content object from a ContentBox JSON structure | |
* @contentData.hint The content structure inflated from JSON | |
* @importLog.hint The string builder import log | |
* @parent.hint If the inflated content object has a parent then it can be linked directly, no inflating necessary. Usually for recursions | |
*/ | |
private function inflateFromStruct( | |
required any contentData, | |
required any importLog, | |
any parent){ | |
// setup | |
var thisContent = arguments.contentData; | |
var badDateRegex = " -\d{4}$"; | |
// Get content by slug, if not found then it returns a new entity so we can persist it. | |
var oContent = findBySlug( slug=thisContent.slug, showUnpublished=true ); | |
// date conversion tests | |
thisContent.publishedDate = reReplace( thisContent.publishedDate, badDateRegex, "" ); | |
thisContent.createdDate = reReplace( thisContent.createdDate, badDateRegex, "" ); | |
if( len( thisContent.expireDate ) ){ | |
thisContent.expireDate = reReplace( thisContent.expireDate, badDateRegex, "" ); | |
} | |
// populate content from data and ignore relationships, we need to build those manually. | |
populator.populateFromStruct( target=oContent, | |
memento=thisContent, | |
exclude="creator,parent,children,categories,customfields,contentversions,comments", | |
composeRelationships=false, | |
nullEmptyInclude="publishedDate,expireDate" ); | |
// determine author else ignore import | |
var oAuthor = authorService.findByUsername( ( structKeyExists( thisContent.creator, "username" ) ? thisContent.creator.username : "" ) ); | |
if( !isNull( oAuthor ) ){ | |
// AUTHOR CREATOR | |
oContent.setCreator( oAuthor ); | |
arguments.importLog.append( "Content author found and linked: #thisContent.slug#<br>" ); | |
// PARENT | |
if( structKeyExists( arguments, "parent") and isObject( arguments.parent ) ){ | |
oContent.setParent( arguments.parent ); | |
arguments.importLog.append( "Content parent passed and linked: #arguments.parent.getSlug()#<br>" ); | |
} | |
else if( isStruct( thisContent.parent ) and structCount( thisContent.parent ) ){ | |
var oParent = findBySlug( slug=thisContent.parent.slug, showUnpublished=true ); | |
// assign if persisted | |
if( oParent.isLoaded() ){ | |
oContent.setParent( oParent ); | |
arguments.importLog.append( "Content parent found and linked: #thisContent.parent.slug#<br>" ); | |
} | |
else{ | |
arguments.importLog.append( "Content parent slug: #thisContent.parent.toString()# was not found so not assigned!<br>" ); | |
} | |
} | |
// CHILDREN | |
if( arrayLen( thisContent.children ) ){ | |
var allChildren = []; | |
// recurse on them and inflate hiearchy | |
for( var thisChild in thisContent.children ){ | |
var inflateResults = inflateFromStruct( contentData=thisChild, importLog=arguments.importLog, parent=oContent ); | |
// continue to next record if author not found | |
if( !inflateResults.authorFound ){ continue; } | |
// Add to array of children to add. | |
arrayAppend( allChildren, inflateResults.content ); | |
} | |
oContent.setChildren( allChildren ); | |
} | |
// CUSTOM FIELDS | |
if( arrayLen( thisContent.customfields ) ){ | |
// wipe out custom fileds if they exist | |
if( oContent.hasCustomField() ){ oContent.getCustomFields().clear(); } | |
// add new custom fields | |
for( var thisCF in thisContent.customfields ){ | |
var args = { key = thisCF.key, value = thisCF.value }; | |
var oField = customFieldService.new(properties=args); | |
oField.setRelatedContent( oContent ); | |
oContent.addCustomField( oField ); | |
} | |
} | |
// CATEGORIES | |
if( arrayLen( thisContent.categories ) ){ | |
// Create categories that don't exist first | |
var allCategories = []; | |
for( var thisCategory in thisContent.categories ){ | |
var oCategory = categoryService.findBySlug( thisCategory.slug ); | |
oCategory = ( isNull( oCategory ) ? populator.populateFromStruct( target=categoryService.new(), memento=thisCategory, exclude="categoryID" ) : oCategory ); | |
// save category if new only | |
if( !oCategory.isLoaded() ){ categoryService.save( entity=oCategory ); } | |
// append to add. | |
arrayAppend( allCategories, oCategory ); | |
} | |
// detach categories and re-attach | |
oContent.setCategories( allCategories ); | |
} | |
// COMMENTS | |
if( arrayLen( thisContent.comments ) ){ | |
var allComments = []; | |
for( var thisComment in thisContent.comments ){ | |
// some conversions | |
thisComment.createdDate = reReplace( thisComment.createdDate, badDateRegex, "" ); | |
// population | |
var oComment = populator.populateFromStruct( target=commentService.new(), | |
memento=thisComment, | |
exclude="commentID", | |
composeRelationships=false ); | |
oComment.setRelatedContent( oContent ); | |
arrayAppend( allComments, oComment ); | |
} | |
oContent.setComments( allComments ); | |
} | |
// CONTENT VERSIONS | |
if( arrayLen( thisContent.contentversions ) ){ | |
var allContentVersions = []; | |
for( var thisVersion in thisContent.contentversions ){ | |
// some conversions | |
thisVersion.createdDate = reReplace( thisVersion.createdDate, badDateRegex, "" ); | |
// population | |
var oVersion = populator.populateFromStruct( target=contentVersionService.new(), | |
memento=thisVersion, | |
exclude="contentVersionID,author", | |
composeRelationships=false ); | |
// Get author | |
var oAuthor = authorService.findByUsername( thisVersion.author.username ); | |
// Only add if author found | |
if( !isNull( oAuthor ) ){ | |
oVersion.setAuthor( oAuthor ); | |
oVersion.setRelatedContent( oContent ); | |
arrayAppend( allContentVersions, oVersion ); | |
} | |
else{ | |
arguments.importLog.append( "Skipping importing version content #thisVersion.version# as author (#thisVersion.author.toString()#) not found!<br>" ); | |
} | |
} | |
oContent.setContentVersions( allContentVersions ); | |
} | |
} // end if author found | |
else{ | |
arguments.importLog.append( "Content author not found (#thisContent.creator.toString()#) skipping: #thisContent.slug#<br>" ); | |
} | |
return { content=oContent, authorFound=( !isNull( oAuthor ) ) }; | |
} | |
/** | |
* Update a content's hits with some async flava | |
* @contentID.hint The content id to update | |
* @async.hint Async or not | |
*/ | |
ContentService function updateHits(required contentID, boolean async=true){ | |
// if in thread already or not async | |
if( systemUtil.inThread() OR !arguments.async ){ | |
return syncUpdateHits( arguments.contentID ); | |
} | |
var threadName = "updateHits_#hash( arguments.contentID & now() )#"; | |
thread name="#threadName#" contentID="#arguments.contentID#"{ | |
variables.syncUpdateHits( attributes.contentID ); | |
} | |
return this; | |
} | |
/** | |
* Returns an array of slugs of all the content objects in the system. | |
*/ | |
array function getAllFlatSlugs(){ | |
var c = newCriteria(); | |
return c.withProjections( property="slug" ) | |
.list( sortOrder="slug asc" ); | |
} | |
/********************************************* PRIVATE *********************************************/ | |
/** | |
* Update the content hits | |
* @contentID.hint The content id to update | |
*/ | |
private function syncUpdateHits(required contentID){ | |
var q = new Query(sql="UPDATE cb_content SET hits = hits + 1 WHERE contentID = #arguments.contentID#").execute(); | |
return this; | |
} | |
} | |
</cfscript> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment