diff --git a/resources/sql/autopatches/20150721.phurl.1.url.sql b/resources/sql/autopatches/20150721.phurl.1.url.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150721.phurl.1.url.sql @@ -0,0 +1,10 @@ +CREATE TABLE {$NAMESPACE}_phurl.phurl_url ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, + longURL VARCHAR(2047) NOT NULL COLLATE {$COLLATE_TEXT}, + description VARCHAR(2047) NOT NULL COLLATE {$COLLATE_TEXT}, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + spacePHID varbinary(64) DEFAULT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20150721.phurl.2.xaction.sql b/resources/sql/autopatches/20150721.phurl.2.xaction.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150721.phurl.2.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phurl.phurl_urltransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20150721.phurl.3.xactioncomment.sql b/resources/sql/autopatches/20150721.phurl.3.xactioncomment.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150721.phurl.3.xactioncomment.sql @@ -0,0 +1,16 @@ +CREATE TABLE {$NAMESPACE}_phurl.phurl_urltransaction_comment ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + transactionPHID VARBINARY(64) DEFAULT NULL, + authorPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentVersion INT UNSIGNED NOT NULL, + content LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} 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 COLLATE {$COLLATE_TEXT} diff --git a/resources/sql/autopatches/20150721.phurl.4.url.sql b/resources/sql/autopatches/20150721.phurl.4.url.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150721.phurl.4.url.sql @@ -0,0 +1,3 @@ +ALTER TABLE {$NAMESPACE}_phurl.phurl_url + ADD dateCreated int unsigned NOT NULL, + ADD dateModified int unsigned NOT NULL; diff --git a/resources/sql/autopatches/20150721.phurl.5.edge.sql b/resources/sql/autopatches/20150721.phurl.5.edge.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150721.phurl.5.edge.sql @@ -0,0 +1,16 @@ +CREATE TABLE {$NAMESPACE}_phurl.edge ( + src VARBINARY(64) NOT NULL, + type INT UNSIGNED NOT NULL, + dst VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + seq INT UNSIGNED NOT NULL, + dataID INT UNSIGNED, + PRIMARY KEY (src, type, dst), + KEY `src` (src, type, dateCreated, seq), + UNIQUE KEY `key_dst` (dst, type, src) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; + +CREATE TABLE {$NAMESPACE}_phurl.edgedata ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT} +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20150721.phurl.6.alias.sql b/resources/sql/autopatches/20150721.phurl.6.alias.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150721.phurl.6.alias.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phurl.phurl_url + ADD alias VARCHAR(64) COLLATE {$COLLATE_SORT}; diff --git a/resources/sql/autopatches/20150721.phurl.7.authorphid.sql b/resources/sql/autopatches/20150721.phurl.7.authorphid.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150721.phurl.7.authorphid.sql @@ -0,0 +1,6 @@ +ALTER TABLE {$NAMESPACE}_phurl.phurl_url + ADD authorPHID VARBINARY(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_phurl.phurl_url + CHANGE description description LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + CHANGE longURL longURL LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2476,6 +2476,21 @@ 'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php', 'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php', 'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php', + 'PhabricatorPhurlApplication' => 'applications/phurl/application/PhabricatorPhurlApplication.php', + 'PhabricatorPhurlController' => 'applications/phurl/controller/PhabricatorPhurlController.php', + 'PhabricatorPhurlDAO' => 'applications/phurl/storage/PhabricatorPhurlDAO.php', + 'PhabricatorPhurlSchemaSpec' => 'applications/phurl/storage/PhabricatorPhurlSchemaSpec.php', + 'PhabricatorPhurlURL' => 'applications/phurl/storage/PhabricatorPhurlURL.php', + 'PhabricatorPhurlURLEditController' => 'applications/phurl/controller/PhabricatorPhurlURLEditController.php', + 'PhabricatorPhurlURLEditor' => 'applications/phurl/editor/PhabricatorPhurlURLEditor.php', + 'PhabricatorPhurlURLListController' => 'applications/phurl/controller/PhabricatorPhurlURLListController.php', + 'PhabricatorPhurlURLPHIDType' => 'applications/phurl/phid/PhabricatorPhurlURLPHIDType.php', + 'PhabricatorPhurlURLQuery' => 'applications/phurl/query/PhabricatorPhurlURLQuery.php', + 'PhabricatorPhurlURLSearchEngine' => 'applications/phurl/query/PhabricatorPhurlURLSearchEngine.php', + 'PhabricatorPhurlURLTransaction' => 'applications/phurl/storage/PhabricatorPhurlURLTransaction.php', + 'PhabricatorPhurlURLTransactionComment' => 'applications/phurl/storage/PhabricatorPhurlURLTransactionComment.php', + 'PhabricatorPhurlURLTransactionQuery' => 'applications/phurl/query/PhabricatorPhurlURLTransactionQuery.php', + 'PhabricatorPhurlURLViewController' => 'applications/phurl/controller/PhabricatorPhurlURLViewController.php', 'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.php', 'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php', 'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php', @@ -6364,6 +6379,32 @@ 'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhrictionApplication' => 'PhabricatorApplication', 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorPhurlApplication' => 'PhabricatorApplication', + 'PhabricatorPhurlController' => 'PhabricatorController', + 'PhabricatorPhurlDAO' => 'PhabricatorLiskDAO', + 'PhabricatorPhurlSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'PhabricatorPhurlURL' => array( + 'PhabricatorPhurlDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorProjectInterface', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorSubscribableInterface', + 'PhabricatorTokenReceiverInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorMentionableInterface', + 'PhabricatorFlaggableInterface', + 'PhabricatorSpacesInterface', + ), + 'PhabricatorPhurlURLEditController' => 'PhabricatorPhurlController', + 'PhabricatorPhurlURLEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorPhurlURLListController' => 'PhabricatorPhurlController', + 'PhabricatorPhurlURLPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorPhurlURLQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorPhurlURLSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorPhurlURLTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorPhurlURLTransactionComment' => 'PhabricatorApplicationTransactionComment', + 'PhabricatorPhurlURLTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorPhurlURLViewController' => 'PhabricatorPhurlController', 'PhabricatorPlatformSite' => 'PhabricatorSite', 'PhabricatorPolicies' => 'PhabricatorPolicyConstants', 'PhabricatorPolicy' => array( diff --git a/src/applications/phurl/application/PhabricatorPhurlApplication.php b/src/applications/phurl/application/PhabricatorPhurlApplication.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/application/PhabricatorPhurlApplication.php @@ -0,0 +1,45 @@ +[1-9]\d*)' => 'PhabricatorPhurlURLViewController', + '/phurl/' => array( + '(?:query/(?P[^/]+)/)?' + => 'PhabricatorPhurlURLListController', + 'url/' => array( + 'create/' + => 'PhabricatorPhurlURLEditController', + 'edit/(?P[1-9]\d*)/' + => 'PhabricatorPhurlURLEditController', + ), + ), + ); + } + +} diff --git a/src/applications/phurl/controller/PhabricatorPhurlController.php b/src/applications/phurl/controller/PhabricatorPhurlController.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/controller/PhabricatorPhurlController.php @@ -0,0 +1,15 @@ +addAction( + id(new PHUIListItemView()) + ->setName(pht('Shorten URL')) + ->setHref($this->getApplicationURI().'url/create/') + ->setIcon('fa-plus-square')); + + return $crumbs; + } +} diff --git a/src/applications/phurl/controller/PhabricatorPhurlURLEditController.php b/src/applications/phurl/controller/PhabricatorPhurlURLEditController.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/controller/PhabricatorPhurlURLEditController.php @@ -0,0 +1,240 @@ +getURIData('id'); + $is_create = !$id; + + $viewer = $request->getViewer(); + $user_phid = $viewer->getPHID(); + $error_name = true; + $error_long_url = true; + $validation_exception = null; + + $next_workflow = $request->getStr('next'); + $uri_query = $request->getStr('query'); + + if ($is_create) { + $url = PhabricatorPhurlURL::initializeNewPhurlURL( + $viewer); + $submit_label = pht('Create'); + $page_title = pht('Shorten URL'); + $subscribers = array(); + $cancel_uri = $this->getApplicationURI(); + } else { + $url = id(new PhabricatorPhurlURLQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + + if (!$url) { + return new Aphront404Response(); + } + + $submit_label = pht('Update'); + $page_title = pht('Update URL'); + + $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( + $url->getPHID()); + + $cancel_uri = '/U'.$url->getID(); + } + + if ($is_create) { + $projects = array(); + } else { + $projects = PhabricatorEdgeQuery::loadDestinationPHIDs( + $url->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + $projects = array_reverse($projects); + } + + $name = $url->getName(); + $long_url = $url->getLongURL(); + $description = $url->getDescription(); + $edit_policy = $url->getEditPolicy(); + $view_policy = $url->getViewPolicy(); + $space = $url->getSpacePHID(); + + if ($request->isFormPost()) { + $xactions = array(); + $name = $request->getStr('name'); + $long_url = $request->getStr('longURL'); + $projects = $request->getArr('projects'); + $description = $request->getStr('description'); + $subscribers = $request->getArr('subscribers'); + $edit_policy = $request->getStr('editPolicy'); + $view_policy = $request->getStr('viewPolicy'); + $space = $request->getStr('spacePHID'); + + $xactions[] = id(new PhabricatorPhurlURLTransaction()) + ->setTransactionType( + PhabricatorPhurlURLTransaction::TYPE_NAME) + ->setNewValue($name); + + $xactions[] = id(new PhabricatorPhurlURLTransaction()) + ->setTransactionType( + PhabricatorPhurlURLTransaction::TYPE_URL) + ->setNewValue($long_url); + + $xactions[] = id(new PhabricatorPhurlURLTransaction()) + ->setTransactionType( + PhabricatorTransactions::TYPE_SUBSCRIBERS) + ->setNewValue(array('=' => array_fuse($subscribers))); + + $xactions[] = id(new PhabricatorPhurlURLTransaction()) + ->setTransactionType( + PhabricatorPhurlURLTransaction::TYPE_DESCRIPTION) + ->setNewValue($description); + + $xactions[] = id(new PhabricatorPhurlURLTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($view_policy); + + $xactions[] = id(new PhabricatorPhurlURLTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) + ->setNewValue($edit_policy); + + $xactions[] = id(new PhabricatorPhurlURLTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) + ->setNewValue($space); + + $editor = id(new PhabricatorPhurlURLEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + try { + $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + $xactions[] = id(new PhabricatorPhurlURLTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $proj_edge_type) + ->setNewValue(array('=' => array_fuse($projects))); + + $xactions = $editor->applyTransactions($url, $xactions); + return id(new AphrontRedirectResponse()) + ->setURI($url->getURI()); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + $error_name = $ex->getShortMessage( + PhabricatorPhurlURLTransaction::TYPE_NAME); + $error_long_url = $ex->getShortMessage( + PhabricatorPhurlURLTransaction::TYPE_URL); + } + } + + $current_policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($url) + ->execute(); + + $name = id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name') + ->setValue($name) + ->setError($error_name); + + $long_url = id(new AphrontFormTextControl()) + ->setLabel(pht('URL')) + ->setName('longURL') + ->setValue($long_url) + ->setError($error_long_url); + + $projects = id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Projects')) + ->setName('projects') + ->setValue($projects) + ->setUser($viewer) + ->setDatasource(new PhabricatorProjectDatasource()); + + $description = id(new PhabricatorRemarkupControl()) + ->setLabel(pht('Description')) + ->setName('description') + ->setValue($description) + ->setUser($viewer); + + $view_policies = id(new AphrontFormPolicyControl()) + ->setUser($viewer) + ->setValue($view_policy) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) + ->setPolicyObject($url) + ->setPolicies($current_policies) + ->setSpacePHID($space) + ->setName('viewPolicy'); + $edit_policies = id(new AphrontFormPolicyControl()) + ->setUser($viewer) + ->setValue($edit_policy) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicyObject($url) + ->setPolicies($current_policies) + ->setName('editPolicy'); + + $subscribers = id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Subscribers')) + ->setName('subscribers') + ->setValue($subscribers) + ->setUser($viewer) + ->setDatasource(new PhabricatorMetaMTAMailableDatasource()); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild($name) + ->appendChild($long_url) + ->appendControl($view_policies) + ->appendControl($edit_policies) + ->appendControl($subscribers) + ->appendChild($projects) + ->appendChild($description); + + + if ($request->isAjax()) { + return $this->newDialog() + ->setTitle($page_title) + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->appendForm($form) + ->addCancelButton($cancel_uri) + ->addSubmitButton($submit_label); + } + + $submit = id(new AphrontFormSubmitControl()) + ->addCancelButton($cancel_uri) + ->setValue($submit_label); + + $form->appendChild($submit); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText($page_title) + ->setForm($form); + + $crumbs = $this->buildApplicationCrumbs(); + + if (!$is_create) { + $crumbs->addTextCrumb($url->getMonogram(), $url->getURI()); + } else { + $crumbs->addTextCrumb(pht('Create URL')); + } + + $crumbs->addTextCrumb($page_title); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeaderText($page_title) + ->setValidationException($validation_exception) + ->appendChild($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $object_box, + ), + array( + 'title' => $page_title, + )); + } +} diff --git a/src/applications/phurl/controller/PhabricatorPhurlURLListController.php b/src/applications/phurl/controller/PhabricatorPhurlURLListController.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/controller/PhabricatorPhurlURLListController.php @@ -0,0 +1,34 @@ +setQueryKey($request->getURIData('queryKey')) + ->setSearchEngine($engine) + ->setNavigation($this->buildSideNav()); + return $this->delegateToController($controller); + } + + public function buildSideNav() { + $user = $this->getRequest()->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new PhabricatorPhurlURLSearchEngine()) + ->setViewer($user) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + +} diff --git a/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php b/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php @@ -0,0 +1,139 @@ +getViewer(); + $id = $request->getURIData('id'); + + $timeline = null; + + $url = id(new PhabricatorPhurlURLQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$url) { + return new Aphront404Response(); + } + + $title = $url->getMonogram(); + $page_title = $title.' '.$url->getName(); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($title, $url->getURI()); + + $timeline = $this->buildTransactionTimeline( + $url, + new PhabricatorPhurlURLTransactionQuery()); + + $header = $this->buildHeaderView($url); + $actions = $this->buildActionView($url); + $properties = $this->buildPropertyView($url); + + $properties->setActionList($actions); + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + $add_comment_header = $is_serious + ? pht('Add Comment') + : pht('More Cowbell'); + $draft = PhabricatorDraft::newFromUserAndKey($viewer, $url->getPHID()); + $comment_uri = $this->getApplicationURI( + '/phurl/comment/'.$url->getID().'/'); + $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) + ->setUser($viewer) + ->setObjectPHID($url->getPHID()) + ->setDraft($draft) + ->setHeaderText($add_comment_header) + ->setAction($comment_uri) + ->setSubmitButtonName(pht('Add Comment')); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $timeline, + $add_comment_form, + ), + array( + 'title' => $page_title, + 'pageObjects' => array($url->getPHID()), + )); + } + + private function buildHeaderView(PhabricatorPhurlURL $url) { + $viewer = $this->getViewer(); + $icon = 'fa-compress'; + $color = 'green'; + $status = pht('Active'); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($url->getName()) + ->setStatus($icon, $color, $status) + ->setPolicyObject($url); + + return $header; + } + + private function buildActionView(PhabricatorPhurlURL $url) { + $viewer = $this->getViewer(); + $id = $url->getID(); + + $actions = id(new PhabricatorActionListView()) + ->setObjectURI($url->getURI()) + ->setUser($viewer) + ->setObject($url); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $url, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("url/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $actions; + } + + private function buildPropertyView(PhabricatorPhurlURL $url) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($url); + + $properties->addProperty( + pht('Original URL'), + $url->getLongURL()); + + $properties->invokeWillRenderEvent(); + + if (strlen($url->getDescription())) { + $description = PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff())->setContent($url->getDescription()), + 'default', + $viewer); + + $properties->addSectionHeader( + pht('Description'), + PHUIPropertyListView::ICON_SUMMARY); + + $properties->addTextContent($description); + } + + return $properties; + } + +} diff --git a/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php @@ -0,0 +1,207 @@ +getTransactionType()) { + case PhabricatorPhurlURLTransaction::TYPE_NAME: + return $object->getName(); + case PhabricatorPhurlURLTransaction::TYPE_URL: + return $object->getLongURL(); + case PhabricatorPhurlURLTransaction::TYPE_DESCRIPTION: + return $object->getDescription(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + switch ($xaction->getTransactionType()) { + case PhabricatorPhurlURLTransaction::TYPE_NAME: + case PhabricatorPhurlURLTransaction::TYPE_URL: + case PhabricatorPhurlURLTransaction::TYPE_DESCRIPTION: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorPhurlURLTransaction::TYPE_NAME: + $object->setName($xaction->getNewValue()); + return; + case PhabricatorPhurlURLTransaction::TYPE_URL: + $object->setLongURL($xaction->getNewValue()); + return; + case PhabricatorPhurlURLTransaction::TYPE_DESCRIPTION: + $object->setDescription($xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorPhurlURLTransaction::TYPE_NAME: + case PhabricatorPhurlURLTransaction::TYPE_URL: + case PhabricatorPhurlURLTransaction::TYPE_DESCRIPTION: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case PhabricatorPhurlURLTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('URL name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + case PhabricatorPhurlURLTransaction::TYPE_URL: + $missing = $this->validateIsEmptyTextField( + $object->getLongURL(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('URL path is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } + + protected function shouldPublishFeedStory( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + protected function supportsSearch() { + return true; + } + + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + protected function getMailSubjectPrefix() { + return pht('[Phurl]'); + } + + protected function getMailTo(PhabricatorLiskDAO $object) { + $phids = array(); + + if ($object->getPHID()) { + $phids[] = $object->getPHID(); + } + $phids[] = $this->getActingAsPHID(); + $phids = array_unique($phids); + + return $phids; + } + + public function getMailTagsMap() { + return array( + PhabricatorPhurlURLTransaction::MAILTAG_CONTENT => + pht( + "A URL's name or path changes."), + PhabricatorPhurlURLTransaction::MAILTAG_OTHER => + pht('Other event activity not listed above occurs.'), + ); + } + + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + $id = $object->getID(); + $name = $object->getName(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject("U{$id}: {$name}") + ->addHeader('Thread-Topic', "U{$id}: ".$object->getName()); + } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions) { + + $description = $object->getDescription(); + $body = parent::buildMailBody($object, $xactions); + + if (strlen($description)) { + $body->addTextSection( + pht('URL DESCRIPTION'), + $object->getDescription()); + } + + $body->addLinkSection( + pht('URL DETAIL'), + PhabricatorEnv::getProductionURI('/U'.$object->getID())); + + + return $body; + } + + +} diff --git a/src/applications/phurl/phid/PhabricatorPhurlURLPHIDType.php b/src/applications/phurl/phid/PhabricatorPhurlURLPHIDType.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/phid/PhabricatorPhurlURLPHIDType.php @@ -0,0 +1,74 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $url = $objects[$phid]; + + $id = $url->getID(); + $name = $url->getName(); + $full_name = $url->getMonogram().' '.$name; + + $handle + ->setName($name) + ->setFullName($full_name) + ->setURI($url->getURI()); + } + } + + public function canLoadNamedObject($name) { + return preg_match('/^U[1-9]\d*$/i', $name); + } + + public function loadNamedObjects( + PhabricatorObjectQuery $query, + array $names) { + + $id_map = array(); + foreach ($names as $name) { + $id = (int)substr($name, 1); + $id_map[$id][] = $name; + } + + $objects = id(new PhabricatorPhurlURLQuery()) + ->setViewer($query->getViewer()) + ->withIDs(array_keys($id_map)) + ->execute(); + + $results = array(); + foreach ($objects as $id => $object) { + foreach (idx($id_map, $id, array()) as $name) { + $results[$name] = $object; + } + } + + return $results; + } +} diff --git a/src/applications/phurl/query/PhabricatorPhurlURLQuery.php b/src/applications/phurl/query/PhabricatorPhurlURLQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/query/PhabricatorPhurlURLQuery.php @@ -0,0 +1,100 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withNames(array $names) { + $this->names = $names; + return $this; + } + + public function withLongURLs(array $long_urls) { + $this->longURLs = $long_urls; + return $this; + } + + public function withAuthorPHIDs(array $author_phids) { + $this->authorPHIDs = $author_phids; + return $this; + } + + protected function getPagingValueMap($cursor, array $keys) { + $url = $this->loadCursorObject($cursor); + return array( + 'id' => $url->getID(), + ); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'url.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'url.phid IN (%Ls)', + $this->phids); + } + + if ($this->authorPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'url.authorPHID IN (%Ls)', + $this->authorPHIDs); + } + + if ($this->names !== null) { + $where[] = qsprintf( + $conn, + 'url.name IN (%Ls)', + $this->names); + } + + if ($this->longURLs !== null) { + $where[] = qsprintf( + $conn, + 'url.longURL IN (%Ls)', + $this->longURLs); + } + + return $where; + } + + protected function getPrimaryTableAlias() { + return 'url'; + } + + public function getQueryApplicationClass() { + return 'PhabricatorPhurlApplication'; + } +} diff --git a/src/applications/phurl/query/PhabricatorPhurlURLSearchEngine.php b/src/applications/phurl/query/PhabricatorPhurlURLSearchEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/query/PhabricatorPhurlURLSearchEngine.php @@ -0,0 +1,94 @@ +setLabel(pht('Created By')) + ->setKey('authorPHIDs') + ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['authorPHIDs']) { + $query->withAuthorPHIDs($map['authorPHIDs']); + } + + return $query; + } + + protected function getURI($path) { + return '/phurl/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'authored' => pht('Authored'), + 'all' => pht('All URLs'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + $viewer = $this->requireViewer(); + + switch ($query_key) { + case 'authored': + return $query->setParameter('authorPHIDs', array($viewer->getPHID())); + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $urls, + PhabricatorSavedQuery $query, + array $handles) { + + assert_instances_of($urls, 'PhabricatorPhurlURL'); + $viewer = $this->requireViewer(); + $list = new PHUIObjectItemListView(); + $handles = $viewer->loadHandles(mpull($urls, 'getAuthorPHID')); + + foreach ($urls as $url) { + $item = id(new PHUIObjectItemView()) + ->setUser($viewer) + ->setObject($url) + ->setHeader($viewer->renderHandle($url->getPHID())); + + $list->addItem($item); + } + + $result = new PhabricatorApplicationSearchResultView(); + $result->setObjectList($list); + $result->setNoDataString(pht('No URLs found.')); + + return $result; + } +} diff --git a/src/applications/phurl/query/PhabricatorPhurlURLTransactionQuery.php b/src/applications/phurl/query/PhabricatorPhurlURLTransactionQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/query/PhabricatorPhurlURLTransactionQuery.php @@ -0,0 +1,10 @@ +buildEdgeSchemata(new PhabricatorPhurlURL()); + } + +} diff --git a/src/applications/phurl/storage/PhabricatorPhurlURL.php b/src/applications/phurl/storage/PhabricatorPhurlURL.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/storage/PhabricatorPhurlURL.php @@ -0,0 +1,171 @@ +setViewer($actor) + ->withClasses(array('PhabricatorPhurlApplication')) + ->executeOne(); + + return id(new PhabricatorPhurlURL()) + ->setAuthorPHID($actor->getPHID()) + ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) + ->setEditPolicy($actor->getPHID()) + ->setSpacePHID($actor->getDefaultSpacePHID()); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text', + 'alias' => 'sort64?', + 'longURL' => 'text', + 'description' => 'text', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_instance' => array( + 'columns' => array('alias'), + 'unique' => true, + ), + 'key_author' => array( + 'columns' => array('authorPHID'), + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorPhurlURLPHIDType::TYPECONST); + } + + public function getMonogram() { + return 'U'.$this->getID(); + } + + public function getURI() { + $uri = '/'.$this->getMonogram(); + return $uri; + } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + $user_phid = $this->getAuthorPHID(); + if ($user_phid) { + $viewer_phid = $viewer->getPHID(); + if ($viewer_phid == $user_phid) { + return true; + } + } + + return false; + } + + public function describeAutomaticCapability($capability) { + return pht('The owner of a URL can always view and edit it.'); + } + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorPhurlURLEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorPhurlURLTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } + +/* -( PhabricatorSubscribableInterface )----------------------------------- */ + + + public function isAutomaticallySubscribed($phid) { + return ($phid == $this->getAuthorPHID()); + } + + public function shouldShowSubscribersProperty() { + return true; + } + + public function shouldAllowSubscription($phid) { + return true; + } + +/* -( PhabricatorTokenReceiverInterface )---------------------------------- */ + + + public function getUsersToNotifyOfTokenGiven() { + return array($this->getAuthorPHID()); + } + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + + $this->openTransaction(); + $this->delete(); + $this->saveTransaction(); + } + +/* -( PhabricatorSpacesInterface )----------------------------------------- */ + + + public function getSpacePHID() { + return $this->spacePHID; + } +} diff --git a/src/applications/phurl/storage/PhabricatorPhurlURLTransaction.php b/src/applications/phurl/storage/PhabricatorPhurlURLTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/storage/PhabricatorPhurlURLTransaction.php @@ -0,0 +1,191 @@ +getTransactionType()) { + case self::TYPE_NAME: + case self::TYPE_URL: + case self::TYPE_DESCRIPTION: + $phids[] = $this->getObjectPHID(); + break; + } + + return $phids; + } + + public function shouldHide() { + $old = $this->getOldValue(); + switch ($this->getTransactionType()) { + case self::TYPE_DESCRIPTION: + return ($old === null); + } + return parent::shouldHide(); + } + + public function getIcon() { + switch ($this->getTransactionType()) { + case self::TYPE_NAME: + case self::TYPE_URL: + case self::TYPE_DESCRIPTION: + return 'fa-pencil'; + break; + } + return parent::getIcon(); + } + + public function getTitle() { + $author_phid = $this->getAuthorPHID(); + $object_phid = $this->getObjectPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $type = $this->getTransactionType(); + switch ($type) { + case self::TYPE_NAME: + if ($old === null) { + return pht( + '%s created this URL.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s changed the name of the URL from %s to %s.', + $this->renderHandleLink($author_phid), + $old, + $new); + } + case self::TYPE_URL: + return pht( + '%s changed the destination of the URL from %s to %s.', + $this->renderHandleLink($author_phid), + $old, + $new); + case self::TYPE_DESCRIPTION: + return pht( + "%s updated the URL's description.", + $this->renderHandleLink($author_phid)); + } + return parent::getTitle(); + } + + public function getTitleForFeed() { + $author_phid = $this->getAuthorPHID(); + $object_phid = $this->getObjectPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $viewer = $this->getViewer(); + + $type = $this->getTransactionType(); + switch ($type) { + case self::TYPE_NAME: + if ($old === null) { + return pht( + '%s created %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } else { + return pht( + '%s changed the name of %s from %s to %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $old, + $new); + } + case self::TYPE_URL: + if ($old === null) { + return pht( + '%s created %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } else { + return pht( + '%s changed the destination of %s from %s to %s', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $old, + $new); + } + case self::TYPE_DESCRIPTION: + return pht( + '%s updated the description of %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + + return parent::getTitleForFeed(); + } + + public function getColor() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_NAME: + case self::TYPE_URL: + case self::TYPE_DESCRIPTION: + return PhabricatorTransactions::COLOR_GREEN; + } + + return parent::getColor(); + } + + + public function hasChangeDetails() { + switch ($this->getTransactionType()) { + case self::TYPE_DESCRIPTION: + return ($this->getOldValue() !== null); + } + + return parent::hasChangeDetails(); + } + + public function renderChangeDetails(PhabricatorUser $viewer) { + switch ($this->getTransactionType()) { + case self::TYPE_DESCRIPTION: + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + return $this->renderTextCorpusChangeDetails( + $viewer, + $old, + $new); + } + + return parent::renderChangeDetails($viewer); + } + + public function getMailTags() { + $tags = array(); + switch ($this->getTransactionType()) { + case self::TYPE_NAME: + case self::TYPE_DESCRIPTION: + case self::TYPE_URL: + $tags[] = self::MAILTAG_CONTENT; + break; + } + return $tags; + } + +} diff --git a/src/applications/phurl/storage/PhabricatorPhurlURLTransactionComment.php b/src/applications/phurl/storage/PhabricatorPhurlURLTransactionComment.php new file mode 100644 --- /dev/null +++ b/src/applications/phurl/storage/PhabricatorPhurlURLTransactionComment.php @@ -0,0 +1,10 @@ + array(), 'db.multimeter' => array(), 'db.spaces' => array(), + 'db.phurl' => array(), 'db.badges' => array(), '0000.legacy.sql' => array( 'legacy' => 0,