Page MenuHomestyx hydra

No OneTemporary

diff --git a/.arcconfig b/.arcconfig
new file mode 100644
index 0000000000..4c4ad2bcfc
--- /dev/null
+++ b/.arcconfig
@@ -0,0 +1,10 @@
+{
+ "project_id" : "aphront",
+ "conduit_uri" : "http://tools.epriestley-conduit.dev1557.facebook.com/api/",
+ "lint_engine" : "PhutilLintEngine",
+ "unit_engine" : "PhutilUnitTestEngine",
+ "copyright_holder" : "Facebook, Inc.",
+ "phutil_libraries" : {
+ "aphront" : "src/"
+ }
+}
diff --git a/.divinerconfig b/.divinerconfig
new file mode 100644
index 0000000000..51d539832b
--- /dev/null
+++ b/.divinerconfig
@@ -0,0 +1,7 @@
+{
+ "name" : "differential",
+ "src_base" : "https://github.com/facebook/differential/blob/master",
+ "groups" : {
+ }
+}
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..00394b55de
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+._*
+/docs/
diff --git a/src/__phutil_library_init__.php b/src/__phutil_library_init__.php
new file mode 100644
index 0000000000..5df92cf21a
--- /dev/null
+++ b/src/__phutil_library_init__.php
@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+phutil_register_library('aphront', __FILE__);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
new file mode 100644
index 0000000000..ee56879f4d
--- /dev/null
+++ b/src/__phutil_library_map__.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
+ * @generated
+ */
+
+phutil_register_library_map(array(
+ 'class' =>
+ array(
+ 'Aphront404Response' => 'aphront/response/404',
+ 'AphrontApplicationConfiguration' => 'aphront/applicationconfiguration',
+ 'AphrontController' => 'aphront/controller',
+ 'AphrontDatabaseConnection' => 'storage/connection/base',
+ 'AphrontDefaultApplicationConfiguration' => 'aphront/default/configuration',
+ 'AphrontDefaultApplicationController' => 'aphront/default/controller',
+ 'AphrontDialogResponse' => 'aphront/response/dialog',
+ 'AphrontDialogView' => 'view/dialog',
+ 'AphrontDirectoryCategory' => 'applications/directory/storage/category',
+ 'AphrontDirectoryCategoryDeleteController' => 'applications/directory/controller/categorydelete',
+ 'AphrontDirectoryCategoryEditController' => 'applications/directory/controller/categoryedit',
+ 'AphrontDirectoryCategoryListController' => 'applications/directory/controller/categorylist',
+ 'AphrontDirectoryController' => 'applications/directory/controller/base',
+ 'AphrontDirectoryDAO' => 'applications/directory/storage/base',
+ 'AphrontDirectoryItem' => 'applications/directory/storage/item',
+ 'AphrontDirectoryItemDeleteController' => 'applications/directory/controller/itemdelete',
+ 'AphrontDirectoryItemEditController' => 'applications/directory/controller/itemedit',
+ 'AphrontDirectoryItemListController' => 'applications/directory/controller/itemlist',
+ 'AphrontDirectoryMainController' => 'applications/directory/controller/main',
+ 'AphrontErrorView' => 'view/form/error',
+ 'AphrontFormControl' => 'view/form/control/base',
+ 'AphrontFormSelectControl' => 'view/form/control/select',
+ 'AphrontFormSubmitControl' => 'view/form/control/submit',
+ 'AphrontFormTextAreaControl' => 'view/form/control/textarea',
+ 'AphrontFormTextControl' => 'view/form/control/text',
+ 'AphrontFormView' => 'view/form/base',
+ 'AphrontLiskDAO' => 'aphront/storage/lisk',
+ 'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql',
+ 'AphrontNullView' => 'view/null',
+ 'AphrontPageView' => 'view/page/base',
+ 'AphrontPanelView' => 'view/layout/panel',
+ 'AphrontQueryConnectionException' => 'storage/exception/connection',
+ 'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost',
+ 'AphrontQueryCountException' => 'storage/exception/count',
+ 'AphrontQueryException' => 'storage/exception/base',
+ 'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing',
+ 'AphrontQueryParameterException' => 'storage/exception/parameter',
+ 'AphrontQueryRecoverableException' => 'storage/exception/recoverable',
+ 'AphrontRedirectResponse' => 'aphront/response/redirect',
+ 'AphrontRequest' => 'aphront/request',
+ 'AphrontResponse' => 'aphront/response/base',
+ 'AphrontStandardPageView' => 'view/page/standard',
+ 'AphrontTableView' => 'view/control/table',
+ 'AphrontURIMapper' => 'aphront/mapper',
+ 'AphrontView' => 'view/base',
+ 'AphrontWebpageResponse' => 'aphront/response/webpage',
+ 'DifferentialAction' => 'applications/review/constants/action',
+ 'DifferentialChangeType' => 'applications/review/constants/changetype',
+ 'DifferentialLintStatus' => 'applications/review/constants/lintstatus',
+ 'DifferentialRevisionStatus' => 'applications/review/constants/revisionstatus',
+ 'DifferentialUnitStatus' => 'applications/review/constants/unitstatus',
+ 'LiskDAO' => 'storage/lisk/dao',
+ ),
+ 'function' =>
+ array(
+ '_qsprintf_check_scalar_type' => 'storage/qsprintf',
+ '_qsprintf_check_type' => 'storage/qsprintf',
+ 'qsprintf' => 'storage/qsprintf',
+ 'queryfx' => 'storage/queryfx',
+ 'queryfx_all' => 'storage/queryfx',
+ 'queryfx_one' => 'storage/queryfx',
+ 'vqsprintf' => 'storage/qsprintf',
+ 'vqueryfx' => 'storage/queryfx',
+ 'xsprintf_query' => 'storage/qsprintf',
+ ),
+ 'requires_class' =>
+ array(
+ 'Aphront404Response' => 'AphrontResponse',
+ 'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
+ 'AphrontDefaultApplicationController' => 'AphrontController',
+ 'AphrontDialogResponse' => 'AphrontResponse',
+ 'AphrontDialogView' => 'AphrontView',
+ 'AphrontDirectoryCategory' => 'AphrontDirectoryDAO',
+ 'AphrontDirectoryCategoryDeleteController' => 'AphrontDirectoryController',
+ 'AphrontDirectoryCategoryEditController' => 'AphrontDirectoryController',
+ 'AphrontDirectoryCategoryListController' => 'AphrontDirectoryController',
+ 'AphrontDirectoryController' => 'AphrontController',
+ 'AphrontDirectoryDAO' => 'AphrontLiskDAO',
+ 'AphrontDirectoryItem' => 'AphrontDirectoryDAO',
+ 'AphrontDirectoryItemDeleteController' => 'AphrontDirectoryController',
+ 'AphrontDirectoryItemEditController' => 'AphrontDirectoryController',
+ 'AphrontDirectoryItemListController' => 'AphrontDirectoryController',
+ 'AphrontDirectoryMainController' => 'AphrontDirectoryController',
+ 'AphrontErrorView' => 'AphrontView',
+ 'AphrontFormControl' => 'AphrontView',
+ 'AphrontFormSelectControl' => 'AphrontFormControl',
+ 'AphrontFormSubmitControl' => 'AphrontFormControl',
+ 'AphrontFormTextAreaControl' => 'AphrontFormControl',
+ 'AphrontFormTextControl' => 'AphrontFormControl',
+ 'AphrontFormView' => 'AphrontView',
+ 'AphrontLiskDAO' => 'LiskDAO',
+ 'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
+ 'AphrontNullView' => 'AphrontView',
+ 'AphrontPageView' => 'AphrontView',
+ 'AphrontPanelView' => 'AphrontView',
+ 'AphrontQueryConnectionException' => 'AphrontQueryException',
+ 'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException',
+ 'AphrontQueryCountException' => 'AphrontQueryException',
+ 'AphrontQueryObjectMissingException' => 'AphrontQueryException',
+ 'AphrontQueryParameterException' => 'AphrontQueryException',
+ 'AphrontQueryRecoverableException' => 'AphrontQueryException',
+ 'AphrontRedirectResponse' => 'AphrontResponse',
+ 'AphrontStandardPageView' => 'AphrontPageView',
+ 'AphrontTableView' => 'AphrontView',
+ 'AphrontWebpageResponse' => 'AphrontResponse',
+ ),
+ 'requires_interface' =>
+ array(
+ ),
+));
diff --git a/src/aphront/applicationconfiguration/AphrontApplicationConfiguration.php b/src/aphront/applicationconfiguration/AphrontApplicationConfiguration.php
new file mode 100644
index 0000000000..32f9bbef9b
--- /dev/null
+++ b/src/aphront/applicationconfiguration/AphrontApplicationConfiguration.php
@@ -0,0 +1,72 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+abstract class AphrontApplicationConfiguration {
+
+ private $request;
+ private $host;
+ private $path;
+
+ abstract public function getApplicationName();
+ abstract public function getURIMap();
+ abstract public function buildRequest();
+
+ final public function setRequest(AphrontRequest $request) {
+ $this->request = $request;
+ return $this;
+ }
+
+ final public function getRequest() {
+ return $this->request;
+ }
+
+ final public function buildController() {
+ $map = $this->getURIMap();
+ $mapper = new AphrontURIMapper($map);
+ $request = $this->getRequest();
+ $path = $request->getPath();
+ list($controller_class, $uri_data) = $mapper->mapPath($path);
+
+ PhutilSymbolLoader::loadClass($controller_class);
+ $controller = newv($controller_class, array($request));
+
+ return array($controller, $uri_data);
+ }
+
+ final public function setHost($host) {
+ $this->host = $host;
+ return $this;
+ }
+
+ final public function getHost() {
+ return $this->host;
+ }
+
+ final public function setPath($path) {
+ $this->path = $path;
+ return $this;
+ }
+
+ final public function getPath() {
+ return $this->path;
+ }
+
+}
diff --git a/src/aphront/applicationconfiguration/__init__.php b/src/aphront/applicationconfiguration/__init__.php
new file mode 100644
index 0000000000..5635a733d7
--- /dev/null
+++ b/src/aphront/applicationconfiguration/__init__.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/mapper');
+
+phutil_require_module('phutil', 'symbols');
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontApplicationConfiguration.php');
diff --git a/src/aphront/controller/AphrontController.php b/src/aphront/controller/AphrontController.php
new file mode 100644
index 0000000000..8bae576d53
--- /dev/null
+++ b/src/aphront/controller/AphrontController.php
@@ -0,0 +1,48 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+abstract class AphrontController {
+
+ private $request;
+
+ public function willProcessRequest(array $uri_data) {
+ return;
+ }
+
+ abstract public function processRequest();
+
+ final public function __construct(AphrontRequest $request) {
+ $this->request = $request;
+ }
+
+ final public function getRequest() {
+ return $this->request;
+ }
+
+ public function buildStandardPageResponse($view) {
+ $page = new AphrontStandardPageView();
+ $page->appendChild($view);
+ $response = new AphrontWebpageResponse();
+ $response->setContent($page->render());
+ return $response;
+ }
+
+}
diff --git a/src/aphront/controller/__init__.php b/src/aphront/controller/__init__.php
new file mode 100644
index 0000000000..1584e8b289
--- /dev/null
+++ b/src/aphront/controller/__init__.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/response/webpage');
+phutil_require_module('aphront', 'view/page/standard');
+
+
+phutil_require_source('AphrontController.php');
diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
new file mode 100644
index 0000000000..a2129f40c3
--- /dev/null
+++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
@@ -0,0 +1,100 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+class AphrontDefaultApplicationConfiguration
+ extends AphrontApplicationConfiguration {
+
+ public function getApplicationName() {
+ return 'aphront-default';
+ }
+
+ public function getURIMap() {
+ return array(
+ '/repository/' => array(
+ '$' => 'RepositoryListController',
+ 'new/$' => 'RepositoryEditController',
+ 'edit/(?<id>\d+)/$' => 'RepositoryEditController',
+ 'delete/(?<id>\d+)/$' => 'RepositoryDeleteController',
+ ),
+ '/' => array(
+ '$' => 'AphrontDirectoryMainController',
+ ),
+ '/directory/' => array(
+ 'item/$' => 'AphrontDirectoryItemListController',
+ 'item/edit/(?:(?<id>\d+)/)?$' => 'AphrontDirectoryItemEditController',
+ 'item/delete/(?<id>\d+)/' => 'AphrontDirectoryItemDeleteController',
+ 'category/$'
+ => 'AphrontDirectoryCategoryListController',
+ 'category/edit/(?:(?<id>\d+)/)?$'
+ => 'AphrontDirectoryCategoryEditController',
+ 'category/delete/(?<id>\d+)/'
+ => 'AphrontDirectoryCategoryDeleteController',
+ ),
+ '.*' => 'AphrontDefaultApplicationController',
+ );
+ }
+
+ public function buildRequest() {
+ $request = new AphrontRequest($this->getHost(), $this->getPath());
+ $request->setRequestData($_GET + $_POST);
+ return $request;
+ }
+
+ public function handleException(Exception $ex) {
+
+ $class = phutil_escape_html(get_class($ex));
+ $message = phutil_escape_html($ex->getMessage());
+
+ $content =
+ '<div class="aphront-unhandled-exception">'.
+ '<h1>Unhandled Exception "'.$class.'": '.$message.'</h1>'.
+ '<code>'.phutil_escape_html((string)$ex).'</code>'.
+ '</div>';
+
+ $view = new AphrontStandardPageView();
+ $view->appendChild($content);
+
+ $response = new AphrontWebpageResponse();
+ $response->setContent($view->render());
+
+ return $response;
+ }
+
+ public function willSendResponse(AphrontResponse $response) {
+ $request = $this->getRequest();
+ if ($response instanceof AphrontDialogResponse) {
+ if (!$request->isAjax()) {
+ $view = new AphrontStandardPageView();
+ $view->appendChild(
+ '<div style="padding: 2em 0;">'.
+ $response->buildResponseString().
+ '</div>');
+ $response = new AphrontWebpageResponse();
+ $response->setContent($view->render());
+ return $response;
+ }
+ }
+
+ return $response;
+ }
+
+
+}
diff --git a/src/aphront/default/configuration/__init__.php b/src/aphront/default/configuration/__init__.php
new file mode 100644
index 0000000000..1332403bd2
--- /dev/null
+++ b/src/aphront/default/configuration/__init__.php
@@ -0,0 +1,17 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/applicationconfiguration');
+phutil_require_module('aphront', 'aphront/request');
+phutil_require_module('aphront', 'aphront/response/webpage');
+phutil_require_module('aphront', 'view/page/standard');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontDefaultApplicationConfiguration.php');
diff --git a/src/aphront/default/controller/AphrontDefaultApplicationController.php b/src/aphront/default/controller/AphrontDefaultApplicationController.php
new file mode 100644
index 0000000000..c2b50f3826
--- /dev/null
+++ b/src/aphront/default/controller/AphrontDefaultApplicationController.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+class AphrontDefaultApplicationController extends AphrontController {
+
+ public function processRequest() {
+ $request = $this->getRequest();
+
+ $path = phutil_escape_html($request->getPath());
+ $host = phutil_escape_html($request->getHost());
+ $controller_name = phutil_escape_html(get_class($this));
+
+ $page = new AphrontStandardPageView();
+
+ $response = new AphrontWebpageResponse();
+ $response->setContent($page->render());
+ return $response;
+ }
+
+}
diff --git a/src/aphront/default/controller/__init__.php b/src/aphront/default/controller/__init__.php
new file mode 100644
index 0000000000..9140c71c0e
--- /dev/null
+++ b/src/aphront/default/controller/__init__.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/controller');
+phutil_require_module('aphront', 'aphront/response/webpage');
+phutil_require_module('aphront', 'view/page/standard');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontDefaultApplicationController.php');
diff --git a/src/aphront/mapper/AphrontURIMapper.php b/src/aphront/mapper/AphrontURIMapper.php
new file mode 100644
index 0000000000..8376b87a3a
--- /dev/null
+++ b/src/aphront/mapper/AphrontURIMapper.php
@@ -0,0 +1,68 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * @group aphront
+ */
+final class AphrontURIMapper {
+
+ private $map;
+
+ final public function __construct(array $map) {
+ $this->map = $map;
+ }
+
+ final public function mapPath($path) {
+ $map = $this->map;
+ foreach ($map as $rule => $value) {
+ list($controller, $data) = $this->tryRule($rule, $value, $path);
+ if ($controller) {
+ foreach ($data as $k => $v) {
+ if (is_numeric($k)) {
+ unset($data[$k]);
+ }
+ }
+ return array($controller, $data);
+ }
+ }
+
+ return array(null, null);
+ }
+
+ final private function tryRule($rule, $value, $path) {
+ $match = null;
+ if (!preg_match('#^'.$rule.'#', $path, $match)) {
+ return array(null, null);
+ }
+
+ if (!is_array($value)) {
+ return array($value, $match);
+ }
+
+ $path = substr($path, strlen($match[0]));
+ foreach ($value as $srule => $sval) {
+ list($controller, $data) = $this->tryRule($srule, $sval, $path);
+ if ($controller) {
+ return array($controller, $data + $match);
+ }
+ }
+
+ return array(null, null);
+ }
+}
diff --git a/src/aphront/mapper/__init__.php b/src/aphront/mapper/__init__.php
new file mode 100644
index 0000000000..46dee32678
--- /dev/null
+++ b/src/aphront/mapper/__init__.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+
+phutil_require_source('AphrontURIMapper.php');
diff --git a/src/aphront/request/AphrontRequest.php b/src/aphront/request/AphrontRequest.php
new file mode 100644
index 0000000000..6d04d61db9
--- /dev/null
+++ b/src/aphront/request/AphrontRequest.php
@@ -0,0 +1,91 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * @group aphront
+ */
+class AphrontRequest {
+
+ const TYPE_AJAX = '__ajax__';
+ const TYPE_FORM = '__form__';
+
+ private $host;
+ private $path;
+ private $requestData;
+
+ final public function __construct($host, $path) {
+ $this->host = $host;
+ $this->path = $path;
+ }
+
+ final public function setRequestData(array $request_data) {
+ $this->requestData = $request_data;
+ return $this;
+ }
+
+ final public function getPath() {
+ return $this->path;
+ }
+
+ final public function getHost() {
+ return $this->host;
+ }
+
+ final public function getInt($name, $default = null) {
+ if (isset($this->requestData[$name])) {
+ return (int)$this->requestData[$name];
+ } else {
+ return $default;
+ }
+ }
+
+ final public function getStr($name, $default = null) {
+ if (isset($this->requestData[$name])) {
+ return (string)$this->requestData[$name];
+ } else {
+ return $default;
+ }
+ }
+
+ final public function getArr($name, $default = null) {
+ if (isset($this->requestData[$name]) &&
+ is_array($this->requestData[$name])) {
+ return $this->requestData[$name];
+ } else {
+ return $default;
+ }
+ }
+
+ final public function getExists($name) {
+ return array_key_exists($name, $this->requestData);
+ }
+
+ final public function isHTTPPost() {
+ return ($_SERVER['REQUEST_METHOD'] == 'POST');
+ }
+
+ final public function isAjax() {
+ return $this->getExists(self::TYPE_AJAX);
+ }
+
+ final public function isFormPost() {
+ return $this->getExists(self::TYPE_FORM) && $this->isHTTPPost();
+ }
+
+}
diff --git a/src/aphront/request/__init__.php b/src/aphront/request/__init__.php
new file mode 100644
index 0000000000..3b8c8f9a1d
--- /dev/null
+++ b/src/aphront/request/__init__.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+
+phutil_require_source('AphrontRequest.php');
diff --git a/src/aphront/response/404/Aphront404Response.php b/src/aphront/response/404/Aphront404Response.php
new file mode 100644
index 0000000000..e04ac4d27f
--- /dev/null
+++ b/src/aphront/response/404/Aphront404Response.php
@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+class Aphront404Response extends AphrontResponse {
+
+ public function buildResponseString() {
+ return '404 Not Found';
+ }
+
+}
diff --git a/src/aphront/response/404/__init__.php b/src/aphront/response/404/__init__.php
new file mode 100644
index 0000000000..379b456f28
--- /dev/null
+++ b/src/aphront/response/404/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/response/base');
+
+
+phutil_require_source('Aphront404Response.php');
diff --git a/src/aphront/response/base/AphrontResponse.php b/src/aphront/response/base/AphrontResponse.php
new file mode 100644
index 0000000000..d632faba80
--- /dev/null
+++ b/src/aphront/response/base/AphrontResponse.php
@@ -0,0 +1,48 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+abstract class AphrontResponse {
+
+ private $request;
+
+ public function setRequest($request) {
+ $this->request = $request;
+ return $this;
+ }
+
+ public function getRequest() {
+ return $this->request;
+ }
+
+ public function getHeaders() {
+ return array();
+ }
+
+ public function getCacheHeaders() {
+ return array(
+ array('Cache-Control', 'private, no-cache, no-store, must-revalidate'),
+ array('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT'),
+ );
+ }
+
+ abstract public function buildResponseString();
+
+}
diff --git a/src/aphront/response/base/__init__.php b/src/aphront/response/base/__init__.php
new file mode 100644
index 0000000000..28e5873f34
--- /dev/null
+++ b/src/aphront/response/base/__init__.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+
+phutil_require_source('AphrontResponse.php');
diff --git a/src/aphront/response/dialog/AphrontDialogResponse.php b/src/aphront/response/dialog/AphrontDialogResponse.php
new file mode 100644
index 0000000000..eab1201518
--- /dev/null
+++ b/src/aphront/response/dialog/AphrontDialogResponse.php
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+final class AphrontDialogResponse extends AphrontResponse {
+
+ private $dialog;
+
+ public function setDialog(AphrontDialogView $dialog) {
+ $this->dialog = $dialog;
+ return $this;
+ }
+
+ public function buildResponseString() {
+ return $this->dialog->render();
+ }
+
+}
diff --git a/src/aphront/response/dialog/__init__.php b/src/aphront/response/dialog/__init__.php
new file mode 100644
index 0000000000..672a4e2b2d
--- /dev/null
+++ b/src/aphront/response/dialog/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/response/base');
+
+
+phutil_require_source('AphrontDialogResponse.php');
diff --git a/src/aphront/response/redirect/AphrontRedirectResponse.php b/src/aphront/response/redirect/AphrontRedirectResponse.php
new file mode 100644
index 0000000000..bb8666583c
--- /dev/null
+++ b/src/aphront/response/redirect/AphrontRedirectResponse.php
@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+class AphrontRedirectResponse extends AphrontResponse {
+
+ private $uri;
+
+ public function setURI($uri) {
+ $this->uri = $uri;
+ return $this;
+ }
+
+ public function getHeaders() {
+ return array(
+ array('Location', $this->uri),
+ );
+ }
+
+ public function buildResponseString() {
+ return '';
+ }
+
+}
diff --git a/src/aphront/response/redirect/__init__.php b/src/aphront/response/redirect/__init__.php
new file mode 100644
index 0000000000..f7d1e32537
--- /dev/null
+++ b/src/aphront/response/redirect/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/response/base');
+
+
+phutil_require_source('AphrontRedirectResponse.php');
diff --git a/src/aphront/response/webpage/AphrontWebpageResponse.php b/src/aphront/response/webpage/AphrontWebpageResponse.php
new file mode 100644
index 0000000000..40ceb9e2eb
--- /dev/null
+++ b/src/aphront/response/webpage/AphrontWebpageResponse.php
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+class AphrontWebpageResponse extends AphrontResponse {
+
+ private $content;
+
+ public function setContent($content) {
+ $this->content = $content;
+ return $this;
+ }
+
+ public function buildResponseString() {
+ return $this->content;
+ }
+
+}
diff --git a/src/aphront/response/webpage/__init__.php b/src/aphront/response/webpage/__init__.php
new file mode 100644
index 0000000000..2585170caf
--- /dev/null
+++ b/src/aphront/response/webpage/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/response/base');
+
+
+phutil_require_source('AphrontWebpageResponse.php');
diff --git a/src/aphront/storage/lisk/AphrontLiskDAO.php b/src/aphront/storage/lisk/AphrontLiskDAO.php
new file mode 100644
index 0000000000..729d91addd
--- /dev/null
+++ b/src/aphront/storage/lisk/AphrontLiskDAO.php
@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+abstract class AphrontLiskDAO extends LiskDAO {
+
+ public function establishConnection($mode) {
+ return new AphrontMySQLDatabaseConnection(
+ array(
+ 'user' => 'root',
+ 'pass' => '',
+ 'host' => 'localhost',
+ 'database' => 'aphront_'.$this->getApplicationName(),
+ ));
+
+ }
+
+ public function getTableName() {
+ $class = strtolower(get_class($this));
+ if (!strncmp($class, 'aphront', 7)) {
+ $class = substr($class, 7);
+ }
+ $app = $this->getApplicationName();
+ if (!strncmp($class, $app, strlen($app))) {
+ $class = substr($class, strlen($app));
+ }
+ return $app.'_'.$class;
+ }
+
+ abstract public function getApplicationName();
+}
diff --git a/src/aphront/storage/lisk/__init__.php b/src/aphront/storage/lisk/__init__.php
new file mode 100644
index 0000000000..0509c024bb
--- /dev/null
+++ b/src/aphront/storage/lisk/__init__.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/connection/mysql');
+phutil_require_module('aphront', 'storage/lisk/dao');
+
+
+phutil_require_source('AphrontLiskDAO.php');
diff --git a/src/applications/directory/controller/base/AphrontDirectoryController.php b/src/applications/directory/controller/base/AphrontDirectoryController.php
new file mode 100644
index 0000000000..69960676b6
--- /dev/null
+++ b/src/applications/directory/controller/base/AphrontDirectoryController.php
@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+abstract class AphrontDirectoryController extends AphrontController {
+
+ public function buildStandardPageResponse($view, array $data) {
+ $page = new AphrontStandardPageView();
+
+ $page->setApplicationName('Directory');
+ $page->setBaseURI('/');
+ $page->setTitle(idx($data, 'title'));
+ $page->setTabs(
+ array(
+ 'directory' => array(
+ 'href' => '/',
+ 'name' => 'Directory',
+ ),
+ 'categories' => array(
+ 'href' => '/directory/category/',
+ 'name' => 'Categories',
+ ),
+ 'items' => array(
+ 'href' => '/directory/item/',
+ 'name' => 'Items',
+ ),
+ ),
+ idx($data, 'tab'));
+ $page->setGlyph("\xE2\x9A\x92");
+ $page->appendChild($view);
+
+ $response = new AphrontWebpageResponse();
+ return $response->setContent($page->render());
+ }
+
+}
diff --git a/src/applications/directory/controller/base/__init__.php b/src/applications/directory/controller/base/__init__.php
new file mode 100644
index 0000000000..4ec6c97a37
--- /dev/null
+++ b/src/applications/directory/controller/base/__init__.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/controller');
+phutil_require_module('aphront', 'aphront/response/webpage');
+phutil_require_module('aphront', 'view/page/standard');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontDirectoryController.php');
diff --git a/src/applications/directory/controller/categorydelete/AphrontDirectoryCategoryDeleteController.php b/src/applications/directory/controller/categorydelete/AphrontDirectoryCategoryDeleteController.php
new file mode 100644
index 0000000000..8e5c7d8013
--- /dev/null
+++ b/src/applications/directory/controller/categorydelete/AphrontDirectoryCategoryDeleteController.php
@@ -0,0 +1,51 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDirectoryCategoryDeleteController
+ extends AphrontDirectoryController {
+
+ public function willProcessRequest(array $data) {
+ $this->id = $data['id'];
+ }
+
+ public function processRequest() {
+
+ $category = id(new AphrontDirectoryCategory())->load($this->id);
+ if (!$category) {
+ return new Aphront404Response();
+ }
+
+ $request = $this->getRequest();
+
+ if ($request->isFormPost()) {
+ $category->delete();
+ return id(new AphrontRedirectResponse())
+ ->setURI('/directory/category/');
+ }
+
+ $dialog = new AphrontDialogView();
+ $dialog->setTitle('Really delete this category?');
+ $dialog->appendChild("Are you sure you want to delete this category?");
+ $dialog->addSubmitButton('Delete');
+ $dialog->addCancelButton('/directory/category/');
+ $dialog->setSubmitURI($request->getPath());
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ }
+
+}
diff --git a/src/applications/directory/controller/categorydelete/__init__.php b/src/applications/directory/controller/categorydelete/__init__.php
new file mode 100644
index 0000000000..4c3cffe6f8
--- /dev/null
+++ b/src/applications/directory/controller/categorydelete/__init__.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/response/404');
+phutil_require_module('aphront', 'aphront/response/dialog');
+phutil_require_module('aphront', 'aphront/response/redirect');
+phutil_require_module('aphront', 'applications/directory/controller/base');
+phutil_require_module('aphront', 'applications/directory/storage/category');
+phutil_require_module('aphront', 'view/dialog');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontDirectoryCategoryDeleteController.php');
diff --git a/src/applications/directory/controller/categoryedit/AphrontDirectoryCategoryEditController.php b/src/applications/directory/controller/categoryedit/AphrontDirectoryCategoryEditController.php
new file mode 100644
index 0000000000..2525a746da
--- /dev/null
+++ b/src/applications/directory/controller/categoryedit/AphrontDirectoryCategoryEditController.php
@@ -0,0 +1,110 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDirectoryCategoryEditController
+ extends AphrontDirectoryController {
+
+ private $id;
+
+ public function willProcessRequest(array $data) {
+ $this->id = idx($data, 'id');
+ }
+
+ public function processRequest() {
+
+ if ($this->id) {
+ $category = id(new AphrontDirectoryCategory())->load($this->id);
+ if (!$category) {
+ return new Aphront404Response();
+ }
+ } else {
+ $category = new AphrontDirectoryCategory();
+ }
+
+ $e_name = true;
+ $errors = array();
+
+ $request = $this->getRequest();
+ if ($request->isFormPost()) {
+ $category->setName($request->getStr('name'));
+ $category->setSequence($request->getStr('sequence'));
+
+ if (!strlen($category->getName())) {
+ $errors[] = 'Category name is required.';
+ $e_name = 'Required';
+ }
+
+ if (!$errors) {
+ $category->save();
+ return id(new AphrontRedirectResponse())
+ ->setURI('/directory/category/');
+ }
+ }
+
+ $error_view = null;
+ if ($errors) {
+ $error_view = id(new AphrontErrorView())
+ ->setTitle('Form Errors')
+ ->setErrors($errors);
+ }
+
+ $form = new AphrontFormView();
+ if ($category->getID()) {
+ $form->setAction('/directory/category/edit/'.$category->getID().'/');
+ } else {
+ $form->setAction('/directory/category/edit/');
+ }
+
+ $categories = id(new AphrontDirectoryCategory())->loadAll();
+ $category_map = mpull($categories, 'getName', 'getID');
+
+ $form
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Name')
+ ->setName('name')
+ ->setValue($category->getName())
+ ->setError($e_name))
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Order')
+ ->setName('sequence')
+ ->setValue((int)$category->getSequence()))
+ ->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->setValue('Save')
+ ->addCancelButton('/directory/category/'));
+
+ $panel = new AphrontPanelView();
+ if ($category->getID()) {
+ $panel->setHeader('Edit Directory Category');
+ } else {
+ $panel->setHeader('Create New Directory Category');
+ }
+
+ $panel->appendChild($form);
+ $panel->setWidth(AphrontPanelView::WIDTH_FORM);
+
+ return $this->buildStandardPageResponse(
+ array($error_view, $panel),
+ array(
+ 'title' => 'Edit Directory Category',
+ ));
+ }
+
+}
diff --git a/src/applications/directory/controller/categoryedit/__init__.php b/src/applications/directory/controller/categoryedit/__init__.php
new file mode 100644
index 0000000000..a3f198f981
--- /dev/null
+++ b/src/applications/directory/controller/categoryedit/__init__.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/response/404');
+phutil_require_module('aphront', 'aphront/response/redirect');
+phutil_require_module('aphront', 'applications/directory/controller/base');
+phutil_require_module('aphront', 'applications/directory/storage/category');
+phutil_require_module('aphront', 'view/form/base');
+phutil_require_module('aphront', 'view/form/control/submit');
+phutil_require_module('aphront', 'view/form/error');
+phutil_require_module('aphront', 'view/layout/panel');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontDirectoryCategoryEditController.php');
diff --git a/src/applications/directory/controller/categorylist/AphrontDirectoryCategoryListController.php b/src/applications/directory/controller/categorylist/AphrontDirectoryCategoryListController.php
new file mode 100644
index 0000000000..b34c2ce01e
--- /dev/null
+++ b/src/applications/directory/controller/categorylist/AphrontDirectoryCategoryListController.php
@@ -0,0 +1,72 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDirectoryCategoryListController
+ extends AphrontDirectoryController {
+
+ public function processRequest() {
+ $categories = id(new AphrontDirectoryCategory())->loadAll();
+ $categories = msort($categories, 'getSequence');
+
+ $rows = array();
+ foreach ($categories as $category) {
+ $rows[] = array(
+ $category->getID(),
+ phutil_render_tag(
+ 'a',
+ array(
+ 'href' => '/directory/category/edit/'.$category->getID().'/',
+ ),
+ phutil_escape_html($category->getName())),
+ phutil_render_tag(
+ 'a',
+ array(
+ 'href' => '/directory/category/delete/'.$category->getID().'/',
+ 'class' => 'button grey small',
+ ),
+ 'Delete'),
+ );
+ }
+
+
+ $table = new AphrontTableView($rows);
+ $table->setHeaders(
+ array(
+ 'ID',
+ 'Name',
+ '',
+ ));
+ $table->setColumnClasses(
+ array(
+ null,
+ 'wide',
+ 'action',
+ ));
+
+ $panel = new AphrontPanelView();
+ $panel->appendChild($table);
+ $panel->setHeader('Directory Categories');
+ $panel->setCreateButton('New Category', '/directory/category/edit/');
+
+ return $this->buildStandardPageResponse($panel, array(
+ 'title' => 'Directory Category List',
+ 'tab' => 'categories',
+ ));
+ }
+
+}
diff --git a/src/applications/directory/controller/categorylist/__init__.php b/src/applications/directory/controller/categorylist/__init__.php
new file mode 100644
index 0000000000..251b79f269
--- /dev/null
+++ b/src/applications/directory/controller/categorylist/__init__.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'applications/directory/controller/base');
+phutil_require_module('aphront', 'applications/directory/storage/category');
+phutil_require_module('aphront', 'view/control/table');
+phutil_require_module('aphront', 'view/layout/panel');
+
+phutil_require_module('phutil', 'markup');
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontDirectoryCategoryListController.php');
diff --git a/src/applications/directory/controller/itemdelete/AphrontDirectoryItemDeleteController.php b/src/applications/directory/controller/itemdelete/AphrontDirectoryItemDeleteController.php
new file mode 100644
index 0000000000..a6321f38c7
--- /dev/null
+++ b/src/applications/directory/controller/itemdelete/AphrontDirectoryItemDeleteController.php
@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDirectoryItemDeleteController extends AphrontDirectoryController {
+
+ public function willProcessRequest(array $data) {
+ $this->id = $data['id'];
+ }
+
+ public function processRequest() {
+
+ $item = id(new AphrontDirectoryItem())->load($this->id);
+ if (!$item) {
+ return new Aphront404Response();
+ }
+
+ $request = $this->getRequest();
+
+ if ($request->isFormPost()) {
+ $item->delete();
+ return id(new AphrontRedirectResponse())
+ ->setURI('/directory/item/');
+ }
+
+ $dialog = new AphrontDialogView();
+ $dialog->setTitle('Really delete this item?');
+ $dialog->appendChild("Are you sure you want to delete this item?");
+ $dialog->addSubmitButton('Delete');
+ $dialog->addCancelButton('/directory/item/');
+ $dialog->setSubmitURI($request->getPath());
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ }
+
+}
diff --git a/src/applications/directory/controller/itemdelete/__init__.php b/src/applications/directory/controller/itemdelete/__init__.php
new file mode 100644
index 0000000000..f2f92cc759
--- /dev/null
+++ b/src/applications/directory/controller/itemdelete/__init__.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/response/404');
+phutil_require_module('aphront', 'aphront/response/dialog');
+phutil_require_module('aphront', 'aphront/response/redirect');
+phutil_require_module('aphront', 'applications/directory/controller/base');
+phutil_require_module('aphront', 'applications/directory/storage/item');
+phutil_require_module('aphront', 'view/dialog');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontDirectoryItemDeleteController.php');
diff --git a/src/applications/directory/controller/itemedit/AphrontDirectoryItemEditController.php b/src/applications/directory/controller/itemedit/AphrontDirectoryItemEditController.php
new file mode 100644
index 0000000000..2d156595c6
--- /dev/null
+++ b/src/applications/directory/controller/itemedit/AphrontDirectoryItemEditController.php
@@ -0,0 +1,137 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDirectoryItemEditController extends AphrontDirectoryController {
+
+ private $id;
+
+ public function willProcessRequest(array $data) {
+ $this->id = idx($data, 'id');
+ }
+
+ public function processRequest() {
+
+ if ($this->id) {
+ $item = id(new AphrontDirectoryItem())->load($this->id);
+ if (!$item) {
+ return new Aphront404Response();
+ }
+ } else {
+ $item = new AphrontDirectoryItem();
+ }
+
+ $e_name = true;
+ $e_href = true;
+ $errors = array();
+
+ $request = $this->getRequest();
+ if ($request->isFormPost()) {
+ $item->setName($request->getStr('name'));
+ $item->setHref($request->getStr('href'));
+ $item->setDescription($request->getStr('description'));
+ $item->setCategoryID($request->getStr('categoryID'));
+ $item->setSequence($request->getStr('sequence'));
+
+ if (!strlen($item->getName())) {
+ $errors[] = 'Item name is required.';
+ $e_name = 'Required';
+ }
+
+ if (!strlen($item->getHref())) {
+ $errors[] = 'Item link is required.';
+ $e_href = 'Required';
+ }
+
+ if (!$errors) {
+ $item->save();
+ return id(new AphrontRedirectResponse())
+ ->setURI('/directory/item/');
+ }
+ }
+
+ $error_view = null;
+ if ($errors) {
+ $error_view = id(new AphrontErrorView())
+ ->setTitle('Form Errors')
+ ->setErrors($errors);
+ }
+
+ $form = new AphrontFormView();
+ if ($item->getID()) {
+ $form->setAction('/directory/item/edit/'.$item->getID().'/');
+ } else {
+ $form->setAction('/directory/item/edit/');
+ }
+
+ $categories = id(new AphrontDirectoryCategory())->loadAll();
+ $category_map = mpull($categories, 'getName', 'getID');
+
+ $form
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Name')
+ ->setName('name')
+ ->setValue($item->getName())
+ ->setError($e_name))
+ ->appendChild(
+ id(new AphrontFormSelectControl())
+ ->setLabel('Category')
+ ->setName('categoryID')
+ ->setOptions($category_map)
+ ->setValue($item->getCategoryID()))
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Link')
+ ->setName('href')
+ ->setValue($item->getHref())
+ ->setError($e_href))
+ ->appendChild(
+ id(new AphrontFormTextAreaControl())
+ ->setLabel('Description')
+ ->setName('description')
+ ->setValue($item->getDescription()))
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Order')
+ ->setName('sequence')
+ ->setCaption(
+ 'Items in a category are sorted by "order", then by name.')
+ ->setValue((int)$item->getSequence()))
+ ->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->setValue('Save')
+ ->addCancelButton('/directory/item/'));
+
+ $panel = new AphrontPanelView();
+ if ($item->getID()) {
+ $panel->setHeader('Edit Directory Item');
+ } else {
+ $panel->setHeader('Create New Directory Item');
+ }
+
+ $panel->appendChild($form);
+ $panel->setWidth(AphrontPanelView::WIDTH_FORM);
+
+ return $this->buildStandardPageResponse(
+ array($error_view, $panel),
+ array(
+ 'title' => 'Edit Directory Item',
+ ));
+ }
+
+}
diff --git a/src/applications/directory/controller/itemedit/__init__.php b/src/applications/directory/controller/itemedit/__init__.php
new file mode 100644
index 0000000000..4cdff77aba
--- /dev/null
+++ b/src/applications/directory/controller/itemedit/__init__.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/response/404');
+phutil_require_module('aphront', 'aphront/response/redirect');
+phutil_require_module('aphront', 'applications/directory/controller/base');
+phutil_require_module('aphront', 'applications/directory/storage/category');
+phutil_require_module('aphront', 'applications/directory/storage/item');
+phutil_require_module('aphront', 'view/form/base');
+phutil_require_module('aphront', 'view/form/control/submit');
+phutil_require_module('aphront', 'view/form/error');
+phutil_require_module('aphront', 'view/layout/panel');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontDirectoryItemEditController.php');
diff --git a/src/applications/directory/controller/itemlist/AphrontDirectoryItemListController.php b/src/applications/directory/controller/itemlist/AphrontDirectoryItemListController.php
new file mode 100644
index 0000000000..e5cc37d946
--- /dev/null
+++ b/src/applications/directory/controller/itemlist/AphrontDirectoryItemListController.php
@@ -0,0 +1,77 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDirectoryItemListController extends AphrontDirectoryController {
+
+ public function processRequest() {
+ $items = id(new AphrontDirectoryItem())->loadAll();
+ $items = msort($items, 'getSortKey');
+
+ $categories = id(new AphrontDirectoryCategory())->loadAll();
+ $category_names = mpull($categories, 'getName', 'getID');
+
+ $rows = array();
+ foreach ($items as $item) {
+ $rows[] = array(
+ $item->getID(),
+ phutil_escape_html(idx($category_names, $item->getCategoryID())),
+ phutil_render_tag(
+ 'a',
+ array(
+ 'href' => '/directory/item/edit/'.$item->getID().'/',
+ ),
+ phutil_escape_html($item->getName())),
+ phutil_render_tag(
+ 'a',
+ array(
+ 'href' => '/directory/item/delete/'.$item->getID().'/',
+ 'class' => 'button grey small',
+ ),
+ 'Delete'),
+ );
+ }
+
+
+ $table = new AphrontTableView($rows);
+ $table->setHeaders(
+ array(
+ 'ID',
+ 'Category',
+ 'Name',
+ '',
+ ));
+ $table->setColumnClasses(
+ array(
+ null,
+ null,
+ 'wide',
+ 'action',
+ ));
+
+ $panel = new AphrontPanelView();
+ $panel->appendChild($table);
+ $panel->setHeader('Directory Items');
+ $panel->setCreateButton('New Item', '/directory/item/edit/');
+
+ return $this->buildStandardPageResponse($panel, array(
+ 'title' => 'Directory Items',
+ 'tab' => 'items',
+ ));
+ }
+
+}
diff --git a/src/applications/directory/controller/itemlist/__init__.php b/src/applications/directory/controller/itemlist/__init__.php
new file mode 100644
index 0000000000..48ffcb97d7
--- /dev/null
+++ b/src/applications/directory/controller/itemlist/__init__.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'applications/directory/controller/base');
+phutil_require_module('aphront', 'applications/directory/storage/category');
+phutil_require_module('aphront', 'applications/directory/storage/item');
+phutil_require_module('aphront', 'view/control/table');
+phutil_require_module('aphront', 'view/layout/panel');
+
+phutil_require_module('phutil', 'markup');
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontDirectoryItemListController.php');
diff --git a/src/applications/directory/controller/main/AphrontDirectoryMainController.php b/src/applications/directory/controller/main/AphrontDirectoryMainController.php
new file mode 100644
index 0000000000..33cb6a65b0
--- /dev/null
+++ b/src/applications/directory/controller/main/AphrontDirectoryMainController.php
@@ -0,0 +1,75 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDirectoryMainController extends AphrontDirectoryController {
+
+ public function processRequest() {
+ $items = id(new AphrontDirectoryItem())->loadAll();
+ $items = msort($items, 'getSortKey');
+
+ $categories = id(new AphrontDirectoryCategory())->loadAll();
+ $categories = msort($categories, 'getSequence');
+
+ $category_map = mpull($categories, 'getName', 'getID');
+ $category_map[0] = 'Free Radicals';
+ $items = mgroup($items, 'getCategoryID');
+
+ $content = array();
+ foreach ($category_map as $id => $category_name) {
+ $category_items = idx($items, $id);
+ if (!$category_items) {
+ continue;
+ }
+
+ $item_markup = array();
+ foreach ($category_items as $item) {
+ $item_markup[] =
+ '<div>'.
+ '<h2>'.
+ phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $item->getHref(),
+ ),
+ phutil_escape_html($item->getName())).
+ '</h2>'.
+ '<p>'.phutil_escape_html($item->getDescription()).'</p>'.
+ '</div>';
+ }
+
+ $content[] =
+ '<div class="aphront-directory-category">'.
+ '<h1>'.phutil_escape_html($category_name).'</h1>'.
+ '<div class="aphront-directory-group">'.
+ implode("\n", $item_markup).
+ '</div>'.
+ '</div>';
+ }
+
+ $content =
+ '<div class="aphront-directory-list">'.
+ implode("\n", $content).
+ '</div>';
+
+ return $this->buildStandardPageResponse($content, array(
+ 'title' => 'Directory',
+ 'tab' => 'directory',
+ ));
+ }
+
+}
diff --git a/src/applications/directory/controller/main/__init__.php b/src/applications/directory/controller/main/__init__.php
new file mode 100644
index 0000000000..812dc47f7c
--- /dev/null
+++ b/src/applications/directory/controller/main/__init__.php
@@ -0,0 +1,17 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'applications/directory/controller/base');
+phutil_require_module('aphront', 'applications/directory/storage/category');
+phutil_require_module('aphront', 'applications/directory/storage/item');
+
+phutil_require_module('phutil', 'markup');
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontDirectoryMainController.php');
diff --git a/src/applications/directory/storage/base/AphrontDirectoryDAO.php b/src/applications/directory/storage/base/AphrontDirectoryDAO.php
new file mode 100644
index 0000000000..03272e74a6
--- /dev/null
+++ b/src/applications/directory/storage/base/AphrontDirectoryDAO.php
@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+abstract class AphrontDirectoryDAO extends AphrontLiskDAO {
+
+ public function getApplicationName() {
+ return 'directory';
+ }
+
+}
diff --git a/src/applications/directory/storage/base/__init__.php b/src/applications/directory/storage/base/__init__.php
new file mode 100644
index 0000000000..2768f3640a
--- /dev/null
+++ b/src/applications/directory/storage/base/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'aphront/storage/lisk');
+
+
+phutil_require_source('AphrontDirectoryDAO.php');
diff --git a/src/applications/directory/storage/category/AphrontDirectoryCategory.php b/src/applications/directory/storage/category/AphrontDirectoryCategory.php
new file mode 100644
index 0000000000..fe723b2d1a
--- /dev/null
+++ b/src/applications/directory/storage/category/AphrontDirectoryCategory.php
@@ -0,0 +1,24 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDirectoryCategory extends AphrontDirectoryDAO {
+
+ protected $name;
+ protected $sequence;
+
+}
diff --git a/src/applications/directory/storage/category/__init__.php b/src/applications/directory/storage/category/__init__.php
new file mode 100644
index 0000000000..13b56b8cee
--- /dev/null
+++ b/src/applications/directory/storage/category/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'applications/directory/storage/base');
+
+
+phutil_require_source('AphrontDirectoryCategory.php');
diff --git a/src/applications/directory/storage/item/AphrontDirectoryItem.php b/src/applications/directory/storage/item/AphrontDirectoryItem.php
new file mode 100644
index 0000000000..d615a01dab
--- /dev/null
+++ b/src/applications/directory/storage/item/AphrontDirectoryItem.php
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDirectoryItem extends AphrontDirectoryDAO {
+
+ protected $name;
+ protected $description;
+ protected $href;
+ protected $categoryID;
+ protected $sequence;
+ protected $imageGUID;
+
+ public function getSortKey() {
+ return sprintf(
+ '%08d:%08d:%s',
+ $this->getCategoryID(),
+ $this->getSequence(),
+ $this->getName());
+ }
+
+}
diff --git a/src/applications/directory/storage/item/__init__.php b/src/applications/directory/storage/item/__init__.php
new file mode 100644
index 0000000000..ca9b6a7c4b
--- /dev/null
+++ b/src/applications/directory/storage/item/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'applications/directory/storage/base');
+
+
+phutil_require_source('AphrontDirectoryItem.php');
diff --git a/src/applications/review/constants/action/DifferentialAction.php b/src/applications/review/constants/action/DifferentialAction.php
new file mode 100755
index 0000000000..431b456210
--- /dev/null
+++ b/src/applications/review/constants/action/DifferentialAction.php
@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class DifferentialAction {
+
+ const ACTION_COMMIT = 'commit';
+ const ACTION_COMMENT = 'none';
+ const ACTION_ACCEPT = 'accept';
+ const ACTION_REJECT = 'reject';
+ const ACTION_ABANDON = 'abandon';
+ const ACTION_REQUEST = 'request_review';
+ const ACTION_RECLAIM = 'reclaim';
+ const ACTION_UPDATE = 'update';
+ const ACTION_RESIGN = 'resign';
+ const ACTION_SUMMARIZE = 'summarize';
+ const ACTION_TESTPLAN = 'testplan';
+ const ACTION_CREATE = 'create';
+ const ACTION_ADDREVIEWERS = 'add_reviewers';
+
+ public static function getActionVerb($action) {
+ static $verbs = array(
+ self::ACTION_COMMENT => 'commented on',
+ self::ACTION_ACCEPT => 'accepted',
+ self::ACTION_REJECT => 'requested changes to',
+ self::ACTION_ABANDON => 'abandoned',
+ self::ACTION_COMMIT => 'committed',
+ self::ACTION_REQUEST => 'requested a review of',
+ self::ACTION_RECLAIM => 'reclaimed',
+ self::ACTION_UPDATE => 'updated',
+ self::ACTION_RESIGN => 'resigned from',
+ self::ACTION_SUMMARIZE => 'summarized',
+ self::ACTION_TESTPLAN => 'explained the test plan for',
+ self::ACTION_CREATE => 'created',
+ self::ACTION_ADDREVIEWERS => 'added reviewers to',
+ );
+
+ if (empty($verbs[$action])) {
+ return $verbs[$action];
+ } else {
+ return 'brazenly "'.$action.'ed"';
+ }
+ }
+
+}
diff --git a/src/applications/review/constants/action/__init__.php b/src/applications/review/constants/action/__init__.php
new file mode 100644
index 0000000000..5620371d9c
--- /dev/null
+++ b/src/applications/review/constants/action/__init__.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+
+phutil_require_source('DifferentialAction.php');
diff --git a/src/applications/review/constants/changetype/DifferentialChangeType.php b/src/applications/review/constants/changetype/DifferentialChangeType.php
new file mode 100755
index 0000000000..f55ffd6fb1
--- /dev/null
+++ b/src/applications/review/constants/changetype/DifferentialChangeType.php
@@ -0,0 +1,125 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class DifferentialChangeType {
+
+ const TYPE_ADD = 1;
+ const TYPE_CHANGE = 2;
+ const TYPE_DELETE = 3;
+ const TYPE_MOVE_AWAY = 4;
+ const TYPE_COPY_AWAY = 5;
+ const TYPE_MOVE_HERE = 6;
+ const TYPE_COPY_HERE = 7;
+ const TYPE_MULTICOPY = 8;
+ const TYPE_MESSAGE = 9;
+ const TYPE_CHILD = 10;
+
+ const FILE_TEXT = 1;
+ const FILE_IMAGE = 2;
+ const FILE_BINARY = 3;
+ const FILE_DIRECTORY = 4;
+ const FILE_SYMLINK = 5;
+ const FILE_DELETED = 6;
+ const FILE_NORMAL = 7;
+
+ public static function getSummaryCharacterForChangeType($type) {
+ static $types = array(
+ self::TYPE_ADD => 'A',
+ self::TYPE_CHANGE => 'M',
+ self::TYPE_DELETE => 'D',
+ self::TYPE_MOVE_AWAY => 'V',
+ self::TYPE_COPY_AWAY => 'P',
+ self::TYPE_MOVE_HERE => 'V',
+ self::TYPE_COPY_HERE => 'P',
+ self::TYPE_MULTICOPY => 'P',
+ self::TYPE_MESSAGE => 'Q',
+ self::TYPE_CHILD => '@',
+ );
+ return idx($types, coalesce($type, '?'), '~');
+ }
+
+ public static function getShortNameForFileType($type) {
+ static $names = array(
+ self::FILE_TEXT => null,
+ self::FILE_DIRECTORY => 'dir',
+ self::FILE_IMAGE => 'img',
+ self::FILE_BINARY => 'bin',
+ self::FILE_SYMLINK => 'sym',
+ );
+ return idx($names, coalesce($type, '?'), '???');
+ }
+
+ public static function isOldLocationChangeType($type) {
+ static $types = array(
+ DifferentialChangeType::TYPE_MOVE_AWAY => true,
+ DifferentialChangeType::TYPE_COPY_AWAY => true,
+ DifferentialChangeType::TYPE_MULTICOPY => true,
+ );
+ return isset($types[$type]);
+ }
+
+ public static function isNewLocationChangeType($type) {
+ static $types = array(
+ DifferentialChangeType::TYPE_MOVE_HERE => true,
+ DifferentialChangeType::TYPE_COPY_HERE => true,
+ );
+ return isset($types[$type]);
+ }
+
+ public static function isDeleteChangeType($type) {
+ static $types = array(
+ DifferentialChangeType::TYPE_DELETE => true,
+ DifferentialChangeType::TYPE_MOVE_AWAY => true,
+ DifferentialChangeType::TYPE_MULTICOPY => true,
+ );
+ return isset($types[$type]);
+ }
+
+ public static function isCreateChangeType($type) {
+ static $types = array(
+ DifferentialChangeType::TYPE_ADD => true,
+ DifferentialChangeType::TYPE_COPY_HERE => true,
+ DifferentialChangeType::TYPE_MOVE_HERE => true,
+ );
+ return isset($types[$type]);
+ }
+
+ public static function isModifyChangeType($type) {
+ static $types = array(
+ DifferentialChangeType::TYPE_CHANGE => true,
+ );
+ return isset($types[$type]);
+ }
+
+ public static function getFullNameForChangeType($type) {
+ static $types = array(
+ self::TYPE_ADD => 'Added',
+ self::TYPE_CHANGE => 'Modified',
+ self::TYPE_DELETE => 'Deleted',
+ self::TYPE_MOVE_AWAY => 'Moved Away',
+ self::TYPE_COPY_AWAY => 'Copied Away',
+ self::TYPE_MOVE_HERE => 'Moved Here',
+ self::TYPE_COPY_HERE => 'Copied Here',
+ self::TYPE_MULTICOPY => 'Deleted After Multiple Copy',
+ self::TYPE_MESSAGE => 'Commit Message',
+ self::TYPE_CHILD => 'Contents Modified',
+ );
+ return idx($types, coalesce($type, '?'), 'Unknown');
+ }
+
+}
diff --git a/src/applications/review/constants/changetype/__init__.php b/src/applications/review/constants/changetype/__init__.php
new file mode 100644
index 0000000000..a37cb89bc3
--- /dev/null
+++ b/src/applications/review/constants/changetype/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('DifferentialChangeType.php');
diff --git a/src/applications/review/constants/lintstatus/DifferentialLintStatus.php b/src/applications/review/constants/lintstatus/DifferentialLintStatus.php
new file mode 100755
index 0000000000..42d74404a3
--- /dev/null
+++ b/src/applications/review/constants/lintstatus/DifferentialLintStatus.php
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class DifferentialLintStatus {
+
+ const LINT_NO = 0;
+ const LINT_WARNINGS = 1;
+ const LINT_OKAY = 2;
+ const LINT_NOT_APPLICABLE = 3;
+ const LINT_SKIP = 4;
+
+}
diff --git a/src/applications/review/constants/lintstatus/__init__.php b/src/applications/review/constants/lintstatus/__init__.php
new file mode 100644
index 0000000000..b2c11a8a44
--- /dev/null
+++ b/src/applications/review/constants/lintstatus/__init__.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+
+phutil_require_source('DifferentialLintStatus.php');
diff --git a/src/applications/review/constants/revisionstatus/DifferentialRevisionStatus.php b/src/applications/review/constants/revisionstatus/DifferentialRevisionStatus.php
new file mode 100755
index 0000000000..092fb31c61
--- /dev/null
+++ b/src/applications/review/constants/revisionstatus/DifferentialRevisionStatus.php
@@ -0,0 +1,39 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class DifferentialRevisionStatus {
+
+ const NEEDS_REVIEW = 0;
+ const NEEDS_REVISION = 1;
+ const ACCEPTED = 2;
+ const COMMITTED = 3;
+ const ABANDONED = 4;
+
+ public static function getNameForRevisionStatus($status) {
+ static $map = array(
+ self::NEEDS_REVIEW => 'Needs Review',
+ self::NEEDS_REVISION => 'Needs Revision',
+ self::ACCEPTED => 'Accepted',
+ self::COMMITTED => 'Committed',
+ self::ABANDONED => 'Abandoned',
+ );
+
+ return idx($map, coalesce($status, '?'), 'Unknown');
+ }
+
+}
diff --git a/src/applications/review/constants/revisionstatus/__init__.php b/src/applications/review/constants/revisionstatus/__init__.php
new file mode 100644
index 0000000000..8d5cc9e019
--- /dev/null
+++ b/src/applications/review/constants/revisionstatus/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('DifferentialRevisionStatus.php');
diff --git a/src/applications/review/constants/unitstatus/DifferentialUnitStatus.php b/src/applications/review/constants/unitstatus/DifferentialUnitStatus.php
new file mode 100755
index 0000000000..8277cdc280
--- /dev/null
+++ b/src/applications/review/constants/unitstatus/DifferentialUnitStatus.php
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class DifferentialUnitStatus {
+
+ const UNIT_NO = 0;
+ const UNIT_FAIL = 1;
+ const UNIT_OKAY = 2;
+ const UNIT_NO_TESTS = 3;
+ const UNIT_NOT_APPLICABLE = 4;
+
+}
diff --git a/src/applications/review/constants/unitstatus/__init__.php b/src/applications/review/constants/unitstatus/__init__.php
new file mode 100644
index 0000000000..1bd564927f
--- /dev/null
+++ b/src/applications/review/constants/unitstatus/__init__.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+
+phutil_require_source('DifferentialUnitStatus.php');
diff --git a/src/storage/connection/base/AphrontDatabaseConnection.php b/src/storage/connection/base/AphrontDatabaseConnection.php
new file mode 100644
index 0000000000..f06769492f
--- /dev/null
+++ b/src/storage/connection/base/AphrontDatabaseConnection.php
@@ -0,0 +1,225 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+abstract class AphrontDatabaseConnection {
+
+ private static $transactionStacks = array();
+ private static $transactionShutdownRegistered = false;
+
+ abstract public function getInsertID();
+ abstract public function getAffectedRows();
+ abstract public function selectAllResults();
+ abstract public function executeRawQuery($raw_query);
+ abstract protected function getTransactionKey();
+
+ abstract public function escapeString($string);
+ abstract public function escapeColumnName($string);
+ abstract public function escapeMultilineComment($string);
+ abstract public function escapeStringForLikeClause($string);
+
+ public function queryData($pattern/*, $arg, $arg, ... */) {
+ $args = func_get_args();
+ array_unshift($args, $this);
+ return call_user_func_array('queryfx_all', $args);
+ }
+
+ public function query($pattern/*, $arg, $arg, ... */) {
+ $args = func_get_args();
+ array_unshift($args, $this);
+ return call_user_func_array('queryfx', $args);
+ }
+
+ // TODO: Probably need to reset these when we catch a connection exception
+ // in the transaction stack.
+ protected function &getLockLevels() {
+ static $levels = array();
+ $key = $this->getTransactionKey();
+ if (!isset($levels[$key])) {
+ $levels[$key] = array(
+ 'read' => 0,
+ 'write' => 0,
+ );
+ }
+
+ return $levels[$key];
+ }
+
+ public function isReadLocking() {
+ $levels = &$this->getLockLevels();
+ return ($levels['read'] > 0);
+ }
+
+ public function isWriteLocking() {
+ $levels = &$this->getLockLevels();
+ return ($levels['write'] > 0);
+ }
+
+ public function startReadLocking() {
+ $levels = &$this->getLockLevels();
+ ++$levels['read'];
+ return $this;
+ }
+
+ public function startWriteLocking() {
+ $levels = &$this->getLockLevels();
+ ++$levels['write'];
+ return $this;
+ }
+
+ public function stopReadLocking() {
+ $levels = &$this->getLockLevels();
+ if ($levels['read'] < 1) {
+ throw new Exception('Unable to stop read locking: not read locking.');
+ }
+ --$levels['read'];
+ return $this;
+ }
+
+ public function stopWriteLocking() {
+ $levels = &$this->getLockLevels();
+ if ($levels['write'] < 1) {
+ throw new Exception('Unable to stop read locking: not write locking.');
+ }
+ --$levels['write'];
+ return $this;
+ }
+
+ protected function &getTransactionStack($key) {
+ if (!self::$transactionShutdownRegistered) {
+ self::$transactionShutdownRegistered = true;
+ register_shutdown_function(
+ array(
+ 'LiskConnection',
+ 'shutdownTransactionStacks',
+ ));
+ }
+
+ if (!isset(self::$transactionStacks[$key])) {
+ self::$transactionStacks[$key] = array();
+ }
+
+ return self::$transactionStacks[$key];
+ }
+
+ public static function shutdownTransactionStacks() {
+ foreach (self::$transactionStacks as $stack) {
+ if ($stack === false) {
+ continue;
+ }
+
+ $count = count($stack);
+ if ($count) {
+ throw new Exception(
+ 'Script exited with '.$count.' open transactions! The '.
+ 'transactions will be implicitly rolled back. Calls to '.
+ 'openTransaction() should always be paired with a call to '.
+ 'saveTransaction() or killTransaction(); you have an unpaired '.
+ 'call somewhere.',
+ $count);
+ }
+ }
+ }
+
+ public function openTransaction() {
+ $key = $this->getTransactionKey();
+ $stack = &$this->getTransactionStack($key);
+
+ $new_transaction = !count($stack);
+
+ // TODO: At least in development, push context information instead of
+ // `true' so we can report (or, at least, guess) where unpaired
+ // transaction calls happened.
+ $stack[] = true;
+
+ end($stack);
+ $key = key($stack);
+
+ if ($new_transaction) {
+ $this->query('START TRANSACTION');
+ } else {
+ $this->query('SAVEPOINT '.$this->getSavepointName($key));
+ }
+ }
+
+ public function isInsideTransaction() {
+ $key = $this->getTransactionKey();
+ $stack = &$this->getTransactionStack($key);
+ return (bool)count($stack);
+ }
+
+ public function saveTransaction() {
+ $key = $this->getTransactionKey();
+ $stack = &$this->getTransactionStack($key);
+
+ if (!count($stack)) {
+ throw new Exception(
+ "No open transaction! Unable to save transaction, since there ".
+ "isn't one.");
+ }
+
+ array_pop($stack);
+
+ if (!count($stack)) {
+ $this->query('COMMIT');
+ }
+ }
+
+ public function saveTransactionUnless($cond) {
+ if ($cond) {
+ $this->killTransaction();
+ } else {
+ $this->saveTransaction();
+ }
+ }
+
+ public function saveTransactionIf($cond) {
+ $this->saveTransactionUnless(!$cond);
+ }
+
+ public function killTransaction() {
+ $key = $this->getTransactionKey();
+ $stack = &$this->getTransactionStack($key);
+
+ if (!count($stack)) {
+ throw new Exception(
+ "No open transaction! Unable to kill transaction, since there ".
+ "isn't one.");
+ }
+
+ $count = count($stack);
+
+ end($stack);
+ $key = key($stack);
+ array_pop($stack);
+
+ if (!count($stack)) {
+ $this->query('ROLLBACK');
+ } else {
+ $this->query(
+ 'ROLLBACK TO SAVEPOINT '.$this->getSavepointName($key)
+ );
+ }
+ }
+
+ protected function getSavepointName($key) {
+ return 'LiskSavepoint_'.$key;
+ }
+}
diff --git a/src/storage/connection/base/__init__.php b/src/storage/connection/base/__init__.php
new file mode 100644
index 0000000000..79093a7f0d
--- /dev/null
+++ b/src/storage/connection/base/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/queryfx');
+
+
+phutil_require_source('AphrontDatabaseConnection.php');
diff --git a/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php b/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php
new file mode 100644
index 0000000000..73decb8f16
--- /dev/null
+++ b/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php
@@ -0,0 +1,198 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
+
+ private $config;
+ private $connection;
+
+ public function __construct(array $configuration) {
+ $this->configuration = $configuration;
+ }
+
+ public function escapeString($string) {
+ if (!$this->connection) {
+ $this->establishConnection();
+ }
+ return mysql_real_escape_string($string, $this->connection);
+ }
+
+ public function escapeColumnName($name) {
+ return '`'.str_replace('`', '\\`', $name).'`';
+ }
+
+ public function escapeMultilineComment($comment) {
+ // These can either terminate a comment, confuse the hell out of the parser,
+ // make MySQL execute the comment as a query, or, in the case of semicolon,
+ // are quasi-dangerous because the semicolon could turn a broken query into
+ // a working query plus an ignored query.
+
+ static $map = array(
+ '--' => '(DOUBLEDASH)',
+ '*/' => '(STARSLASH)',
+ '//' => '(SLASHSLASH)',
+ '#' => '(HASH)',
+ '!' => '(BANG)',
+ ';' => '(SEMICOLON)',
+ );
+
+ $comment = str_replace(
+ array_keys($map),
+ array_values($map),
+ $comment);
+
+ // For good measure, kill anything else that isn't a nice printable
+ // character.
+ $comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment);
+
+ return '/* '.$comment.' */';
+ }
+
+ public function escapeStringForLikeClause($value) {
+ $value = $this->escapeString($value);
+ // Ideally the query shouldn't be modified after safely escaping it,
+ // but we need to escape _ and % within LIKE terms.
+ $value = str_replace(
+ // Even though we've already escaped, we need to replace \ with \\
+ // because MYSQL unescapes twice inside a LIKE clause. See note
+ // at mysql.com. However, if the \ is being used to escape a single
+ // quote ('), then the \ should not be escaped. Thus, after all \
+ // are replaced with \\, we need to revert instances of \\' back to
+ // \'.
+ array('\\', '\\\\\'', '_', '%'),
+ array('\\\\', '\\\'', '\_', '\%'),
+ $value);
+ return $value;
+ }
+
+ private function getConfiguration($key, $default = null) {
+ return idx($this->configuration, $key, $default);
+ }
+
+ private function establishConnection() {
+ $this->connection = null;
+
+ $user = $this->getConfiguration('user');
+ $host = $this->getConfiguration('host');
+
+ $conn = @mysql_connect(
+ $host,
+ $user,
+ $this->getConfiguration('pass'),
+ $new_link = true,
+ $flags = 0);
+
+ if (!$conn) {
+ $errno = mysql_errno();
+ $error = mysql_error();
+ throw new AphrontQueryConnectionException(
+ "Attempt to connect to {$user}@{$host} failed with error #{$errno}: ".
+ "{$error}.");
+ }
+
+ $ret = @mysql_select_db($this->getConfiguration('database'), $conn);
+ if (!$ret) {
+ $this->throwQueryException($conn);
+ }
+
+ $this->connection = $conn;
+ }
+
+ public function getInsertID() {
+ return mysql_insert_id($this->requireConnection());
+ }
+
+ public function getAffectedRows() {
+ return mysql_affected_rows($this->requireConnection());
+ }
+
+ public function getTransactionKey() {
+ return (int)$this->requireConnection();
+ }
+
+ private function requireConnection() {
+ if (!$this->connection) {
+ $this->establishConnection();
+ }
+ return $this->connection;
+ }
+
+ public function selectAllResults() {
+ $result = array();
+ $res = $this->lastResult;
+ if ($res == null) {
+ throw new Exception('No query result to fetch from!');
+ }
+ while (($row = mysql_fetch_assoc($res)) !== false) {
+ $result[] = $row;
+ }
+ return $result;
+ }
+
+ public function executeRawQuery($raw_query) {
+ $this->lastResult = null;
+ $retries = 3;
+ while ($retries--) {
+ try {
+ if (!$this->connection) {
+ $this->establishConnection();
+ }
+
+ $result = mysql_query($raw_query, $this->connection);
+
+ if ($result) {
+ $this->lastResult = $result;
+ break;
+ }
+
+ $this->throwQueryException($this->connection);
+ } catch (AphrontQueryConnectionLostException $ex) {
+ if (!$retries) {
+ throw $ex;
+ }
+ if ($this->isInsideTransaction()) {
+ throw $ex;
+ }
+ $this->connection = null;
+ }
+ }
+ }
+
+ private function throwQueryException($connection) {
+ $errno = mysql_errno($connection);
+ $error = mysql_error($connection);
+
+ switch ($errno) {
+ case 2013: // Connection Dropped
+ case 2006: // Gone Away
+ throw new AphrontQueryConnectionLostException("#{$errno}: {$error}");
+ break;
+ case 1213: // Deadlock
+ case 1205: // Lock wait timeout exceeded
+ throw new AphrontQueryRecoverableException("#{$errno}: {$error}");
+ break;
+ default:
+ // TODO: 1062 is syntax error, and quite terrible in production.
+ throw new AphrontQueryException("#{$errno}: {$error}");
+ }
+ }
+
+}
diff --git a/src/storage/connection/mysql/__init__.php b/src/storage/connection/mysql/__init__.php
new file mode 100644
index 0000000000..7f74d3fddd
--- /dev/null
+++ b/src/storage/connection/mysql/__init__.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/connection/base');
+phutil_require_module('aphront', 'storage/exception/base');
+phutil_require_module('aphront', 'storage/exception/connection');
+phutil_require_module('aphront', 'storage/exception/connectionlost');
+phutil_require_module('aphront', 'storage/exception/recoverable');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontMySQLDatabaseConnection.php');
diff --git a/src/storage/exception/base/AphrontQueryException.php b/src/storage/exception/base/AphrontQueryException.php
new file mode 100644
index 0000000000..4f6db2ba92
--- /dev/null
+++ b/src/storage/exception/base/AphrontQueryException.php
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+class AphrontQueryException extends Exception { }
diff --git a/src/storage/exception/base/__init__.php b/src/storage/exception/base/__init__.php
new file mode 100644
index 0000000000..feb8ad536a
--- /dev/null
+++ b/src/storage/exception/base/__init__.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+
+phutil_require_source('AphrontQueryException.php');
diff --git a/src/storage/exception/connection/AphrontQueryConnectionException.php b/src/storage/exception/connection/AphrontQueryConnectionException.php
new file mode 100644
index 0000000000..2d4a4e35d8
--- /dev/null
+++ b/src/storage/exception/connection/AphrontQueryConnectionException.php
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+class AphrontQueryConnectionException extends AphrontQueryException { }
diff --git a/src/storage/exception/connection/__init__.php b/src/storage/exception/connection/__init__.php
new file mode 100644
index 0000000000..5faef3d23d
--- /dev/null
+++ b/src/storage/exception/connection/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/exception/base');
+
+
+phutil_require_source('AphrontQueryConnectionException.php');
diff --git a/src/storage/exception/connectionlost/AphrontQueryConnectionLostException.php b/src/storage/exception/connectionlost/AphrontQueryConnectionLostException.php
new file mode 100644
index 0000000000..3a73b41fd4
--- /dev/null
+++ b/src/storage/exception/connectionlost/AphrontQueryConnectionLostException.php
@@ -0,0 +1,23 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+class AphrontQueryConnectionLostException
+ extends AphrontQueryRecoverableException { }
diff --git a/src/storage/exception/connectionlost/__init__.php b/src/storage/exception/connectionlost/__init__.php
new file mode 100644
index 0000000000..23583c498d
--- /dev/null
+++ b/src/storage/exception/connectionlost/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/exception/recoverable');
+
+
+phutil_require_source('AphrontQueryConnectionLostException.php');
diff --git a/src/storage/exception/count/AphrontQueryCountException.php b/src/storage/exception/count/AphrontQueryCountException.php
new file mode 100644
index 0000000000..85712d4bb4
--- /dev/null
+++ b/src/storage/exception/count/AphrontQueryCountException.php
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+class AphrontQueryCountException extends AphrontQueryException { }
diff --git a/src/storage/exception/count/__init__.php b/src/storage/exception/count/__init__.php
new file mode 100644
index 0000000000..c6d6bdece2
--- /dev/null
+++ b/src/storage/exception/count/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/exception/base');
+
+
+phutil_require_source('AphrontQueryCountException.php');
diff --git a/src/storage/exception/objectmissing/AphrontQueryObjectMissingException.php b/src/storage/exception/objectmissing/AphrontQueryObjectMissingException.php
new file mode 100644
index 0000000000..7459b431f6
--- /dev/null
+++ b/src/storage/exception/objectmissing/AphrontQueryObjectMissingException.php
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+class AphrontQueryObjectMissingException extends AphrontQueryException { }
diff --git a/src/storage/exception/objectmissing/__init__.php b/src/storage/exception/objectmissing/__init__.php
new file mode 100644
index 0000000000..232ae73031
--- /dev/null
+++ b/src/storage/exception/objectmissing/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/exception/base');
+
+
+phutil_require_source('AphrontQueryObjectMissingException.php');
diff --git a/src/storage/exception/parameter/AphrontQueryParameterException.php b/src/storage/exception/parameter/AphrontQueryParameterException.php
new file mode 100644
index 0000000000..af0ef5340f
--- /dev/null
+++ b/src/storage/exception/parameter/AphrontQueryParameterException.php
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+class AphrontQueryParameterException extends AphrontQueryException {
+
+ private $query;
+
+ public function __construct($query, $message) {
+ parent::__construct($message." Query: ".$query);
+ $this->query = $query;
+ }
+
+ public function getQuery() {
+ return $this->query;
+ }
+
+}
diff --git a/src/storage/exception/parameter/__init__.php b/src/storage/exception/parameter/__init__.php
new file mode 100644
index 0000000000..682623ec52
--- /dev/null
+++ b/src/storage/exception/parameter/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/exception/base');
+
+
+phutil_require_source('AphrontQueryParameterException.php');
diff --git a/src/storage/exception/recoverable/AphrontQueryRecoverableException.php b/src/storage/exception/recoverable/AphrontQueryRecoverableException.php
new file mode 100644
index 0000000000..e1fe588ff5
--- /dev/null
+++ b/src/storage/exception/recoverable/AphrontQueryRecoverableException.php
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+class AphrontQueryRecoverableException extends AphrontQueryException { }
diff --git a/src/storage/exception/recoverable/__init__.php b/src/storage/exception/recoverable/__init__.php
new file mode 100644
index 0000000000..2c0e999b22
--- /dev/null
+++ b/src/storage/exception/recoverable/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/exception/base');
+
+
+phutil_require_source('AphrontQueryRecoverableException.php');
diff --git a/src/storage/lisk/dao/LiskDAO.php b/src/storage/lisk/dao/LiskDAO.php
new file mode 100644
index 0000000000..de90777d70
--- /dev/null
+++ b/src/storage/lisk/dao/LiskDAO.php
@@ -0,0 +1,1109 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Simple object-authoritative data access object that makes it easy to build
+ * stuff that you need to save to a database. Basically, it means that the
+ * amount of boilerplate code (and, particularly, boilerplate SQL) you need
+ * to write is greatly reduced.
+ *
+ * Lisk makes it fairly easy to build something quickly and end up with
+ * reasonably high-quality code when you're done (e.g., getters and setters,
+ * objects, transactions, reasonably structured OO code). It's also very thin:
+ * you can break past it and use MySQL and other lower-level tools when you
+ * need to in those couple of cases where it doesn't handle your workflow
+ * gracefully.
+ *
+ * However, Lisk won't scale past one database and lacks many of the features
+ * of modern DAOs like Hibernate: for instance, it does not support joins or
+ * polymorphic storage.
+ *
+ * This means that Lisk is well-suited for tools like Differential, but often a
+ * poor choice elsewhere. And it is strictly unsuitable for many projects.
+ *
+ * Lisk's model is object-authoritative: the PHP class definition is the
+ * master authority for what the object looks like.
+ *
+ * =Building New Objects=
+ *
+ * To create new Lisk objects, extend @{class:LiskDAO} and implement
+ * @{method:establishConnection}. It should return an AphrontDatabaseConnection;
+ * this will tell Lisk where to save your objects.
+ *
+ * class Dog extends LiskDAO {
+ *
+ * protected $name;
+ * protected $breed;
+ *
+ * public function establishConnection() {
+ * return $some_connection_object;
+ * }
+ * }
+ *
+ * Now, you should create your table:
+ *
+ * CREATE TABLE dog (
+ * id int unsigned not null auto_increment primary key,
+ * name varchar(32) not null,
+ * breed varchar(32) not null,
+ * dateCreated int unsigned not null,
+ * dateModified int unsigned not null
+ * );
+ *
+ * For each property in your class, add a column with the same name to the
+ * table (see getConfiguration() for information about changing this mapping).
+ * Additionally, you should create the three columns `id`, `dateCreated` and
+ * `dateModified`. Lisk will automatically manage these, using them to implement
+ * autoincrement IDs and timestamps. If you do not want to use these features,
+ * see getConfiguration() for information on disabling them. At a bare minimum,
+ * you must normally have an `id` column which is a primary or unique key with a
+ * numeric type, although you can change its name by overriding getIDKey() or
+ * disable it entirely by overriding getIDKey() to return null. Note that many
+ * methods rely on a single-part primary key and will no longer work (they will
+ * throw) if you disable it.
+ *
+ * As you add more properties to your class in the future, remember to add them
+ * to the database table as well.
+ *
+ * Lisk will now automatically handle these operations: getting and setting
+ * properties, saving objects, loading individual objects, loading groups
+ * of objects, updating objects, managing IDs, updating timestamps whenever
+ * an object is created or modified, and some additional specialized
+ * operations.
+ *
+ * = Creating, Retrieving, Updating, and Deleting =
+ *
+ * To create and persist a Lisk object, use save():
+ *
+ * $dog = id(new Dog())
+ * ->setName('Sawyer')
+ * ->setBreed('Pug')
+ * ->save();
+ *
+ * Note that **Lisk automatically builds getters and setters for all of your
+ * object's properties** via __call(). You can override these by defining
+ * versions yourself.
+ *
+ * Calling save() will persist the object to the database. After calling
+ * save(), you can call getID() to retrieve the object's ID.
+ *
+ * To load objects by ID, use the load() method:
+ *
+ * $dog = id(new Dog())->load($id);
+ *
+ * This will load the Dog record with ID $id into $dog, or ##null## if no such
+ * record exists (load() is an instance method rather than a static method
+ * because PHP does not support late static binding, at least until PHP 5.3).
+ *
+ * To update an object, change its properties and save it:
+ *
+ * $dog->setBreed('Lab')->save();
+ *
+ * To delete an object, call delete():
+ *
+ * $dog->delete();
+ *
+ * That's Lisk CRUD in a nutshell.
+ *
+ * = Queries =
+ *
+ * Often, you want to load a bunch of objects, or execute a more specialized
+ * query. Use loadAllWhere() or loadOneWhere() to do this:
+ *
+ * $pugs = $dog->loadAllWhere('breed = %s', 'Pug');
+ * $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');
+ *
+ * These methods work like @{function:queryfx}, but only take half of a query
+ * (the part after the WHERE keyword). Lisk will handle the connection, columns,
+ * and object construction; you are responsible for the rest of it.
+ * loadAllWhere() returns a list of objects, while loadOneWhere() returns a
+ * single object (or null).
+ *
+ * @task config Configuring Lisk
+ * @task load Loading Objects
+ * @task info Examining Objects
+ * @task save Writing Objects
+ * @task hook Hooks and Callbacks
+ * @task util Utilities
+ *
+ * @group storage
+ */
+abstract class LiskDAO {
+
+ const CONFIG_OPTIMISTIC_LOCKS = 'enable-locks';
+ const CONFIG_IDS = 'id-mechanism';
+ const CONFIG_TIMESTAMPS = 'timestamps';
+ const CONFIG_AUX_GUID = 'auxiliary-guid';
+ const CONFIG_SERIALIZATION = 'col-serialization';
+
+ const SERIALIZATION_NONE = 'id';
+ const SERIALIZATION_JSON = 'json';
+ const SERIALIZATION_PHP = 'php';
+
+ const IDS_AUTOINCREMENT = 'ids-auto';
+ const IDS_GUID = 'ids-guid';
+ const IDS_MANUAL = 'ids-manual';
+
+ /**
+ * Build an empty object.
+ *
+ * @return obj Empty object.
+ */
+ public function __construct() {
+ $id_key = $this->getIDKey();
+ if ($id_key) {
+ $this->$id_key = null;
+ }
+ }
+
+ abstract protected function establishConnection($mode);
+
+
+/* -( Configuring Lisk )--------------------------------------------------- */
+
+
+ /**
+ * Change Lisk behaviors, like optimistic locks and timestamps. If you want
+ * to change these behaviors, you should override this method in your child
+ * class and change the options you're interested in. For example:
+ *
+ * public function getConfiguration() {
+ * return array(
+ * Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
+ * ) + parent::getConfiguration();
+ * }
+ *
+ * The available options are:
+ *
+ * CONFIG_OPTIMISTIC_LOCKS
+ * Lisk automatically performs optimistic locking on objects, which protects
+ * you from read-modify-write concurrency problems. Lock failures are
+ * detected at write time and arise when two users read an object, then both
+ * save it. In theory, you should detect these failures and accommodate them
+ * in some sensible way (for instance, by showing the user differences
+ * between the original record and the copy they are trying to update, and
+ * prompting them to merge them). In practice, most Lisk tools are quick
+ * and dirty and don't get to that level of sophistication, but optimistic
+ * locks can still protect you from yourself sometimes. If you don't want
+ * to use optimistic locks, you can disable them. The performance cost of
+ * doing this locking is very very small (optimistic locks were chosen
+ * because they're simple and cheap, and highly optimized for the case where
+ * collisions are rare). By default, this option is OFF.
+ *
+ * CONFIG_IDS
+ * Lisk objects need to have a unique identifying ID. The three mechanisms
+ * available for generating this ID are IDS_AUTOINCREMENT (default, assumes
+ * the ID column is an autoincrement primary key), IDS_GUID (to generate a
+ * unique GUID for each object) or IDS_MANUAL (you are taking full
+ * responsibility for ID management).
+ *
+ * CONFIG_TIMESTAMPS
+ * Lisk can automatically handle keeping track of a `dateCreated' and
+ * `dateModified' column, which it will update when it creates or modifies
+ * an object. If you don't want to do this, you may disable this option.
+ * By default, this option is ON.
+ *
+ * CONFIG_AUX_GUID
+ * This option can be enabled by being set to some truthy value. The meaning
+ * of this value is defined by your guid generation mechanism. If this option
+ * is enabled, a `guid' property will be populated with a unique GUID when an
+ * object is created (or if it is saved and does not currently have one). You
+ * need to override generateGUID() and hook it into your GUID generation
+ * mechanism for this to work. By default, this option is OFF.
+ *
+ * CONFIG_SERIALIZATION
+ * You can optionally provide a column serialization map that will be applied
+ * to values when they are written to the database. For example:
+ *
+ * self::CONFIG_SERIALIZATION => array(
+ * 'complex' => self::SERIALIZATION_JSON,
+ * )
+ *
+ * This will cause Lisk to JSON-serialize the 'complex' field before it is
+ * written, and unserialize it when it is read.
+ *
+ *
+ * @return dictionary Map of configuration options to values.
+ *
+ * @task config
+ */
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_OPTIMISTIC_LOCKS => false,
+ self::CONFIG_IDS => self::IDS_AUTOINCREMENT,
+ self::CONFIG_TIMESTAMPS => true,
+ );
+ }
+
+
+ /**
+ * Determine the setting of a configuration option for this class of objects.
+ *
+ * @param const Option name, one of the CONFIG_* constants.
+ * @return mixed Option value, if configured (null if unavailable).
+ *
+ * @task config
+ */
+ public function getConfigOption($option_name) {
+ static $options = null;
+
+ if (!isset($options)) {
+ $options = $this->getConfiguration();
+ }
+
+ return idx($options, $option_name);
+ }
+
+
+/* -( Loading Objects )---------------------------------------------------- */
+
+
+ /**
+ * Load an object by ID. You need to invoke this as an instance method, not
+ * a class method, because PHP doesn't have late static binding (until
+ * PHP 5.3.0). For example:
+ *
+ * $dog = id(new Dog())->load($dog_id);
+ *
+ * @param int Numeric ID identifying the object to load.
+ * @return obj|null Identified object, or null if it does not exist.
+ *
+ * @task load
+ */
+ public function load($id) {
+ if (!($id = (int)$id)) {
+ throw new Exception("Bogus ID provided to load().");
+ }
+
+ return $this->loadOneWhere(
+ '%C = %d',
+ $this->getIDKeyForUse(),
+ $id);
+ }
+
+
+ /**
+ * Loads all of the objects, unconditionally.
+ *
+ * @return dict Dictionary of all persisted objects of this type, keyed
+ * on object ID.
+ *
+ * @task load
+ */
+ public function loadAll() {
+ return $this->loadAllWhere('1 = 1');
+ }
+
+
+ /**
+ * Load all objects which match a WHERE clause. You provide everything after
+ * the 'WHERE'; Lisk handles everything up to it. For example:
+ *
+ * $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);
+ *
+ * The pattern and arguments are as per queryfx().
+ *
+ * @param string queryfx()-style SQL WHERE clause.
+ * @param ... Zero or more conversions.
+ * @return dict Dictionary of matching objects, keyed on ID.
+ *
+ * @task load
+ */
+ public function loadAllWhere($pattern/*, $arg, $arg, $arg ... */) {
+ $args = func_get_args();
+ $data = call_user_func_array(
+ array($this, 'loadRawDataWhere'),
+ $args);
+ return $this->loadAllFromArray($data);
+ }
+
+
+ /**
+ * Load a single object identified by a 'WHERE' clause. You provide
+ * everything after the 'WHERE', and Lisk builds the first half of the
+ * query. See loadAllWhere(). This method is similar, but returns a single
+ * result instead of a list.
+ *
+ * @param string queryfx()-style SQL WHERE clause.
+ * @param ... Zero or more conversions.
+ * @return obj|null Matching object, or null if no object matches.
+ *
+ * @task load
+ */
+ public function loadOneWhere($pattern/*, $arg, $arg, $arg ... */) {
+ $args = func_get_args();
+ $data = call_user_func_array(
+ array($this, 'loadRawDataWhere'),
+ $args);
+
+ if (count($data) > 1) {
+ throw new AphrontQueryCountException(
+ "More than 1 result from loadOneWhere()!");
+ }
+
+ $data = reset($data);
+ if (!$data) {
+ return null;
+ }
+
+ return $this->loadFromArray($data);
+ }
+
+
+ protected function loadRawDataWhere($pattern/*, $arg, $arg, $arg ... */) {
+ $connection = $this->getConnection('r');
+
+ $lock_clause = '';
+ if ($connection->isReadLocking()) {
+ $lock_clause = 'FOR UPDATE';
+ } else if ($connection->isWriteLocking()) {
+ $lock_clause = 'LOCK IN SHARE MODE';
+ }
+
+ $args = func_get_args();
+ $args = array_slice($args, 1);
+
+ $pattern = 'SELECT * FROM %T WHERE '.$pattern.' %Q';
+ array_unshift($args, $this->getTableName());
+ array_push($args, $lock_clause);
+ array_unshift($args, $pattern);
+
+ return call_user_func_array(
+ array($connection, 'queryData'),
+ $args);
+ }
+
+
+ /**
+ * Reload an object from the database, discarding any changes to persistent
+ * properties. If the object uses optimistic locks and you are in a locking
+ * mode while transactional, this will effectively synchronize the locks.
+ * This is pretty heady. It is unlikely you need to use this method.
+ *
+ * @return this
+ *
+ * @task load
+ */
+ public function reload() {
+
+ if (!$this->getID()) {
+ throw new Exception("Unable to reload object that hasn't been loaded!");
+ }
+
+ $use_locks = $this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS);
+
+ if (!$use_locks) {
+ $result = $this->loadOneWhere(
+ '%C = %d',
+ $this->getIDKeyForUse(),
+ $this->getID());
+ } else {
+ $result = $this->loadOneWhere(
+ '%C = %d AND %C = %d',
+ $this->getIDKeyForUse(),
+ $this->getID(),
+ 'version',
+ $this->getVersion());
+ }
+
+ if (!$result) {
+ throw new AphrontQueryObjectMissingException($use_locks);
+ }
+
+ return $this;
+ }
+
+
+ /**
+ * Initialize this object's properties from a dictionary. Generally, you
+ * load single objects with loadOneWhere(), but sometimes it may be more
+ * convenient to pull data from elsewhere directly (e.g., a complicated
+ * join via queryData()) and then load from an array representation.
+ *
+ * @param dict Dictionary of properties, which should be equivalent to
+ * selecting a row from the table or calling getProperties().
+ * @return this
+ *
+ * @task load
+ */
+ public function loadFromArray(array $row) {
+ $map = array();
+ foreach ($row as $k => $v) {
+ $map[$k] = $v;
+ }
+
+ $this->willReadData($map);
+
+ foreach ($map as $prop => $value) {
+ $this->$prop = $value;
+ }
+
+ $this->didReadData();
+
+ return $this;
+ }
+
+
+ /**
+ * Initialize a list of objects from a list of dictionaries. Usually you
+ * load lists of objects with loadAllWhere(), but sometimes that isn't
+ * flexible enough. One case is if you need to do joins to select the right
+ * objects:
+ *
+ * function loadAllWithOwner($owner) {
+ * $data = $this->queryData(
+ * 'SELECT d.*
+ * FROM owner o
+ * JOIN owner_has_dog od ON o.id = od.ownerID
+ * JOIN dog d ON od.dogID = d.id
+ * WHERE o.id = %d',
+ * $owner);
+ * return $this->loadAllFromArray($data);
+ * }
+ *
+ * This is a lot messier than loadAllWhere(), but more flexible.
+ *
+ * @param list List of property dictionaries.
+ * @return dict List of constructed objects, keyed on ID.
+ *
+ * @task load
+ */
+ public function loadAllFromArray(array $rows) {
+ $result = array();
+
+ $id_key = $this->getIDKey();
+
+ foreach ($rows as $row) {
+ $obj = clone $this;
+ if ($id_key) {
+ $result[$row[$id_key]] = $obj->loadFromArray($row);
+ } else {
+ $result[] = $obj->loadFromArray($row);
+ }
+ }
+
+ return $result;
+ }
+
+
+/* -( Examining Objects )-------------------------------------------------- */
+
+
+ /**
+ * Retrieve the unique, numerical ID identifying this object. This value
+ * will be null if the object hasn't been persisted.
+ *
+ * @return int Unique numerical ID.
+ *
+ * @task info
+ */
+ public function getID() {
+ $id_key = $this->getIDKeyForUse();
+ return $this->$id_key;
+ }
+
+
+ /**
+ * Retrieve a list of all object properties. Note that some may be
+ * "transient", which means they should not be persisted to the database.
+ * Transient properties can be identified by calling
+ * getTransientProperties().
+ *
+ * @return dict Dictionary of normalized (lowercase) to canonical (original
+ * case) property names.
+ *
+ * @task info
+ */
+ protected function getProperties() {
+ static $properties = null;
+ if (!isset($properties)) {
+ $class = new ReflectionClass(get_class($this));
+ $properties = array();
+ foreach ($class->getProperties() as $p) {
+ $properties[strtolower($p->getName())] = $p->getName();
+ }
+
+ $id_key = $this->getIDKey();
+ if ($id_key) {
+ if (!isset($properties[strtolower($id_key)])) {
+ $properties[strtolower($id_key)] = $id_key;
+ }
+ }
+
+ if ($this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS)) {
+ $properties['version'] = 'version';
+ }
+
+ if ($this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
+ $properties['datecreated'] = 'dateCreated';
+ $properties['datemodified'] = 'dateModified';
+ }
+
+ if (!$this->isGUIDPrimaryID() &&
+ $this->getConfigOption(self::CONFIG_AUX_GUID)) {
+ $properties['guid'] = 'guid';
+ }
+ }
+ return $properties;
+ }
+
+
+ /**
+ * Check if a property exists on this object.
+ *
+ * @return string|null Canonical property name, or null if the property
+ * does not exist.
+ *
+ * @task info
+ */
+ protected function checkProperty($property) {
+ static $properties = null;
+ if (!isset($properties)) {
+ $properties = $this->getProperties();
+ }
+
+ return idx($properties, strtolower($property));
+ }
+
+
+ /**
+ * Get or build the database connection for this object.
+ *
+ * @return LiskDatabaseConnection Lisk connection object.
+ *
+ * @task info
+ */
+ protected function getConnection($mode) {
+ if ($mode != 'r' && $mode != 'w') {
+ throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'.");
+ }
+
+ // TODO: We don't do anything with the read/write mode right now, but
+ // should.
+
+ if (!isset($this->__connection)) {
+ $this->__connection = $this->establishConnection($mode);
+ }
+
+ return $this->__connection;
+ }
+
+
+ /**
+ * Convert this object into a property dictionary. This dictionary can be
+ * restored into an object by using loadFromArray() (unless you're using
+ * legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you should
+ * just go ahead and die in a fire).
+ *
+ * @return dict Dictionary of object properties.
+ *
+ * @task info
+ */
+ protected function getPropertyValues() {
+ $map = array();
+ foreach ($this->getProperties() as $p) {
+ // We may receive a warning here for properties we've implicitly added
+ // through configuration; squelch it.
+ $map[$p] = @$this->$p;
+ }
+ return $map;
+ }
+
+
+ /**
+ * Convert this object into a property dictionary containing only properties
+ * which will be persisted to the database.
+ *
+ * @return dict Dictionary of persistent object properties.
+ *
+ * @task info
+ */
+ protected function getPersistentPropertyValues() {
+ $map = $this->getPropertyValues();
+ foreach ($this->getTransientProperties() as $p) {
+ unset($map[$p]);
+ }
+ return $map;
+ }
+
+
+/* -( Writing Objects )---------------------------------------------------- */
+
+
+ /**
+ * Persist this object to the database. In most cases, this is the only
+ * method you need to call to do writes. If the object has not yet been
+ * inserted this will do an insert; if it has, it will do an update.
+ *
+ * @return this
+ *
+ * @task save
+ */
+ public function save() {
+ if ($this->shouldInsertWhenSaved()) {
+ return $this->insert();
+ } else {
+ return $this->update();
+ }
+ }
+
+
+ /**
+ * Save this object, forcing the query to use REPLACE regardless of object
+ * state.
+ *
+ * @return this
+ *
+ * @task save
+ */
+ public function replace() {
+ return $this->insertRecordIntoDatabase('REPLACE');
+ }
+
+
+ /**
+ * Save this object, forcing the query to use INSERT regardless of object
+ * state.
+ *
+ * @return this
+ *
+ * @task save
+ */
+ public function insert() {
+ return $this->insertRecordIntoDatabase('INSERT');
+ }
+
+
+ /**
+ * Save this object, forcing the query to use UPDATE regardless of object
+ * state.
+ *
+ * @return this
+ *
+ * @task save
+ */
+ public function update() {
+ $use_locks = $this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS);
+
+ $this->willSaveObject();
+ $data = $this->getPersistentPropertyValues();
+ $this->willWriteData($data);
+
+ $map = array();
+ foreach ($data as $k => $v) {
+ if ($use_locks && $k == 'version') {
+ continue;
+ }
+ $map[$k] = $v;
+ }
+
+ $conn = $this->getConnection('w');
+
+ foreach ($map as $key => $value) {
+ $map[$key] = qsprintf($conn, '%C = %ns', $key, $value);
+ }
+ $map = implode(', ', $map);
+
+ if ($use_locks) {
+ $conn->query(
+ 'UPDATE %T SET %Q, version = version + 1 WHERE %C = %d AND %C = %d',
+ $this->getTableName(),
+ $map,
+ $this->getIDKeyForUse(),
+ $this->getID(),
+ 'version',
+ $this->getVersion());
+ } else {
+ $conn->query(
+ 'UPDATE %T SET %Q WHERE %C = %d',
+ $this->getTableName(),
+ $map,
+ $this->getIDKeyForUse(),
+ $this->getID());
+ }
+
+ if ($conn->getAffectedRows() !== 1) {
+ throw new AphrontQueryObjectMissingException($use_locks);
+ }
+
+ if ($use_locks) {
+ $this->setVersion($this->getVersion() + 1);
+ }
+
+ $this->didWriteData();
+
+ return $this;
+ }
+
+
+ /**
+ * Delete this object, permanently.
+ *
+ * @return this
+ *
+ * @task save
+ */
+ public function delete() {
+ $this->willDelete();
+
+ $conn = $this->getConnection('w');
+ $conn->query(
+ 'DELETE FROM %T WHERE %C = %d',
+ $this->getTableName(),
+ $this->getIDKeyForUse(),
+ $this->getID());
+
+ $this->didDelete();
+
+ return $this;
+ }
+
+
+ /**
+ * Internal implementation of INSERT and REPLACE.
+ *
+ * @param const Either "INSERT" or "REPLACE", to force the desired mode.
+ *
+ * @task save
+ */
+ protected function insertRecordIntoDatabase($mode) {
+ $this->willSaveObject();
+ $data = $this->getPersistentPropertyValues();
+
+ $id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
+ switch ($id_mechanism) {
+ // If we are using autoincrement IDs, let MySQL assign the value for the
+ // ID column.
+ case self::IDS_AUTOINCREMENT:
+ unset($data[$this->getIDKeyForUse()]);
+ break;
+ case self::IDS_GUID:
+ if (empty($data[$this->getIDKeyForUse()])) {
+ $guid = $this->generateGUID();
+ $this->setID($guid);
+ $data[$this->getIDKeyForUse()] = $guid;
+ }
+ break;
+ case self::IDS_MANUAL:
+ break;
+ default:
+ throw new Exception('Unknown CONFIG_IDs mechanism!');
+ }
+
+ if ($this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS)) {
+ $data['version'] = 0;
+ }
+
+ $this->willWriteData($data);
+
+ $columns = array_keys($data);
+ foreach ($columns as $k => $property) {
+ $columns[$k] = $property;
+ }
+
+ $conn = $this->getConnection('w');
+
+ $conn->query(
+ '%Q INTO %T (%LC) VALUES (%Ls)',
+ $mode,
+ $this->getTableName(),
+ $columns,
+ $data);
+
+ // Update the object with the initial Version value
+ if ($this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS)) {
+ $this->setVersion(0);
+ }
+
+ // Only use the insert id if this table is using auto-increment ids
+ if ($id_mechanism === self::IDS_AUTOINCREMENT) {
+ $this->setID($conn->getInsertID());
+ }
+
+ $this->didWriteData();
+
+ return $this;
+ }
+
+
+ /**
+ * Method used to determine whether to insert or update when saving.
+ *
+ * @return bool true if the record should be inserted
+ */
+ protected function shouldInsertWhenSaved() {
+ $key_type = $this->getConfigOption(self::CONFIG_IDS);
+ $use_locks = $this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS);
+
+ if ($key_type == self::IDS_MANUAL) {
+ if ($use_locks) {
+ // If we are manually keyed and the object has a version (which means
+ // that it has been saved to the DB before), do an update, otherwise
+ // perform an insert.
+ if ($this->getID() && $this->getVersion() !== null) {
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ throw new Exception(
+ 'You are not using optimistic locks, but are using manual IDs. You '.
+ 'must override the shouldInsertWhenSaved() method to properly '.
+ 'detect when to insert a new record.');
+ }
+ } else {
+ return !$this->getID();
+ }
+ }
+
+
+/* -( Hooks and Callbacks )------------------------------------------------ */
+
+
+ /**
+ * Retrieve the database table name. By default, this is the class name.
+ *
+ * @return string Table name for object storage.
+ *
+ * @task hook
+ */
+ public function getTableName() {
+ return get_class($this);
+ }
+
+
+ /**
+ * Helper: Whether this class is configured to use GUIDs as the primary ID.
+ * @task internal
+ */
+ private function isGUIDPrimaryID() {
+ return ($this->getConfigOption(self::CONFIG_IDS) === self::IDS_GUID);
+ }
+
+
+ /**
+ * Retrieve the primary key column, "id" by default. If you can not
+ * reasonably name your ID column "id", override this method.
+ *
+ * @return string Name of the ID column.
+ *
+ * @task hook
+ */
+ public function getIDKey() {
+ return
+ $this->isGUIDPrimaryID() ?
+ 'guid' :
+ 'id';
+ }
+
+
+ protected function getIDKeyForUse() {
+ $id_key = $this->getIDKey();
+ if (!$id_key) {
+ throw new Exception(
+ "This DAO does not have a single-part primary key. The method you ".
+ "called requires a single-part primary key.");
+ }
+ return $id_key;
+ }
+
+
+ /**
+ * Generate a new GUID, used by CONFIG_AUX_GUID and IDS_GUID.
+ *
+ * @return guid Unique, newly allocated GUID.
+ *
+ * @task hook
+ */
+ protected function generateGUID() {
+ throw new Exception(
+ "To use CONFIG_AUX_GUID or IDS_GUID, you need to overload ".
+ "generateGUID() to perform GUID generation.");
+ }
+
+
+ /**
+ * If your object has properties which you don't want to be persisted to the
+ * database, you can override this method and specify them.
+ *
+ * @return list List of properties which should NOT be persisted.
+ * Property names should be in normalized (lowercase) form.
+ * By default, all properties are persistent.
+ *
+ * @task hook
+ */
+ protected function getTransientProperties() {
+ return array();
+ }
+
+
+ /**
+ * Hook to apply serialization or validation to data before it is written to
+ * the database. See also willReadData().
+ *
+ * @task hook
+ */
+ protected function willWriteData(array &$data) {
+ $this->applyLiskDataSerialization($data, false);
+ }
+
+
+ /**
+ * Hook to perform actions after data has been written to the database.
+ *
+ * @task hook
+ */
+ protected function didWriteData() {}
+
+
+ /**
+ * Hook to make internal object state changes prior to INSERT, REPLACE or
+ * UPDATE.
+ *
+ * @task hook
+ */
+ protected function willSaveObject() {
+ $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS);
+
+ if ($use_timestamps) {
+ if (!$this->getDateCreated()) {
+ $this->setDateCreated(time());
+ }
+ $this->setDateModified(time());
+ }
+
+ if (($this->isGUIDPrimaryID() && !$this->getID())) {
+ // If GUIDs are the primary ID, the subclass could have overridden the
+ // name of the ID column.
+ $this->setID($this->generateGUID());
+ } else if ($this->getConfigOption(self::CONFIG_AUX_GUID) &&
+ !$this->getGUID()) {
+ // The subclass could still want GUIDs.
+ $this->setGUID($this->generateGUID());
+ }
+ }
+
+
+ /**
+ * Hook to apply serialization or validation to data as it is read from the
+ * database. See also willWriteData().
+ *
+ * @task hook
+ */
+ protected function willReadData(array &$data) {
+ $this->applyLiskDataSerialization($data, $deserialize = true);
+ }
+
+ /**
+ * Hook to perform an action on data after it is read from the database.
+ *
+ * @task hook
+ */
+ protected function didReadData() {}
+
+ /**
+ * Hook to perform an action before the deletion of an object.
+ *
+ * @task hook
+ */
+ protected function willDelete() {}
+
+ /**
+ * Hook to perform an action after the deletion of an object.
+ *
+ * @task hook
+ */
+ protected function didDelete() {}
+
+/* -( Utilities )---------------------------------------------------------- */
+
+
+ /**
+ * Applies configured serialization to a dictionary of values.
+ *
+ * @task util
+ */
+ protected function applyLiskDataSerialization(array &$data, $deserialize) {
+ $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
+ if ($serialization) {
+ foreach (array_intersect_key($serialization, $data) as $col => $format) {
+ switch ($format) {
+ case self::SERIALIZATION_NONE:
+ break;
+ case self::SERIALIZATION_PHP:
+ if ($deserialize) {
+ $data[$col] = unserialize($data[$col]);
+ } else {
+ $data[$col] = serialize($data[$col]);
+ }
+ break;
+ case self::SERIALIZATION_JSON:
+ if ($deserialize) {
+ $data[$col] = json_decode($data[$col], true);
+ } else {
+ $data[$col] = json_encode($data[$col]);
+ }
+ break;
+ default:
+ throw new Exception("Unknown serialization format '{$format}'.");
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Black magic. Builds implied get*() and set*() for all properties.
+ *
+ * @param string Method name.
+ * @param list Argument vector.
+ * @return mixed get*() methods return the property value. set*() methods
+ * return $this.
+ * @task util
+ */
+ public function __call($method, $args) {
+ if (!strncmp($method, 'get', 3)) {
+ $property = substr($method, 3);
+ if (!($property = $this->checkProperty($property))) {
+ throw new Exception("Bad getter call: {$method}");
+ }
+ if (count($args) !== 0) {
+ throw new Exception("Getter call should have zero args: {$method}");
+ }
+ return @$this->$property;
+ }
+
+ if (!strncmp($method, 'set', 3)) {
+ $property = substr($method, 3);
+ $property = $this->checkProperty($property);
+ if (!$property) {
+ throw new Exception("Bad setter call: {$method}");
+ }
+ if (count($args) !== 1) {
+ throw new Exception("Setter should have exactly one arg: {$method}");
+ }
+ if ($property == 'ID') {
+ $property = $this->getIDKeyForUse();
+ }
+ $this->$property = $args[0];
+ return $this;
+ }
+
+ throw new Exception("Unable to resolve method: {$method}.");
+ }
+}
diff --git a/src/storage/lisk/dao/__init__.php b/src/storage/lisk/dao/__init__.php
new file mode 100644
index 0000000000..94245f8632
--- /dev/null
+++ b/src/storage/lisk/dao/__init__.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/exception/count');
+phutil_require_module('aphront', 'storage/exception/objectmissing');
+phutil_require_module('aphront', 'storage/qsprintf');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('LiskDAO.php');
diff --git a/src/storage/qsprintf/__init__.php b/src/storage/qsprintf/__init__.php
new file mode 100644
index 0000000000..644b47aacd
--- /dev/null
+++ b/src/storage/qsprintf/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/exception/parameter');
+
+phutil_require_module('phutil', 'xsprintf');
+
+
+phutil_require_source('qsprintf.php');
diff --git a/src/storage/qsprintf/qsprintf.php b/src/storage/qsprintf/qsprintf.php
new file mode 100644
index 0000000000..befb0f4c73
--- /dev/null
+++ b/src/storage/qsprintf/qsprintf.php
@@ -0,0 +1,309 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Format an SQL query. This function behaves like sprintf(), except that
+ * all the normal conversions (like %s) will be properly escaped, and
+ * additional conversions are supported:
+ *
+ * %nd, %ns, %nf
+ * "Nullable" versions of %d, %s and %f. Will produce 'NULL' if the
+ * argument is a strict null.
+ *
+ * %=d, %=s, %=f
+ * "Nullable Test" versions of %d, %s and %f. If you pass a value, you
+ * get "= 3"; if you pass null, you get "IS NULL". For instance, this
+ * will work properly if `hatID' is a nullable column and $hat is null.
+ *
+ * qsprintf($conn, 'WHERE hatID %=d', $hat);
+ *
+ * %Ld, %Ls, %Lf
+ * "List" versions of %d, %s and %f. These are appropriate for use in
+ * an "IN" clause. For example:
+ *
+ * qsprintf($conn, 'WHERE hatID IN(%Ld)', $list_of_hats);
+ *
+ * %T ("Table")
+ * Escapes a table name.
+ *
+ * %C, %LC
+ * Escapes a column name or a list of column names.
+ *
+ * %K ("Comment")
+ * Escapes a comment.
+ *
+ * %Q ("Query Fragment")
+ * Injects a raw query fragment. Extremely dangerous! Not escaped!
+ *
+ * %~ ("Substring")
+ * Escapes a substring query for a LIKE (or NOT LIKE) clause. For example:
+ *
+ * // Find all rows with $search as a substing of `name`.
+ * qsprintf($conn, 'WHERE name LIKE %~', $search);
+ *
+ * See also %> and %<.
+ *
+ * %> ("Prefix")
+ * Escapes a prefix query for a LIKE clause. For example:
+ *
+ * // Find all rows where `name` starts with $prefix.
+ * qsprintf($conn, 'WHERE name LIKE %>', $prefix);
+ *
+ * %< ("Suffix")
+ * Escapes a suffix query for a LIKE clause. For example:
+ *
+ * // Find all rows where `name` ends with $suffix.
+ * qsprintf($conn, 'WHERE name LIKE %<', $suffix);
+ *
+ * @group storage
+ */
+function qsprintf($conn, $pattern/*, ... */) {
+ $args = func_get_args();
+ array_shift($args);
+ return xsprintf('xsprintf_query', $conn, $args);
+}
+
+/**
+ * @group storage
+ */
+function vqsprintf($conn, $pattern, array $argv) {
+ array_unshift($argv, $pattern);
+ return xsprintf('xsprintf_query', $conn, $argv);
+}
+
+
+/**
+ * xsprintf() callback for encoding SQL queries. See qsprintf().
+ * @group storage
+ */
+function xsprintf_query($userdata, &$pattern, &$pos, &$value, &$length) {
+ $type = $pattern[$pos];
+ $conn = $userdata;
+ $next = (strlen($pattern) > $pos + 1) ? $pattern[$pos + 1] : null;
+
+ $nullable = false;
+ $done = false;
+
+ $prefix = '';
+
+ switch ($type) {
+ case '=': // Nullable test
+ switch ($next) {
+ case 'd':
+ case 'f':
+ case 's':
+ $pattern = substr_replace($pattern, '', $pos, 1);
+ $length = strlen($pattern);
+ $type = 's';
+ if ($value === null) {
+ $value = 'IS NULL';
+ $done = true;
+ } else {
+ $prefix = '= ';
+ $type = $next;
+ }
+ break;
+ default:
+ throw new Exception('Unknown conversion, try %=d, %=s, or %=f.');
+ }
+ break;
+
+ case 'n': // Nullable...
+ switch ($next) {
+ case 'd': // ...integer.
+ case 'f': // ...float.
+ case 's': // ...string.
+ $pattern = substr_replace($pattern, '', $pos, 1);
+ $length = strlen($pattern);
+ $type = $next;
+ $nullable = true;
+ break;
+ default:
+ throw new Exception('Unknown conversion, try %nd or %ns.');
+ }
+ break;
+
+ case 'L': // List of..
+ _qsprintf_check_type($value, "L{$next}", $pattern);
+ $pattern = substr_replace($pattern, '', $pos, 1);
+ $length = strlen($pattern);
+ $type = 's';
+ $done = true;
+
+ switch ($next) {
+ case 'd': // ...integers.
+ $value = implode(', ', array_map('intval', $value));
+ break;
+ case 's': // ...strings.
+ foreach ($value as $k => $v) {
+ $value[$k] = "'".$conn->escapeString($v)."'";
+ }
+ $value = implode(', ', $value);
+ break;
+ case 'C': // ...columns.
+ foreach ($value as $k => $v) {
+ $value[$k] = $conn->escapeColumnName($v);
+ }
+ $value = implode(', ', $value);
+ break;
+ default:
+ throw new Exception("Unknown conversion %L{$next}.");
+ }
+ break;
+ }
+
+ if (!$done) {
+ _qsprintf_check_type($value, $type, $pattern);
+ switch ($type) {
+ case 's': // String
+ if ($nullable && $value === null) {
+ $value = 'NULL';
+ } else {
+ $value = "'".$conn->escapeString($value)."'";
+ }
+ $type = 's';
+ break;
+
+ case 'Q': // Query Fragment
+ $type = 's';
+ break;
+
+ case '~': // Like Substring
+ case '>': // Like Prefix
+ case '<': // Like Suffix
+ $value = $conn->escapeStringForLikeClause($value);
+ switch ($type) {
+ case '~': $value = "'%".$value."%'"; break;
+ case '>': $value = "'" .$value."%'"; break;
+ case '<': $value = "'%".$value. "'"; break;
+ }
+ $type = 's';
+ break;
+
+ case 'f': // Float
+ if ($nullable && $value === null) {
+ $value = 'NULL';
+ } else {
+ $value = (float)$value;
+ }
+ $type = 's';
+ break;
+
+ case 'd': // Integer
+ if ($nullable && $value === null) {
+ $value = 'NULL';
+ } else {
+ $value = (int)$value;
+ }
+ $type = 's';
+ break;
+
+ case 'T': // Table
+ case 'C': // Column
+ $value = $conn->escapeColumnName($value);
+ $type = 's';
+ break;
+
+ case 'K': // Komment
+ $value = $conn->escapeMultilineComment($value);
+ $type = 's';
+ break;
+
+ default:
+ throw new Exception("Unknown conversion '%{$type}'.");
+
+ }
+ }
+
+ if ($prefix) {
+ $value = $prefix.$value;
+ }
+ $pattern[$pos] = $type;
+}
+
+
+/**
+ * @group storage
+ */
+function _qsprintf_check_type($value, $type, $query) {
+ switch ($type) {
+ case 'Ld': case 'Ls': case 'LC': case 'LA': case 'LO':
+ if (!is_array($value)) {
+ throw new AphrontQueryParameterException(
+ $query,
+ "Expected array argument for %{$type} conversion.");
+ }
+ if (empty($value)) {
+ throw new AphrontQueryParameterException(
+ $query,
+ "Array for %{$type} conversion is empty.");
+ }
+
+ foreach ($value as $scalar) {
+ _qsprintf_check_scalar_type($scalar, $type, $query);
+ }
+ break;
+ default:
+ _qsprintf_check_scalar_type($value, $type, $query);
+ }
+}
+
+
+/**
+ * @group storage
+ */
+function _qsprintf_check_scalar_type($value, $type, $query) {
+ switch ($type) {
+ case 'Q': case 'LC': case 'T': case 'C':
+ if (!is_string($value)) {
+ throw new AphrontQueryParameterException(
+ $query,
+ "Expected a string for %{$type} conversion.");
+ }
+ break;
+
+ case 'Ld': case 'd': case 'f':
+ if (!is_null($value) && !is_scalar($value)) {
+ throw new AphrontQueryParameterException(
+ $query,
+ "Expected a scalar or null for %{$type} conversion.");
+ }
+ break;
+
+ case 'Ls': case 's':
+ case '~': case '>': case '<': case 'K':
+ if (!is_null($value) && !is_scalar($value)) {
+ throw new AphrontQueryParameterException(
+ $query,
+ "Expected a scalar or null for %{$type} conversion.");
+ }
+ break;
+
+ case 'LA': case 'LO':
+ if (!is_null($value) && !is_scalar($value) &&
+ !(is_array($value) && !empty($value))) {
+ throw new AphrontQueryParameterException(
+ $query,
+ "Expected a scalar or null or non-empty array for ".
+ "%{$type} conversion.");
+ }
+ break;
+ default:
+ throw new Exception("Unknown conversion '{$type}'.");
+ }
+}
diff --git a/src/storage/queryfx/__init__.php b/src/storage/queryfx/__init__.php
new file mode 100644
index 0000000000..c64372fa52
--- /dev/null
+++ b/src/storage/queryfx/__init__.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'storage/exception/count');
+phutil_require_module('aphront', 'storage/qsprintf');
+
+
+phutil_require_source('queryfx.php');
diff --git a/src/storage/queryfx/queryfx.php b/src/storage/queryfx/queryfx.php
new file mode 100644
index 0000000000..2a7d8d231f
--- /dev/null
+++ b/src/storage/queryfx/queryfx.php
@@ -0,0 +1,58 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group storage
+ */
+function queryfx(AphrontDatabaseConnection $conn, $sql/*, ... */) {
+ $argv = func_get_args();
+ $query = call_user_func_array('qsprintf', $argv);
+ return $conn->executeRawQuery($query);
+}
+
+/**
+ * @group storage
+ */
+function vqueryfx($conn, $sql, $argv) {
+ array_unshift($argv, $conn, $sql);
+ return call_user_func_array('queryfx', $argv);
+}
+
+/**
+ * @group storage
+ */
+function queryfx_all($conn, $sql/*, ... */) {
+ $argv = func_get_args();
+ $ret = call_user_func_array('queryfx', $argv);
+ return $conn->selectAllResults($ret);
+}
+
+/**
+ * @group storage
+ */
+function queryfx_one($conn, $sql/*, ... */) {
+ $argv = func_get_args();
+ $ret = call_user_func_array('queryfx_all', $argv);
+ if (count($ret) > 1) {
+ throw new AphrontQueryCountException(
+ 'Query returned more than one row.');
+ } else if (count($ret)) {
+ return reset($ret);
+ }
+ return null;
+}
diff --git a/src/view/base/AphrontView.php b/src/view/base/AphrontView.php
new file mode 100755
index 0000000000..263f56b98d
--- /dev/null
+++ b/src/view/base/AphrontView.php
@@ -0,0 +1,52 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+abstract class AphrontView {
+
+ protected $children = array();
+
+ final public function appendChild($child) {
+ $this->children[] = $child;
+ return $this;
+ }
+
+ final protected function renderChildren() {
+ $out = array();
+ foreach ($this->children as $child) {
+ $out[] = $this->renderChild($child);
+ }
+ return implode('', $out);
+ }
+
+ private function renderChild($child) {
+ if ($child instanceof AphrontView) {
+ return $child->render();
+ } else if (is_array($child)) {
+ $out = array();
+ foreach ($child as $element) {
+ $out[] = $this->renderChild($element);
+ }
+ return implode('', $out);
+ } else {
+ return $child;
+ }
+ }
+
+ abstract public function render();
+
+}
diff --git a/src/view/base/__init__.php b/src/view/base/__init__.php
new file mode 100644
index 0000000000..7c6ce51617
--- /dev/null
+++ b/src/view/base/__init__.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+
+phutil_require_source('AphrontView.php');
diff --git a/src/view/control/table/AphrontTableView.php b/src/view/control/table/AphrontTableView.php
new file mode 100755
index 0000000000..94925d15b5
--- /dev/null
+++ b/src/view/control/table/AphrontTableView.php
@@ -0,0 +1,134 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontTableView extends AphrontView {
+
+ protected $data;
+ protected $headers;
+ protected $rowClasses = array();
+ protected $columnClasses = array();
+ protected $zebraStripes = true;
+ protected $noDataString;
+ protected $className;
+
+ public function __construct(array $data) {
+ $this->data = $data;
+ }
+
+ public function setHeaders(array $headers) {
+ $this->headers = $headers;
+ return $this;
+ }
+
+ public function setColumnClasses(array $column_classes) {
+ $this->columnClasses = $column_classes;
+ return $this;
+ }
+
+ public function setRowClasses(array $row_classes) {
+ $this->rowClasses = $row_classes;
+ return $this;
+ }
+
+ public function setNoDataString($no_data_string) {
+ $this->noDataString = $no_data_string;
+ return $this;
+ }
+
+ public function setClassName($class_name) {
+ $this->className = $class_name;
+ return $this;
+ }
+
+ public function setZebraStripes($zebra_stripes) {
+ $this->zebraStripes = $zebra_stripes;
+ return $this;
+ }
+
+ public function render() {
+ $class = $this->className;
+ if ($class !== null) {
+ $class = ' class="aphront-table-view '.$class.'"';
+ } else {
+ $class = ' class="aphront-table-view"';
+ }
+ $table = array('<table'.$class.'>');
+
+ $col_classes = array();
+ foreach ($this->columnClasses as $key => $class) {
+ if (strlen($class)) {
+ $col_classes[] = ' class="'.$class.'"';
+ } else {
+ $col_classes[] = null;
+ }
+ }
+
+ $headers = $this->headers;
+ if ($headers) {
+ $table[] = '<tr>';
+ foreach ($headers as $col_num => $header) {
+ $class = idx($col_classes, $col_num);
+ $table[] = '<th'.$class.'>'.$header.'</th>';
+ }
+ $table[] = '</tr>';
+ }
+
+ $data = $this->data;
+ if ($data) {
+ $row_num = 0;
+ foreach ($data as $row) {
+ while (count($row) > count($col_classes)) {
+ $col_classes[] = null;
+ }
+ $class = idx($this->rowClasses, $row_num);
+ if ($this->zebraStripes && ($row_num % 2)) {
+ if ($class !== null) {
+ $class = 'alt alt-'.$class;
+ } else {
+ $class = 'alt';
+ }
+ }
+ if ($class !== null) {
+ $class = ' class="'.$class.'"';
+ }
+ $table[] = '<tr'.$class.'>';
+ $col_num = 0;
+ foreach ($row as $value) {
+ $class = $col_classes[$col_num];
+ if ($class !== null) {
+ $table[] = '<td'.$class.'>';
+ } else {
+ $table[] = '<td>';
+ }
+ $table[] = $value.'</td>';
+ ++$col_num;
+ }
+ ++$row_num;
+ }
+ } else {
+ $colspan = max(count($headers), 1);
+ $table[] =
+ '<tr class="no-data"><td colspan="'.$colspan.'">'.
+ coalesce($this->noDataString, 'No data available.').
+ '</td></tr>';
+ }
+ $table[] = '</table>';
+ return implode('', $table);
+ }
+}
+
diff --git a/src/view/control/table/__init__.php b/src/view/control/table/__init__.php
new file mode 100644
index 0000000000..ccf2f80eda
--- /dev/null
+++ b/src/view/control/table/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/base');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontTableView.php');
diff --git a/src/view/dialog/AphrontDialogView.php b/src/view/dialog/AphrontDialogView.php
new file mode 100755
index 0000000000..9ffb498154
--- /dev/null
+++ b/src/view/dialog/AphrontDialogView.php
@@ -0,0 +1,90 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontDialogView extends AphrontView {
+
+ private $title;
+ private $submitButton;
+ private $cancelURI;
+ private $submitURI;
+
+ public function setSubmitURI($uri) {
+ $this->submitURI = $uri;
+ return $this;
+ }
+
+ public function setTitle($title) {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function getTitle() {
+ return $this->title;
+ }
+
+ public function addSubmitButton($text = 'Okay') {
+ $this->submitButton = $text;
+ return $this;
+ }
+
+ public function addCancelButton($uri) {
+ $this->cancelURI = $uri;
+ return $this;
+ }
+
+ final public function render() {
+
+ $buttons = array();
+ if ($this->submitButton) {
+ $buttons[] =
+ '<button name="__submit__">'.
+ phutil_escape_html($this->submitButton).
+ '</button>';
+ }
+
+ if ($this->cancelURI) {
+ $buttons[] = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $this->cancelURI,
+ 'class' => 'button grey',
+ ),
+ 'Cancel');
+ }
+
+ return phutil_render_tag(
+ 'form',
+ array(
+ 'class' => 'aphront-dialog-view',
+ 'action' => $this->submitURI,
+ 'method' => 'post',
+ ),
+ '<input type="hidden" name="__form__" value="1" />'.
+ '<div class="aphront-dialog-head">'.
+ phutil_escape_html($this->title).
+ '</div>'.
+ '<div class="aphront-dialog-body">'.
+ $this->renderChildren().
+ '</div>'.
+ '<div class="aphront-dialog-tail">'.
+ implode('', $buttons).
+ '<div style="clear: both;"></div>'.
+ '</div>');
+ }
+
+}
diff --git a/src/view/dialog/__init__.php b/src/view/dialog/__init__.php
new file mode 100644
index 0000000000..efb4dc14cc
--- /dev/null
+++ b/src/view/dialog/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/base');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontDialogView.php');
diff --git a/src/view/form/base/AphrontFormView.php b/src/view/form/base/AphrontFormView.php
new file mode 100755
index 0000000000..bb03038d86
--- /dev/null
+++ b/src/view/form/base/AphrontFormView.php
@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class AphrontFormView extends AphrontView {
+
+ private $action;
+ private $method = 'POST';
+ private $header;
+ private $data = array();
+
+ public function setAction($action) {
+ $this->action = $action;
+ return $this;
+ }
+
+ public function setMethod($method) {
+ $this->method = $method;
+ return $this;
+ }
+
+ public function render() {
+ return phutil_render_tag(
+ 'form',
+ array(
+ 'action' => $this->action,
+ 'method' => $this->method,
+ 'class' => 'aphront-form-view',
+ ),
+ $this->renderDataInputs().
+ $this->renderChildren());
+ }
+
+ private function renderDataInputs() {
+ $data = $this->data + array(
+ '__form__' => 1,
+ );
+ $inputs = array();
+ foreach ($data as $key => $value) {
+ $inputs[] = phutil_render_tag(
+ 'input',
+ array(
+ 'type' => 'hidden',
+ 'name' => $key,
+ 'value' => $value,
+ ));
+ }
+ return implode("\n", $inputs);
+ }
+
+}
diff --git a/src/view/form/base/__init__.php b/src/view/form/base/__init__.php
new file mode 100644
index 0000000000..74bc6816be
--- /dev/null
+++ b/src/view/form/base/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/base');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontFormView.php');
diff --git a/src/view/form/control/base/AphrontFormControl.php b/src/view/form/control/base/AphrontFormControl.php
new file mode 100755
index 0000000000..9fa982b131
--- /dev/null
+++ b/src/view/form/control/base/AphrontFormControl.php
@@ -0,0 +1,127 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+abstract class AphrontFormControl extends AphrontView {
+
+ private $label;
+ private $caption;
+ private $error;
+ private $name;
+ private $value;
+
+ public function setLabel($label) {
+ $this->label = $label;
+ return $this;
+ }
+
+ public function getLabel() {
+ return $this->label;
+ }
+
+ public function setCaption($caption) {
+ $this->caption = $caption;
+ return $this;
+ }
+
+ public function getCaption() {
+ return $this->caption;
+ }
+
+ public function setError($error) {
+ $this->error = $error;
+ return $this;
+ }
+
+ public function getError() {
+ return $this->error;
+ }
+
+ public function setName($name) {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName() {
+ return $this->name;
+ }
+
+ public function setValue($value) {
+ $this->value = $value;
+ return $this;
+ }
+
+ public function getValue() {
+ return $this->value;
+ }
+
+ abstract protected function renderInput();
+ abstract protected function getCustomControlClass();
+
+ final public function render() {
+ $custom_class = $this->getCustomControlClass();
+
+ if (strlen($this->getLabel())) {
+ $label =
+ '<label>'.
+ phutil_escape_html($this->getLabel()).
+ ':'.
+ '</label>';
+ } else {
+ $label = null;
+ $custom_class .= ' aphront-form-control-nolabel';
+ }
+
+ $input =
+ '<div class="aphront-form-input">'.
+ $this->renderInput().
+ '</div>';
+
+ if (strlen($this->getError())) {
+ $error = $this->getError();
+ if ($error === true) {
+ $error = '*';
+ } else {
+ $error = "\xC2\xAB ".$error;
+ }
+ $error =
+ '<div class="aphront-form-error">'.
+ phutil_escape_html($error).
+ '</div>';
+ } else {
+ $error = null;
+ }
+
+ if (strlen($this->getCaption())) {
+ $caption =
+ '<div class="aphront-form-caption">'.
+ phutil_escape_html($this->getCaption()).
+ '</div>';
+ } else {
+ $caption = null;
+ }
+
+ return
+ '<div class="aphront-form-control '.$custom_class.'">'.
+ $error.
+ $label.
+ $input.
+ $caption.
+ '<div style="clear: both;"></div>'.
+ '</div>';
+ }
+}
diff --git a/src/view/form/control/base/__init__.php b/src/view/form/control/base/__init__.php
new file mode 100644
index 0000000000..0699e96bd3
--- /dev/null
+++ b/src/view/form/control/base/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/base');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontFormControl.php');
diff --git a/src/view/form/control/select/AphrontFormSelectControl.php b/src/view/form/control/select/AphrontFormSelectControl.php
new file mode 100755
index 0000000000..a15eda8f49
--- /dev/null
+++ b/src/view/form/control/select/AphrontFormSelectControl.php
@@ -0,0 +1,56 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontFormSelectControl extends AphrontFormControl {
+
+ protected function getCustomControlClass() {
+ return 'aphront-form-control-select';
+ }
+
+ private $options;
+
+ public function setOptions(array $options) {
+ $this->options = $options;
+ return $this;
+ }
+
+ public function getOptions() {
+ return $this->options;
+ }
+
+ protected function renderInput() {
+ $options = array();
+ foreach ($this->getOptions() as $value => $label) {
+ $options[] = phutil_render_tag(
+ 'option',
+ array(
+ 'selected' => ($value == $this->getValue()) ? 'selected' : null,
+ 'value' => $value,
+ ),
+ phutil_escape_html($label));
+ }
+
+ return phutil_render_tag(
+ 'select',
+ array(
+ 'name' => $this->getName(),
+ ),
+ implode("\n", $options));
+ }
+
+}
diff --git a/src/view/form/control/select/__init__.php b/src/view/form/control/select/__init__.php
new file mode 100644
index 0000000000..a52e7d2455
--- /dev/null
+++ b/src/view/form/control/select/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/form/control/base');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontFormSelectControl.php');
diff --git a/src/view/form/control/submit/AphrontFormSubmitControl.php b/src/view/form/control/submit/AphrontFormSubmitControl.php
new file mode 100755
index 0000000000..16dd7b9a5e
--- /dev/null
+++ b/src/view/form/control/submit/AphrontFormSubmitControl.php
@@ -0,0 +1,48 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontFormSubmitControl extends AphrontFormControl {
+
+ protected $cancelButton;
+
+ public function addCancelButton($href, $label = 'Cancel') {
+ $this->cancelButton = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $href,
+ 'class' => 'button grey',
+ ),
+ phutil_escape_html($label));
+ return $this;
+ }
+
+ protected function getCustomControlClass() {
+ return 'aphront-form-control-submit';
+ }
+
+ protected function renderInput() {
+ return phutil_render_tag(
+ 'button',
+ array(
+ 'name' => '__submit__',
+ ),
+ phutil_escape_html($this->getValue())).
+ $this->cancelButton;
+ }
+
+}
diff --git a/src/view/form/control/submit/__init__.php b/src/view/form/control/submit/__init__.php
new file mode 100644
index 0000000000..e77d2ceb4b
--- /dev/null
+++ b/src/view/form/control/submit/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/form/control/base');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontFormSubmitControl.php');
diff --git a/src/view/form/control/text/AphrontFormTextControl.php b/src/view/form/control/text/AphrontFormTextControl.php
new file mode 100755
index 0000000000..f62fd6f463
--- /dev/null
+++ b/src/view/form/control/text/AphrontFormTextControl.php
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontFormTextControl extends AphrontFormControl {
+
+ protected function getCustomControlClass() {
+ return 'aphront-form-control-text';
+ }
+
+ protected function renderInput() {
+ return phutil_render_tag(
+ 'input',
+ array(
+ 'type' => 'text',
+ 'name' => $this->getName(),
+ 'value' => $this->getValue(),
+ ));
+ }
+
+}
diff --git a/src/view/form/control/text/__init__.php b/src/view/form/control/text/__init__.php
new file mode 100644
index 0000000000..e46731b72e
--- /dev/null
+++ b/src/view/form/control/text/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/form/control/base');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontFormTextControl.php');
diff --git a/src/view/form/control/textarea/AphrontFormTextAreaControl.php b/src/view/form/control/textarea/AphrontFormTextAreaControl.php
new file mode 100755
index 0000000000..f52cbeb9d6
--- /dev/null
+++ b/src/view/form/control/textarea/AphrontFormTextAreaControl.php
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontFormTextAreaControl extends AphrontFormControl {
+
+ protected function getCustomControlClass() {
+ return 'aphront-form-control-textarea';
+ }
+
+ protected function renderInput() {
+ return phutil_render_tag(
+ 'textarea',
+ array(
+ 'name' => $this->getName(),
+ ),
+ phutil_escape_html($this->getValue()));
+ }
+
+}
diff --git a/src/view/form/control/textarea/__init__.php b/src/view/form/control/textarea/__init__.php
new file mode 100644
index 0000000000..84284cde8f
--- /dev/null
+++ b/src/view/form/control/textarea/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/form/control/base');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontFormTextAreaControl.php');
diff --git a/src/view/form/error/AphrontErrorView.php b/src/view/form/error/AphrontErrorView.php
new file mode 100755
index 0000000000..6ab50560ca
--- /dev/null
+++ b/src/view/form/error/AphrontErrorView.php
@@ -0,0 +1,64 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class AphrontErrorView extends AphrontView {
+
+ private $title;
+ private $errors;
+
+ public function setTitle($title) {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function setErrors(array $errors) {
+ $this->errors = $errors;
+ return $this;
+ }
+
+ final public function render() {
+
+ $errors = $this->errors;
+ if ($errors) {
+ $list = array();
+ foreach ($errors as $error) {
+ $list[] = phutil_render_tag(
+ 'li',
+ array(),
+ phutil_escape_html($error));
+ }
+ $list = '<ul>'.implode("\n", $list).'</ul>';
+ } else {
+ $list = null;
+ }
+
+ $title = $this->title;
+ if (strlen($title)) {
+ $title = '<h1>'.phutil_escape_html($title).'</h1>';
+ } else {
+ $title = null;
+ }
+
+ return
+ '<div class="aphront-error-view">'.
+ $title.
+ $list.
+ '</div>';
+
+ }
+}
diff --git a/src/view/form/error/__init__.php b/src/view/form/error/__init__.php
new file mode 100644
index 0000000000..6f2261e3a3
--- /dev/null
+++ b/src/view/form/error/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/base');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontErrorView.php');
diff --git a/src/view/layout/panel/AphrontPanelView.php b/src/view/layout/panel/AphrontPanelView.php
new file mode 100755
index 0000000000..105be61596
--- /dev/null
+++ b/src/view/layout/panel/AphrontPanelView.php
@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class AphrontPanelView extends AphrontView {
+
+ const WIDTH_FULL = 'full';
+ const WIDTH_FORM = 'form';
+
+ private $createButton;
+ private $header;
+ private $width;
+
+ public function setCreateButton($create_button, $href) {
+
+ $this->createButton = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $href,
+ 'class' => 'create-button button green',
+ ),
+ $create_button);
+
+ return $this;
+ }
+
+ public function setHeader($header) {
+ $this->header = $header;
+ return $this;
+ }
+
+ public function setWidth($width) {
+ $this->width = $width;
+ return $this;
+ }
+
+ public function render() {
+ if ($this->header !== null) {
+ $header = '<h1>'.$this->header.'</h1>';
+ } else {
+ $header = null;
+ }
+
+ if ($this->createButton !== null) {
+ $button = $this->createButton;
+ } else {
+ $button = null;
+ }
+
+ $table = $this->renderChildren();
+
+ $class = array('aphront-panel-view');
+ if ($this->width) {
+ $class[] = 'aphront-panel-width-'.$this->width;
+ }
+
+ return
+ '<div class="'.implode(' ', $class).'">'.
+ $button.
+ $header.
+ $table.
+ '</div>';
+ }
+
+}
diff --git a/src/view/layout/panel/__init__.php b/src/view/layout/panel/__init__.php
new file mode 100644
index 0000000000..392b1c72d1
--- /dev/null
+++ b/src/view/layout/panel/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/base');
+
+phutil_require_module('phutil', 'markup');
+
+
+phutil_require_source('AphrontPanelView.php');
diff --git a/src/view/null/AphrontNullView.php b/src/view/null/AphrontNullView.php
new file mode 100755
index 0000000000..da10e465d9
--- /dev/null
+++ b/src/view/null/AphrontNullView.php
@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+final class AphrontNullView extends AphrontView {
+
+ public function render() {
+ return $this->renderChildren();
+ }
+
+}
diff --git a/src/view/null/__init__.php b/src/view/null/__init__.php
new file mode 100644
index 0000000000..3e6bb66b3b
--- /dev/null
+++ b/src/view/null/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/base');
+
+
+phutil_require_source('AphrontNullView.php');
diff --git a/src/view/page/base/AphrontPageView.php b/src/view/page/base/AphrontPageView.php
new file mode 100755
index 0000000000..f0883ecd46
--- /dev/null
+++ b/src/view/page/base/AphrontPageView.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontPageView extends AphrontView {
+
+ private $title;
+
+ public function setTitle($title) {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function getTitle() {
+ return $this->title;
+ }
+
+ protected function getHead() {
+ return '';
+ }
+
+ protected function getBody() {
+ return $this->renderChildren();
+ }
+
+ protected function getTail() {
+ return '';
+ }
+
+ public function render() {
+
+ $title = $this->getTitle();
+ $head = $this->getHead();
+ $body = $this->getBody();
+ $tail = $this->getTail();
+
+ return <<<EOHTML
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>{$title}</title>
+ {$head}
+ </head>
+ <body>
+ {$body}
+ </body>
+ {$tail}
+</html>
+
+EOHTML;
+ }
+
+}
diff --git a/src/view/page/base/__init__.php b/src/view/page/base/__init__.php
new file mode 100644
index 0000000000..ec0cdcbaa2
--- /dev/null
+++ b/src/view/page/base/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/base');
+
+
+phutil_require_source('AphrontPageView.php');
diff --git a/src/view/page/standard/AphrontStandardPageView.php b/src/view/page/standard/AphrontStandardPageView.php
new file mode 100755
index 0000000000..c82582d27d
--- /dev/null
+++ b/src/view/page/standard/AphrontStandardPageView.php
@@ -0,0 +1,110 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontStandardPageView extends AphrontPageView {
+
+ private $baseURI;
+ private $applicationName;
+ private $tabs = array();
+ private $selectedTab;
+ private $glyph;
+
+ public function setApplicationName($application_name) {
+ $this->applicationName = $application_name;
+ return $this;
+ }
+
+ public function getApplicationName() {
+ return $this->applicationName;
+ }
+
+ public function setBaseURI($base_uri) {
+ $this->baseURI = $base_uri;
+ return $this;
+ }
+
+ public function getBaseURI() {
+ return $this->baseURI;
+ }
+
+ public function setTabs(array $tabs, $selected_tab) {
+ $this->tabs = $tabs;
+ $this->selectedTab = $selected_tab;
+ return $this;
+ }
+
+ public function getTitle() {
+ return $this->getGlyph().' '.parent::getTitle();
+ }
+
+ protected function getHead() {
+ return
+ '<link rel="stylesheet" type="text/css" href="/rsrc/css/base.css" />';
+ }
+
+ public function setGlyph($glyph) {
+ $this->glyph = $glyph;
+ return $this;
+ }
+
+ public function getGlyph() {
+ return $this->glyph;
+ }
+
+ protected function getBody() {
+
+ $tabs = array();
+ foreach ($this->tabs as $name => $tab) {
+ $tabs[] = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => idx($tab, 'href'),
+ 'class' => ($name == $this->selectedTab)
+ ? 'aphront-selected-tab'
+ : null,
+ ),
+ phutil_escape_html(idx($tab, 'name')));
+ }
+ $tabs = implode('', $tabs);
+ if ($tabs) {
+ $tabs = '<span class="aphront-head-tabs">'.$tabs.'</span>';
+ }
+
+ return
+ '<div class="aphront-standard-page">'.
+ '<div class="aphront-standard-header">'.
+ '<a href="/">Aphront</a> '.
+ phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $this->getBaseURI(),
+ 'class' => 'aphront-head-appname',
+ ),
+ phutil_escape_html($this->getApplicationName())).
+ $tabs.
+ '</div>'.
+ $this->renderChildren().
+ '<div style="clear: both;"></div>'.
+ '</div>';
+ }
+
+ protected function getTail() {
+ return '';
+ }
+
+}
diff --git a/src/view/page/standard/__init__.php b/src/view/page/standard/__init__.php
new file mode 100644
index 0000000000..4e589ecc60
--- /dev/null
+++ b/src/view/page/standard/__init__.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('aphront', 'view/page/base');
+
+phutil_require_module('phutil', 'markup');
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontStandardPageView.php');
diff --git a/webroot/index.php b/webroot/index.php
new file mode 100644
index 0000000000..f9df44a471
--- /dev/null
+++ b/webroot/index.php
@@ -0,0 +1,86 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+setup_aphront_basics();
+
+$host = $_SERVER['HTTP_HOST'];
+$path = $_REQUEST['__path__'];
+
+// Based on the host and path, choose which application should serve the
+// request. The default is the Aphront demo, but you'll want to replace this
+// with whichever other applications you're running.
+
+switch ($host) {
+ default:
+ phutil_require_module('phutil', 'autoload');
+ phutil_autoload_class('AphrontDefaultApplicationConfiguration');
+ $application = new AphrontDefaultApplicationConfiguration();
+ break;
+}
+
+$application->setHost($host);
+$application->setPath($path);
+$request = $application->buildRequest();
+$application->setRequest($request);
+list($controller, $uri_data) = $application->buildController();
+$controller->willProcessRequest($uri_data);
+try {
+ $response = $controller->processRequest();
+} catch (Exception $ex) {
+ $response = $application->handleException($ex);
+}
+
+$response = $application->willSendResponse($response);
+
+$response->setRequest($request);
+
+$response_string = $response->buildResponseString();
+$headers = $response->getCacheHeaders();
+$headers = array_merge($headers, $response->getHeaders());
+foreach ($headers as $header) {
+ list($header, $value) = $header;
+ header("{$header}: {$value}");
+}
+echo $response_string;
+
+
+/**
+ * @group aphront
+ */
+function setup_aphront_basics() {
+ $aphront_root = dirname(dirname(__FILE__));
+ $libraries_root = dirname($aphront_root);
+
+ ini_set('include_path', ini_get('include_path').':'.$libraries_root.'/');
+ @include_once 'libphutil/src/__phutil_library_init__.php';
+ if (!@constant('__LIBPHUTIL__')) {
+ echo "ERROR: Unable to load libphutil. Update your PHP 'include_path' to ".
+ "include the parent directory of libphutil/.\n";
+ exit(1);
+ }
+
+ if (!ini_get('date.timezone')) {
+ date_default_timezone_set('America/Los_Angeles');
+ }
+
+ phutil_load_library($aphront_root.'/src');
+}
+
+function __autoload($class_name) {
+ PhutilSymbolLoader::loadClass($class_name);
+}
diff --git a/webroot/rsrc/css/base.css b/webroot/rsrc/css/base.css
new file mode 100644
index 0000000000..f305a544c2
--- /dev/null
+++ b/webroot/rsrc/css/base.css
@@ -0,0 +1,534 @@
+html {
+ overflow-y: scroll;
+}
+
+body, div, dl, dt, dd, ul, ol, li,
+h1, h2, h3, h4, h5, h6,
+pre, form, fieldset,
+p, blockquote, th, td, button {
+ margin: 0;
+ padding: 0;
+ outline: 0;
+ border: 0;
+}
+
+html {
+ padding-bottom: 16em;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+fieldset, img {
+ border: 0;
+}
+
+address, caption, cite, code, dfn, th, var {
+ font-style: normal;
+ font-weight: normal;
+}
+
+ol, ul {
+ list-style: none;
+}
+
+caption, th {
+ text-align: left;
+}
+
+td, th {
+ vertical-align: top;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-size: 100%;
+ font-weight: bold;
+}
+
+body {
+ font: 13px/1.231 'lucida grande', tahoma, verdana, arial, sans-serif;
+ background: #ACACAC;
+ direction: ltr;
+ text-align: left;
+ unicode-bidi: embed;
+ *font-size: small;
+}
+
+select, input, button, textarea, button {
+ font: 99% 'lucida grande', tahoma, verdana, arial, clean, sans-serif;
+}
+
+table {
+ font-size: inherit;
+ font: 100%;
+}
+
+h1 {
+ font-size: 16px;
+}
+
+h2 {
+ font-size: 14px;
+}
+
+a {
+ -moz-outline-style: none;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+a:visited {
+ color: #3b5998;
+}
+
+a:link {
+ color: #3b5998;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+img {
+ display: block;
+}
+
+/******************************************************************************/
+
+/* Buttons */
+
+/******************************************************************************/
+
+button,
+a.button,
+a.button:visited,
+input.inputsubmit {
+ background: #5e77aa url('/rsrc/image/sprite.png') 0 0 repeat-x;
+ border: 1px solid #29447e;
+ border-bottom-color: #1a356e;
+ color: #fff;
+ cursor: pointer;
+ font-weight: bold;
+ text-align: center;
+ white-space: nowrap;
+ display: inline-block;
+ font-size: 13px;
+ overflow: visible;
+ padding: 2px 8px 3px 8px;
+ line-height: 18px;
+ vertical-align: baseline;
+ width: auto;
+ box-shadow: 0px 1px 0px rgba(0,0,0,.12);
+ -moz-box-shadow: 0px 1px 0px rgba(0,0,0,.12);
+ -webkit-box-shadow: 0px 1px 0px rgba(0,0,0,.12);
+}
+
+button {
+ *padding: 2px 4px 1px 8px;
+ _padding-right: 6px;
+}
+
+a.button,
+a.button:visited {
+ *padding: 3px 8px 4px;
+}
+
+/* Buttons with images (full size only) */
+button.icon,
+a.icon,
+a.icon:visited {
+ padding-left: 0;
+ position: relative;
+ text-indent: 29px;
+}
+
+/* Fix for IE7 within table cells ? */
+td button {
+ *width: 100%;
+ *padding-right: 8px;
+}
+
+button:active,
+a.button:active {
+ background-color: #4f6aa3;
+ background-position: 0 -100px;
+ border-bottom-color: #29447e;
+}
+
+button.green,
+a.green,
+a.green:visited {
+ background-color: #6da952;
+ background-position: 0 -50px;
+ border: 1px solid #3b6e22;
+ border-bottom-color: #2c5a15;
+}
+
+button.green:active,
+a.green:active {
+ background-color: #5e9d43;
+ background-position: 0 -150px;
+ border-bottom-color: #3b6e22;
+}
+
+button.grey,
+input.inputaux,
+a.grey,
+a.grey:visited,
+a.button.disabled,
+button.disabled {
+ background-color: #e4e5e5;
+ background-position: 0 -250px;
+ border: 1px solid #999;
+ border-bottom-color: #888;
+ color: #333;
+ box-shadow: 0px 1px 0px rgba(0,0,0,.07);
+ -moz-box-shadow: 0px 1px 0px rgba(0,0,0,.07);
+ -webkit-box-shadow: 0px 1px 0px rgba(0,0,0,.07);
+}
+
+a.disabled,
+button.disabled {
+ filter:alpha(opacity=50);
+ -moz-opacity:0.5;
+ -khtml-opacity: 0.5;
+ opacity: 0.5;
+}
+
+button.grey:active,
+a.grey:active,
+button.grey_active {
+ background-color: #dddddd;
+ background-position: 0 -200px;
+ border-bottom-color: #999;
+}
+
+button:active::-moz-focus-inner,
+button:focus::-moz-focus-inner {
+ border-color: #405071;
+}
+
+button.green:active::-moz-focus-inner,
+button.green:focus::-moz-focus-inner {
+ border-color: #4c713b;
+}
+
+button.grey:active::-moz-focus-inner,
+button.grey:focus::-moz-focus-inner {
+ border-color: #666;
+}
+
+a.button:hover {
+ text-decoration: none;
+}
+
+button.small,
+a.small,
+a.small:visited {
+ padding: 2px 7px;
+ height: auto;
+ font-size: 11px;
+ line-height: 16px;
+}
+
+
+/******************************************************************************/
+
+/* Aphront */
+
+/******************************************************************************/
+
+.aphront-standard-page {
+ background: #ffffff;
+ border-bottom: 1px solid #888888;
+ font-size: 14px;
+
+ -webkit-box-shadow: 0 0 6px #000;
+ -mox-box-shadow: 0 0 6px #000;
+ box-shadow: 0 0 6px #000;
+}
+
+.aphront-standard-header {
+ background: #003366;
+ color: white;
+ padding: 1em 1em 0.5em 1em;
+ overflow: hidden;
+ position: relative;
+}
+
+.aphront-standard-header a {
+ color: white;
+}
+
+.aphront-standard-header .aphront-head-tabs {
+ padding: 0 1em;
+ font-size: 13px;
+ font-weight: bold;
+}
+
+.aphront-standard-header .aphront-head-tabs a {
+ border-bottom: 3px solid transparent;
+ padding: 0.5em 0.75em;
+ position: relative;
+ bottom: 2px;
+}
+
+.aphront-standard-header .aphront-head-tabs a.aphront-selected-tab {
+ border-bottom-color: #cccccc;
+}
+
+.aphront-standard-header .aphront-head-appname {
+ padding: 0 1em;
+ text-transform: uppercase;
+}
+
+
+.aphront-directory-list {
+ margin: 1em 3% 8em;
+}
+
+.aphront-directory-category h1 {
+ border-bottom: 1px solid #cccccc;
+ margin-bottom: .5em;
+ padding-bottom: .1em;
+}
+
+.aphront-directory-list h2 {
+ font-size: 14px;
+ font-weight: bold;
+ padding: 0;
+ margin: 0;
+}
+
+.aphront-directory-list p {
+ color: #444444;
+ font-size: 12px;
+ padding: .05em .5em .5em;
+}
+
+.aphront-directory-category {
+ padding: 10px;
+ width: 300px;
+ float: left;
+}
+
+.aphront-directory-group {
+ padding: 0 .5em 3em;
+}
+
+
+.aphront-panel-view {
+ background: #f3f3f3;
+ border: 1px solid #c0c0c0;
+ border-width: 1px 0 0;
+ padding: 1em 2em;
+ margin: 1em 2em;
+}
+
+.aphront-panel-view h1 {
+ font-size: 14px;
+ font-weight: bold;
+ padding: 2px 0 8px;
+}
+
+.aphront-panel-view a.create-button {
+ float: right;
+}
+
+.aphront-panel-width-form {
+ width: 720px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+.aphront-table-view {
+ width: 100%;
+ border-collapse: collapse;
+ background: #fdfdfd;
+ border: 1px solid #003366;
+}
+
+.aphront-table-view tr.alt {
+ background: #efefef;
+}
+
+.aphront-table-view th {
+ font-size: 12px;
+ font-weight: bold;
+ padding: 4px 8px;
+ background: #003366;
+ color: white;
+ white-space: nowrap;
+}
+
+.aphront-table-view td.header {
+ padding: 4px 8px;
+ background: #3b5998;
+ color: white;
+ white-space: nowrap;
+ text-align: right;
+}
+
+.aphront-table-view td {
+ vertical-align: top;
+ padding: 4px 8px;
+ font-size: 11px;
+ white-space: nowrap;
+}
+
+.aphront-table-view td.action {
+ padding-top: 1px;
+ padding-bottom: 1px;
+}
+
+.aphront-table-view td.larger {
+ font-size: 14px;
+}
+
+.aphront-table-view td.pri {
+ font-weight: bold;
+}
+
+.aphront-table-view td.wide {
+ white-space: normal;
+ width: 100%;
+}
+
+.aphront-table-view td.right {
+ text-align: right;
+}
+
+.aphront-table-view td.mono {
+ font-family: "Monaco", monospace;
+ font-size: 10px;
+}
+
+.aphront-table-view tr.no-data td {
+ padding: 1em;
+ text-align: center;
+ color: #888888;
+ font-style: italic;
+}
+
+
+/******************************************************************************/
+
+/* forms */
+
+/******************************************************************************/
+
+.aphront-form-view {
+ background: #e7e7e7;
+ border: 1px solid #c4c4c4;
+ padding: 1em;
+}
+
+.aphront-form-view label {
+ padding-top: 4px;
+ width: 14%;
+ float: left;
+ text-align: right;
+ font-weight: bold;
+ font-size: 13px;
+ color: #666666;
+}
+
+.aphront-form-input {
+ margin-left: 15%;
+ margin-right: 25%;
+ width: 60%;
+}
+
+.aphront-form-error {
+ width: 23%;
+ float: right;
+ color: #aa0000;
+ font-weight: bold;
+ padding-top: 4px;
+}
+
+.aphront-form-input input,
+.aphront-form-input textarea {
+ font-size: 12px;
+ width: 100%;
+}
+
+.aphront-form-input textarea {
+ height: 12em;
+}
+
+.aphront-form-control {
+ padding: 4px;
+}
+
+.aphront-form-control-submit button,
+.aphront-form-control-submit a.button {
+ float: right;
+ margin: 1em 0 0em 2%;
+}
+
+.aphront-form-view .aphront-form-caption {
+ font-size: 11px;
+ color: #444444;
+ text-align: right;
+ clear: both;
+ margin-right: 25%;
+ margin-left: 15%;
+}
+
+.aphront-error-view {
+ width: 720px;
+ margin: 1em auto;
+ border: 1px solid #aa0000;
+ padding: 1em;
+ background: #f9b9bc;
+}
+
+
+
+/******************************************************************************/
+
+/* dialog */
+
+/******************************************************************************/
+
+
+.aphront-dialog-view {
+ width: 480px;
+ padding: 8px;
+ background: #666;
+ margin: auto;
+}
+
+.aphront-dialog-head {
+ background: #003366;
+ border: none;
+ font-size: 15px;
+ padding: 5px 12px 6px;
+ color: #ffffff;
+}
+
+
+.aphront-dialog-body {
+ background: #ffffff;
+ padding: 16px 12px;
+ border: none;
+ overflow: hidden;
+}
+
+.aphront-dialog-tail {
+ border: none;
+ background: #ededed;
+ padding: 0.5em;
+ text-align: right;
+}
+
+.aphront-dialog-tail button,
+.aphront-dialog-tail a.button {
+ float: right;
+ margin-left: .5em;
+}
+
+
diff --git a/webroot/rsrc/image/sprite.png b/webroot/rsrc/image/sprite.png
new file mode 100644
index 0000000000..f4cf0312ff
Binary files /dev/null and b/webroot/rsrc/image/sprite.png differ

File Metadata

Mime Type
text/x-diff
Expires
Thu, Aug 14, 5:42 PM (12 h, 14 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
201919
Default Alt Text
(194 KB)

Event Timeline