From f59fdf61ca76d91966dfd7211c264207000684bc Mon Sep 17 00:00:00 2001 From: Pavel Dmitriev Date: Sat, 13 Aug 2022 12:31:47 +0300 Subject: [PATCH] S3 files, unAuth methods in API, bugifx s3 files download, packages version fix, oauth fix, time intervals, --- .drone.yml | 111 +++++++++++++++++++++++ .gitignore | 3 +- CHANGELOG_RU.md | 60 +++++++++++++ api/v2.php | 60 ++++++++----- cli/installPackages.php | 13 +++ core/fox/cache.php | 59 +++++++++++- core/fox/common.php | 25 +++++- core/fox/file.php | 150 ++++++++++++++++++++++++++++++- core/fox/moduleInfo.php | 5 ++ core/fox/modules.php | 1 + core/fox/oAuthProfile.php | 3 +- core/fox/objectStorageClient.php | 4 + core/fox/request.php | 3 + core/fox/s3client.php | 18 ++++ core/fox/time.php | 12 +++ core/fox/webhookCallable.php | 25 ++++++ core/fox/xcrypt.php | 2 - hook/.htaccess | 4 + hook/404.html | 1 + hook/index.php | 105 ++++++++++++++++++++++ static/js/core/api.js | 14 +++ static/js/core/coreModules.js | 4 +- static/js/core/lang_ru.js | 2 + static/js/core/login.js | 14 +-- static/theme/core/main.css | 1 + 25 files changed, 658 insertions(+), 41 deletions(-) create mode 100644 .drone.yml create mode 100644 core/fox/webhookCallable.php create mode 100644 hook/.htaccess create mode 100644 hook/404.html create mode 100644 hook/index.php diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..83df9ad --- /dev/null +++ b/.drone.yml @@ -0,0 +1,111 @@ +--- +kind: pipeline +name: SonarQube check + +image_pull_secrets: + - dockerconfig + +trigger: + branch: + - develop + event: + - push + - custom + +steps: + - name: SonarQube check + image: sonarsource/sonar-scanner-cli + environment: + SONAR_PROJECT: + from_secret: sonarProjectId + SONAR_TOKEN: + from_secret: sonarToken + SONAR_HOST: + from_secret: sonarHost + TEST: test + + commands: + - sonar-scanner -Dsonar.projectKey=$${SONAR_PROJECT} -Dsonar.sources=. -Dsonar.host.url=$${SONAR_HOST} -Dsonar.login=$${SONAR_TOKEN} + depends_on: + - clone + +--- +kind: pipeline +name: Build image + +image_pull_secrets: + - dockerconfig + +trigger: + ref: + - refs/heads/testing + - refs/heads/master + - refs/tags/* + +steps: + - name: Prepare image + image: mxfox.ru/mxfox/fox-web-basic:latest + commands: + - | + apt-get update -y + apt-get install curl -y + cd /tmp + curl -sS https://getcomposer.org/installer -o composer-setup.php + php composer-setup.php --install-dir=/usr/local/bin --filename=composer + DEBIAN_FRONTEND=noninteractive TZ=Europe/Moscow apt-get -y install tzdata + cd - + - composer install + - | + find . -name "*~" -prune -exec rm -rf '{}' \; + find . -name "*.bak" -prune -exec rm -rf '{}' \; + find . -name "*.old" -prune -exec rm -rf '{}' \; + find . -name ".git" -prune -exec rm -rf '{}' \; + find . -name ".settings" -prune -exec rm -rf '{}' \; + find . -name ".buildpath" -prune -exec rm -rf '{}' \; + find . -name ".project" -prune -exec rm -rf '{}' \; + find . -name "README.*" -prune -exec rm -rf '{}' \; + find . -name "*.md" -prune -exec rm -rf '{}' \; + find . -name "composer.*" -prune -exec rm -rf '{}' \; + find . -name ".travis*" -prune -exec rm -rf '{}' \; + find . -name "installed.json" -prune -exec rm -rf '{}' \; + find . -name "*.sample" -prune -exec rm -rf '{}' \; + find . -type d ! -path './.git/**' ! -path './static/**' ! -path "./static" ! -path ./*/modules/*/static* -exec bash -c 'test -f {}/.htaccess && echo -n "[ SKIP ] " || (cp ./docker-build/.htaccess {} && echo -n "[ ADD ] ") && echo {}/.htaccess' \; + cp docker-build/Dockerfile Dockerfile + rm -f composer.* + + - name: Build docker image + image: mxfox.ru/mxfox/docker-dind.buildx:latest + privileged: true + environment: + DOCKER_AUTH: + from_secret: dockerconfig + IMAGE_PREFIX: mxfox.ru/mxfox/chimera-mk2-core + + commands: + - buildx-bgstart.sh + - echo $${DOCKER_AUTH} > ~/.docker/config.json + - echo "CB ${CI_COMMIT_BRANCH}" + - echo "DT ${DRONE_TAG}" + - | + if [ -n "${DRONE_TAG}" ] + then + export xBuildSuffix=" -t $${IMAGE_PREFIX}:${DRONE_TAG} -t $${IMAGE_PREFIX}:latest --push" + else + export xBuildSuffix=" -t $${IMAGE_PREFIX}:${CI_COMMIT_BRANCH}-${CI_BUILD_NUMBER}-${DRONE_COMMIT_SHA:0:10} -t $${IMAGE_PREFIX}:${CI_COMMIT_BRANCH} --push" + fi + + - docker buildx build --platform linux/amd64,linux/arm64 . $${xBuildSuffix} + +--- +kind: pipeline +name: Export code +image_pull_secrets: + - dockerconfig +trigger: + ref: + - refs/tags/* +steps: + - name: Export code + image: mxfox.ru/mxfox/fox-web-basic:latest + commands: + - echo "Not implemented yet" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3c867f0..9720747 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ /temp-test /composer.lock /vendor -.scannerwork \ No newline at end of file +.scannerwork +.vscode diff --git a/CHANGELOG_RU.md b/CHANGELOG_RU.md index 6843dc0..eee9c3a 100644 --- a/CHANGELOG_RU.md +++ b/CHANGELOG_RU.md @@ -1,3 +1,63 @@ +## V.1.0.0 +1. Add disabled flag for createLeftPanel and crealeTabsPanel +2. Remove context menu title if empty title +3. onEnter for fieldGroup +4. Поиск по группам пользователей +5. Поиск по пользователям +6. onContextMenu для поля UI.fieldAdd(type=label) +7. Кликабельность поля типа "label" для UI.fieldAdd +8. Исправлена ошибка загрузки модуля в Firefox +9. Исправлена ошибка /users/x +10. Исправлена Ошибка создания форм с полем passwordNew +11. Исправлено Изменение хэша при изменение URL oAuth +12. Установка модулей и fox-start.d +13. Скрывать меню при обновлении +14. Добавить подпись Powered by chimera fox как было на mark1 +15. Добавить заглушку на случай если браузер не поддерживается +17. Установщик пакетов с модулями +18. Экранирование параметра id в baseClass::__construct() @type=string|int +19. added searchResult class, updated system to use it in search +20. Implemented fox\modules::getByFeature and fox\moduleInfo::getByFeature +21. added UI.collectForm().getVals() method +22. Updated session/modules logic +23. Added chars and length props for passwordNew field, added string ref type for smartClick +24. Added Filrebase/jwt into composer.json, added id for tabs-anchor +25. Added API.session.checkAccess(rule, module) +26. added qGetSql() and qGetSqlSelectTemplate() +27. Updated UI panels +28. Implemented usage of user session config if exists +29. Added public function isMember(user $user) +30. Added href (smartClick) type into UI.addField() +31. Added click-protection for foxClick: click blocked if selection not empty +32. table.datatable td(th).icon - icon size reduced +33. Updated Breadcrumbs for use objects +34. Added attrs.disabled translation into item for UI.fieldAdd +35. Implemented companies management +36. Added onClick and onContextMenu for fieldBlock, added .onEnter ext. +37. Added async key into api.exec and disabled flag into LR-tabs +38. API.session.checkAccess fix +39. Added tabs manipulation methods +40. Switched common::getGUIDc to UUID:v4 +41. Added UI.getClipboard(callback) function +42. Added onFail callback into UI.getClipboard +43. Iplemented ref.onContextMenu for adField with type label and href +44. Added API.loadModule method +45. Blanker z-index changed +46. added baseClass->search page and pagesize type conversion +47. Added marker into s3client->listobjects method, added listAllObjects and added forced garbage collection +48. Added $__foxRequestInstance +49. Added count into default search, fixed foxpager +50. added UI.stamp2isodateq and UI.stamp2isodatens funcitons +51. Added attrs into accordion panel +52. Updated xSearch to extend orderBy +53. Webhook implemented +54. fox\request @property-read $rawRequestBody +55. multipart cache +(Last commit processed #6b3a7f420d) + + + + ## V.0.9.0 RC ### Бэк 1. Огранизация прозрачного REST API через интерфейс fox\externalCallable - позволяет с минимальными затратами но при этом со всеми необходимыми проверками организовать доступ к нужным сущностям. diff --git a/api/v2.php b/api/v2.php index 6d9a1ea..18036c2 100644 --- a/api/v2.php +++ b/api/v2.php @@ -30,7 +30,8 @@ try { } $request->shift(); $modules=moduleInfo::getAll(); - if (!array_key_exists(request::get()->module, $modules)) { + $reqModule=request::get()->module; + if (!array_key_exists($reqModule, $modules)) { if (request::get()->authOK) { throw new foxException("Invalid module",404); } else { @@ -38,11 +39,8 @@ try { } } - if ($modules[request::get()->module]->authRequired && !request::get()->authOK) { - throw new foxException("Unauthorized",401); - } - $modNS=$modules[request::get()->module]->namespace; + $modNS=$modules[$reqModule]->namespace; request::get()->shift(); $className=$modNS."\\".request::get()->module; if (!class_exists($className)) { @@ -60,24 +58,46 @@ try { $apiFunction=fox\common::clearInput($request->function,"a-zA-Z0-9"); $apiXFunction=empty($request->parameters[0])?NULL:fox\common::clearInput($request->parameters[0],"a-zA-Z0-9"); - $apiCallMethod="API_".$apiMethod."_".$apiFunction; - $apiXCallMethod="APIX_".$apiMethod."_".$apiXFunction; - $apiZCallMethod="API_".$apiMethod; - - if (method_exists($className, $apiCallMethod)) { - $rv=$className::$apiCallMethod($request); - } else if (($apiXFunction!==null) && method_exists($className, $apiXCallMethod)) { - $rv=$className::$apiXCallMethod($request); - } else if (method_exists($className, $apiZCallMethod)) { - $rv=$className::$apiZCallMethod($request); - } else if (method_exists($className, "APICall")) { - $rv=$className::apiCall(request::get()); + + if ($modules[$reqModule]->authRequired && !request::get()->authOK) { + + $apiPCallMethod="API_UnAuth_".$apiMethod."_".$apiFunction; + $apiPXCallMethod="APIX_UnAuth_".$apiMethod."_".$apiXFunction; + $apiPZCallMethod="API_UnAuth_".$apiMethod; + + if (method_exists($className, $apiPCallMethod)) { + $rv=$className::$apiPCallMethod($request); + } else if (($apiXFunction!==null) && method_exists($className, $apiPXCallMethod)) { + $rv=$className::$apiPXCallMethod($request); + } else if (method_exists($className, $apiPZCallMethod)) { + $rv=$className::$apiPZCallMethod($request); + } else { + throw new foxException("Unauthorized",401); + } + + foxRequestResult::throw("200", "OK", $rv); + } else { - throw new foxException("Method not allowed", 405); + + $apiCallMethod="API_".$apiMethod."_".$apiFunction; + $apiXCallMethod="APIX_".$apiMethod."_".$apiXFunction; + $apiZCallMethod="API_".$apiMethod; + + if (method_exists($className, $apiCallMethod)) { + $rv=$className::$apiCallMethod($request); + } else if (($apiXFunction!==null) && method_exists($className, $apiXCallMethod)) { + $rv=$className::$apiXCallMethod($request); + } else if (method_exists($className, $apiZCallMethod)) { + $rv=$className::$apiZCallMethod($request); + } else if (method_exists($className, "APICall")) { + $rv=$className::apiCall(request::get()); + } else { + throw new foxException("Method not allowed", 405); + } + + foxRequestResult::throw("200", "OK", $rv); } - foxRequestResult::throw("200", "OK", $rv); - } catch (fox\foxRequestResult $e) { ob_clean(); header('Content-Type: application/json; charset=utf-8'); diff --git a/cli/installPackages.php b/cli/installPackages.php index b788f4f..87956fd 100644 --- a/cli/installPackages.php +++ b/cli/installPackages.php @@ -2,6 +2,7 @@ use fox\modules; use fox\config; +use fox\moduleInfo; include(__DIR__."/../Autoloader.php"); @@ -49,6 +50,18 @@ foreach (scandir(modules::packagesDir) as $file) { if (file_exists(modules::modulesDir."/".$newModInfo->name."/fox-start.d")) { system("chmod a+x \"".modules::modulesDir."/".$newModInfo->name."/fox-start.d/\"*"); } + + $mods = moduleInfo::getAll(); + + foreach($mods as $mod) { + if ($newModInfo->name==$mod->instanceOf) { + $mod->modVersion=$newModInfo->version; + $mod->modBuild=$newModInfo->build; + $mod->save(); + } + } + + print "OK\n"; } } diff --git a/core/fox/cache.php b/core/fox/cache.php index d431a6d..5b1a05a 100644 --- a/core/fox/cache.php +++ b/core/fox/cache.php @@ -14,6 +14,8 @@ namespace fox; class cache { + protected const chunkSize=1024000; + public ?\Memcached $mcd = null; protected $prefix = null; @@ -84,9 +86,26 @@ class cache return false; } if ($encrypt) { - $this->mcd->set($this->prefix . "." . $key, xcrypt::encrypt(json_encode($val)), $TTL); + $str=xcrypt::encrypt(json_encode($val)); } else { - $this->mcd->set($this->prefix . "." . $key, json_encode($val), $TTL); + $str=json_encode($val); + } + + $this->del($key); + + $len=strlen($str); + if ($len <= static::chunkSize) { + $this->mcd->set($this->prefix . "." . $key, $str, $TTL); + } else { + # multipart + $md5=md5($str); + $chunks=ceil($len/static::chunkSize); + + $this->mcd->set($this->prefix . "." . $key.".MPX00", json_encode(["len"=>$len, "md5"=>$md5,"chunks"=>$chunks]), $TTL); + for ($i = 0; $i<$chunks; $i++) { + $xzval=substr($str,$i*static::chunkSize,static::chunkSize); + $this->mcd->set($this->prefix . "." . $key.".MPX0".($i+1), $xzval, $TTL); + } } } @@ -101,10 +120,31 @@ class cache $xval=$this->mcd->get($this->prefix . "." . $key); + if ($xval==null) { + $idx=$this->mcd->get($this->prefix . "." . $key.".MPX00"); + if ($idx) { + $idx=json_decode($idx); + if ($idx) { + $xval=""; + for ($i=1; $i <=$idx->chunks; $i++) { + $xzval=$this->mcd->get($this->prefix . "." . $key.".MPX0".$i); + $xval .= $xzval; + } + $xlen=strlen($xval); + $xmd5=md5($xval); + + if ($xlen!=$idx->len || $xmd5!=$idx->md5) { + $this->del($key); + $xval=null; + } + } + } + } if ($xval==null || $xval=="null") { return null; } - $rv= json_decode($this->mcd->get($this->prefix . "." . $key), $array); + + $rv= json_decode($xval, $array); if ($rv !== null) { return $rv; } - return json_decode(xcrypt::decrypt($this->mcd->get($this->prefix . "." . $key)), $array); + return json_decode(xcrypt::decrypt($xval), $array); } public function del($key) { @@ -116,6 +156,17 @@ class cache } $this->mcd->delete($this->prefix . "." . $key); + + $idx=$this->mcd->get($this->prefix . "." . $key.".MPX00"); + if ($idx) { + $idx=json_decode($idx); + if ($idx) { + $this->mcd->delete($this->prefix . "." . $key.".MPX00"); + for ($i=1; $i <=$idx->chunks; $i++) { + $this->mcd->delete($this->prefix . "." . $key.".MPX0".$i); + } + } + } } } diff --git a/core/fox/common.php b/core/fox/common.php index baaec4a..a81d985 100644 --- a/core/fox/common.php +++ b/core/fox/common.php @@ -4,7 +4,8 @@ namespace fox; /** * * Class fox\common - * + * Common methods ex. generation GUID + * * @copyright MX STAR LLC 2021 * @version 4.0.0 * @author Pavel Dmitriev @@ -88,14 +89,34 @@ class common return $str; } + /** + * Generate uuid withous curly braces in lowercase + * xxx....xxx + **/ + + static function getUUID() + { + return strtolower(UUID::v4()); + } + + /** + * Generate GUID withous curly braces + * XXX.....XXX + **/ + static function getGUIDc() { return strtoupper(UUID::v4()); } + /** + * Generate GUID with curly braces + * { XXX.....XXX } + **/ + static function getGUID() { - return chr(123) . getGUIDc() . chr(125); + return chr(123) . static::getGUIDc() . chr(125); } static function fullname2qname($first, $mid, $last) diff --git a/core/fox/file.php b/core/fox/file.php index 4baa1d1..8ac0af3 100644 --- a/core/fox/file.php +++ b/core/fox/file.php @@ -1,5 +1,8 @@ expireStamp=new time(); + $this->uploadStamp=time::current(); } public static $sqlColumns = [ + "uuid" => [ + "type" => "CHAR(36)", + "index"=>"UNIQUE", + ], "fileName" => [ "type" => "VARCHAR(255)", ], @@ -39,6 +56,10 @@ class file extends baseClass { "type" => "VARCHAR(255)", "index" => "INDEX" ], + "refId" => [ + "type" => "VARCHAR(255)", + ], + "ownerId" => [ "type" => "INT", "index" => "INDEX", @@ -49,9 +70,134 @@ class file extends baseClass { "index" => "INDEX", "nullable"=>true ], + "uploadStamp" => [ + "type" => "DATETIME", + "index" => "INDEX", + "nullable"=>true, + ], ]; - + + + protected function validateSave() + { + if (empty($this->uuid)) { $this->uuid = common::getUUID(); } + return true; + } + + protected static function xSearch($where, $pattern, ?array $options, sql $sql) { + + $xWhere=""; + if (!empty($options["uuid"])) { + $xWhere .= ($xWhere?" AND ":"")." `uuid` = '".common::clearInput($options["uuid"],"0-9a-z-")."'"; + } + + if (!empty($options["instance"])) { + $xWhere .= ($xWhere?" AND ":"")." `module` = '".common::clearInput($options["instance"])."'"; + } + + if (!empty($options["class"])) { + $xWhere .= ($xWhere?" AND ":"")." `class` = '".common::clearInput($options["class"],'0-9A-Za-z_\.\-\\\/')."'"; + } + + if (!empty($options["refId"])) { + $xWhere .= ($xWhere?" AND ":"")." `refId` = '".common::clearInput($options["refId"])."'"; + } + + + if ($xWhere) { + if ($where) { + $where = "(".$where.") AND (".$xWhere.")"; + } else { + $where = $xWhere; + } + } + return ["where"=>$where, "join"=>null]; + } + + protected static function prepareFileUpload($data, $fileName, object $ref, string $instance=null, user $owner=null, $ttl=null) { + $f=new static(); + $f->fileName=$fileName; + $f->module=$instance; + if ($owner) { $f->ownerId=$owner->id; } + if ($ttl) { $f->expireStamp=time::current()->addSec($ttl); } + $f->class = $ref::class; + $f->uuid=common::getUUID(); + return $f; + } + + public static function directUpload($data, $fileName, object $ref, string $instance=null, user $owner=null, $ttl=null) { + $f=static::prepareFileUpload($data, $fileName, $ref, $instance, $owner, $ttl); + $s3=new s3client(); + if (!$s3->headBucket(static::defaultBucket)) { + $s3->createBucket(static::defaultBucket); + } + $s3->putObject(static::defaultBucket,$f->uuid, $data); + $f->save(); + return $f; + } + + public static function getByUUID(string $uuid) : file { + $files = file::search(options: ["uuid"=>$uuid]); + if ($files->result) { + return array_shift($files->result); + } else { + throw new foxException("File not found", 404); + } + } + + public function getContent() { + if ($this->uuid) { + $s3=new s3client(); + return $s3->getObject(static::defaultBucket, $this->uuid); + } + } + + public function getDownloadToken() { + if (empty($this->id)) { + throw new foxException("Empty fileId not allowed here"); + }; + $token=common::genPasswd(32); + + $c=new cache(); + $c->set('fileDnldToken-'.$token,$this,static::fileTokenTTL); + return $token; + } + + public static function getByToken($token) { + $c=new cache(); + $ftile=$c->get('fileDnldToken-'.$token,true); + if ($ftile) { + $file = new static($ftile); + $c->del('fileDnldToken-'.$token); + return $file; + } else { + throw new foxException("Invalid token",404); + } + } + + public static function getUploadToken($data, $fileName, object $ref, string $instance=null, user $owner=null) { + $f=static::prepareFileUpload($data, $fileName, $ref, $instance, $owner); + $token=common::genPasswd(32); + $c=new cache(); + $c->set('fileUpldToken-'.$token,$f,static::fileTokenTTL); + return $token; + } + + #### REST API + + public static function API_UnAuth_GET(request $request) { + if (!empty($request->parameters)) throw new foxException("Invalid request", 400); + $file2=file::getByToken($request->function); + header_remove('Content-Type'); + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename='.$file2->fileName); + print($file2->getContent()); + exit; + } + } + ?> \ No newline at end of file diff --git a/core/fox/moduleInfo.php b/core/fox/moduleInfo.php index 66db468..4e67094 100644 --- a/core/fox/moduleInfo.php +++ b/core/fox/moduleInfo.php @@ -33,6 +33,8 @@ class moduleInfo extends baseClass implements externalCallable public string $modVersion = "1.0.0"; + public ?string $modBuild=null; + public string $modPriority = "9999"; public time $installDate; @@ -67,6 +69,9 @@ class moduleInfo extends baseClass implements externalCallable "type" => "INT", "index" => "INDEX" ], + "modBuild"=> [ + "type"=>"VARCHAR(255)", + ], "singleInstanceOnly" => [ "type" => "SKIP" ], diff --git a/core/fox/modules.php b/core/fox/modules.php index dfbeeb3..c040019 100644 --- a/core/fox/modules.php +++ b/core/fox/modules.php @@ -162,6 +162,7 @@ class modules implements externalCallable if ($modDesc->name != $modInfo->name) { throw new foxException("Module name mismatch for ".$modInfo->name); } $modInfo->title=$modDesc->title; $modInfo->modVersion=$modDesc->version; + @$modInfo->modBuild=$modDesc->build; } catch (\Exception $e) { trigger_error($e->getMessage()); continue; diff --git a/core/fox/oAuthProfile.php b/core/fox/oAuthProfile.php index 851229e..a71cad8 100644 --- a/core/fox/oAuthProfile.php +++ b/core/fox/oAuthProfile.php @@ -40,6 +40,7 @@ class oAuthProfile extends baseClass implements externalCallable { protected function validateSave() { if (empty($this->hash)) { + trigger_error($this->hash); $this->hash=xcrypt::hash(json_encode($this)); } return true; @@ -142,7 +143,7 @@ class oAuthProfile extends baseClass implements externalCallable { if (!empty($request->requestBody->clientId)) { $m->clientId=common::clearInput($request->requestBody->clientId,"0-9A-Za-z._:/-"); } if (!empty($request->requestBody->clientKey)) { $m->clientKey=common::clearInput($request->requestBody->clientKey,"0-9A-Za-z._@-"); } $m->config=$request->requestBody->config; - $m->hash=common::clearInput($request->requestBody->hash,"0-9A-Za-z._@-"); + if (property_exists($request->requestBody,"hash")) { $m->hash=common::clearInput($request->requestBody->hash,"0-9A-Za-z._@-"); } $m->save(); static::log($request->instance,__FUNCTION__, "OAuth profile ".$m->name." updated.",$request->user,"oAuthProfile",$m->id,null,logEntry::sevInfo,["changelog"=>$m->changelog]); } diff --git a/core/fox/objectStorageClient.php b/core/fox/objectStorageClient.php index 37daea5..11159e1 100644 --- a/core/fox/objectStorageClient.php +++ b/core/fox/objectStorageClient.php @@ -28,5 +28,9 @@ interface objectStorageClient public function createBucket($bucket); public function deleteBucket($bucket); + + public function headBucket($bucket); + + public function headObject($bucket, $key); } ?> \ No newline at end of file diff --git a/core/fox/request.php b/core/fox/request.php index ae56e10..62c4e43 100644 --- a/core/fox/request.php +++ b/core/fox/request.php @@ -12,6 +12,7 @@ namespace fox; * * @property-read user $user * @property-read $userId + * @property-read $rawRequestBody * */ class request extends baseClass implements noSqlMigration @@ -164,6 +165,8 @@ class request extends baseClass implements noSqlMigration throw new foxException('Unauthorized', 401); } return $this->token->user; + case "rawRequestBody": + return file_get_contents("php://input"); default: return parent::__get($key); } diff --git a/core/fox/s3client.php b/core/fox/s3client.php index ea4da0f..d6d54ea 100644 --- a/core/fox/s3client.php +++ b/core/fox/s3client.php @@ -1,6 +1,8 @@ s3=null; } + + + public function headBucket($bucket) { + try { + if ($this->s3->headBucket(["Bucket" => $this->prefix . $bucket])) { + return true; + } + } catch (Exception $e) { + return false; + + } + } + + public function headObject($bucket, $key) { + return false; + } } ?> \ No newline at end of file diff --git a/core/fox/time.php b/core/fox/time.php index 8e5f6ee..b820b22 100644 --- a/core/fox/time.php +++ b/core/fox/time.php @@ -100,6 +100,18 @@ class time implements stringExportable, stringImportable, \JsonSerializable return $this->stamp; } + public static function formatInterval($val) { + $durWeeks=floor($val/604800); + $durXWeeks=$val % 604800; + $durDays=floor($durXWeeks/86400); + $durXDays=$durXWeeks % 86400; + $durHrs=floor($durXDays/3600); + $durXHrs=$durXDays % 3600; + $durMins=floor($durXHrs/60); + $durSecs=floor($durXHrs % 60); + + return ($durWeeks>0?$durWeeks."W ":"").($durDays>0?$durDays."D ":"").str_pad($durHrs,2, "0", STR_PAD_LEFT).":".str_pad($durMins,2, "0", STR_PAD_LEFT).":".str_pad($durSecs,2, "0", STR_PAD_LEFT); + } } ?> \ No newline at end of file diff --git a/core/fox/webhookCallable.php b/core/fox/webhookCallable.php new file mode 100644 index 0000000..c603359 --- /dev/null +++ b/core/fox/webhookCallable.php @@ -0,0 +1,25 @@ +_(request $request); + * Example: + * public static function HOOK_POST_members(request $request); + * public static function HOOKCall(request $request); + * + */ + +} +?> \ No newline at end of file diff --git a/core/fox/xcrypt.php b/core/fox/xcrypt.php index 976561a..147c00e 100644 --- a/core/fox/xcrypt.php +++ b/core/fox/xcrypt.php @@ -31,8 +31,6 @@ class xcrypt static function decrypt($val, $key = null) { - $ENC_KEY = substr(md5(config::get("masterSecret")), 0, 24); - if (isset($key)) { $ENC_KEY = substr(md5($key), 0, 24); } else { diff --git a/hook/.htaccess b/hook/.htaccess new file mode 100644 index 0000000..20f10f0 --- /dev/null +++ b/hook/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine On +RewriteRule . index.php [END] +RewriteRule . - [END,R=404] + diff --git a/hook/404.html b/hook/404.html new file mode 100644 index 0000000..563324f --- /dev/null +++ b/hook/404.html @@ -0,0 +1 @@ +0000404 \ No newline at end of file diff --git a/hook/index.php b/hook/index.php new file mode 100644 index 0000000..0d3b7d8 --- /dev/null +++ b/hook/index.php @@ -0,0 +1,105 @@ +module !== 'hook') { + throw new foxException("Invalid request",400); + } + + $request->shift(); + + $modules=moduleInfo::getAll(); + + if (!array_key_exists(request::get()->module, $modules)) { + throw new foxException("Invalid module",404); + } + $request->shift(); + + $modNS=$modules[request::get()->instance]->namespace; + $className=$modNS."\\".request::get()->module; + + if (!class_exists($className)) { + throw new foxException("Not found",404); + } + + if(!is_a($className, fox\webhookCallable::class,true)) { + throw new foxException("Not found",404); + } + + $hookMethod=fox\common::clearInput($request->method,"A-Z"); + $hookFunction=fox\common::clearInput($request->function,"a-zA-Z0-9"); + $hookXFunction=empty($request->parameters[0])?NULL:fox\common::clearInput($request->parameters[0],"a-zA-Z0-9"); + + $hookCallMethod="HOOK_".$hookMethod."_".$hookFunction; + $hookXCallMethod="HOOKX_".$hookMethod."_".$hookXFunction; + $hookZCallMethod="HOOK_".$hookMethod; + + if (method_exists($className, $hookCallMethod)) { + $rv=$className::$hookCallMethod($request); + } else if (($hookXFunction!==null) && method_exists($className, $hookXCallMethod)) { + $rv=$className::$hookXCallMethod($request); + } else if (method_exists($className, $hookZCallMethod)) { + $rv=$className::$hookZCallMethod($request); + } else if (method_exists($className, "HOOKCall")) { + $rv=$className::hookCall(request::get()); + } else { + throw new foxException("Method not allowed", 405); + } + + if ($rv!==false) { + ob_clean(); + header('Content-Type: application/json; charset=utf-8'); + foxRequestResult::throw("200", "OK", $rv); + } + +} catch (fox\foxRequestResult $e) { + ob_clean(); + header('Content-Type: application/json; charset=utf-8'); + header('HTTP/1.0 '.$e->getCode().' '.$e->getMessage(), true, $e->getCode()); + if ($e->retVal===null) { + print json_encode(["status"=>$e->getMessage()]); + } else { + print json_encode($e->retVal); + } + exit; +} catch (fox\foxException $e) { + if (($e->getCode()>=400 && $e->getCode()<500) || ($e->getCode() == 501) || ($e->getCode() >= 600 && $e->getCode()<900)) { + trigger_error($e->getStatus().": ".$e->getCode().": ".$e->getMessage()." in ".$e->getFile()." at line ".$e->getLine(), E_USER_WARNING); + print(json_encode(["error"=>["code"=>$e->getCode(),"message"=>$e->getMessage(), "xCode"=>$e->getXCode()]])); + if ($e->getCode()>=400 && $e->getCode()<502) { + header('HTTP/1.0 '.$e->getCode().' '.$e->getMessage(), true, $e->getCode()); + } else { + header('HTTP/1.0 501 '.$e->getMessage(), true, $e->getCode()); + } + } else { + trigger_error($e->getStatus().": ".$e->getCode().": ".$e->getMessage()." in ".$e->getFile()." at line ".$e->getLine(), E_USER_WARNING); + print(json_encode(["error"=>["errCode"=>500,"message"=>"Internal server error","xCode"=>$e->getXCode()]])); + header('HTTP/1.0 500 Internal server error', true, 500); + } + exit; +} catch (Exception $e) { + trigger_error($e->getCode().": ".$e->getMessage()." in ".$e->getFile()." at line ".$e->getLine(), E_USER_WARNING); + print(json_encode(["error"=>["errCode"=>500,"message"=>"Internal server error", "xCode"=>"ERR"]])); + header('HTTP/1.0 500 Internal server error', true, 500); + throw($e); +} +exit; + +?> \ No newline at end of file diff --git a/static/js/core/api.js b/static/js/core/api.js index 4a87362..975acfb 100644 --- a/static/js/core/api.js +++ b/static/js/core/api.js @@ -105,6 +105,20 @@ export function exec(requestType, method , data, onSuccess,noblank,onError,versi }); } +export function getFileByToken(token, fileName) { + let url='/api/v2/core/file/'+token; + + const a = document.createElement('a') + a.href = url + if (fileName != undefined) { + a.download=fileName; + } + + document.body.appendChild(a) + a.click() + document.body.removeChild(a); +} + export class settings { static async load() { if (sessionStorage.getItem("baseSettings")==undefined) { diff --git a/static/js/core/coreModules.js b/static/js/core/coreModules.js index e294b79..f265e27 100644 --- a/static/js/core/coreModules.js +++ b/static/js/core/coreModules.js @@ -34,7 +34,7 @@ function reloadInstalled() { $("",{class: "", text: mod.name}).appendTo(row); $("",{class: "", text: mod.enabled}).appendTo(row); $("",{class: "", text: mod.instanceOf}).appendTo(row); - $("",{class: "", text: mod.modVersion}).appendTo(row); + $("",{class: "", text: mod.modVersion+"."+mod.modBuild}).appendTo(row); $("",{class: "", text: mod.title}).appendTo(row); $("",{class: "", text: UI.stamp2date(mod.installDate,true)}).appendTo(row); $("",{class: "", text: UI.stamp2date(mod.updateDate,true)}).appendTo(row); @@ -157,7 +157,7 @@ function reloadAvail() { let row=$("",{}).bind('contextmenu', mlaContextMenuOpen).addClass("contextMenu").attr("modName",mod.name).attr("modTitle",mod.title); $("",{class: "idx", text: i}).appendTo(row); $("",{class: "", text: mod.name}).appendTo(row); - $("",{class: "", text: mod.modVersion}).appendTo(row); + $("",{class: "", text: mod.modVersion+"."+mod.modBuild}).appendTo(row); $("",{class: "", text: mod.title}).appendTo(row); $("",{class: "", html: mod.singleInstanceOnly?'':""}).appendTo(row); $("",{class: "", text: mod.instancesCount}).appendTo(row); diff --git a/static/js/core/lang_ru.js b/static/js/core/lang_ru.js index ae856c8..8eb58b7 100644 --- a/static/js/core/lang_ru.js +++ b/static/js/core/lang_ru.js @@ -42,6 +42,7 @@ export var langItem={ open: "Открыть", dialodAddButton: "Добавить", dialodCreateButton: "Создать", + dialodSaveButton: "Записать", dialodInfoTitle: "Информация", dialogRegisterButton: "Регистрация", dialogRecoveryButton: "Восстановить", @@ -69,6 +70,7 @@ export var langItem={ confCodeFmtErr: "Неверный формат кода подтверждения", recoverFormEmtyX: "Должно быть заполнено одно из полей", oauthLoginFailedUNR: "Пользователь не зарегистрирован в системе. Для продолжения работы нужно зарегистрироваться на начальной странице.", + oauthLoginFailed500: "Сервер авторизации вернул неверные данные либо недоступен. Повторите попытку через некоторое время", type: "Тип", types: "Типы", company: "Компания", diff --git a/static/js/core/login.js b/static/js/core/login.js index f621e2c..0266a11 100644 --- a/static/js/core/login.js +++ b/static/js/core/login.js @@ -39,16 +39,16 @@ export function load() { } else { // auth API.exec("POST", "auth/oauth",{hash: hash, code: code},loginSuccessCallback,false,function onFail(json) { - console.log(json); + let buttons={}; + buttons["OK"]=function() { + UI.closeDialog("dialogInfo"); + load(); + } + if (json.status.code==401 && json.data.error.xCode=="UNR") { - let buttons={}; - buttons["OK"]=function() { - UI.closeDialog("dialogInfo"); - load(); - } UI.showInfoDialog(langPack.core.iface.oauthLoginFailedUNR,langPack.core.iface.loginFailedDialogTitle,buttons); } else { - console.log("Error:",json); + UI.showInfoDialog(langPack.core.iface.oauthLoginFailed500,langPack.core.iface.loginFailedDialogTitle,buttons); } }); } diff --git a/static/theme/core/main.css b/static/theme/core/main.css index a64af47..8c441e0 100644 --- a/static/theme/core/main.css +++ b/static/theme/core/main.css @@ -34,6 +34,7 @@ --mxs-switch-off: rgba(00,64,128,0.2); --mxs-switch-disabled: rgba(50,50,50,1); --mxs-switch-blocked: rgba(255,111,0,0.8); +--mxs-switch-warn: var(--mxs-switch-blocked); --mxs-switch-alert: rgba(255,35,0,0.8); --mxs-title-font: 'Jura', sans-serif;