2009-11-02 05:02:57 +01:00
< ? php
2015-11-02 00:27:42 +01:00
2017-09-06 05:49:23 +02:00
namespace SilverStripe\ContentReview\Extensions ;
use Exception ;
use SilverStripe\CMS\Model\SiteTree ;
use SilverStripe\ContentReview\Jobs\ContentReviewNotificationJob ;
use SilverStripe\ContentReview\Models\ContentReviewLog ;
use SilverStripe\Core\Config\Config ;
use SilverStripe\Core\Injector\Injector ;
2017-09-13 03:37:13 +02:00
use SilverStripe\Core\Manifest\ModuleLoader ;
2017-09-06 05:49:23 +02:00
use SilverStripe\Forms\FieldList ;
use SilverStripe\Forms\LiteralField ;
use SilverStripe\Forms\FormAction ;
use SilverStripe\Forms\CompositeField ;
use SilverStripe\Forms\Tab ;
use SilverStripe\Forms\DateField ;
2017-09-13 01:16:47 +02:00
use SilverStripe\Forms\DateTimeField ;
2017-09-06 05:49:23 +02:00
use SilverStripe\Forms\DropdownField ;
use SilverStripe\Forms\GridField\GridField ;
use SilverStripe\Forms\GridField\GridFieldConfig ;
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor ;
use SilverStripe\Forms\GridField\GridFieldDataColumns ;
use SilverStripe\Forms\GridField\GridFieldSortableHeader ;
use SilverStripe\Forms\ListboxField ;
use SilverStripe\Forms\OptionsetField ;
use SilverStripe\Forms\ReadonlyField ;
use SilverStripe\Forms\HeaderField ;
use SilverStripe\ORM\ArrayList ;
use SilverStripe\ORM\DataExtension ;
use SilverStripe\ORM\DataObject ;
use SilverStripe\ORM\DB ;
use SilverStripe\ORM\FieldType\DBDatetime ;
use SilverStripe\ORM\FieldType\DBDate ;
use SilverStripe\ORM\SS_List ;
use SilverStripe\Security\Group ;
use SilverStripe\Security\Member ;
use SilverStripe\Security\Permission ;
use SilverStripe\Security\PermissionProvider ;
use SilverStripe\Security\Security ;
use SilverStripe\SiteConfig\SiteConfig ;
use SilverStripe\View\Requirements ;
use Symbiote\QueuedJobs\DataObjects\QueuedJobDescriptor ;
2017-09-07 06:14:37 +02:00
use Symbiote\QueuedJobs\Services\QueuedJobService ;
2017-09-06 05:49:23 +02:00
2009-11-02 05:02:57 +01:00
/**
2015-11-02 00:27:42 +01:00
* Set dates at which content needs to be reviewed and provide a report and emails to alert
* to content needing review .
*
* @ property string $ContentReviewType
* @ property int $ReviewPeriodDays
* @ property Date $NextReviewDate
* @ property string $LastEditedByName
* @ property string $OwnerNames
2009-11-02 05:02:57 +01:00
*
2015-11-02 00:27:42 +01:00
* @ method DataList ReviewLogs ()
* @ method DataList ContentReviewGroups ()
* @ method DataList ContentReviewUsers ()
2009-11-02 05:02:57 +01:00
*/
2015-11-02 00:27:42 +01:00
class SiteTreeContentReview extends DataExtension implements PermissionProvider
{
/**
* @ var array
*/
private static $db = array (
" ContentReviewType " => " Enum('Inherit, Disabled, Custom', 'Inherit') " ,
" ReviewPeriodDays " => " Int " ,
" NextReviewDate " => " Date " ,
" LastEditedByName " => " Varchar(255) " ,
" OwnerNames " => " Varchar(255) " ,
);
/**
* @ var array
*/
private static $defaults = array (
" ContentReviewType " => " Inherit " ,
);
/**
* @ var array
*/
private static $has_many = array (
2017-09-06 05:49:23 +02:00
" ReviewLogs " => ContentReviewLog :: class ,
2015-11-02 00:27:42 +01:00
);
/**
* @ var array
*/
private static $belongs_many_many = array (
2017-09-06 05:49:23 +02:00
" ContentReviewGroups " => Group :: class ,
" ContentReviewUsers " => Member :: class ,
2015-11-02 00:27:42 +01:00
);
/**
* @ var array
*/
private static $schedule = array (
0 => " No automatic review date " ,
1 => " 1 day " ,
7 => " 1 week " ,
30 => " 1 month " ,
60 => " 2 months " ,
91 => " 3 months " ,
121 => " 4 months " ,
152 => " 5 months " ,
183 => " 6 months " ,
365 => " 12 months " ,
);
/**
* @ return array
*/
public static function get_schedule ()
{
2017-09-06 05:49:23 +02:00
return Config :: inst () -> get ( static :: class , 'schedule' );
2015-11-02 00:27:42 +01:00
}
/**
* Takes a list of groups and members and return a list of unique member .
*
* @ param SS_List $groups
* @ param SS_List $members
*
* @ return ArrayList
*/
public static function merge_owners ( SS_List $groups , SS_List $members )
{
$contentReviewOwners = new ArrayList ();
if ( $groups -> count ()) {
$groupIDs = array ();
foreach ( $groups as $group ) {
$familyIDs = $group -> collateFamilyIDs ();
if ( is_array ( $familyIDs )) {
$groupIDs = array_merge ( $groupIDs , array_values ( $familyIDs ));
}
}
array_unique ( $groupIDs );
if ( count ( $groupIDs )) {
2017-09-06 05:49:23 +02:00
$groupMembers = DataObject :: get ( Member :: class ) -> where ( " \" Group \" . \" ID \" IN ( " . implode ( " , " , $groupIDs ) . " ) " )
2015-11-02 00:27:42 +01:00
-> leftJoin ( " Group_Members " , " \" Member \" . \" ID \" = \" Group_Members \" . \" MemberID \" " )
2017-09-06 05:49:23 +02:00
/** @skipUpgrade */
-> leftJoin ( 'Group' , " \" Group_Members \" . \" GroupID \" = \" Group \" . \" ID \" " );
2015-11-02 00:27:42 +01:00
$contentReviewOwners -> merge ( $groupMembers );
}
}
$contentReviewOwners -> merge ( $members );
$contentReviewOwners -> removeDuplicates ();
return $contentReviewOwners ;
}
/**
* @ param FieldList $actions
*/
public function updateCMSActions ( FieldList $actions )
{
2017-09-11 02:01:41 +02:00
if ( ! $this -> canBeReviewedBy ( Security :: getCurrentUser ())) {
return ;
}
2016-05-17 06:34:00 +02:00
2017-09-13 03:37:13 +02:00
$module = ModuleLoader :: getModule ( 'silverstripe/contentreview' );
Requirements :: css ( $module -> getRelativeResourcePath ( 'client/dist/styles/contentreview.css' ));
Requirements :: javascript ( $module -> getRelativeResourcePath ( 'client/dist/js/contentreview.js' ));
2016-05-17 06:34:00 +02:00
2017-09-11 02:01:41 +02:00
$reviewTab = LiteralField :: create ( 'ContentReviewButton' , $this -> owner -> renderWith ( __CLASS__ . '_button' ));
$actions -> insertAfter ( 'MajorActions' , $reviewTab );
2015-11-02 00:27:42 +01:00
}
/**
* Returns false if the content review have disabled .
*
* @ param SiteTree $page
*
* @ return bool | Date
*/
public function getReviewDate ( SiteTree $page = null )
{
if ( $page === null ) {
$page = $this -> owner ;
}
2017-09-06 05:49:23 +02:00
if ( $page -> obj ( 'NextReviewDate' ) -> exists ()) {
return $page -> obj ( 'NextReviewDate' );
2015-11-02 00:27:42 +01:00
}
$options = $this -> owner -> getOptions ();
if ( ! $options ) {
return false ;
}
if ( ! $options -> ReviewPeriodDays ) {
return false ;
}
// Failover to check on ReviewPeriodDays + LastEdited
2017-09-06 06:53:45 +02:00
$nextReviewUnixSec = strtotime ( ' + ' . $options -> ReviewPeriodDays . ' days' , DBDatetime :: now () -> getTimestamp ());
2017-09-06 05:49:23 +02:00
$date = DBDate :: create ( 'NextReviewDate' );
$date -> setValue ( $nextReviewUnixSec );
2015-11-02 00:27:42 +01:00
return $date ;
}
/**
* Get the object that have the information about the content review settings . Either :
*
* - a SiteTreeContentReview decorated object
* - the default SiteTree config
* - false if this page have it ' s content review disabled
*
* Will go through parents and root pages will use the site config if their setting is Inherit .
*
* @ return bool | DataObject
*
* @ throws Exception
*/
public function getOptions ()
{
if ( $this -> owner -> ContentReviewType == " Custom " ) {
return $this -> owner ;
}
if ( $this -> owner -> ContentReviewType == " Disabled " ) {
return false ;
}
$page = $this -> owner ;
// $page is inheriting it's settings from it's parent, find
// the first valid parent with a valid setting
while ( $parent = $page -> Parent ()) {
// Root page, use site config
if ( ! $parent -> exists ()) {
return SiteConfig :: current_site_config ();
}
if ( $parent -> ContentReviewType == " Custom " ) {
return $parent ;
}
if ( $parent -> ContentReviewType == " Disabled " ) {
return false ;
}
$page = $parent ;
}
throw new Exception ( " This shouldn't really happen, as per usual developer logic. " );
}
/**
* @ return string
*/
public function getOwnerNames ()
{
$options = $this -> getOptions ();
$names = array ();
if ( ! $options ) {
return " " ;
}
foreach ( $options -> OwnerGroups () as $group ) {
$names [] = $group -> getBreadcrumbs ( " > " );
}
foreach ( $options -> OwnerUsers () as $group ) {
$names [] = $group -> getName ();
}
return implode ( " , " , $names );
}
/**
* @ return null | string
*/
public function getEditorName ()
{
2017-09-06 05:49:23 +02:00
$member = Security :: getCurrentUser ();
2015-11-02 00:27:42 +01:00
if ( $member ) {
2016-05-17 06:34:00 +02:00
return $member -> getTitle ();
2015-11-02 00:27:42 +01:00
}
return null ;
}
/**
* Get all Members that are Content Owners to this page . This includes checking group
* hierarchy and adding any direct users .
*
* @ return ArrayList
*/
public function ContentReviewOwners ()
{
return SiteTreeContentReview :: merge_owners (
$this -> OwnerGroups (),
$this -> OwnerUsers ()
);
}
/**
* @ return ManyManyList
*/
public function OwnerGroups ()
{
return $this -> owner -> getManyManyComponents ( " ContentReviewGroups " );
}
/**
* @ return ManyManyList
*/
public function OwnerUsers ()
{
return $this -> owner -> getManyManyComponents ( " ContentReviewUsers " );
}
/**
* @ param FieldList $fields
*/
public function updateSettingsFields ( FieldList $fields )
{
2017-09-13 03:37:13 +02:00
$module = ModuleLoader :: getModule ( 'silverstripe/contentreview' );
Requirements :: javascript ( $module -> getRelativeResourcePath ( 'client/dist/js/contentreview.js' ));
2015-11-02 00:27:42 +01:00
// Display read-only version only
if ( ! Permission :: check ( " EDIT_CONTENT_REVIEW_FIELDS " )) {
$schedule = self :: get_schedule ();
$contentOwners = ReadonlyField :: create ( " ROContentOwners " , _t ( " ContentReview.CONTENTOWNERS " , " Content Owners " ), $this -> getOwnerNames ());
$nextReviewAt = DateField :: create ( 'RONextReviewDate' , _t ( " ContentReview.NEXTREVIEWDATE " , " Next review date " ), $this -> owner -> NextReviewDate );
if ( ! isset ( $schedule [ $this -> owner -> ReviewPeriodDays ])) {
$reviewFreq = ReadonlyField :: create ( " ROReviewPeriodDays " , _t ( " ContentReview.REVIEWFREQUENCY " , " Review frequency " ), $schedule [ 0 ]);
} else {
$reviewFreq = ReadonlyField :: create ( " ROReviewPeriodDays " , _t ( " ContentReview.REVIEWFREQUENCY " , " Review frequency " ), $schedule [ $this -> owner -> ReviewPeriodDays ]);
}
$logConfig = GridFieldConfig :: create ()
2017-09-06 05:49:23 +02:00
-> addComponent ( Injector :: inst () -> create ( GridFieldSortableHeader :: class ))
-> addComponent ( $logColumns = Injector :: inst () -> create ( GridFieldDataColumns :: class ));
2015-11-02 00:27:42 +01:00
// Cast the value to the users preferred date format
$logColumns -> setFieldCasting ( array (
2017-09-13 01:16:47 +02:00
'Created' => DateTimeField :: class . '->value' ,
2015-11-02 00:27:42 +01:00
));
$logs = GridField :: create ( " ROReviewNotes " , " Review Notes " , $this -> owner -> ReviewLogs (), $logConfig );
$optionsFrom = ReadonlyField :: create ( " ROType " , _t ( " ContentReview.SETTINGSFROM " , " Options are " ), $this -> owner -> ContentReviewType );
$fields -> addFieldsToTab ( " Root.ContentReview " , array (
$contentOwners ,
$nextReviewAt -> performReadonlyTransformation (),
$reviewFreq ,
$optionsFrom ,
$logs ,
));
return ;
}
$options = array ();
$options [ " Disabled " ] = _t ( " ContentReview.DISABLE " , " Disable content review " );
$options [ " Inherit " ] = _t ( " ContentReview.INHERIT " , " Inherit from parent page " );
$options [ " Custom " ] = _t ( " ContentReview.CUSTOM " , " Custom settings " );
$viewersOptionsField = OptionsetField :: create ( " ContentReviewType " , _t ( " ContentReview.OPTIONS " , " Options " ), $options );
$users = Permission :: get_members_by_permission ( array ( " CMS_ACCESS_CMSMain " , " ADMIN " ));
$usersMap = $users -> map ( " ID " , " Title " ) -> toArray ();
asort ( $usersMap );
$userField = ListboxField :: create ( " OwnerUsers " , _t ( " ContentReview.PAGEOWNERUSERS " , " Users " ), $usersMap )
2015-11-18 04:38:28 +01:00
-> addExtraClass ( 'custom-setting' )
2015-11-02 00:27:42 +01:00
-> setAttribute ( " data-placeholder " , _t ( " ContentReview.ADDUSERS " , " Add users " ))
-> setDescription ( _t ( 'ContentReview.OWNERUSERSDESCRIPTION' , 'Page owners that are responsible for reviews' ));
$groupsMap = array ();
foreach ( Group :: get () as $group ) {
$groupsMap [ $group -> ID ] = $group -> getBreadcrumbs ( " > " );
}
asort ( $groupsMap );
$groupField = ListboxField :: create ( " OwnerGroups " , _t ( " ContentReview.PAGEOWNERGROUPS " , " Groups " ), $groupsMap )
2015-11-18 04:38:28 +01:00
-> addExtraClass ( 'custom-setting' )
2015-11-02 00:27:42 +01:00
-> setAttribute ( " data-placeholder " , _t ( " ContentReview.ADDGROUP " , " Add groups " ))
-> setDescription ( _t ( " ContentReview.OWNERGROUPSDESCRIPTION " , " Page owners that are responsible for reviews " ));
$reviewDate = DateField :: create ( " NextReviewDate " , _t ( " ContentReview.NEXTREVIEWDATE " , " Next review date " ))
-> setDescription ( _t ( " ContentReview.NEXTREVIEWDATADESCRIPTION " , " Leave blank for no review " ));
2015-11-17 03:12:20 +01:00
$reviewFrequency = DropdownField :: create (
2015-11-18 04:38:28 +01:00
" ReviewPeriodDays " ,
_t ( " ContentReview.REVIEWFREQUENCY " , " Review frequency " ),
self :: get_schedule ()
)
-> addExtraClass ( 'custom-setting' )
2015-11-02 00:27:42 +01:00
-> setDescription ( _t ( " ContentReview.REVIEWFREQUENCYDESCRIPTION " , " The review date will be set to this far in the future whenever the page is published " ));
$notesField = GridField :: create ( " ReviewNotes " , " Review Notes " , $this -> owner -> ReviewLogs (), GridFieldConfig_RecordEditor :: create ());
$fields -> addFieldsToTab ( " Root.ContentReview " , array (
2017-09-06 05:49:23 +02:00
HeaderField :: create ( 'ContentReviewHeader' , _t ( " ContentReview.REVIEWHEADER " , " Content review " ), 2 ),
2015-11-02 00:27:42 +01:00
$viewersOptionsField ,
CompositeField :: create (
$userField ,
$groupField ,
$reviewDate ,
$reviewFrequency
2015-11-17 03:12:20 +01:00
) -> addExtraClass ( " review-settings " ),
2015-11-02 00:27:42 +01:00
ReadonlyField :: create ( " ROContentOwners " , _t ( " ContentReview.CONTENTOWNERS " , " Content Owners " ), $this -> getOwnerNames ()),
$notesField ,
));
}
/**
* Creates a ContentReviewLog and connects it to this Page .
*
* @ param Member $reviewer
* @ param string $message
*/
public function addReviewNote ( Member $reviewer , $message )
{
$reviewLog = ContentReviewLog :: create ();
$reviewLog -> Note = $message ;
$reviewLog -> ReviewerID = $reviewer -> ID ;
$this -> owner -> ReviewLogs () -> add ( $reviewLog );
}
/**
* Advance review date to the next date based on review period or set it to null
* if there is no schedule . Returns true if date was required and false is content
* review is 'off' .
*
* @ return bool
*/
public function advanceReviewDate ()
{
2017-09-06 06:53:45 +02:00
$nextDateTimestamp = false ;
2015-11-02 00:27:42 +01:00
$options = $this -> getOptions ();
if ( $options && $options -> ReviewPeriodDays ) {
2017-09-06 06:53:45 +02:00
$nextDateTimestamp = strtotime (
' + ' . $options -> ReviewPeriodDays . ' days' ,
DBDatetime :: now () -> getTimestamp ()
);
2015-11-02 00:27:42 +01:00
2017-09-06 05:49:23 +02:00
$this -> owner -> NextReviewDate = DBDate :: create () -> setValue ( $nextDateTimestamp ) -> Format ( 'y-MM-dd' );
2015-11-02 00:27:42 +01:00
$this -> owner -> write ();
}
2017-09-06 06:53:45 +02:00
return ( bool ) $nextDateTimestamp ;
2015-11-02 00:27:42 +01:00
}
/**
* Check if a review is due by a member for this owner .
*
* @ param Member $member
*
* @ return bool
*/
public function canBeReviewedBy ( Member $member = null )
{
if ( ! $this -> owner -> obj ( " NextReviewDate " ) -> exists ()) {
return false ;
}
if ( $this -> owner -> obj ( " NextReviewDate " ) -> InFuture ()) {
return false ;
}
$options = $this -> getOptions ();
2017-09-06 05:49:23 +02:00
2017-07-11 23:23:30 +02:00
if ( ! $options ) {
return false ;
}
2015-11-02 00:27:42 +01:00
2017-09-06 05:49:23 +02:00
if ( ! $options || ! $options -> hasExtension ( __CLASS__ )) {
2017-03-23 23:52:35 +01:00
return false ;
}
2015-11-02 00:27:42 +01:00
if ( $options -> OwnerGroups () -> count () == 0 && $options -> OwnerUsers () -> count () == 0 ) {
return false ;
}
if ( ! $member ) {
return true ;
}
if ( $member -> inGroups ( $options -> OwnerGroups ())) {
return true ;
}
if ( $options -> OwnerUsers () -> find ( " ID " , $member -> ID )) {
return true ;
}
return false ;
}
/**
* Set the review data from the review period , if set .
*/
public function onBeforeWrite ()
{
2016-05-17 06:34:00 +02:00
// Only update if DB fields have been changed
$changedFields = $this -> owner -> getChangedFields ( true , 2 );
2017-09-06 05:49:23 +02:00
if ( $changedFields ) {
2016-05-17 06:34:00 +02:00
$this -> owner -> LastEditedByName = $this -> owner -> getEditorName ();
$this -> owner -> OwnerNames = $this -> owner -> getOwnerNames ();
}
2015-11-02 00:27:42 +01:00
// If the user changed the type, we need to recalculate the review date.
if ( $this -> owner -> isChanged ( " ContentReviewType " , 2 )) {
if ( $this -> owner -> ContentReviewType == " Disabled " ) {
$this -> setDefaultReviewDateForDisabled ();
} elseif ( $this -> owner -> ContentReviewType == " Custom " ) {
$this -> setDefaultReviewDateForCustom ();
} else {
$this -> setDefaultReviewDateForInherited ();
}
}
// Ensure that a inherited page always have a next review date
if ( $this -> owner -> ContentReviewType == " Inherit " && ! $this -> owner -> NextReviewDate ) {
$this -> setDefaultReviewDateForInherited ();
}
// We need to update all the child pages that inherit this setting. We can only
// change children after this record has been created, otherwise the stageChildren
// method will grab all pages in the DB (this messes up unit testing)
if ( ! $this -> owner -> exists ()) {
return ;
}
2016-05-17 06:34:00 +02:00
// parent page change its review period
2015-11-02 00:27:42 +01:00
// && !$this->owner->isChanged('ContentReviewType', 2)
2017-09-06 05:49:23 +02:00
if ( $this -> owner -> isChanged ( 'ReviewPeriodDays' , 2 )) {
2017-09-06 06:53:45 +02:00
$nextReviewUnixSec = strtotime (
' + ' . $this -> owner -> ReviewPeriodDays . ' days' ,
DBDatetime :: now () -> getTimestamp ()
);
2017-09-06 05:49:23 +02:00
$this -> owner -> NextReviewDate = DBDate :: create () -> setValue ( $nextReviewUnixSec ) -> Format ( 'y-MM-dd' );
2015-11-02 00:27:42 +01:00
}
}
private function setDefaultReviewDateForDisabled ()
{
$this -> owner -> NextReviewDate = null ;
}
protected function setDefaultReviewDateForCustom ()
{
2015-11-18 04:38:28 +01:00
// Don't overwrite existing value
2015-11-02 00:27:42 +01:00
if ( $this -> owner -> NextReviewDate ) {
return ;
}
$this -> owner -> NextReviewDate = null ;
$nextDate = $this -> getReviewDate ();
if ( is_object ( $nextDate )) {
$this -> owner -> NextReviewDate = $nextDate -> getValue ();
} else {
$this -> owner -> NextReviewDate = $nextDate ;
}
}
protected function setDefaultReviewDateForInherited ()
{
2015-11-18 04:38:28 +01:00
// Don't overwrite existing value
if ( $this -> owner -> NextReviewDate ) {
return ;
}
2015-11-17 03:12:20 +01:00
2015-11-02 00:27:42 +01:00
$options = $this -> getOptions ();
$nextDate = null ;
2015-11-17 03:12:20 +01:00
if ( $options instanceof SiteTree ) {
$nextDate = $this -> getReviewDate ( $options );
2015-11-02 00:27:42 +01:00
} elseif ( $options instanceof SiteConfig ) {
$nextDate = $this -> getReviewDate ();
}
if ( is_object ( $nextDate )) {
$this -> owner -> NextReviewDate = $nextDate -> getValue ();
} else {
$this -> owner -> NextReviewDate = $nextDate ;
}
}
/**
* Provide permissions to the CMS .
*
* @ return array
*/
public function providePermissions ()
{
return array (
" EDIT_CONTENT_REVIEW_FIELDS " => array (
" name " => " Set content owners and review dates " ,
" category " => _t ( " Permissions.CONTENT_CATEGORY " , " Content permissions " ),
" sort " => 50 ,
),
);
}
/**
* If the queued jobs module is installed , queue up the first job for 9 am tomorrow morning
* ( by default ) .
*/
public function requireDefaultRecords ()
{
2017-09-06 05:49:23 +02:00
if ( class_exists ( ContentReviewNotificationJob :: class )) {
2015-11-02 00:27:42 +01:00
// Ensure there is not already a job queued
2017-09-06 05:49:23 +02:00
if ( QueuedJobDescriptor :: get () -> filter ( " Implementation " , ContentReviewNotificationJob :: class ) -> first ()) {
2015-11-02 00:27:42 +01:00
return ;
}
2017-09-06 05:49:23 +02:00
$nextRun = Injector :: inst () -> create ( ContentReviewNotificationJob :: class );
$runHour = Config :: inst () -> get ( ContentReviewNotificationJob :: class , " first_run_hour " );
2015-11-02 00:27:42 +01:00
$firstRunTime = date ( " Y-m-d H:i:s " , mktime ( $runHour , 0 , 0 , date ( " m " ), date ( " d " ) + 1 , date ( " y " )));
2017-09-07 06:14:37 +02:00
singleton ( QueuedJobService :: class ) -> queueJob (
2015-11-02 00:27:42 +01:00
$nextRun ,
$firstRunTime
);
DB :: alteration_message ( sprintf ( " Added ContentReviewNotificationJob to run at %s " , $firstRunTime ));
}
}
2009-11-02 05:02:57 +01:00
}