@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement bin/remove, for structured destruction of objects

Summary:
Ref T4749. Ref T3265. Ref T4909. Several goals here:

- Move user destruction to the CLI to limit the power of rogue admins.
- Start consolidating all "destroy named object" scripts into a single UI, to make it easier to know how to destroy things.
- Structure object destruction so we can do a better and more automatic job of cleaning up transactions, edges, search indexes, etc.
- Log when we destroy objects so there's a record if data goes missing.

Test Plan: Used `bin/remove destroy` to destroy several users.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T3265, T4749, T4909

Differential Revision: https://secure.phabricator.com/D8940

+391 -115
+1
bin/remove
··· 1 + ../scripts/setup/manage_remove.php
+9
resources/sql/autopatches/20140501.remove.1.dlog.sql
··· 1 + CREATE TABLE {$NAMESPACE}_system.system_destructionlog ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + objectClass VARCHAR(128) NOT NULL COLLATE utf8_bin, 4 + rootLogID INT UNSIGNED, 5 + objectPHID VARCHAR(64) COLLATE utf8_bin, 6 + objectMonogram VARCHAR(64) COLLATE utf8_bin, 7 + epoch INT UNSIGNED NOT NULL, 8 + KEY `key_epoch` (epoch) 9 + ) ENGINE=InnoDB, COLLATE utf8_general_ci;
+21
scripts/setup/manage_remove.php
··· 1 + #!/usr/bin/env php 2 + <?php 3 + 4 + $root = dirname(dirname(dirname(__FILE__))); 5 + require_once $root.'/scripts/__init_script__.php'; 6 + 7 + $args = new PhutilArgumentParser($argv); 8 + $args->setTagline('remove objects'); 9 + $args->setSynopsis(<<<EOSYNOPSIS 10 + **remove** __command__ [__options__] 11 + Administrative tool for destroying objects permanently. 12 + 13 + EOSYNOPSIS 14 + ); 15 + $args->parseStandardArguments(); 16 + 17 + $workflows = id(new PhutilSymbolLoader()) 18 + ->setAncestorClass('PhabricatorSystemRemoveWorkflow') 19 + ->loadObjects(); 20 + $workflows[] = new PhutilHelpArgumentWorkflow(); 21 + $args->parseWorkflows($workflows);
+14
src/__phutil_library_map__.php
··· 1471 1471 'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php', 1472 1472 'PhabricatorDefaultFileStorageEngineSelector' => 'applications/files/engineselector/PhabricatorDefaultFileStorageEngineSelector.php', 1473 1473 'PhabricatorDefaultSearchEngineSelector' => 'applications/search/selector/PhabricatorDefaultSearchEngineSelector.php', 1474 + 'PhabricatorDestructableInterface' => 'applications/system/interface/PhabricatorDestructableInterface.php', 1475 + 'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php', 1474 1476 'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php', 1475 1477 'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php', 1476 1478 'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php', ··· 2179 2181 'PhabricatorSystemActionLog' => 'applications/system/storage/PhabricatorSystemActionLog.php', 2180 2182 'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php', 2181 2183 'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php', 2184 + 'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php', 2185 + 'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php', 2186 + 'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php', 2187 + 'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php', 2188 + 'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php', 2182 2189 'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php', 2183 2190 'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php', 2184 2191 'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php', ··· 4297 4304 'PhabricatorDebugController' => 'PhabricatorController', 4298 4305 'PhabricatorDefaultFileStorageEngineSelector' => 'PhabricatorFileStorageEngineSelector', 4299 4306 'PhabricatorDefaultSearchEngineSelector' => 'PhabricatorSearchEngineSelector', 4307 + 'PhabricatorDestructionEngine' => 'Phobject', 4300 4308 'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions', 4301 4309 'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions', 4302 4310 'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator', ··· 5103 5111 'PhabricatorSystemActionLog' => 'PhabricatorSystemDAO', 5104 5112 'PhabricatorSystemActionRateLimitException' => 'Exception', 5105 5113 'PhabricatorSystemDAO' => 'PhabricatorLiskDAO', 5114 + 'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector', 5115 + 'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO', 5116 + 'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow', 5117 + 'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow', 5118 + 'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow', 5106 5119 'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', 5107 5120 'PhabricatorTestCase' => 'ArcanistPhutilTestCase', 5108 5121 'PhabricatorTestController' => 'PhabricatorController', ··· 5159 5172 1 => 'PhutilPerson', 5160 5173 2 => 'PhabricatorPolicyInterface', 5161 5174 3 => 'PhabricatorCustomFieldInterface', 5175 + 4 => 'PhabricatorDestructableInterface', 5162 5176 ), 5163 5177 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', 5164 5178 'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',
+9 -51
src/applications/people/controller/PhabricatorPeopleDeleteController.php
··· 27 27 return $this->buildDeleteSelfResponse($profile_uri); 28 28 } 29 29 30 - $errors = array(); 31 - 32 - $v_username = ''; 33 - $e_username = true; 34 - if ($request->isFormPost()) { 35 - $v_username = $request->getStr('username'); 36 - 37 - if (!strlen($v_username)) { 38 - $errors[] = pht( 39 - 'You must type the username to confirm that you want to delete '. 40 - 'this user account.'); 41 - $e_username = pht('Required'); 42 - } else if ($v_username != $user->getUsername()) { 43 - $errors[] = pht( 44 - 'You must type the username correctly to confirm that you want '. 45 - 'to delete this user account.'); 46 - $e_username = pht('Incorrect'); 47 - } 48 - 49 - if (!$errors) { 50 - id(new PhabricatorUserEditor()) 51 - ->setActor($admin) 52 - ->deleteUser($user); 53 - 54 - $done_uri = $this->getApplicationURI(); 55 - 56 - return id(new AphrontRedirectResponse())->setURI($done_uri); 57 - } 58 - } 59 - 60 30 $str1 = pht( 61 31 'Be careful when deleting users! This will permanently and '. 62 32 'irreversibly destroy this user account.'); ··· 66 36 'disable them, not delete them. If you delete them, it will no longer '. 67 37 'be possible to (for example) search for objects they created, and you '. 68 38 'will lose other information about their history. Disabling them '. 69 - 'instead will prevent them from logging in but not destroy any of '. 39 + 'instead will prevent them from logging in, but will not destroy any of '. 70 40 'their data.'); 71 41 72 42 $str3 = pht( ··· 74 44 'so on), but less safe to delete established users. If possible, '. 75 45 'disable them instead.'); 76 46 47 + $str4 = pht( 48 + 'To permanently destroy this user, run this command:'); 49 + 77 50 $form = id(new AphrontFormView()) 78 51 ->setUser($admin) 79 52 ->appendRemarkupInstructions( 80 53 pht( 81 - 'To confirm that you want to permanently and irrevocably destroy '. 82 - 'this user account, type their username:')) 83 - ->appendChild( 84 - id(new AphrontFormStaticControl()) 85 - ->setLabel(pht('Username')) 86 - ->setValue($user->getUsername())) 87 - ->appendChild( 88 - id(new AphrontFormTextControl()) 89 - ->setLabel(pht('Confirm')) 90 - ->setValue($v_username) 91 - ->setName('username') 92 - ->setError($e_username)); 93 - 94 - if ($errors) { 95 - $errors = id(new AphrontErrorView())->setErrors($errors); 96 - } 54 + " phabricator/ $ ./bin/remove destroy %s\n", 55 + csprintf('%R', '@'.$user->getUsername()))); 97 56 98 57 return $this->newDialog() 99 58 ->setWidth(AphrontDialogView::WIDTH_FORM) 100 - ->setTitle(pht('Really Delete User?')) 59 + ->setTitle(pht('Permanently Delete User')) 101 60 ->setShortTitle(pht('Delete User')) 102 - ->appendChild($errors) 103 61 ->appendParagraph($str1) 104 62 ->appendParagraph($str2) 105 63 ->appendParagraph($str3) 64 + ->appendParagraph($str4) 106 65 ->appendChild($form->buildLayoutView()) 107 - ->addSubmitButton(pht('Delete User')) 108 - ->addCancelButton($profile_uri); 66 + ->addCancelButton($profile_uri, pht('Close')); 109 67 } 110 68 111 69 private function buildDeleteSelfResponse($profile_uri) {
-63
src/applications/people/editor/PhabricatorUserEditor.php
··· 332 332 return $this; 333 333 } 334 334 335 - /** 336 - * @task role 337 - */ 338 - public function deleteUser(PhabricatorUser $user, $disable) { 339 - $actor = $this->requireActor(); 340 - 341 - if (!$user->getID()) { 342 - throw new Exception("User has not been created yet!"); 343 - } 344 - 345 - if ($actor->getPHID() == $user->getPHID()) { 346 - throw new Exception("You can not delete yourself!"); 347 - } 348 - 349 - $user->openTransaction(); 350 - $externals = id(new PhabricatorExternalAccount())->loadAllWhere( 351 - 'userPHID = %s', 352 - $user->getPHID()); 353 - foreach ($externals as $external) { 354 - $external->delete(); 355 - } 356 - 357 - $prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 358 - 'userPHID = %s', 359 - $user->getPHID()); 360 - foreach ($prefs as $pref) { 361 - $pref->delete(); 362 - } 363 - 364 - $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 365 - 'userPHID = %s', 366 - $user->getPHID()); 367 - foreach ($profiles as $profile) { 368 - $profile->delete(); 369 - } 370 - 371 - $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 372 - 'userPHID = %s', 373 - $user->getPHID()); 374 - foreach ($keys as $key) { 375 - $key->delete(); 376 - } 377 - 378 - $emails = id(new PhabricatorUserEmail())->loadAllWhere( 379 - 'userPHID = %s', 380 - $user->getPHID()); 381 - foreach ($emails as $email) { 382 - $email->delete(); 383 - } 384 - 385 - $log = PhabricatorUserLog::initializeNewLog( 386 - $actor, 387 - $user->getPHID(), 388 - PhabricatorUserLog::ACTION_DELETE); 389 - $log->save(); 390 - 391 - $user->delete(); 392 - 393 - $user->saveTransaction(); 394 - 395 - return $this; 396 - } 397 - 398 335 399 336 /* -( Adding, Removing and Changing Email )-------------------------------- */ 400 337
+69 -1
src/applications/people/storage/PhabricatorUser.php
··· 5 5 implements 6 6 PhutilPerson, 7 7 PhabricatorPolicyInterface, 8 - PhabricatorCustomFieldInterface { 8 + PhabricatorCustomFieldInterface, 9 + PhabricatorDestructableInterface { 9 10 10 11 const SESSION_TABLE = 'phabricator_session'; 11 12 const NAMETOKEN_TABLE = 'user_nametoken'; ··· 137 138 // To satisfy PhutilPerson. 138 139 public function getSex() { 139 140 return $this->sex; 141 + } 142 + 143 + public function getMonogram() { 144 + return '@'.$this->getUsername(); 140 145 } 141 146 142 147 public function getTranslation() { ··· 813 818 $this->customFields = $fields; 814 819 return $this; 815 820 } 821 + 822 + 823 + /* -( PhabricatorDestructableInterface )----------------------------------- */ 824 + 825 + 826 + public function destroyObjectPermanently( 827 + PhabricatorDestructionEngine $engine) { 828 + 829 + $this->openTransaction(); 830 + $this->delete(); 831 + 832 + $externals = id(new PhabricatorExternalAccount())->loadAllWhere( 833 + 'userPHID = %s', 834 + $this->getPHID()); 835 + foreach ($externals as $external) { 836 + $external->delete(); 837 + } 838 + 839 + $prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 840 + 'userPHID = %s', 841 + $this->getPHID()); 842 + foreach ($prefs as $pref) { 843 + $pref->delete(); 844 + } 845 + 846 + $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 847 + 'userPHID = %s', 848 + $this->getPHID()); 849 + foreach ($profiles as $profile) { 850 + $profile->delete(); 851 + } 852 + 853 + $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 854 + 'userPHID = %s', 855 + $this->getPHID()); 856 + foreach ($keys as $key) { 857 + $key->delete(); 858 + } 859 + 860 + $emails = id(new PhabricatorUserEmail())->loadAllWhere( 861 + 'userPHID = %s', 862 + $this->getPHID()); 863 + foreach ($emails as $email) { 864 + $email->delete(); 865 + } 866 + 867 + $sessions = id(new PhabricatorAuthSession())->loadAllWhere( 868 + 'userPHID = %s', 869 + $this->getPHID()); 870 + foreach ($sessions as $session) { 871 + $session->delete(); 872 + } 873 + 874 + $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 875 + 'userPHID = %s', 876 + $this->getPHID()); 877 + foreach ($factors as $factor) { 878 + $factor->delete(); 879 + } 880 + 881 + $this->saveTransaction(); 882 + } 883 + 816 884 817 885 }
+60
src/applications/system/engine/PhabricatorDestructionEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorDestructionEngine extends Phobject { 4 + 5 + private $rootLogID; 6 + 7 + public function destroyObject(PhabricatorDestructableInterface $object) { 8 + $log = id(new PhabricatorSystemDestructionLog()) 9 + ->setEpoch(time()) 10 + ->setObjectClass(get_class($object)); 11 + 12 + if ($this->rootLogID) { 13 + $log->setRootLogID($this->rootLogID); 14 + } 15 + 16 + $object_phid = null; 17 + if (method_exists($object, 'getPHID')) { 18 + try { 19 + $object_phid = $object->getPHID(); 20 + $log->setObjectPHID($object_phid); 21 + } catch (Exception $ex) { 22 + // Ignore. 23 + } 24 + } 25 + 26 + if (method_exists($object, 'getMonogram')) { 27 + try { 28 + $log->setObjectMonogram($object->getMonogram()); 29 + } catch (Exception $ex) { 30 + // Ignore. 31 + } 32 + } 33 + 34 + $log->save(); 35 + 36 + if (!$this->rootLogID) { 37 + $this->rootLogID = $log->getID(); 38 + } 39 + 40 + $object->destroyObjectPermanently($this); 41 + 42 + if ($object_phid) { 43 + $this->destroyEdges($object_phid); 44 + } 45 + } 46 + 47 + private function destroyEdges($src_phid) { 48 + $edges = id(new PhabricatorEdgeQuery()) 49 + ->withSourcePHIDs(array($src_phid)) 50 + ->execute(); 51 + 52 + $editor = id(new PhabricatorEdgeEditor()) 53 + ->setSuppressEvents(true); 54 + foreach ($edges as $edge) { 55 + $editor->removeEdge($edge['src'], $edge['type'], $edge['dst']); 56 + } 57 + $editor->save(); 58 + } 59 + 60 + }
+21
src/applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php
··· 1 + <?php 2 + 3 + final class PhabricatorSystemDestructionGarbageCollector 4 + extends PhabricatorGarbageCollector { 5 + 6 + public function collectGarbage() { 7 + $ttl = phutil_units('90 days in seconds'); 8 + 9 + $table = new PhabricatorSystemDestructionLog(); 10 + $conn_w = $table->establishConnection('w'); 11 + 12 + queryfx( 13 + $conn_w, 14 + 'DELETE FROM %T WHERE epoch < %d LIMIT 100', 15 + $table->getTableName(), 16 + time() - $ttl); 17 + 18 + return ($conn_w->getAffectedRows() == 100); 19 + } 20 + 21 + }
+24
src/applications/system/interface/PhabricatorDestructableInterface.php
··· 1 + <?php 2 + 3 + interface PhabricatorDestructableInterface { 4 + 5 + public function destroyObjectPermanently( 6 + PhabricatorDestructionEngine $engine); 7 + 8 + } 9 + 10 + 11 + // TEMPLATE IMPLEMENTATION ///////////////////////////////////////////////////// 12 + 13 + 14 + /* -( PhabricatorDestructableInterface )----------------------------------- */ 15 + /* 16 + 17 + public function destroyObjectPermanently( 18 + PhabricatorDestructionEngine $engine) { 19 + 20 + <<<$this->nuke();>>> 21 + 22 + } 23 + 24 + */
+110
src/applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php
··· 1 + <?php 2 + 3 + final class PhabricatorSystemRemoveDestroyWorkflow 4 + extends PhabricatorSystemRemoveWorkflow { 5 + 6 + public function didConstruct() { 7 + $this 8 + ->setName('destroy') 9 + ->setSynopsis(pht('Permanently destroy objects.')) 10 + ->setExamples('**destroy** [__options__] __object__ ...') 11 + ->setArguments( 12 + array( 13 + array( 14 + 'name' => 'force', 15 + 'help' => pht('Destroy objects without prompting.'), 16 + ), 17 + array( 18 + 'name' => 'objects', 19 + 'wildcard' => true, 20 + ), 21 + )); 22 + } 23 + 24 + public function execute(PhutilArgumentParser $args) { 25 + $console = PhutilConsole::getConsole(); 26 + 27 + $object_names = $args->getArg('objects'); 28 + if (!$object_names) { 29 + throw new PhutilArgumentUsageException( 30 + pht('Specify one or more objects to destroy.')); 31 + } 32 + 33 + $object_query = id(new PhabricatorObjectQuery()) 34 + ->setViewer($this->getViewer()) 35 + ->withNames($object_names); 36 + 37 + $object_query->execute(); 38 + 39 + $named_objects = $object_query->getNamedResults(); 40 + foreach ($object_names as $object_name) { 41 + if (empty($named_objects[$object_name])) { 42 + throw new PhutilArgumentUsageException( 43 + pht('No such object "%s" exists!', $object_name)); 44 + } 45 + } 46 + 47 + foreach ($named_objects as $object_name => $object) { 48 + if (!($object instanceof PhabricatorDestructableInterface)) { 49 + throw new PhutilArgumentUsageException( 50 + pht( 51 + 'Object "%s" can not be destroyed (it does not implement %s).', 52 + $object_name, 53 + 'PhabricatorDestructableInterface')); 54 + } 55 + } 56 + 57 + $console->writeOut( 58 + "<bg:red>**%s**</bg>\n\n", 59 + pht(' IMPORTANT: OBJECTS WILL BE PERMANENTLY DESTROYED! ')); 60 + 61 + $console->writeOut( 62 + pht( 63 + "There is no way to undo this operation or ever retrieve this data.". 64 + "\n\n". 65 + "These %s object(s) will be **completely destroyed forever**:". 66 + "\n\n", 67 + new PhutilNumber(count($named_objects)))); 68 + 69 + foreach ($named_objects as $object_name => $object) { 70 + $console->writeOut( 71 + " - %s (%s)\n", 72 + $object_name, 73 + get_class($object)); 74 + } 75 + 76 + $force = $args->getArg('force'); 77 + if (!$force) { 78 + $ok = $console->confirm( 79 + pht( 80 + 'Are you absolutely certain you want to destroy these %s object(s)?', 81 + new PhutilNumber(count($named_objects)))); 82 + if (!$ok) { 83 + throw new PhutilArgumentUsageException( 84 + pht('Aborted, your objects are safe.')); 85 + } 86 + } 87 + 88 + $console->writeOut("%s\n", pht('Destroying objects...')); 89 + 90 + foreach ($named_objects as $object_name => $object) { 91 + $console->writeOut( 92 + pht( 93 + "Destroying %s **%s**...\n", 94 + get_class($object), 95 + $object_name)); 96 + 97 + id(new PhabricatorDestructionEngine()) 98 + ->destroyObject($object); 99 + } 100 + 101 + $console->writeOut( 102 + "%s\n", 103 + pht( 104 + 'Permanently destroyed %s object(s).', 105 + new PhutilNumber(count($named_objects)))); 106 + 107 + return 0; 108 + } 109 + 110 + }
+30
src/applications/system/management/PhabricatorSystemRemoveLogWorkflow.php
··· 1 + <?php 2 + 3 + final class PhabricatorSystemRemoveLogWorkflow 4 + extends PhabricatorSystemRemoveWorkflow { 5 + 6 + public function didConstruct() { 7 + $this 8 + ->setName('log') 9 + ->setSynopsis(pht('Show a log of permanently destroyed objects.')) 10 + ->setExamples('**log**') 11 + ->setArguments(array()); 12 + } 13 + 14 + public function execute(PhutilArgumentParser $args) { 15 + $console = PhutilConsole::getConsole(); 16 + 17 + $table = new PhabricatorSystemDestructionLog(); 18 + foreach (new LiskMigrationIterator($table) as $row) { 19 + $console->writeOut( 20 + "[%s]\t%s\t%s\t%s\n", 21 + phabricator_datetime($row->getEpoch(), $this->getViewer()), 22 + $row->getObjectClass(), 23 + $row->getObjectPHID(), 24 + $row->getObjectMonogram()); 25 + } 26 + 27 + return 0; 28 + } 29 + 30 + }
+6
src/applications/system/management/PhabricatorSystemRemoveWorkflow.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorSystemRemoveWorkflow 4 + extends PhabricatorManagementWorkflow { 5 + 6 + }
+17
src/applications/system/storage/PhabricatorSystemDestructionLog.php
··· 1 + <?php 2 + 3 + final class PhabricatorSystemDestructionLog extends PhabricatorSystemDAO { 4 + 5 + protected $objectClass; 6 + protected $rootLogID; 7 + protected $objectPHID; 8 + protected $objectMonogram; 9 + protected $epoch; 10 + 11 + public function getConfiguration() { 12 + return array( 13 + self::CONFIG_TIMESTAMPS => false, 14 + ) + parent::getConfiguration(); 15 + } 16 + 17 + }