From a9fc343d9eda64981af902b2551aebcf277fa7b0 Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Thu, 30 Oct 2014 08:59:21 -0700 Subject: [PATCH] Phriction - start the move towards transactions and an editor Summary: This implements as little as possible to stick a working transactions + editor codepath in the basic create / edit flow. Aside from the transaction tables, this also required adding a mailKey to a phrictionDocument. Future work would include adding more transactions types for things like "move" and all the pertinent support. Even future work is to add things like policies which will work easily in the transaction framework. Ref T4029. Test Plan: - made a wiki doc - edit a wiki doc - had someone subscribe to a wiki doc and edited it For all three, the edits worked, a reasonable email was sent out, and feed stories were generated. - made a wiki doc at a /location/like/this document "stubs" were made as expected in /location and /location/like Reviewers: epriestley Reviewed By: epriestley Subscribers: chad, Korvin, epriestley Maniphest Tasks: T4029 Differential Revision: https://secure.phabricator.com/D10756 --- .../20141025.phriction.1.xaction.sql | 19 ++ .../20141025.phriction.2.xaction.sql | 16 + .../20141025.phriction.mailkey.sql | 2 + src/__phutil_library_map__.php | 10 + .../controller/PhrictionEditController.php | 30 +- .../editor/PhrictionDocumentEditor.php | 2 +- .../editor/PhrictionTransactionEditor.php | 298 ++++++++++++++++++ .../phriction/mail/PhrictionReplyHandler.php | 41 +++ .../query/PhrictionTransactionQuery.php | 10 + .../phriction/storage/PhrictionDocument.php | 22 ++ .../storage/PhrictionTransaction.php | 186 +++++++++++ .../storage/PhrictionTransactionComment.php | 10 + 12 files changed, 631 insertions(+), 15 deletions(-) create mode 100644 resources/sql/autopatches/20141025.phriction.1.xaction.sql create mode 100644 resources/sql/autopatches/20141025.phriction.2.xaction.sql create mode 100644 resources/sql/autopatches/20141025.phriction.mailkey.sql create mode 100644 src/applications/phriction/editor/PhrictionTransactionEditor.php create mode 100644 src/applications/phriction/mail/PhrictionReplyHandler.php create mode 100644 src/applications/phriction/query/PhrictionTransactionQuery.php create mode 100644 src/applications/phriction/storage/PhrictionTransaction.php create mode 100644 src/applications/phriction/storage/PhrictionTransactionComment.php diff --git a/resources/sql/autopatches/20141025.phriction.1.xaction.sql b/resources/sql/autopatches/20141025.phriction.1.xaction.sql new file mode 100644 index 0000000000..c2527cf1bc --- /dev/null +++ b/resources/sql/autopatches/20141025.phriction.1.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phriction.phriction_transaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) COLLATE utf8_bin NOT NULL, + authorPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + objectPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + viewPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + editPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + commentPHID VARCHAR(64) COLLATE utf8_bin DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE utf8_bin NOT NULL, + oldValue LONGTEXT COLLATE utf8_bin NOT NULL, + newValue LONGTEXT COLLATE utf8_bin NOT NULL, + contentSource LONGTEXT COLLATE utf8_bin NOT NULL, + metadata LONGTEXT COLLATE utf8_bin NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/autopatches/20141025.phriction.2.xaction.sql b/resources/sql/autopatches/20141025.phriction.2.xaction.sql new file mode 100644 index 0000000000..ddbb4c5da9 --- /dev/null +++ b/resources/sql/autopatches/20141025.phriction.2.xaction.sql @@ -0,0 +1,16 @@ +CREATE TABLE {$NAMESPACE}_phriction.phriction_transaction_comment ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + transactionPHID VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + authorPHID VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + viewPolicy VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + editPolicy VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + commentVersion INT UNSIGNED NOT NULL, + content LONGTEXT CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + contentSource LONGTEXT CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + isDeleted TINYINT(1) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 diff --git a/resources/sql/autopatches/20141025.phriction.mailkey.sql b/resources/sql/autopatches/20141025.phriction.mailkey.sql new file mode 100644 index 0000000000..7f0bd4d1f2 --- /dev/null +++ b/resources/sql/autopatches/20141025.phriction.mailkey.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phriction.phriction_document + ADD mailKey VARCHAR(20) NOT NULL COLLATE utf8_bin; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 085f86d8e9..bf4b1842bf 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2771,9 +2771,14 @@ phutil_register_library_map(array( 'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php', 'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php', 'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php', + 'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php', 'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php', 'PhrictionSearchEngine' => 'applications/phriction/query/PhrictionSearchEngine.php', 'PhrictionSearchIndexer' => 'applications/phriction/search/PhrictionSearchIndexer.php', + 'PhrictionTransaction' => 'applications/phriction/storage/PhrictionTransaction.php', + 'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php', + 'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php', + 'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php', 'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php', 'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php', 'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php', @@ -5948,9 +5953,14 @@ phutil_register_library_map(array( 'PhrictionMoveController' => 'PhrictionController', 'PhrictionNewController' => 'PhrictionController', 'PhrictionRemarkupRule' => 'PhutilRemarkupRule', + 'PhrictionReplyHandler' => 'PhabricatorMailReplyHandler', 'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhrictionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhrictionSearchIndexer' => 'PhabricatorSearchDocumentIndexer', + 'PhrictionTransaction' => 'PhabricatorApplicationTransaction', + 'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment', + 'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PonderAddAnswerView' => 'AphrontView', 'PonderAnswer' => array( 'PonderDAO', diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php index b5475e7940..850e5f47fc 100644 --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -73,14 +73,8 @@ final class PhrictionEditController return new Aphront404Response(); } } - $document = new PhrictionDocument(); - $document->setSlug($slug); - - $content = new PhrictionContent(); - $content->setSlug($slug); - - $default_title = PhabricatorSlug::getDefaultTitle($slug); - $content->setTitle($default_title); + $document = PhrictionDocument::initializeNewDocument($user, $slug); + $content = $document->getContent(); } } @@ -174,13 +168,21 @@ final class PhrictionEditController } if (!count($errors)) { - $editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug())) - ->setActor($user) - ->setTitle($title) - ->setContent($request->getStr('content')) - ->setDescription($notes); - $editor->save(); + $xactions = array(); + $xactions[] = id(new PhrictionTransaction()) + ->setTransactionType(PhrictionTransaction::TYPE_TITLE) + ->setNewValue($title); + $xactions[] = id(new PhrictionTransaction()) + ->setTransactionType(PhrictionTransaction::TYPE_CONTENT) + ->setNewValue($request->getStr('content')); + + $editor = id(new PhrictionTransactionEditor()) + ->setActor($user) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setDescription($notes) + ->applyTransactions($document, $xactions); if ($draft) { $draft->delete(); diff --git a/src/applications/phriction/editor/PhrictionDocumentEditor.php b/src/applications/phriction/editor/PhrictionDocumentEditor.php index e3f900844e..84b0e137ce 100644 --- a/src/applications/phriction/editor/PhrictionDocumentEditor.php +++ b/src/applications/phriction/editor/PhrictionDocumentEditor.php @@ -106,7 +106,7 @@ final class PhrictionDocumentEditor extends PhabricatorEditor { return $this->execute(PhrictionChangeType::CHANGE_DELETE, true); } - private function stub() { + public function stub() { return $this->execute(PhrictionChangeType::CHANGE_STUB, true); } diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php new file mode 100644 index 0000000000..c857ff979d --- /dev/null +++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php @@ -0,0 +1,298 @@ +description = $description; + return $this; + } + + private function getDescription() { + return $this->description; + } + + private function setOldContent(PhrictionContent $content) { + $this->oldContent = $content; + return $this; + } + + private function getOldContent() { + return $this->oldContent; + } + + private function setNewContent(PhrictionContent $content) { + $this->newContent = $content; + return $this; + } + + private function getNewContent() { + return $this->newContent; + } + + public function getEditorApplicationClass() { + return 'PhabricatorPhrictionApplication'; + } + + public function getEditorObjectsDescription() { + return pht('Phriction Documents'); + } + + public function getTransactionTypes() { + $types = parent::getTransactionTypes(); + + $types[] = PhabricatorTransactions::TYPE_COMMENT; + $types[] = PhrictionTransaction::TYPE_TITLE; + $types[] = PhrictionTransaction::TYPE_CONTENT; + + /* TODO + $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; + $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; + */ + + return $types; + } + + protected function getCustomTransactionOldValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_TITLE: + if ($this->getIsNewObject()) { + return null; + } + return $this->getOldContent()->getTitle(); + case PhrictionTransaction::TYPE_CONTENT: + if ($this->getIsNewObject()) { + return null; + } + return $this->getOldContent()->getContent(); + } + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_TITLE: + case PhrictionTransaction::TYPE_CONTENT: + return $xaction->getNewValue(); + } + } + + protected function shouldApplyInitialEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_TITLE: + case PhrictionTransaction::TYPE_CONTENT: + return true; + } + } + return parent::shouldApplyInitialEffects($object, $xactions); + } + + protected function applyInitialEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + $this->setOldContent($object->getContent()); + $this->setNewContent($this->buildNewContentTemplate($object)); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_TITLE: + case PhrictionTransaction::TYPE_CONTENT: + $object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS); + return; + } + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_TITLE: + $this->getNewContent()->setTitle($xaction->getNewValue()); + break; + case PhrictionTransaction::TYPE_CONTENT: + $this->getNewContent()->setContent($xaction->getNewValue()); + break; + default: + break; + } + } + + protected function applyFinalEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + $save_content = false; + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_TITLE: + case PhrictionTransaction::TYPE_CONTENT: + $save_content = true; + break; + default: + break; + } + } + + if ($save_content) { + $content = $this->getNewContent(); + $content->setDocumentID($object->getID()); + $content->save(); + + $object->setContentID($content->getID()); + $object->save(); + $object->attachContent($content); + } + + if ($this->getIsNewObject()) { + // Stub out empty parent documents if they don't exist + $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); + if ($ancestral_slugs) { + $ancestors = id(new PhrictionDocument())->loadAllWhere( + 'slug IN (%Ls)', + $ancestral_slugs); + $ancestors = mpull($ancestors, null, 'getSlug'); + foreach ($ancestral_slugs as $slug) { + // We check for change type to prevent near-infinite recursion + if (!isset($ancestors[$slug]) && + $content->getChangeType() != + PhrictionChangeType::CHANGE_STUB) { + id(PhrictionDocumentEditor::newForSlug($slug)) + ->setActor($this->getActor()) + ->setTitle(PhabricatorSlug::getDefaultTitle($slug)) + ->setContent('') + ->setDescription(pht('Empty Parent Document')) + ->stub(); + } + } + } + } + return $xactions; + } + + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + + $xactions = mfilter($xactions, 'shouldHide', true); + return $xactions; + } + + protected function getMailSubjectPrefix() { + return '[Phriction]'; + } + + protected function getMailTo(PhabricatorLiskDAO $object) { + return array( + $object->getContent()->getAuthorPHID(), + $this->getActingAsPHID(), + ); + } + + public function getMailTagsMap() { + return array( + PhrictionTransaction::MAILTAG_TITLE => + pht("A document's title changes."), + PhrictionTransaction::MAILTAG_CONTENT => + pht("A document's content changes."), + ); + } + + protected function buildReplyHandler(PhabricatorLiskDAO $object) { + return id(new PhrictionReplyHandler()) + ->setMailReceiver($object); + } + + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + $id = $object->getID(); + $title = $object->getContent()->getTitle(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject($title) + ->addHeader('Thread-Topic', $object->getPHID()); + } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions) { + + $body = parent::buildMailBody($object, $xactions); + + if ($this->getIsNewObject()) { + $body->addTextSection( + pht('DOCUMENT CONTENT'), + $object->getContent()->getContent()); + } + + $body->addTextSection( + pht('DOCUMENT DETAIL'), + PhabricatorEnv::getProductionURI( + PhrictionDocument::getSlugURI($object->getSlug()))); + + return $body; + } + + protected function shouldPublishFeedStory( + PhabricatorLiskDAO $object, + array $xactions) { + return $this->shouldSendMail($object, $xactions); + } + + protected function getFeedRelatedPHIDs( + PhabricatorLiskDAO $object, + array $xactions) { + + $phids = parent::getFeedRelatedPHIDs($object, $xactions); + // TODO - once the editor supports moves, we'll need to surface the + // "from document phid" to related phids. + return $phids; + } + + protected function supportsSearch() { + return true; + } + + protected function shouldApplyHeraldRules( + PhabricatorLiskDAO $object, + array $xactions) { + return false; + } + + private function buildNewContentTemplate( + PhrictionDocument $document) { + + $new_content = new PhrictionContent(); + $new_content->setSlug($document->getSlug()); + $new_content->setAuthorPHID($this->getActor()->getPHID()); + $new_content->setChangeType(PhrictionChangeType::CHANGE_EDIT); + + $new_content->setTitle($this->getOldContent()->getTitle()); + $new_content->setContent($this->getOldContent()->getContent()); + + if (strlen($this->getDescription())) { + $new_content->setDescription($this->getDescription()); + } + $new_content->setVersion($this->getOldContent()->getVersion() + 1); + + return $new_content; + } + +} diff --git a/src/applications/phriction/mail/PhrictionReplyHandler.php b/src/applications/phriction/mail/PhrictionReplyHandler.php new file mode 100644 index 0000000000..1f6564dbd7 --- /dev/null +++ b/src/applications/phriction/mail/PhrictionReplyHandler.php @@ -0,0 +1,41 @@ +getDefaultPrivateReplyHandlerEmailAddress( + $handle, + PhrictionDocumentPHIDType::TYPECONST); + } + + public function getPublicReplyHandlerEmailAddress() { + return $this->getDefaultPublicReplyHandlerEmailAddress( + PhrictionDocumentPHIDType::TYPECONST); + } + + public function getReplyHandlerDomain() { + return PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain'); + } + + public function getReplyHandlerInstructions() { + if ($this->supportsReplies()) { + // TODO: Implement. + return null; + } else { + return null; + } + } + + protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { + // TODO: Implement. + return null; + } + +} diff --git a/src/applications/phriction/query/PhrictionTransactionQuery.php b/src/applications/phriction/query/PhrictionTransactionQuery.php new file mode 100644 index 0000000000..c43006364b --- /dev/null +++ b/src/applications/phriction/query/PhrictionTransactionQuery.php @@ -0,0 +1,10 @@ +setSlug($slug); + + $content = new PhrictionContent(); + $content->setSlug($slug); + + $default_title = PhabricatorSlug::getDefaultTitle($slug); + $content->setTitle($default_title); + $document->attachContent($content); + + return $document; + } + + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); + } + public static function getSlugURI($slug, $type = 'document') { static $types = array( 'document' => '/w/', diff --git a/src/applications/phriction/storage/PhrictionTransaction.php b/src/applications/phriction/storage/PhrictionTransaction.php new file mode 100644 index 0000000000..afbd1e2f98 --- /dev/null +++ b/src/applications/phriction/storage/PhrictionTransaction.php @@ -0,0 +1,186 @@ +getTransactionType()) { + case self::TYPE_CONTENT: + $blocks[] = $this->getNewValue(); + break; + } + + return $blocks; + } + + public function shouldHide() { + switch ($this->getTransactionType()) { + case self::TYPE_CONTENT: + if ($this->getOldValue() === null) { + return true; + } else { + return false; + } + break; + } + + return parent::shouldHide(); + } + + public function getActionStrength() { + switch ($this->getTransactionType()) { + case self::TYPE_TITLE: + return 1.4; + case self::TYPE_CONTENT: + return 1.3; + } + + return parent::getActionStrength(); + } + + public function getActionName() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_TITLE: + if ($old === null) { + return pht('Created'); + } + + return pht('Retitled'); + + case self::TYPE_CONTENT: + return pht('Edited'); + + } + + return parent::getActionName(); + } + + public function getIcon() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_TITLE: + case self::TYPE_CONTENT: + return 'fa-pencil'; + } + + return parent::getIcon(); + } + + + + public function getTitle() { + $author_phid = $this->getAuthorPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_TITLE: + if ($old === null) { + return pht( + '%s created this document.', + $this->renderHandleLink($author_phid)); + } + return pht( + '%s changed the title from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + + case self::TYPE_CONTENT: + return pht( + '%s edited the document content.', + $this->renderHandleLink($author_phid)); + + } + + return parent::getTitle(); + } + + public function getTitleForFeed(PhabricatorFeedStory $story) { + $author_phid = $this->getAuthorPHID(); + $object_phid = $this->getObjectPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_TITLE: + if ($old === null) { + return pht( + '%s created %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + + return pht( + '%s renamed %s from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $old, + $new); + + case self::TYPE_CONTENT: + return pht( + '%s edited the content of %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + + } + return parent::getTitleForFeed($story); + } + + public function hasChangeDetails() { + switch ($this->getTransactionType()) { + case self::TYPE_CONTENT: + return true; + } + return parent::hasChangeDetails(); + } + + public function renderChangeDetails(PhabricatorUser $viewer) { + return $this->renderTextCorpusChangeDetails( + $viewer, + $this->getOldValue(), + $this->getNewValue()); + } + + public function getMailTags() { + $tags = array(); + switch ($this->getTransactionType()) { + case self::TYPE_TITLE: + $tags[] = self::MAILTAG_TITLE; + break; + case self::TYPE_CONTENT: + $tags[] = self::MAILTAG_CONTENT; + break; + } + return $tags; + } + +} diff --git a/src/applications/phriction/storage/PhrictionTransactionComment.php b/src/applications/phriction/storage/PhrictionTransactionComment.php new file mode 100644 index 0000000000..6f0aee04c5 --- /dev/null +++ b/src/applications/phriction/storage/PhrictionTransactionComment.php @@ -0,0 +1,10 @@ +