Source of file DataChangeRecord.php
Size: 11,595 Bytes - Last Modified: 2021-12-23T10:25:23+00:00
/var/www/docs.ssmods.com/process/src/src/Model/DataChangeRecord.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 | <?php namespace Symbiote\DataChange\Model; use SilverStripe\ORM\DataObject; use SilverStripe\View\Requirements; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\ToggleCompositeField; use SilverStripe\Forms\ReadonlyField; use SilverStripe\Core\Injector\Injector; use SilverStripe\Versioned\DataDifferencer; use SilverStripe\Versioned\Versioned; use SilverStripe\Security\Member; use SilverStripe\Control\Director; /** * Record a change to a dataobject; use this to track data changes of objects * * @author marcus@symbiote.com.au * @license BSD License http://silverstripe.org/bsd-license/ */ class DataChangeRecord extends DataObject { private static $table_name = 'DataChangeRecord'; private static $db = [ 'ChangeType' => 'Varchar', 'ObjectTitle' => 'Varchar(255)', 'Before' => 'Text', 'After' => 'Text', 'Stage' => 'Text', 'CurrentEmail' => 'Text', 'CurrentURL' => 'Varchar(255)', 'Referer' => 'Varchar(255)', 'RemoteIP' => 'Varchar(128)', 'Agent' => 'Varchar(255)', 'GetVars' => 'Text', 'PostVars' => 'Text', ]; private static $has_one = array( 'ChangedBy' => 'SilverStripe\Security\Member', 'ChangeRecord' => 'SilverStripe\ORM\DataObject', ); private static $summary_fields = array( 'ChangeType' => 'Change Type', 'ChangeRecordClass' => 'Record Class', 'ChangeRecordID' => 'Record ID', 'ObjectTitle' => 'Record Title', 'ChangedBy.Title' => 'User', 'Created' => 'Modification Date', ); private static $searchable_fields = array( 'ChangeType', 'ObjectTitle', 'ChangeRecordClass', 'ChangeRecordID', ); private static $default_sort = 'ID DESC'; /** * Should request variables be saved too? * * @var boolean */ private static $save_request_vars = false; private static $field_blacklist = array('Password'); private static $request_vars_blacklist = array('url', 'SecurityID'); public function getCMSFields($params = null) { Requirements::css('symbiote/silverstripe-datachange-tracker: client/css/datachange-tracker.css'); $fields = FieldList::create( ToggleCompositeField::create( 'Details', 'Details', array( ReadonlyField::create('ChangeType', 'Type of change'), ReadonlyField::create('ChangeRecordClass', 'Record Class'), ReadonlyField::create('ChangeRecordID', 'Record ID'), ReadonlyField::create('ObjectTitle', 'Record Title'), ReadonlyField::create('Created', 'Modification Date'), ReadonlyField::create('Stage', 'Stage'), ReadonlyField::create('User', 'User', $this->getMemberDetails()), ReadonlyField::create('CurrentURL', 'URL'), ReadonlyField::create('Referer', 'Referer'), ReadonlyField::create('RemoteIP', 'Remote IP'), ReadonlyField::create('Agent', 'Agent'), ) )->setStartClosed(false)->addExtraClass('datachange-field'), ToggleCompositeField::create( 'RawData', 'Raw Data', array( ReadonlyField::create('Before'), ReadonlyField::create('After'), ReadonlyField::create('GetVars'), ReadonlyField::create('PostVars'), ) )->setStartClosed(false)->addExtraClass('datachange-field') ); if (strlen($this->Before) && strlen($this->ChangeRecordClass) && class_exists($this->ChangeRecordClass)) { $before = Injector::inst()->create($this->ChangeRecordClass, $this->prepareForDataDifferencer($this->Before), true); $after = Injector::inst()->create($this->ChangeRecordClass, $this->prepareForDataDifferencer($this->After), true); $diff = DataDifferencer::create($before, $after); // The solr search service injector dependency causes issues with comparison, since it has public variables that are stored in an array. $diff->ignoreFields(array('searchService')); $diffed = $diff->diffedData(); $diffText = ''; $changedFields = array(); foreach ($diffed->toMap() as $field => $prop) { if (is_object($prop)) { continue; } if (is_array($prop)) { $prop = json_encode($prop); } $changedFields[] = $readOnly = \SilverStripe\Forms\ReadonlyField::create( 'ChangedField'.$field, $field, $prop ); $readOnly->addExtraClass('datachange-field'); } $fields->insertBefore( ToggleCompositeField::create('FieldChanges', 'Changed Fields', $changedFields) ->setStartClosed(false) ->addExtraClass('datachange-field'), 'RawData' ); } // Flags fields that cannot be rendered with 'forTemplate'. This prevents bugs where // WorkflowService (of AdvancedWorkflow Module) and BlockManager (of Sheadawson/blocks module) get put // into a field and break the page. $fieldsToRemove = array(); foreach ($fields->dataFields() as $field) { $value = $field->Value(); if ($value && is_object($value)) { if ((method_exists($value, 'hasMethod') && !$value->hasMethod('forTemplate')) || !method_exists( $value, 'forTemplate' )) { $field->setValue('[Missing '.get_class($value).'::forTemplate]'); } } } $fields = $fields->makeReadonly(); return $fields; } /** * Track a change to a DataObject * * @return DataChangeRecord * */ public function track(DataObject $changedObject, $type = 'Change') { $changes = $changedObject->getChangedFields(true, 2); if (count($changes)) { // remove any changes to ignored fields $ignored = $changedObject->hasMethod('getIgnoredFields') ? $changedObject->getIgnoredFields() : null; if ($ignored) { $changes = array_diff_key($changes, $ignored); foreach ($ignored as $ignore) { if (isset($changes[$ignore])) { unset($changes[$ignore]); } } } } foreach (self::config()->field_blacklist as $key) { if (isset($changes[$key])) { unset($changes[$key]); } } if ((empty($changes) && $type == 'Change')) { return; } if ($type === 'Delete' && Versioned::get_reading_mode() === 'Stage.Live') { $type = 'Delete from Live'; } $this->ChangeType = $type; $this->ChangeRecordClass = $changedObject->ClassName; $this->ChangeRecordID = $changedObject->ID; // @TODO this will cause issue for objects without titles $this->ObjectTitle = $changedObject->Title; $this->Stage = Versioned::get_reading_mode(); $before = array(); $after = array(); if ($type != 'Change' && $type != 'New') { // If we are (un)publishing we want to store the entire object $before = ($type === 'Unpublish') ? $changedObject->toMap() : null; $after = ($type === 'Publish') ? $changedObject->toMap() : null; } else { // Else we're tracking the changes to the object foreach ($changes as $field => $change) { if ($field == 'SecurityID') { continue; } $before[$field] = $change['before']; $after[$field] = $change['after']; } } if ($this->Before && $this->Before !== 'null' && is_array($before)) { //merge the old array last to keep it's value as we want keep the earliest version of each field $this->Before = json_encode(array_replace(json_decode($this->Before, true), $before)); } else { $this->Before = json_encode($before); } if ($this->After && $this->After !== 'null' && is_array($after)) { //merge the new array last to keep it's value as we want the newest version of each field $this->After = json_encode(array_replace($after, json_decode($this->After, true))); } else { $this->After = json_encode($after); } if (self::config()->save_request_vars) { foreach (self::config()->request_vars_blacklist as $key) { unset($_GET[$key]); unset($_POST[$key]); } $this->GetVars = isset($_GET) ? json_encode($_GET) : null; $this->PostVars = isset($_POST) ? json_encode($_POST) : null; } $this->ChangedByID = Member::currentUserID(); if (Member::currentUserID() && Member::currentUser()) { $this->CurrentEmail = Member::currentUser()->Email; } if (isset($_SERVER['SERVER_NAME'])) { $protocol = 'http'; $protocol = isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on" ? 'https://' : 'http://'; $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '80'; $this->CurrentURL = $protocol.$_SERVER["SERVER_NAME"].":".$port.$_SERVER["REQUEST_URI"]; } elseif (Director::is_cli()) { $this->CurrentURL = 'CLI'; } else { $this->CurrentURL = 'Could not determine current URL'; } $this->RemoteIP = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : (Director::is_cli() ? 'CLI' : 'Unknown remote addr'); $this->Referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; $this->Agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $this->write(); return $this; } /** * @return boolean * */ public function canDelete($member = null) { return false; } /** * @return string * */ public function getTitle() { return $this->ChangeRecordClass.' #'.$this->ChangeRecordID; } /** * Return a description/summary of the user * * @return string * */ public function getMemberDetails() { if ($user = $this->ChangedBy()) { $name = $user->getTitle(); if ($user->Email) { $name .= " <$user->Email>"; } return $name; } } private function prepareForDataDifferencer($jsonData) { // NOTE(Jake): 2018-06-21 // // Data Differencer cannot handle arrays within an array, // // So JSON data that comes from MultiValueField / Text DB fields // causes errors to be thrown. // // So solve this, we simply only decode to a depth of 1. (rather than the 512 default) // $resultJsonData = json_decode($jsonData, true, 1); return $resultJsonData; } } |