Added Profile PIC edition

( default gravatar )

Change-Id: Ib15b9c64ebfb8b21b52175d68e179de38146b765
Signed-off-by: smarcet <smarcet@gmail.com>
This commit is contained in:
smarcet 2020-08-03 18:03:41 -03:00
parent 5a260de3d3
commit 48dfbb1664
21 changed files with 1508 additions and 415 deletions

View File

@ -111,7 +111,7 @@ abstract class APICRUDController extends JsonController
* @return array
*/
protected function getUpdatePayload():array{
return Input::All();
return request()->all();
}
/**
@ -131,6 +131,10 @@ abstract class APICRUDController extends JsonController
protected function curateCreatePayload(array $payload):array {
return $payload;
}
protected function onUpdate($id, $payload){
return $this->service->update($id, $payload);
}
/**
* @param $id
* @param array $payload
@ -148,7 +152,7 @@ abstract class APICRUDController extends JsonController
throw $ex->setMessages($validation->messages()->toArray());
}
$entity = $this->service->update($id, $this->curateUpdatePayload($payload));
$entity = $this->onUpdate($id, $this->curateUpdatePayload($payload));
return $this->updated(SerializerRegistry::getInstance()->getSerializer($entity, $this->serializerType())->serialize());
}

View File

@ -11,6 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Http\Controllers\APICRUDController;
use App\Http\Utils\HTMLCleaner;
use App\ModelSerializers\SerializerRegistry;
@ -24,11 +25,14 @@ use OAuth2\Services\ITokenService;
use OpenId\Services\IUserService;
use models\exceptions\EntityNotFoundException;
use Utils\Services\ILogService;
use Illuminate\Http\Request as LaravelRequest;
/**
* Class UserApiController
* @package App\Http\Controllers\Api
*/
final class UserApiController extends APICRUDController {
final class UserApiController extends APICRUDController
{
/**
* @var ITokenService
@ -48,7 +52,8 @@ final class UserApiController extends APICRUDController {
ILogService $log_service,
IUserService $user_service,
ITokenService $token_service
){
)
{
parent::__construct($user_repository, $user_service, $log_service);
$this->token_service = $token_service;
}
@ -56,26 +61,26 @@ final class UserApiController extends APICRUDController {
/**
* @return array
*/
protected function getFilterRules():array
protected function getFilterRules(): array
{
return [
'first_name' => ['=@', '=='],
'last_name' => ['=@', '=='],
'full_name' => ['=@', '=='],
'email' => ['=@', '=='],
'first_name' => ['=@', '=='],
'last_name' => ['=@', '=='],
'full_name' => ['=@', '=='],
'email' => ['=@', '=='],
];
}
/**
* @return array
*/
protected function getFilterValidatorRules():array
protected function getFilterValidatorRules(): array
{
return [
'first_name' => 'nullable|string',
'last_name' => 'nullable|string',
'full_name' => 'nullable|string',
'email' => 'nullable|string',
'first_name' => 'nullable|string',
'last_name' => 'nullable|string',
'full_name' => 'nullable|string',
'email' => 'nullable|string',
];
}
@ -83,22 +88,18 @@ final class UserApiController extends APICRUDController {
* @param $id
* @return mixed
*/
public function unlock($id){
public function unlock($id)
{
try {
$entity = $this->service->unlockUser($id);
return $this->updated(SerializerRegistry::getInstance()->getSerializer($entity)->serialize());
}
catch (ValidationException $ex1)
{
} catch (ValidationException $ex1) {
Log::warning($ex1);
return $this->error412(array($ex1->getMessage()));
}
catch (EntityNotFoundException $ex2)
{
} catch (EntityNotFoundException $ex2) {
Log::warning($ex2);
return $this->error404(array('message' => $ex2->getMessage()));
}
catch (Exception $ex) {
} catch (Exception $ex) {
Log::error($ex);
return $this->error500($ex);
}
@ -108,28 +109,25 @@ final class UserApiController extends APICRUDController {
* @param $id
* @return mixed
*/
public function lock($id){
public function lock($id)
{
try {
$entity = $this->service->lockUser($id);
return $this->updated(SerializerRegistry::getInstance()->getSerializer($entity)->serialize());
}
catch (ValidationException $ex1)
{
} catch (ValidationException $ex1) {
Log::warning($ex1);
return $this->error412(array($ex1->getMessage()));
}
catch (EntityNotFoundException $ex2)
{
} catch (EntityNotFoundException $ex2) {
Log::warning($ex2);
return $this->error404(array('message' => $ex2->getMessage()));
}
catch (Exception $ex) {
} catch (Exception $ex) {
Log::error($ex);
return $this->error500($ex);
}
}
protected function getAllSerializerType():string{
protected function getAllSerializerType(): string
{
return SerializerRegistry::SerializerType_Private;
}
@ -138,36 +136,33 @@ final class UserApiController extends APICRUDController {
* @param $value
* @return mixed
*/
public function revokeMyToken($value){
public function revokeMyToken($value)
{
try{
$hint = Input::get('hint','none');
try {
$hint = Input::get('hint', 'none');
switch($hint){
case 'access-token':{
$this->token_service->revokeAccessToken($value,true);
}
break;
switch ($hint) {
case 'access-token':
{
$this->token_service->revokeAccessToken($value, true);
}
break;
case 'refresh-token':
$this->token_service->revokeRefreshToken($value,true);
$this->token_service->revokeRefreshToken($value, true);
break;
default:
throw new Exception(sprintf("hint %s not allowed",$hint));
throw new Exception(sprintf("hint %s not allowed", $hint));
break;
}
return $this->deleted();
}
catch (ValidationException $ex1)
{
} catch (ValidationException $ex1) {
Log::warning($ex1);
return $this->error412(array( $ex1->getMessage()));
}
catch (EntityNotFoundException $ex2)
{
return $this->error412(array($ex1->getMessage()));
} catch (EntityNotFoundException $ex2) {
Log::warning($ex2);
return $this->error404(array('message' => $ex2->getMessage()));
}
catch (Exception $ex) {
} catch (Exception $ex) {
Log::error($ex);
return $this->error500($ex);
}
@ -179,44 +174,46 @@ final class UserApiController extends APICRUDController {
protected function getUpdatePayloadValidationRules(): array
{
return [
'first_name' => 'required|string',
'last_name' => 'required|string',
'email' => 'required|email',
'identifier' => 'sometimes|string',
'bio' => 'nullable|string',
'address1' => 'nullable|string',
'address2' => 'nullable|string',
'city' => 'nullable|string',
'state' => 'nullable|string',
'post_code' => 'nullable|string',
'country_iso_code' => 'nullable|country_iso_alpha2_code',
'second_email' => 'nullable|email',
'third_email' => 'nullable|email',
'gender' => 'nullable|string',
'gender_specify' => 'nullable|string',
'statement_of_interest' => 'nullable|string',
'irc' => 'nullable|string',
'linked_in_profile' => 'nullable|string',
'github_user' => 'nullable|string',
'wechat_user' => 'nullable|string',
'twitter_name' => 'nullable|string',
'language' => 'nullable|string',
'birthday' => 'nullable|date_format:U',
'password' => 'sometimes|string|min:8|confirmed',
'email_verified' => 'nullable|boolean',
'active' => 'nullable|boolean',
'phone_number' => 'nullable|string',
'company' => 'nullable|string',
'first_name' => 'required|string',
'last_name' => 'required|string',
'email' => 'required|email',
'identifier' => 'sometimes|string',
'bio' => 'nullable|string',
'address1' => 'nullable|string',
'address2' => 'nullable|string',
'city' => 'nullable|string',
'state' => 'nullable|string',
'post_code' => 'nullable|string',
'country_iso_code' => 'nullable|country_iso_alpha2_code',
'second_email' => 'nullable|email',
'third_email' => 'nullable|email',
'gender' => 'nullable|string',
'gender_specify' => 'nullable|string',
'statement_of_interest' => 'nullable|string',
'irc' => 'nullable|string',
'linked_in_profile' => 'nullable|string',
'github_user' => 'nullable|string',
'wechat_user' => 'nullable|string',
'twitter_name' => 'nullable|string',
'language' => 'nullable|string',
'birthday' => 'nullable|date_format:U',
'password' => 'sometimes|string|min:8|confirmed',
'email_verified' => 'nullable|boolean',
'active' => 'nullable|boolean',
'phone_number' => 'nullable|string',
'company' => 'nullable|string',
];
}
protected function curateUpdatePayload(array $payload):array {
protected function curateUpdatePayload(array $payload): array
{
return HTMLCleaner::cleanData($payload, [
'bio', 'statement_of_interest'
]);
}
protected function curateCreatePayload(array $payload):array {
protected function curateCreatePayload(array $payload): array
{
return HTMLCleaner::cleanData($payload, [
'bio', 'statement_of_interest'
]);
@ -228,47 +225,77 @@ final class UserApiController extends APICRUDController {
protected function getCreatePayloadValidationRules(): array
{
return [
'first_name' => 'required|string',
'last_name' => 'required|string',
'email' => 'required|email',
'identifier' => 'sometimes|string',
'bio' => 'nullable|string',
'address1' => 'nullable|string',
'address2' => 'nullable|string',
'city' => 'nullable|string',
'state' => 'nullable|string',
'post_code' => 'nullable|string',
'country_iso_code' => 'nullable|country_iso_alpha2_code',
'second_email' => 'nullable|email',
'third_email' => 'nullable|email',
'gender' => 'nullable|string',
'statement_of_interest' => 'nullable|string',
'irc' => 'nullable|string',
'linked_in_profile' => 'nullable|string',
'github_user' => 'nullable|string',
'wechat_user' => 'nullable|string',
'twitter_name' => 'nullable|string',
'language' => 'nullable|string',
'birthday' => 'nullable|date_format:U',
'password' => 'sometimes|string|min:8|confirmed',
'email_verified' => 'nullable|boolean',
'active' => 'nullable|boolean',
'phone_number' => 'nullable|string',
'company' => 'nullable|string',
'first_name' => 'required|string',
'last_name' => 'required|string',
'email' => 'required|email',
'identifier' => 'sometimes|string',
'bio' => 'nullable|string',
'address1' => 'nullable|string',
'address2' => 'nullable|string',
'city' => 'nullable|string',
'state' => 'nullable|string',
'post_code' => 'nullable|string',
'country_iso_code' => 'nullable|country_iso_alpha2_code',
'second_email' => 'nullable|email',
'third_email' => 'nullable|email',
'gender' => 'nullable|string',
'statement_of_interest' => 'nullable|string',
'irc' => 'nullable|string',
'linked_in_profile' => 'nullable|string',
'github_user' => 'nullable|string',
'wechat_user' => 'nullable|string',
'twitter_name' => 'nullable|string',
'language' => 'nullable|string',
'birthday' => 'nullable|date_format:U',
'password' => 'sometimes|string|min:8|confirmed',
'email_verified' => 'nullable|boolean',
'active' => 'nullable|boolean',
'phone_number' => 'nullable|string',
'company' => 'nullable|string',
];
}
/**
* @param LaravelRequest $request
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function updateMe(){
if(!Auth::check())
public function updateMe(LaravelRequest $request)
{
if (!Auth::check())
return $this->error403();
$myId = Auth::user()->getId();
return $this->update($myId);
}
protected function serializerType():string{
/**
* @return array
*/
protected function getUpdatePayload():array{
$payload = request()->all();
if(isset($payload['user'])){
return json_decode($payload['user'],true);
}
return $payload;
}
/**
* @param $id
* @param $payload
* @return \models\utils\IEntity
*/
protected function onUpdate($id, $payload){
$user = parent::onUpdate($id, $payload);
$file = request()->file('pic');
if (!is_null($file)) {
$user = $this->service->updateProfilePhoto($id, $file);
}
return $user;
}
protected function serializerType(): string
{
return SerializerRegistry::SerializerType_Private;
}
}

View File

@ -29,7 +29,8 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\App\Http\Middleware\SingleAccessPoint::class
\App\Http\Middleware\SingleAccessPoint::class,
\App\Http\Middleware\ParseMultipartFormDataInputForNonPostRequests::class,
];
/**

View File

@ -0,0 +1,56 @@
<?php namespace App\Http\Middleware;
/**
* Copyright 2020 OpenStack Foundation
* 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.
**/
use Closure;
use utils\ParseMultiPartFormDataInputStream;
use Illuminate\Support\Facades\Log;
/**
* Class ParseMultipartFormDataInputForNonPostRequests
* @package App\Http\Middleware
*/
final class ParseMultipartFormDataInputForNonPostRequests
{
/*
* Content-Type: multipart/form-data - only works for POST requests. All others fail, this is a bug in PHP since 2011.
* See comments here: https://github.com/laravel/framework/issues/13457
*
* This middleware converts all multi-part/form-data for NON-POST requests, into a properly formatted
* request variable for Laravel 5.6. It uses the ParseInputStream class, found here:
* https://gist.github.com/devmycloud/df28012101fbc55d8de1737762b70348
*/
public function handle($request, Closure $next)
{
if ($request->method() == 'POST' OR $request->method() == 'GET') {
return $next($request);
}
if (preg_match('/multipart\/form-data/', $request->headers->get('Content-Type')) or
preg_match('/multipart\/form-data/', $request->headers->get('content-type'))
) {
$parser = new ParseMultiPartFormDataInputStream(file_get_contents('php://input'));
$params = $parser->getInput();
$data = $params['parameters'];
$files = $params['files'];
if (count($files) > 0) {
Log::debug("ParseMultipartFormDataInputForNonPostRequests: files ".json_encode($files));
$request->files->add($files);
}
if (count($data) > 0) {
Log::debug("ParseMultipartFormDataInputForNonPostRequests: parameters ".json_encode($data));
$request->request->add($data);
}
}
return $next($request);
}
}

View File

@ -243,8 +243,12 @@ final class ParseMultiPartFormDataInputStream
$val = self::boolVal($val);
if(!empty($val) && is_int($val))
$val = intval($val);
if(!empty($val) && is_numeric($val))
$val = intval($val);
if(!empty($val) && is_double($val))
$val = doubleval($val);
if(!empty($val) && is_string($val))
$val = strval($val);
if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) {
$data[$tmp[1]][] = $val;
} else {

View File

@ -12,7 +12,6 @@
* limitations under the License.
**/
use App\Events\UserEmailUpdated;
use App\Jobs\PublishUserCreated;
use App\Jobs\PublishUserDeleted;
use App\Jobs\PublishUserUpdated;
use App\libs\Auth\Factories\UserFactory;
@ -21,9 +20,11 @@ use App\Services\AbstractService;
use Auth\IUserNameGeneratorService;
use Auth\Repositories\IUserRepository;
use Auth\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use models\exceptions\EntityNotFoundException;
use models\exceptions\ValidationException;
use models\utils\IEntity;
@ -31,6 +32,7 @@ use OpenId\Services\IUserService;
use Utils\Db\ITransactionService;
use Utils\Services\ILogService;
use Utils\Services\IServerConfigurationService;
use App\libs\Utils\FileSystem\FileNameSanitizer;
/**
* Class UserService
* @package Services\OpenId
@ -297,4 +299,37 @@ final class UserService extends AbstractService implements IUserService
}
});
}
/**
* @inheritDoc
*/
public function updateProfilePhoto($user_id, UploadedFile $file, $max_file_size = 10485760): User
{
return $this->tx_service->transaction(function() use($user_id, $file, $max_file_size) {
$allowed_extensions = ['png', 'jpg', 'jpeg'];
$user = $this->repository->getById($user_id);
if(is_null($user) || !$user instanceof User)
throw new EntityNotFoundException("user not found");
$fileName = $file->getClientOriginalName();
$fileExt = $file->extension() ?? pathinfo($fileName, PATHINFO_EXTENSION);
if (!in_array($fileExt, $allowed_extensions)) {
throw new ValidationException(sprintf( "file does not has a valid extension (%s).", join(",", $allowed_extensions)));
}
if ($file->getSize() > $max_file_size) {
throw new ValidationException(sprintf("file exceeds max_file_size (%s MB).", ($max_file_size / 1024) / 1024));
}
// normalize fileName
$fileName = FileNameSanitizer::sanitize($fileName);
$path = User::getProfilePicFolder();
Storage::disk('swift')->putFileAs($path, $file, $fileName);
$user->setPic($fileName);
return $user;
});
}
}

View File

@ -21,6 +21,7 @@ use App\Events\UserEmailVerified;
use Doctrine\Common\Collections\Criteria;
use Illuminate\Auth\Authenticatable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use models\exceptions\ValidationException;
use Models\OAuth2\ApiScope;
use Models\OAuth2\Client;
@ -293,6 +294,12 @@ class User extends BaseEntity
*/
private $phone_number;
/**
* @ORM\Column(name="pic", type="string")
* @var string
*/
private $pic;
// relations
/**
@ -798,9 +805,19 @@ class User extends BaseEntity
*/
public function getPic(): string
{
if(!empty($this->pic)){
return Storage::disk('swift')->url(sprintf("%s/%s", self::getProfilePicFolder(), $this->pic));
}
return $this->getGravatarUrl();
}
/**
* @param string $pic
*/
public function setPic(string $pic){
$this->pic = $pic;
}
/**
* Get either a Gravatar URL or complete image tag for a specified email address.
*/
@ -1711,4 +1728,9 @@ SQL;
$this->phone_number = $phone_number;
}
const ProfilePicFolder = 'profile_pics';
public static function getProfilePicFolder():string{
return self::ProfilePicFolder;
}
}

View File

@ -13,6 +13,7 @@
**/
use App\Services\IBaseService;
use Auth\User;
use Illuminate\Http\UploadedFile;
use models\exceptions\EntityNotFoundException;
use models\exceptions\ValidationException;
/**
@ -61,4 +62,14 @@ interface IUserService extends IBaseService
*/
public function saveProfileInfo($user_id, $show_pic, $show_full_name, $show_email, $identifier);
/**
* @param $user_id
* @param UploadedFile $file
* @param int $max_file_size
* @throws EntityNotFoundException
* @throws ValidationException
* @return User
*/
public function updateProfilePhoto($user_id, UploadedFile $file, $max_file_size = 10485760):User;
}

View File

@ -0,0 +1,40 @@
<?php namespace App\libs\Utils\FileSystem;
/**
* Copyright 2020 OpenStack Foundation
* 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.
**/
use Behat\Transliterator\Transliterator;
/**
* Class FileNameSanitizer
* @package App\Services\FileSystem
*/
final class FileNameSanitizer
{
private static $default_replacements = [
'/\s/' => '-', // remove whitespace
'/_/' => '-', // underscores to dashes
'/[^A-Za-z0-9+.\-]+/' => '', // remove non-ASCII chars, only allow alphanumeric plus dash and dot
'/[\-]{2,}/' => '-', // remove duplicate dashes
'/^[\.\-_]+/' => '', // Remove all leading dots, dashes or underscores
];
/**
* @param string $filename
* @return string
*/
public static function sanitize(string $filename):string {
$filename = trim(Transliterator::utf8ToAscii($filename));
foreach(self::$default_replacements as $regex => $replace) {
$filename = preg_replace($regex, $replace, $filename);
}
return $filename;
}
}

View File

@ -0,0 +1,357 @@
<?php namespace App\Services\FileSystem\Swift;
/**
* Copyright 2020 OpenStack Foundation
* 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.
**/
use GuzzleHttp\Psr7\Stream;
use GuzzleHttp\Psr7\StreamWrapper;
use League\Flysystem\Util;
use League\Flysystem\Config;
use League\Flysystem\Adapter\AbstractAdapter;
use OpenStack\Common\Error\BadResponseError;
use OpenStack\ObjectStore\v1\Models\Container;
use OpenStack\ObjectStore\v1\Models\StorageObject;
use League\Flysystem\Adapter\Polyfill\NotSupportingVisibilityTrait;
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
/**
* Class SwiftAdapter
* @package App\Services\FileSystem\Swift
*/
final class SwiftAdapter extends AbstractAdapter
{
use StreamedCopyTrait;
use NotSupportingVisibilityTrait;
/**
* @var Container
*/
protected $container;
/**
* Constructor
*
* @param Container $container
* @param string $prefix
*/
public function __construct(Container $container, $prefix = null)
{
$this->setPathPrefix($prefix);
$this->container = $container;
}
/**
* {@inheritdoc}
*/
public function write($path, $contents, Config $config, $size = 0)
{
$path = $this->applyPathPrefix($path);
$data = $this->getWriteData($path, $config);
$type = 'content';
if (is_a($contents, 'GuzzleHttp\Psr7\Stream')) {
$type = 'stream';
}
$data[$type] = $contents;
// Create large object if the stream is larger than 300 MiB (default).
if ($type === 'stream' && $size > $config->get('swiftLargeObjectThreshold', 314572800)) {
// Set the segment size to 100 MiB by default as suggested in OVH docs.
$data['segmentSize'] = $config->get('swiftSegmentSize', 104857600);
// Set segment container to the same container by default.
$data['segmentContainer'] = $config->get('swiftSegmentContainer', $this->container->name);
$response = $this->container->createLargeObject($data);
} else {
$response = $this->container->createObject($data);
}
return $this->normalizeObject($response);
}
/**
* {@inheritdoc}
*/
public function writeStream($path, $resource, Config $config)
{
return $this->write($path, new Stream($resource), $config, fstat($resource)['size']);
}
/**
* {@inheritdoc}
*/
public function update($path, $contents, Config $config)
{
return $this->write($path, $contents, $config);
}
/**
* {@inheritdoc}
*/
public function updateStream($path, $resource, Config $config)
{
return $this->write($path, new Stream($resource), $config, fstat($resource)['size']);
}
/**
* {@inheritdoc}
*/
public function rename($path, $newpath)
{
$object = $this->getObject($path);
$newLocation = $this->applyPathPrefix($newpath);
$destination = '/'.$this->container->name.'/'.ltrim($newLocation, '/');
try {
$response = $object->copy(compact('destination'));
} catch (BadResponseError $e) {
return false;
}
$object->delete();
return true;
}
/**
* {@inheritdoc}
*/
public function delete($path)
{
$object = $this->getObjectInstance($path);
try {
$object->delete();
} catch (BadResponseError $e) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function deleteDir($dirname)
{
$objects = $this->container->listObjects([
'prefix' => $this->applyPathPrefix($dirname)
]);
try {
foreach ($objects as $object) {
$object->containerName = $this->container->name;
$object->delete();
}
} catch (BadResponseError $e) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function createDir($dirname, Config $config)
{
return ['path' => $dirname];
}
/**
* {@inheritdoc}
*/
public function has($path)
{
try {
$object = $this->getObject($path);
} catch (BadResponseError $e) {
$code = $e->getResponse()->getStatusCode();
if ($code == 404) return false;
throw $e;
}
return $this->normalizeObject($object);
}
/**
* {@inheritdoc}
*/
public function read($path)
{
$object = $this->getObject($path);
$data = $this->normalizeObject($object);
$stream = $object->download();
$stream->rewind();
$data['contents'] = $stream->getContents();
return $data;
}
/**
* {@inheritdoc}
*/
public function readStream($path)
{
$object = $this->getObject($path);
$data = $this->normalizeObject($object);
$stream = $object->download();
$stream->rewind();
$data['stream'] = StreamWrapper::getResource($stream);
return $data;
}
/**
* {@inheritdoc}
*/
public function listContents($directory = '', $recursive = false)
{
$location = $this->applyPathPrefix($directory);
$objectList = $this->container->listObjects([
'prefix' => $directory
]);
$response = iterator_to_array($objectList);
return Util::emulateDirectories(array_map([$this, 'normalizeObject'], $response));
}
/**
* {@inheritdoc}
*/
public function getMetadata($path)
{
$object = $this->getObject($path);
return $this->normalizeObject($object);
}
/**
* {@inheritdoc}
*/
public function getSize($path)
{
return $this->getMetadata($path);
}
/**
* {@inheritdoc}
*/
public function getMimetype($path)
{
return $this->getMetadata($path);
}
/**
* {@inheritdoc}
*/
public function getTimestamp($path)
{
return $this->getMetadata($path);
}
/**
* Get the data properties to write or update an object.
*
* @param string $path
* @param Config $config
*
* @return array
*/
protected function getWriteData($path, $config)
{
return ['name' => $path];
}
/**
* Get an object instance.
*
* @param string $path
*
* @return StorageObject
*/
protected function getObjectInstance($path)
{
$location = $this->applyPathPrefix($path);
$object = $this->container->getObject($location);
return $object;
}
/**
* Get an object instance and retrieve its metadata from storage.
*
* @param string $path
*
* @return StorageObject
*/
protected function getObject($path)
{
$object = $this->getObjectInstance($path);
$object->retrieve();
return $object;
}
/**
* Normalize Openstack "StorageObject" object into an array
*
* @param StorageObject $object
* @return array
*/
protected function normalizeObject(StorageObject $object)
{
$name = $this->removePathPrefix($object->name);
$mimetype = explode('; ', $object->contentType);
if ($object->lastModified instanceof \DateTimeInterface) {
$timestamp = $object->lastModified->getTimestamp();
} else {
$timestamp = strtotime($object->lastModified);
}
return [
'type' => 'file',
'dirname' => Util::dirname($name),
'path' => $name,
'timestamp' => $timestamp,
'mimetype' => reset($mimetype),
'size' => $object->contentLength,
];
}
public function getTemporaryLink(string $path): ?string
{
return $this->getUrl($path);
}
public function getTemporaryUrl(string $path): ?string
{
return $this->getUrl($path);
}
public function getUrl(string $path): ?string
{
$obj = $this->container->getObject($path);
if(is_null($obj))
return null;
return $obj->getPublicUri();
}
}

View File

@ -0,0 +1,85 @@
<?php namespace App\libs\Utils\FileSystem;
/**
* Copyright 2020 OpenStack Foundation
* 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.
**/
use App\Services\FileSystem\Swift\SwiftAdapter;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem;
use Illuminate\Support\ServiceProvider;
use OpenStack\OpenStack;
/**
* Class SwiftServiceProvider
* @package App\Services\FileSystem\Swift
*/
final class SwiftServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Storage::extend('swift', function ($app, $config) {
$configOptions = [
'authUrl' => $config["auth_url"],
'region' => $config["region"],
];
$userName = $config["user_name"] ?? null;
$userPassword = $config["api_key"] ?? null;
if(!empty($userName) && !empty($userPassword)){
$configOptions['user'] = [
'name' => $userName,
'password' => $userPassword,
'domain' => ['id' => $config["user_domain"] ?? 'default']
];
$configOptions['scope' ] = [
'project' => [
'name' => $config["project_name"],
'domain' => ['id' => $config["project_domain"] ?? 'default']
],
];
}
$appCredentialId = $config["app_credential_id"] ?? null;
$appCredentialSecret = $config["app_credential_secret"] ?? null;
if(!empty($appCredentialId) && !empty($appCredentialSecret)){
$configOptions['application_credential'] = [
'id' => $appCredentialId,
'secret' => $appCredentialSecret,
];
}
$openstackClient = new OpenStack($configOptions);
$container = $openstackClient->objectStoreV1()->getContainer($config["container"]);
return new Filesystem(new SwiftAdapter($container));
});
}
}

View File

@ -11,6 +11,12 @@
],
"license": "MIT",
"type": "project",
"repositories": [
{
"type": "vcs",
"url": "https://github.com/OpenStackweb/openstack"
}
],
"require": {
"php": "^7.1.3",
"ext-json": "*",
@ -39,7 +45,9 @@
"sokil/php-isocodes": "^3.0",
"vladimir-yuldashev/laravel-queue-rabbitmq": "v7.5.0",
"zendframework/zend-crypt": "3.3.0",
"zendframework/zend-math": "3.1.1"
"zendframework/zend-math": "3.1.1",
"behat/transliterator": "^1.2",
"php-opencloud/openstack": "dev-master"
},
"require-dev": {
"filp/whoops": "^2.0",

874
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -162,6 +162,7 @@ return [
// Doctrine Beberlei (Query/Type) extensions install them:
LaravelDoctrine\Extensions\BeberleiExtensionsServiceProvider::class,
\App\Models\Utils\MySQLExtensionsServiceProvider::class,
\App\libs\Utils\FileSystem\SwiftServiceProvider::class,
],
/*

View File

@ -54,13 +54,14 @@ return [
'visibility' => 'public',
],
's3' => [
'driver' => 's3',
'key' => 'your-key',
'secret' => 'your-secret',
'region' => 'your-region',
'bucket' => 'your-bucket',
],
'swift' => [
'driver' => 'swift',
'auth_url' => env('CLOUD_STORAGE_AUTH_URL'),
'region' => env('CLOUD_STORAGE_REGION'),
'app_credential_id' => env('CLOUD_STORAGE_APP_CREDENTIAL_ID'),
'app_credential_secret' => env('CLOUD_STORAGE_APP_CREDENTIAL_SECRET'),
'container' => env('CLOUD_STORAGE_CONTAINER'),
]
],

View File

@ -0,0 +1,50 @@
<?php namespace Database\Migrations;
/**
* Copyright 2020 OpenStack Foundation
* 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.
**/
use Doctrine\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema as Schema;
use LaravelDoctrine\Migrations\Schema\Builder;
use LaravelDoctrine\Migrations\Schema\Table;
/**
* Class Version20200803193707
* @package Database\Migrations
*/
class Version20200803193707 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
$builder = new Builder($schema);
if($schema->hasTable("users") && !$builder->hasColumn("users","pic") ) {
$builder->table('users', function (Table $table) {
$table->string('pic')->setNotnull(false)->setLength(512);
});
}
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
$builder = new Builder($schema);
if($schema->hasTable("users") && $builder->hasColumn("users","pic") ) {
$builder->table('users', function (Table $table) {
$table->dropColumn('pic');
});
}
}
}

View File

@ -324,6 +324,7 @@ span.help-block::before {
border: 0;
vertical-align: middle;
border-radius: 50%;
height: 100px;
}
.help-block{

View File

@ -152,13 +152,21 @@ $(document).ready(function() {
var href = $(this).attr('action');
var data = new FormData();
data.append('user', JSON.stringify(user));
if($('#pic', form)[0].files.length > 0)
data.append('pic', $('#pic', form)[0].files[0]);
$.ajax(
{
type: "PUT",
url: href,
data: JSON.stringify(user),
contentType: "application/json; charset=utf-8",
dataType: "json",
data: data,
cache: false,
contentType: false,
processData: false,
timeout:60000,
success: function (data, textStatus, jqXHR) {
$('body').ajax_loader('stop');
@ -167,7 +175,7 @@ $(document).ready(function() {
type: "success",
text: "User info updated successfully!",
});
$('#spam-type').val(data.spam_type);
location.reload();
},
error: function (jqXHR, textStatus, errorThrown) {
$('body').ajax_loader('stop');
@ -180,6 +188,21 @@ $(document).ready(function() {
return false;
});
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
$('#img-pic').attr('src', e.target.result);
}
reader.readAsDataURL(input.files[0]);
}
}
$("#pic", form).change(function() {
readURL(this);
});
$("#password_container").hide();
$("body").on("click", ".change-password-link", function(event){

View File

@ -92,14 +92,21 @@ $(document).ready(function() {
}
var href = $(this).attr('action');
var data = new FormData();
data.append('user', JSON.stringify(user));
if($('#pic', form)[0].files.length > 0)
data.append('pic', $('#pic', form)[0].files[0]);
$.ajax(
{
type: "PUT",
url: href,
data: JSON.stringify(user),
contentType: "application/json; charset=utf-8",
dataType: "json",
data: data,
cache: false,
contentType: false,
processData: false,
timeout:60000,
success: function (data,textStatus,jqXHR) {
$('body').ajax_loader('stop');
@ -108,7 +115,6 @@ $(document).ready(function() {
type: "success",
text: "User info updated successfully!",
});
},
error: function (jqXHR, textStatus, errorThrown) {
$('body').ajax_loader('stop');
@ -121,6 +127,21 @@ $(document).ready(function() {
return false;
});
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
$('#img-pic').attr('src', e.target.result);
}
reader.readAsDataURL(input.files[0]);
}
}
$("#pic", form).change(function() {
readURL(this);
});
$("#password_container").hide();
$("body").on("click", ".change-password-link", function(event){

View File

@ -12,10 +12,23 @@
</div>
</div>
<div class="row">
<form id="user-form" name="user-form" role="form"
autocomplete="off"
action='{!!URL::action("Api\\UserApiController@update",["id" => $user->id])!!}'>
<div class="form-group col-xs-10 col-sm-4 col-md-6 col-lg-6">
<form id="user-form" name="user-form"
role="form"
style="padding-top: 20px"
autocomplete="off"
enctype="multipart/form-data"
method="post"
action='{!!URL::action("Api\\UserApiController@update",["id" => $user->id])!!}'>
@method('PUT')
@csrf
<div class="form-group col-xs-12 col-sm-12 col-md-12 col-lg-12">
<span class="control-label col-md-2">
<img src="{!! $user->pic !!}" class="img-circle" id="img-pic" title="Profile pic">
</span>
<input type="file" name="pic" id="pic"/>
</div>
<div class="form-group col-xs-10 col-sm-4 col-md-6 col-lg-6">
<label for="first_name">First name</label>
<input autocomplete="off" class="form-control" type="text" name="first_name" id="first_name" value="{!! $user->first_name !!}" data-lpignore="true">
</div>
@ -189,11 +202,11 @@
/>&nbsp;Email Verified?
</label>
</div>
<div class="col-xs-10 col-sm-4 col-md-12 col-lg-12">
<div class="col-xs-10 col-sm-4 col-md-12 col-lg-12" style="padding-bottom: 20px">
<label for="spam-type">Spam Type</label>
<input type="text" readonly class="form-control" id="spam-type" name="spam-type" data-lpignore="true" value="{!! $user->spam_type !!}">
</div>
<button type="submit" class="btn btn-default btn-lg btn-primary">Save</button>
<button style="margin-left: 15px;" type="submit" class="btn btn-default btn-lg btn-primary">Save</button>
<input type="hidden" name="id" id="id" value="{!! $user->id !!}"/>
</form>
</div>

View File

@ -21,23 +21,24 @@
</div>
<div class="row">
<div class="col-md-12">
<form id="user-form" name="user-form" role="form"
<form id="user-form" name="user-form"
role="form"
autocomplete="off"
enctype="multipart/form-data"
method="post"
style="padding-bottom: 20px"
action='{!!URL::action("Api\\UserApiController@updateMe") !!}'>
@method('PUT')
@csrf
<legend><span class="glyphicon glyphicon-info-sign pointable" aria-hidden="true"
title="this information will be public on your profile page"></span>&nbsp;{!! Config::get('app.app_name') !!} Account Settings:
</legend>
<div class="form-group col-xs-12 col-sm-12 col-md-12 col-lg-12">
<span class="control-label col-md-2">
<img src="{!! $user->pic !!}" class="img-circle" title="Gravatar profile pic">
<img src="{!! $user->pic !!}" class="img-circle" id="img-pic" title="Profile pic">
</span>
<div class="col-md-10" style="margin-top: 5px;">
<p class="help-block">
{!! Config::get('app.app_name') !!} uses the 'Gravatar' profile picture associated with your email address. You can customise your profile pic at <a href="http://en.gravatar.com/" target="_blank">Gravatar.com</a>
</p>
</div>
<input type="file" name="pic" id="pic"/>
</div>
<div class="form-group col-xs-10 col-sm-4 col-md-6 col-lg-6">