mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 06:42:42 +01:00
Phabricator file upload application.
This commit is contained in:
parent
2c7e71cac5
commit
0eac30d80d
25 changed files with 848 additions and 16 deletions
|
@ -17,8 +17,11 @@ phutil_register_library_map(array(
|
||||||
'AphrontDialogResponse' => 'aphront/response/dialog',
|
'AphrontDialogResponse' => 'aphront/response/dialog',
|
||||||
'AphrontDialogView' => 'view/dialog',
|
'AphrontDialogView' => 'view/dialog',
|
||||||
'AphrontErrorView' => 'view/form/error',
|
'AphrontErrorView' => 'view/form/error',
|
||||||
|
'AphrontFileResponse' => 'aphront/response/file',
|
||||||
'AphrontFormControl' => 'view/form/control/base',
|
'AphrontFormControl' => 'view/form/control/base',
|
||||||
|
'AphrontFormFileControl' => 'view/form/control/file',
|
||||||
'AphrontFormSelectControl' => 'view/form/control/select',
|
'AphrontFormSelectControl' => 'view/form/control/select',
|
||||||
|
'AphrontFormStaticControl' => 'view/form/control/static',
|
||||||
'AphrontFormSubmitControl' => 'view/form/control/submit',
|
'AphrontFormSubmitControl' => 'view/form/control/submit',
|
||||||
'AphrontFormTextAreaControl' => 'view/form/control/textarea',
|
'AphrontFormTextAreaControl' => 'view/form/control/textarea',
|
||||||
'AphrontFormTextControl' => 'view/form/control/text',
|
'AphrontFormTextControl' => 'view/form/control/text',
|
||||||
|
@ -59,6 +62,13 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorDirectoryItemEditController' => 'applications/directory/controller/itemedit',
|
'PhabricatorDirectoryItemEditController' => 'applications/directory/controller/itemedit',
|
||||||
'PhabricatorDirectoryItemListController' => 'applications/directory/controller/itemlist',
|
'PhabricatorDirectoryItemListController' => 'applications/directory/controller/itemlist',
|
||||||
'PhabricatorDirectoryMainController' => 'applications/directory/controller/main',
|
'PhabricatorDirectoryMainController' => 'applications/directory/controller/main',
|
||||||
|
'PhabricatorFile' => 'applications/files/storage/file',
|
||||||
|
'PhabricatorFileController' => 'applications/files/controller/base',
|
||||||
|
'PhabricatorFileDAO' => 'applications/files/storage/base',
|
||||||
|
'PhabricatorFileListController' => 'applications/files/controller/list',
|
||||||
|
'PhabricatorFileStorageBlob' => 'applications/files/storage/storageblob',
|
||||||
|
'PhabricatorFileUploadController' => 'applications/files/controller/upload',
|
||||||
|
'PhabricatorFileViewController' => 'applications/files/controller/view',
|
||||||
'PhabricatorLiskDAO' => 'applications/base/storage/lisk',
|
'PhabricatorLiskDAO' => 'applications/base/storage/lisk',
|
||||||
'PhabricatorPHID' => 'applications/phid/storage/phid',
|
'PhabricatorPHID' => 'applications/phid/storage/phid',
|
||||||
'PhabricatorPHIDAllocateController' => 'applications/phid/controller/allocate',
|
'PhabricatorPHIDAllocateController' => 'applications/phid/controller/allocate',
|
||||||
|
@ -90,8 +100,11 @@ phutil_register_library_map(array(
|
||||||
'AphrontDialogResponse' => 'AphrontResponse',
|
'AphrontDialogResponse' => 'AphrontResponse',
|
||||||
'AphrontDialogView' => 'AphrontView',
|
'AphrontDialogView' => 'AphrontView',
|
||||||
'AphrontErrorView' => 'AphrontView',
|
'AphrontErrorView' => 'AphrontView',
|
||||||
|
'AphrontFileResponse' => 'AphrontResponse',
|
||||||
'AphrontFormControl' => 'AphrontView',
|
'AphrontFormControl' => 'AphrontView',
|
||||||
|
'AphrontFormFileControl' => 'AphrontFormControl',
|
||||||
'AphrontFormSelectControl' => 'AphrontFormControl',
|
'AphrontFormSelectControl' => 'AphrontFormControl',
|
||||||
|
'AphrontFormStaticControl' => 'AphrontFormControl',
|
||||||
'AphrontFormSubmitControl' => 'AphrontFormControl',
|
'AphrontFormSubmitControl' => 'AphrontFormControl',
|
||||||
'AphrontFormTextAreaControl' => 'AphrontFormControl',
|
'AphrontFormTextAreaControl' => 'AphrontFormControl',
|
||||||
'AphrontFormTextControl' => 'AphrontFormControl',
|
'AphrontFormTextControl' => 'AphrontFormControl',
|
||||||
|
@ -121,6 +134,13 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorDirectoryItemEditController' => 'PhabricatorDirectoryController',
|
'PhabricatorDirectoryItemEditController' => 'PhabricatorDirectoryController',
|
||||||
'PhabricatorDirectoryItemListController' => 'PhabricatorDirectoryController',
|
'PhabricatorDirectoryItemListController' => 'PhabricatorDirectoryController',
|
||||||
'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController',
|
'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController',
|
||||||
|
'PhabricatorFile' => 'PhabricatorFileDAO',
|
||||||
|
'PhabricatorFileController' => 'PhabricatorController',
|
||||||
|
'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
|
||||||
|
'PhabricatorFileListController' => 'PhabricatorFileController',
|
||||||
|
'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
|
||||||
|
'PhabricatorFileUploadController' => 'PhabricatorFileController',
|
||||||
|
'PhabricatorFileViewController' => 'PhabricatorFileController',
|
||||||
'PhabricatorLiskDAO' => 'LiskDAO',
|
'PhabricatorLiskDAO' => 'LiskDAO',
|
||||||
'PhabricatorPHID' => 'PhabricatorPHIDDAO',
|
'PhabricatorPHID' => 'PhabricatorPHIDDAO',
|
||||||
'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController',
|
'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController',
|
||||||
|
|
|
@ -51,6 +51,13 @@ class AphrontDefaultApplicationConfiguration
|
||||||
'category/delete/(?<id>\d+)/'
|
'category/delete/(?<id>\d+)/'
|
||||||
=> 'PhabricatorDirectoryCategoryDeleteController',
|
=> 'PhabricatorDirectoryCategoryDeleteController',
|
||||||
),
|
),
|
||||||
|
'/file/' => array(
|
||||||
|
'$' => 'PhabricatorFileListController',
|
||||||
|
'upload/$' => 'PhabricatorFileUploadController',
|
||||||
|
'(?<view>info)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
|
||||||
|
'(?<view>view)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
|
||||||
|
'(?<view>download)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
|
||||||
|
),
|
||||||
'/phid/' => array(
|
'/phid/' => array(
|
||||||
'$' => 'PhabricatorPHIDListController',
|
'$' => 'PhabricatorPHIDListController',
|
||||||
'type/$' => 'PhabricatorPHIDTypeListController',
|
'type/$' => 'PhabricatorPHIDTypeListController',
|
||||||
|
|
70
src/aphront/response/file/AphrontFileResponse.php
Normal file
70
src/aphront/response/file/AphrontFileResponse.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?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 AphrontFileResponse extends AphrontResponse {
|
||||||
|
|
||||||
|
private $content;
|
||||||
|
private $mimeType;
|
||||||
|
private $download;
|
||||||
|
|
||||||
|
public function setDownload($download) {
|
||||||
|
$this->download = $download;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDownload() {
|
||||||
|
return $this->download;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMimeType($mime_type) {
|
||||||
|
$this->mimeType = $mime_type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMimeType() {
|
||||||
|
return $this->mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContent($content) {
|
||||||
|
$this->content = $content;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildResponseString() {
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeaders() {
|
||||||
|
$headers = array(
|
||||||
|
array('Content-Type', $this->getMimeType()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->getDownload()) {
|
||||||
|
$headers[] = array(
|
||||||
|
'Content-Disposition',
|
||||||
|
'attachment; filename='.$this->getDownload(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abstract class PhabricatorFileController extends PhabricatorController {
|
||||||
|
|
||||||
|
public function buildStandardPageResponse($view, array $data) {
|
||||||
|
$page = new PhabricatorStandardPageView();
|
||||||
|
|
||||||
|
$page->setApplicationName('Files');
|
||||||
|
$page->setBaseURI('/file/');
|
||||||
|
$page->setTitle(idx($data, 'title'));
|
||||||
|
$page->setGlyph("\xE2\x87\xAA");
|
||||||
|
$page->appendChild($view);
|
||||||
|
|
||||||
|
$response = new AphrontWebpageResponse();
|
||||||
|
return $response->setContent($page->render());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
src/applications/files/controller/base/__init__.php
Normal file
16
src/applications/files/controller/base/__init__.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'aphront/response/webpage');
|
||||||
|
phutil_require_module('phabricator', 'applications/base/controller/base');
|
||||||
|
phutil_require_module('phabricator', 'view/page/standard');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorFileController.php');
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?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 PhabricatorFileListController extends PhabricatorFileController {
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
$files = id(new PhabricatorFile())->loadAllWhere(
|
||||||
|
'1 = 1 ORDER BY id DESC LIMIT 100');
|
||||||
|
|
||||||
|
$rows = array();
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$rows[] = array(
|
||||||
|
phutil_escape_html($file->getPHID()),
|
||||||
|
phutil_escape_html($file->getName()),
|
||||||
|
phutil_escape_html($file->getByteSize()),
|
||||||
|
phutil_render_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'class' => 'small button grey',
|
||||||
|
'href' => '/file/info/'.$file->getPHID().'/',
|
||||||
|
),
|
||||||
|
'Info'),
|
||||||
|
phutil_render_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'class' => 'small button grey',
|
||||||
|
'href' => '/file/view/'.$file->getPHID().'/',
|
||||||
|
),
|
||||||
|
'View'),
|
||||||
|
phutil_render_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'class' => 'small button grey',
|
||||||
|
'href' => '/file/download/'.$file->getPHID().'/',
|
||||||
|
),
|
||||||
|
'Download'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = new AphrontTableView($rows);
|
||||||
|
$table->setHeaders(
|
||||||
|
array(
|
||||||
|
'PHID',
|
||||||
|
'Name',
|
||||||
|
'Size',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
));
|
||||||
|
$table->setColumnClasses(
|
||||||
|
array(
|
||||||
|
null,
|
||||||
|
'wide',
|
||||||
|
null,
|
||||||
|
'action',
|
||||||
|
'action',
|
||||||
|
'action',
|
||||||
|
));
|
||||||
|
|
||||||
|
$panel = new AphrontPanelView();
|
||||||
|
$panel->appendChild($table);
|
||||||
|
$panel->setHeader('Files');
|
||||||
|
$panel->setCreateButton('Upload File', '/file/upload/');
|
||||||
|
|
||||||
|
return $this->buildStandardPageResponse($panel, array(
|
||||||
|
'title' => 'Files',
|
||||||
|
'tab' => 'files',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
18
src/applications/files/controller/list/__init__.php
Normal file
18
src/applications/files/controller/list/__init__.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'applications/files/controller/base');
|
||||||
|
phutil_require_module('phabricator', 'applications/files/storage/file');
|
||||||
|
phutil_require_module('phabricator', 'view/control/table');
|
||||||
|
phutil_require_module('phabricator', 'view/layout/panel');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'markup');
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorFileListController.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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class PhabricatorFileUploadController extends PhabricatorFileController {
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
|
||||||
|
$request = $this->getRequest();
|
||||||
|
if ($request->isFormPost()) {
|
||||||
|
$file = PhabricatorFile::newFromPHPUpload(
|
||||||
|
idx($_FILES, 'file'),
|
||||||
|
array(
|
||||||
|
'name' => $request->getStr('name'),
|
||||||
|
));
|
||||||
|
|
||||||
|
return id(new AphrontRedirectResponse())
|
||||||
|
->setURI('/file/info/'.phutil_escape_uri($file->getPHID()).'/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = new AphrontFormView();
|
||||||
|
$form->setAction('/file/upload/');
|
||||||
|
|
||||||
|
$form
|
||||||
|
->setEncType('multipart/form-data')
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormFileControl())
|
||||||
|
->setLabel('File')
|
||||||
|
->setName('file')
|
||||||
|
->setError(true))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormTextControl())
|
||||||
|
->setLabel('Name')
|
||||||
|
->setName('name')
|
||||||
|
->setCaption('Optional file display name.'))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormSubmitControl())
|
||||||
|
->setValue('Upload')
|
||||||
|
->addCancelButton('/file/'));
|
||||||
|
|
||||||
|
$panel = new AphrontPanelView();
|
||||||
|
$panel->setHeader('Upload File');
|
||||||
|
|
||||||
|
$panel->appendChild($form);
|
||||||
|
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
|
||||||
|
|
||||||
|
return $this->buildStandardPageResponse(
|
||||||
|
array($panel),
|
||||||
|
array(
|
||||||
|
'title' => 'Upload File',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
src/applications/files/controller/upload/__init__.php
Normal file
20
src/applications/files/controller/upload/__init__.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'aphront/response/redirect');
|
||||||
|
phutil_require_module('phabricator', 'applications/files/controller/base');
|
||||||
|
phutil_require_module('phabricator', 'applications/files/storage/file');
|
||||||
|
phutil_require_module('phabricator', 'view/form/base');
|
||||||
|
phutil_require_module('phabricator', 'view/form/control/submit');
|
||||||
|
phutil_require_module('phabricator', 'view/layout/panel');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'markup');
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorFileUploadController.php');
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?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 PhabricatorFileViewController extends PhabricatorFileController {
|
||||||
|
|
||||||
|
private $phid;
|
||||||
|
private $view;
|
||||||
|
|
||||||
|
public function willProcessRequest(array $data) {
|
||||||
|
$this->phid = $data['phid'];
|
||||||
|
$this->view = $data['view'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
|
||||||
|
$file = id(new PhabricatorFile())->loadOneWhere(
|
||||||
|
'phid = %s',
|
||||||
|
$this->phid);
|
||||||
|
if (!$file) {
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->view) {
|
||||||
|
case 'download':
|
||||||
|
case 'view':
|
||||||
|
$data = $file->loadFileData();
|
||||||
|
$response = new AphrontFileResponse();
|
||||||
|
$response->setContent($data);
|
||||||
|
$response->setMimeType($file->getMimeType());
|
||||||
|
if ($this->view == 'download') {
|
||||||
|
$response->setDownload($file->getName());
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = new AphrontFormView();
|
||||||
|
$form->setAction('/file/view/'.$file->getPHID().'/');
|
||||||
|
$form
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel('Name')
|
||||||
|
->setName('name')
|
||||||
|
->setValue($file->getName()))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel('PHID')
|
||||||
|
->setName('phid')
|
||||||
|
->setValue($file->getPHID()))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel('Created')
|
||||||
|
->setName('created')
|
||||||
|
->setValue(date('Y-m-d g:i:s A', $file->getDateCreated())))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel('Mime Type')
|
||||||
|
->setName('mime')
|
||||||
|
->setValue($file->getMimeType()))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel('Size')
|
||||||
|
->setName('size')
|
||||||
|
->setValue($file->getByteSize().' bytes'))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel('Engine')
|
||||||
|
->setName('storageEngine')
|
||||||
|
->setValue($file->getStorageEngine()))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel('Format')
|
||||||
|
->setName('storageFormat')
|
||||||
|
->setValue($file->getStorageFormat()))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel('Handle')
|
||||||
|
->setName('storageHandle')
|
||||||
|
->setValue($file->getStorageHandle()))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormSubmitControl())
|
||||||
|
->setValue('View File'));
|
||||||
|
|
||||||
|
$panel = new AphrontPanelView();
|
||||||
|
$panel->setHeader('File Info - '.$file->getName());
|
||||||
|
|
||||||
|
$panel->appendChild($form);
|
||||||
|
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
|
||||||
|
|
||||||
|
return $this->buildStandardPageResponse(
|
||||||
|
array($panel),
|
||||||
|
array(
|
||||||
|
'title' => 'File Info - '.$file->getName(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
20
src/applications/files/controller/view/__init__.php
Normal file
20
src/applications/files/controller/view/__init__.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'aphront/response/404');
|
||||||
|
phutil_require_module('phabricator', 'aphront/response/file');
|
||||||
|
phutil_require_module('phabricator', 'applications/files/controller/base');
|
||||||
|
phutil_require_module('phabricator', 'applications/files/storage/file');
|
||||||
|
phutil_require_module('phabricator', 'view/form/base');
|
||||||
|
phutil_require_module('phabricator', 'view/form/control/submit');
|
||||||
|
phutil_require_module('phabricator', 'view/layout/panel');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorFileViewController.php');
|
25
src/applications/files/storage/base/PhabricatorFileDAO.php
Normal file
25
src/applications/files/storage/base/PhabricatorFileDAO.php
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class PhabricatorFileDAO extends PhabricatorLiskDAO {
|
||||||
|
|
||||||
|
public function getApplicationName() {
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
src/applications/files/storage/base/__init__.php
Normal file
12
src/applications/files/storage/base/__init__.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'applications/base/storage/lisk');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorFileDAO.php');
|
170
src/applications/files/storage/file/PhabricatorFile.php
Normal file
170
src/applications/files/storage/file/PhabricatorFile.php
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
<?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 PhabricatorFile extends PhabricatorFileDAO {
|
||||||
|
|
||||||
|
const STORAGE_ENGINE_BLOB = 'blob';
|
||||||
|
|
||||||
|
const STORAGE_FORMAT_RAW = 'raw';
|
||||||
|
|
||||||
|
const PHID_TYPE = 'FILE';
|
||||||
|
|
||||||
|
// TODO: We need to reconcile this with MySQL packet size.
|
||||||
|
const FILE_SIZE_BYTE_LIMIT = 12582912;
|
||||||
|
|
||||||
|
protected $phid;
|
||||||
|
protected $name;
|
||||||
|
protected $mimeType;
|
||||||
|
protected $byteSize;
|
||||||
|
|
||||||
|
protected $storageEngine;
|
||||||
|
protected $storageFormat;
|
||||||
|
protected $storageHandle;
|
||||||
|
|
||||||
|
public function getConfiguration() {
|
||||||
|
return array(
|
||||||
|
self::CONFIG_AUX_PHID => true,
|
||||||
|
) + parent::getConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generatePHID() {
|
||||||
|
return PhabricatorPHID::generateNewPHID(self::PHID_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function newFromPHPUpload($spec, array $params = array()) {
|
||||||
|
if (!$spec) {
|
||||||
|
throw new Exception("No file was uploaded!");
|
||||||
|
}
|
||||||
|
|
||||||
|
$err = idx($spec, 'error');
|
||||||
|
if ($err) {
|
||||||
|
throw new Exception("File upload failed with error '{$err}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp_name = idx($spec, 'tmp_name');
|
||||||
|
$is_valid = @is_uploaded_file($tmp_name);
|
||||||
|
if (!$is_valid) {
|
||||||
|
throw new Exception("File is not an uploaded file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_data = Filesystem::readFile($tmp_name);
|
||||||
|
$file_size = idx($spec, 'size');
|
||||||
|
|
||||||
|
if (strlen($file_data) != $file_size) {
|
||||||
|
throw new Exception("File size disagrees with uploaded size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_name = nonempty(
|
||||||
|
idx($params, 'name'),
|
||||||
|
idx($spec, 'name'));
|
||||||
|
$params = array(
|
||||||
|
'name' => $file_name,
|
||||||
|
) + $params;
|
||||||
|
|
||||||
|
return self::newFromFileData($file_data, $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function newFromFileData($data, array $params = array()) {
|
||||||
|
$file_size = strlen($data);
|
||||||
|
|
||||||
|
if ($file_size > self::FILE_SIZE_BYTE_LIMIT) {
|
||||||
|
throw new Exception("File is too large to store.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_name = idx($params, 'name');
|
||||||
|
$file_name = self::normalizeFileName($file_name);
|
||||||
|
|
||||||
|
$file = new PhabricatorFile();
|
||||||
|
$file->setName($file_name);
|
||||||
|
$file->setByteSize(strlen($data));
|
||||||
|
|
||||||
|
$blob = new PhabricatorFileStorageBlob();
|
||||||
|
$blob->setData($data);
|
||||||
|
$blob->save();
|
||||||
|
|
||||||
|
// TODO: This stuff is almost certainly YAGNI, but we could imagine having
|
||||||
|
// an alternate disk store and gzipping or encrypting things or something
|
||||||
|
// crazy like that and this isn't toooo much extra code.
|
||||||
|
$file->setStorageEngine(self::STORAGE_ENGINE_BLOB);
|
||||||
|
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
|
||||||
|
$file->setStorageHandle($blob->getID());
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tmp = new TempFile();
|
||||||
|
Filesystem::writeFile($tmp, $data);
|
||||||
|
list($stdout) = execx('file -b --mime %s', $tmp);
|
||||||
|
$file->setMimeType($stdout);
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
// Be robust here since we don't really care that much about mime types.
|
||||||
|
}
|
||||||
|
|
||||||
|
$file->save();
|
||||||
|
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function normalizeFileName($file_name) {
|
||||||
|
return preg_replace('/[^a-zA-Z0-9.~_-]/', '_', $file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete() {
|
||||||
|
$this->openTransaction();
|
||||||
|
switch ($this->getStorageEngine()) {
|
||||||
|
case self::STORAGE_ENGINE_BLOB:
|
||||||
|
$handle = $this->getStorageHandle();
|
||||||
|
$blob = id(new PhabricatorFileStorageBlob())->load($handle);
|
||||||
|
$blob->delete();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Exception("Unknown storage engine!");
|
||||||
|
}
|
||||||
|
|
||||||
|
$ret = parent::delete();
|
||||||
|
$this->saveTransaction();
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadFileData() {
|
||||||
|
|
||||||
|
$handle = $this->getStorageHandle();
|
||||||
|
$data = null;
|
||||||
|
|
||||||
|
switch ($this->getStorageEngine()) {
|
||||||
|
case self::STORAGE_ENGINE_BLOB:
|
||||||
|
$blob = id(new PhabricatorFileStorageBlob())->load($handle);
|
||||||
|
if (!$blob) {
|
||||||
|
throw new Exception("Failed to load file blob data.");
|
||||||
|
}
|
||||||
|
$data = $blob->getData();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Exception("Unknown storage engine.");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->getStorageFormat()) {
|
||||||
|
case self::STORAGE_FORMAT_RAW:
|
||||||
|
$data = $data;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Exception("Unknown storage format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
19
src/applications/files/storage/file/__init__.php
Normal file
19
src/applications/files/storage/file/__init__.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'applications/files/storage/base');
|
||||||
|
phutil_require_module('phabricator', 'applications/files/storage/storageblob');
|
||||||
|
phutil_require_module('phabricator', 'applications/phid/storage/phid');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'filesystem');
|
||||||
|
phutil_require_module('phutil', 'filesystem/tempfile');
|
||||||
|
phutil_require_module('phutil', 'future/exec');
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorFile.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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class PhabricatorFileStorageBlob extends PhabricatorFileDAO {
|
||||||
|
|
||||||
|
protected $data;
|
||||||
|
|
||||||
|
}
|
12
src/applications/files/storage/storageblob/__init__.php
Normal file
12
src/applications/files/storage/storageblob/__init__.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'applications/files/storage/base');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorFileStorageBlob.php');
|
|
@ -35,13 +35,14 @@ class PhabricatorPHID extends PhabricatorPHIDDAO {
|
||||||
if (!$urandom) {
|
if (!$urandom) {
|
||||||
throw new Exception("Failed to open /dev/urandom!");
|
throw new Exception("Failed to open /dev/urandom!");
|
||||||
}
|
}
|
||||||
$entropy = fread($urandom, 16);
|
$entropy = fread($urandom, 20);
|
||||||
if (strlen($entropy) != 16) {
|
if (strlen($entropy) != 20) {
|
||||||
throw new Exception("Failed to read from /dev/urandom!");
|
throw new Exception("Failed to read from /dev/urandom!");
|
||||||
}
|
}
|
||||||
|
|
||||||
$uniq = sha1($entropy);
|
$uniq = sha1($entropy);
|
||||||
$phid = 'PHID-'.$type.'-X-'.$uniq;
|
$uniq = substr($uniq, 0, 20);
|
||||||
|
$phid = 'PHID-'.$type.'-'.$uniq;
|
||||||
|
|
||||||
$phid_rec = new PhabricatorPHID();
|
$phid_rec = new PhabricatorPHID();
|
||||||
$phid_rec->setPHIDType($type);
|
$phid_rec->setPHIDType($type);
|
||||||
|
|
|
@ -22,6 +22,7 @@ final class AphrontFormView extends AphrontView {
|
||||||
private $method = 'POST';
|
private $method = 'POST';
|
||||||
private $header;
|
private $header;
|
||||||
private $data = array();
|
private $data = array();
|
||||||
|
private $encType;
|
||||||
|
|
||||||
public function setAction($action) {
|
public function setAction($action) {
|
||||||
$this->action = $action;
|
$this->action = $action;
|
||||||
|
@ -33,13 +34,19 @@ final class AphrontFormView extends AphrontView {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setEncType($enc_type) {
|
||||||
|
$this->encType = $enc_type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function render() {
|
public function render() {
|
||||||
return phutil_render_tag(
|
return phutil_render_tag(
|
||||||
'form',
|
'form',
|
||||||
array(
|
array(
|
||||||
'action' => $this->action,
|
'action' => $this->action,
|
||||||
'method' => $this->method,
|
'method' => $this->method,
|
||||||
'class' => 'aphront-form-view',
|
'class' => 'aphront-form-view',
|
||||||
|
'enctype' => $this->encType,
|
||||||
),
|
),
|
||||||
$this->renderDataInputs().
|
$this->renderDataInputs().
|
||||||
$this->renderChildren());
|
$this->renderChildren());
|
||||||
|
|
35
src/view/form/control/file/AphrontFormFileControl.php
Executable file
35
src/view/form/control/file/AphrontFormFileControl.php
Executable file
|
@ -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 AphrontFormFileControl extends AphrontFormControl {
|
||||||
|
|
||||||
|
protected function getCustomControlClass() {
|
||||||
|
return 'aphront-form-file-text';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function renderInput() {
|
||||||
|
return phutil_render_tag(
|
||||||
|
'input',
|
||||||
|
array(
|
||||||
|
'type' => 'file',
|
||||||
|
'name' => $this->getName(),
|
||||||
|
'disabled' => $this->getDisabled() ? 'disabled' : null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
src/view/form/control/file/__init__.php
Normal file
14
src/view/form/control/file/__init__.php
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'view/form/control/base');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'markup');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('AphrontFormFileControl.php');
|
29
src/view/form/control/static/AphrontFormStaticControl.php
Executable file
29
src/view/form/control/static/AphrontFormStaticControl.php
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
<?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 AphrontFormStaticControl extends AphrontFormControl {
|
||||||
|
|
||||||
|
protected function getCustomControlClass() {
|
||||||
|
return 'aphront-form-control-static';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function renderInput() {
|
||||||
|
return phutil_escape_html($this->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
src/view/form/control/static/__init__.php
Normal file
14
src/view/form/control/static/__init__.php
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'view/form/control/base');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'markup');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('AphrontFormStaticControl.php');
|
|
@ -36,14 +36,17 @@ class AphrontFormSubmitControl extends AphrontFormControl {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function renderInput() {
|
protected function renderInput() {
|
||||||
return phutil_render_tag(
|
$submit_button = null;
|
||||||
'button',
|
if ($this->getValue()) {
|
||||||
array(
|
$submit_button = phutil_render_tag(
|
||||||
'name' => '__submit__',
|
'button',
|
||||||
'disabled' => $this->getDisabled() ? 'disabled' : null,
|
array(
|
||||||
),
|
'name' => '__submit__',
|
||||||
phutil_escape_html($this->getValue())).
|
'disabled' => $this->getDisabled() ? 'disabled' : null,
|
||||||
$this->cancelButton;
|
),
|
||||||
|
phutil_escape_html($this->getValue()));
|
||||||
|
}
|
||||||
|
return $submit_button.$this->cancelButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -476,8 +476,6 @@ a.small:visited {
|
||||||
clear: both;
|
clear: both;
|
||||||
margin-right: 25%;
|
margin-right: 25%;
|
||||||
margin-left: 15%;
|
margin-left: 15%;
|
||||||
margin-top: 3px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.aphront-error-view {
|
.aphront-error-view {
|
||||||
|
|
Loading…
Reference in a new issue