Compare commits

...

2 Commits

Author SHA1 Message Date
0bd7421371 add:标签,球馆 2025-04-29 15:55:35 +08:00
32612f3103 feat(upload): 添加华为 OBS 对象存储支持
- 在 addons.php 中添加了与华为 OBS 相关的钩子
- 新增了对华为 OBS 上传功能的实现,包括分片上传和合并
- 优化了上传参数处理和错误处理
- 支持客户端和服务端两种上传模式
2025-04-26 15:49:23 +08:00
68 changed files with 20717 additions and 4 deletions

1
addons/address/.addonrc Normal file
View File

@@ -0,0 +1 @@
{"files":["public\\assets\\addons\\address\\js\\gcoord.min.js","public\\assets\\addons\\address\\js\\jquery.autocomplete.js"],"license":"regular","licenseto":"34485","licensekey":"Fr1gIoX2PBbGmMnp GFErXGAvLGllzXV\/ASjNRQ==","domains":["localhost"],"licensecodes":[],"validations":["757319b447175b6ca1882635b132a594"]}

View File

@@ -0,0 +1,32 @@
<?php
namespace addons\address;
use think\Addons;
/**
* 地址选择
* @author [MiniLing] <[laozheyouxiang@163.com]>
*/
class Address extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
}

34
addons/address/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,34 @@
require([], function () {
//绑定data-toggle=addresspicker属性点击事件
$(document).on('click', "[data-toggle='addresspicker']", function () {
var that = this;
var callback = $(that).data('callback');
var input_id = $(that).data("input-id") ? $(that).data("input-id") : "";
var lat_id = $(that).data("lat-id") ? $(that).data("lat-id") : "";
var lng_id = $(that).data("lng-id") ? $(that).data("lng-id") : "";
var zoom_id = $(that).data("zoom-id") ? $(that).data("zoom-id") : "";
var lat = lat_id ? $("#" + lat_id).val() : '';
var lng = lng_id ? $("#" + lng_id).val() : '';
var zoom = zoom_id ? $("#" + zoom_id).val() : '';
var url = "/addons/address/index/select";
url += (lat && lng) ? '?lat=' + lat + '&lng=' + lng + (input_id ? "&address=" + $("#" + input_id).val() : "") + (zoom ? "&zoom=" + zoom : "") : '';
Fast.api.open(url, '位置选择', {
callback: function (res) {
input_id && $("#" + input_id).val(res.address).trigger("change");
lat_id && $("#" + lat_id).val(res.lat).trigger("change");
lng_id && $("#" + lng_id).val(res.lng).trigger("change");
zoom_id && $("#" + zoom_id).val(res.zoom).trigger("change");
try {
//执行回调函数
if (typeof callback === 'function') {
callback.call(that, res);
}
} catch (e) {
}
}
});
});
});

132
addons/address/config.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
return [
[
'name' => 'maptype',
'title' => '默认地图类型',
'type' => 'radio',
'content' => [
'baidu' => '百度地图',
'amap' => '高德地图',
'tencent' => '腾讯地图',
],
'value' => 'amap',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'zoom',
'title' => '默认缩放级别',
'type' => 'string',
'content' => [],
'value' => '11',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'lat',
'title' => '默认Lat',
'type' => 'string',
'content' => [],
'value' => '23.002569',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'lng',
'title' => '默认Lng',
'type' => 'string',
'content' => [],
'value' => '113.752215',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'baidukey',
'title' => '百度地图KEY',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'amapkey',
'title' => '高德地图KEY',
'type' => 'string',
'content' => [],
'value' => '78c9a922ba2d29f005eaa89e3f1b00bb',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'amapsecurityjscode',
'title' => '高德地图安全密钥',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'tencentkey',
'title' => '腾讯地图KEY',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'coordtype',
'title' => '坐标系类型',
'type' => 'select',
'content' => [
'DEFAULT' => '默认(使用所选地图默认坐标系)',
'GCJ02' => 'GCJ-02',
'BD09' => 'BD-09',
],
'value' => 'DEFAULT',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => '__tips__',
'title' => '温馨提示',
'type' => '',
'content' => [],
'value' => '请先申请对应地图的Key配置后再使用',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => 'alert-danger-light',
],
];

View File

@@ -0,0 +1,64 @@
<?php
namespace addons\address\controller;
use think\addons\Controller;
use think\Config;
use think\Hook;
class Index extends Controller
{
// 首页
public function index()
{
// 语言检测
$lang = $this->request->langset();
$lang = preg_match("/^([a-zA-Z\-_]{2,10})\$/i", $lang) ? $lang : 'zh-cn';
$site = Config::get("site");
// 配置信息
$config = [
'site' => array_intersect_key($site, array_flip(['name', 'cdnurl', 'version', 'timezone', 'languages'])),
'upload' => null,
'modulename' => 'addons',
'controllername' => 'index',
'actionname' => 'index',
'jsname' => 'addons/address',
'moduleurl' => '',
'language' => $lang
];
$config = array_merge($config, Config::get("view_replace_str"));
// 配置信息后
Hook::listen("config_init", $config);
// 加载当前控制器语言包
$this->view->assign('site', $site);
$this->view->assign('config', $config);
return $this->view->fetch();
}
// 选择地址
public function select()
{
$config = get_addon_config('address');
$zoom = (int)$this->request->get('zoom', $config['zoom']);
$lng = (float)$this->request->get('lng');
$lat = (float)$this->request->get('lat');
$address = $this->request->get('address');
$lng = $lng ?: $config['lng'];
$lat = $lat ?: $config['lat'];
$this->view->assign('zoom', $zoom);
$this->view->assign('lng', $lng);
$this->view->assign('lat', $lat);
$this->view->assign('address', $address);
$maptype = $config['maptype'];
if (!isset($config[$maptype . 'key']) || !$config[$maptype . 'key']) {
$this->error("请在配置中配置对应类型地图的密钥");
}
return $this->view->fetch('index/' . $maptype);
}
}

10
addons/address/info.ini Normal file
View File

@@ -0,0 +1,10 @@
name = address
title = 地址位置选择插件
intro = 地图位置选择插件,可返回地址和经纬度
author = FastAdmin
website = https://www.fastadmin.net
version = 1.1.8
state = 1
url = /addons/address
license = regular
licenseto = 34485

View File

@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>地址选择器</title>
<link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
<link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
<style type="text/css">
body {
margin: 0;
padding: 0;
}
#container {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.confirm {
position: absolute;
bottom: 30px;
right: 4%;
z-index: 99;
height: 50px;
width: 50px;
line-height: 50px;
font-size: 15px;
text-align: center;
background-color: white;
background: #1ABC9C;
color: white;
border: none;
cursor: pointer;
border-radius: 50%;
}
.search {
position: absolute;
width: 400px;
top: 0;
left: 50%;
padding: 5px;
margin-left: -200px;
}
.amap-marker-label {
border: 0;
background-color: transparent;
}
.info {
padding: .75rem 1.25rem;
margin-bottom: 1rem;
border-radius: .25rem;
position: fixed;
top: 2rem;
background-color: white;
width: auto;
min-width: 22rem;
border-width: 0;
left: 1.8rem;
box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);
}
</style>
</head>
<body>
<div class="search">
<div class="input-group">
<input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
<span class="input-group-btn">
<button type="submit" name="search" id="search-btn" class="btn btn-success">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
<div class="confirm">确定</div>
<div id="container"></div>
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: "{$config.amapsecurityjscode|default=''}",
}
</script>
<script type="text/javascript" src="//webapi.amap.com/maps?v=1.4.11&key={$config.amapkey|default=''}&plugin=AMap.ToolBar,AMap.Autocomplete,AMap.PlaceSearch,AMap.Geocoder"></script>
<!-- UI组件库 1.0 -->
<script src="//webapi.amap.com/ui/1.0/main.js?v=1.0.11"></script>
<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
<script src="__CDN__/assets/addons/address/js/gcoord.min.js"></script>
<script type="text/javascript">
$(function () {
var as, map, geocoder, address, fromtype, totype;
address = "{$address|htmlentities}";
var lng = Number("{$lng}");
var lat = Number("{$lat}");
fromtype = "GCJ02";
totype = "{$config.coordtype|default='DEFAULT'}"
totype = totype === 'DEFAULT' ? "GCJ02" : totype;
if (lng && lat && fromtype !== totype) {
var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
lng = result[0] || lng;
lat = result[1] || lat;
}
var init = function () {
AMapUI.loadUI(['misc/PositionPicker', 'misc/PoiPicker'], function (PositionPicker, PoiPicker) {
//加载PositionPickerloadUI的路径参数为模块名中 'ui/' 之后的部分
map = new AMap.Map('container', {
zoom: parseInt('{$zoom}'),
center: [lng, lat]
});
geocoder = new AMap.Geocoder({
radius: 1000 //范围默认500
});
var positionPicker = new PositionPicker({
mode: 'dragMarker',//设定为拖拽地图模式,可选'dragMap'、'dragMarker',默认为'dragMap'
map: map//依赖地图对象
});
//输入提示
var autoOptions = {
input: "place"
};
var relocation = function (lnglat, addr) {
lng = lnglat.lng;
lat = lnglat.lat;
map.panTo([lng, lat]);
positionPicker.start(lnglat);
if (addr) {
// var label = '<div class="info">地址:' + addr + '<br>经度:' + lng + '<br>纬度:' + lat + '</div>';
var label = '<div class="info">地址:' + addr + '</div>';
positionPicker.marker.setLabel({
content: label //显示内容
});
} else {
geocoder.getAddress(lng + ',' + lat, function (status, result) {
if (status === 'complete' && result.regeocode) {
var address = result.regeocode.formattedAddress;
// var label = '<div class="info">地址:' + address + '<br>经度:' + lng + '<br>纬度:' + lat + '</div>';
var label = '<div class="info">地址:' + address + '</div>';
positionPicker.marker.setLabel({
content: label //显示内容
});
} else {
console.log(JSON.stringify(result));
}
});
}
};
var auto = new AMap.Autocomplete(autoOptions);
//构造地点查询类
var placeSearch = new AMap.PlaceSearch({
map: map
});
//注册监听,当选中某条记录时会触发
AMap.event.addListener(auto, "select", function (e) {
placeSearch.setCity(e.poi.adcode);
placeSearch.search(e.poi.name, function (status, result) {
$(map.getAllOverlays("marker")).each(function (i, j) {
j.on("click", function () {
relocation(j.De.position);
});
});
}); //关键字查询查询
});
AMap.event.addListener(map, 'click', function (e) {
relocation(e.lnglat);
});
//加载工具条
var tool = new AMap.ToolBar();
map.addControl(tool);
var poiPicker = new PoiPicker({
input: 'place',
placeSearchOptions: {
map: map,
pageSize: 6 //关联搜索分页
}
});
poiPicker.on('poiPicked', function (poiResult) {
poiPicker.hideSearchResults();
$('.poi .nearpoi').text(poiResult.item.name);
$('.address .info').text(poiResult.item.address);
$('#address').val(poiResult.item.address);
$("#place").val(poiResult.item.name);
relocation(poiResult.item.location);
});
positionPicker.on('success', function (positionResult) {
console.log(positionResult);
as = positionResult.position;
address = positionResult.address;
lat = as.lat;
lng = as.lng;
});
positionPicker.on('fail', function (positionResult) {
address = '';
});
positionPicker.start();
if (address) {
// 添加label
var label = '<div class="info">地址:' + address + '</div>';
positionPicker.marker.setLabel({
content: label //显示内容
});
}
//点击搜索按钮
$(document).on('click', '#search-btn', function () {
if ($("#place").val() == '')
return;
placeSearch.search($("#place").val(), function (status, result) {
$(map.getAllOverlays("marker")).each(function (i, j) {
j.on("click", function () {
relocation(j.De.position);
});
});
});
});
});
};
//点击确定后执行回调赋值
var close = function (data) {
var index = parent.Layer.getFrameIndex(window.name);
var callback = parent.$("#layui-layer" + index).data("callback");
//再执行关闭
parent.Layer.close(index);
//再调用回传函数
if (typeof callback === 'function') {
callback.call(undefined, data);
}
};
//点击搜索按钮
$(document).on('click', '.confirm', function () {
var zoom = map.getZoom();
var data = {lat: lat, lng: lng, zoom: zoom, address: address};
if (fromtype !== totype) {
var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
data.lng = (result[0] || data.lng).toFixed(5);
data.lat = (result[1] || data.lat).toFixed(5);
console.log(data, result, fromtype, totype);
}
close(data);
});
init();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>地址选择器</title>
<link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
<link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
<style type="text/css">
body {
margin: 0;
padding: 0;
}
#container {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.confirm {
position: absolute;
bottom: 30px;
right: 4%;
z-index: 99;
height: 50px;
width: 50px;
line-height: 50px;
font-size: 15px;
text-align: center;
background-color: white;
background: #1ABC9C;
color: white;
border: none;
cursor: pointer;
border-radius: 50%;
}
.search {
position: absolute;
width: 400px;
top: 0;
left: 50%;
padding: 5px;
margin-left: -200px;
}
label.BMapLabel {
max-width: inherit;
padding: .75rem 1.25rem;
margin-bottom: 1rem;
background-color: white;
width: auto;
min-width: 22rem;
border: none;
box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);
}
</style>
</head>
<body>
<div class="search">
<div class="input-group">
<input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
<div id="searchResultPanel" style="border:1px solid #C0C0C0;width:150px;height:auto; display:none;"></div>
<span class="input-group-btn">
<button type="button" name="search" id="search-btn" class="btn btn-success">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
<div class="confirm">确定</div>
<div id="container"></div>
<script type="text/javascript" src="//api.map.baidu.com/api?v=2.0&ak={$config.baidukey|default=''}"></script>
<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
<script src="__CDN__/assets/addons/address/js/gcoord.min.js"></script>
<script type="text/javascript">
$(function () {
var map, marker, point, fromtype, totype;
var zoom = parseInt("{$zoom}");
var address = "{$address|htmlentities}";
var lng = Number("{$lng}");
var lat = Number("{$lat}");
fromtype = "BD09";
totype = "{$config.coordtype|default='DEFAULT'}"
totype = totype === 'DEFAULT' ? "BD09" : totype;
if (lng && lat && fromtype !== totype) {
var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
lng = result[0] || lng;
lat = result[1] || lat;
}
var geocoder = new BMap.Geocoder();
var addPointMarker = function (point, addr) {
deletePoint();
addPoint(point);
if (addr) {
addMarker(point, addr);
} else {
geocoder.getLocation(point, function (rs) {
addMarker(point, rs.address);
});
}
};
var addPoint = function (point) {
lng = point.lng;
lat = point.lat;
marker = new BMap.Marker(point);
map.addOverlay(marker);
map.panTo(point);
};
var addMarker = function (point, addr) {
address = addr;
// var labelhtml = '<div class="info">地址:' + address + '<br>经度:' + point.lng + '<br>纬度:' + point.lat + '</div>';
var labelhtml = '<div class="info">地址:' + address + '</div>';
var label = new BMap.Label(labelhtml, {offset: new BMap.Size(16, 20)});
label.setStyle({
border: 'none',
padding: '.75rem 1.25rem'
});
marker.setLabel(label);
};
var deletePoint = function () {
var allOverlay = map.getOverlays();
for (var i = 0; i < allOverlay.length; i++) {
map.removeOverlay(allOverlay[i]);
}
};
var init = function () {
map = new BMap.Map("container"); // 创建地图实例
var point = new BMap.Point(lng, lat); // 创建点坐标
map.enableScrollWheelZoom(true); //开启鼠标滚轮缩放
map.centerAndZoom(point, zoom); // 初始化地图,设置中心点坐标和地图级别
var size = new BMap.Size(10, 20);
map.addControl(new BMap.CityListControl({
anchor: BMAP_ANCHOR_TOP_LEFT,
offset: size,
}));
if ("{$lng}" != '' && "{$lat}" != '') {
addPointMarker(point, address);
}
ac = new BMap.Autocomplete({"input": "place", "location": map}); //建立一个自动完成的对象
ac.addEventListener("onhighlight", function (e) { //鼠标放在下拉列表上的事件
var str = "";
var _value = e.fromitem.value;
var value = "";
if (e.fromitem.index > -1) {
value = _value.province + _value.city + _value.district + _value.street + _value.business;
}
str = "FromItem<br />index = " + e.fromitem.index + "<br />value = " + value;
value = "";
if (e.toitem.index > -1) {
_value = e.toitem.value;
value = _value.province + _value.city + _value.district + _value.street + _value.business;
}
str += "<br />ToItem<br />index = " + e.toitem.index + "<br />value = " + value;
$("#searchResultPanel").html(str);
});
ac.addEventListener("onconfirm", function (e) { //鼠标点击下拉列表后的事件
var _value = e.item.value;
myValue = _value.province + _value.city + _value.district + _value.street + _value.business;
$("#searchResultPanel").html("onconfirm<br />index = " + e.item.index + "<br />myValue = " + myValue);
setPlace();
});
function setPlace(text) {
map.clearOverlays(); //清除地图上所有覆盖物
function myFun() {
var results = local.getResults();
var result = local.getResults().getPoi(0);
var point = result.point; //获取第一个智能搜索的结果
map.centerAndZoom(point, 18);
// map.addOverlay(new BMap.Marker(point)); //添加标注
if (result.type != 0) {
address = results.province + results.city + result.address;
} else {
address = result.address;
}
addPointMarker(point, address);
}
var local = new BMap.LocalSearch(map, { //智能搜索
onSearchComplete: myFun
});
local.search(text || myValue);
}
map.addEventListener("click", function (e) {
//通过点击百度地图可以获取到对应的point, 由point的lng、lat属性就可以获取对应的经度纬度
addPointMarker(e.point);
});
//点击搜索按钮
$(document).on('click', '#search-btn', function () {
if ($("#place").val() == '')
return;
setPlace($("#place").val());
});
};
var close = function (data) {
var index = parent.Layer.getFrameIndex(window.name);
var callback = parent.$("#layui-layer" + index).data("callback");
//再执行关闭
parent.Layer.close(index);
//再调用回传函数
if (typeof callback === 'function') {
callback.call(undefined, data);
}
};
//点击确定后执行回调赋值
$(document).on('click', '.confirm', function () {
var zoom = map.getZoom();
var data = {lat: lat, lng: lng, zoom: zoom, address: address};
if (fromtype !== totype) {
var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
data.lng = (result[0] || data.lng).toFixed(5);
data.lat = (result[1] || data.lat).toFixed(5);
console.log(data, result, fromtype, totype);
}
close(data);
});
init();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<title>地图位置(经纬度)选择插件</title>
<link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
<link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
</head>
<body>
<div class="container">
<div class="bs-docs-section clearfix">
<div class="row">
<div class="col-lg-12">
<div class="page-header">
<h2>地图位置(经纬度)选择示例</h2>
</div>
<div class="bs-component">
<form action="" method="post" role="form">
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="address" placeholder="地址">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="lng" placeholder="经度">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="lat" placeholder="纬度">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="zoom" placeholder="缩放">
</div>
<button type="button" class="btn btn-primary" data-toggle='addresspicker' data-input-id="address" data-lng-id="lng" data-lat-id="lat" data-zoom-id="zoom">点击选择</button>
</form>
</div>
<div class="page-header">
<h2 id="code">调用代码</h2>
</div>
<div class="bs-component">
<textarea class="form-control" rows="17">
<form action="" method="post" role="form">
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="address" placeholder="地址">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="lng" placeholder="经度">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="lat" placeholder="纬度">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="zoom" placeholder="缩放">
</div>
<button type="button" class="btn btn-primary" data-toggle='addresspicker' data-input-id="address" data-lng-id="lng" data-lat-id="lat" data-zoom-id="zoom">点击选择</button>
</form>
</textarea>
</div>
<div class="page-header">
<h2>参数说明</h2>
</div>
<div class="bs-component" style="background:#fff;">
<table class="table table-bordered">
<thead>
<tr>
<th>参数</th>
<th>释义</th>
</tr>
</thead>
<tbody>
<tr>
<td>data-input-id</td>
<td>填充地址的文本框ID</td>
</tr>
<tr>
<td>data-lng-id</td>
<td>填充经度的文本框ID</td>
</tr>
<tr>
<td>data-lat-id</td>
<td>填充纬度的文本框ID</td>
</tr>
<tr>
<td>data-zoom-id</td>
<td>填充缩放的文本框ID</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!--@formatter:off-->
<script type="text/javascript">
var require = {
config: {$config|json_encode}
};
</script>
<!--@formatter:on-->
<script>
require.callback = function () {
define('addons/address', ['jquery', 'bootstrap', 'frontend', 'template'], function ($, undefined, Frontend, Template) {
var Controller = {
index: function () {
}
};
return Controller;
});
define('lang', function () {
return [];
});
}
</script>
<script src="__CDN__/assets/js/require.min.js" data-main="__CDN__/assets/js/require-frontend.min.js?v={$site.version}"></script>
</body>
</html>

View File

@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>地址选择器</title>
<link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
<link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
<style type="text/css">
body {
margin: 0;
padding: 0;
}
#container {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.confirm {
position: absolute;
bottom: 30px;
right: 4%;
z-index: 99;
height: 50px;
width: 50px;
line-height: 50px;
font-size: 15px;
text-align: center;
background-color: white;
background: #1ABC9C;
color: white;
border: none;
cursor: pointer;
border-radius: 50%;
}
.search {
position: absolute;
width: 400px;
top: 0;
left: 50%;
padding: 5px;
margin-left: -200px;
}
.autocomplete-search {
text-align: left;
cursor: default;
background: #fff;
border-radius: 2px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
position: absolute;
display: none;
z-index: 1036;
max-height: 254px;
overflow: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.autocomplete-search .autocomplete-suggestion {
padding: 5px;
}
.autocomplete-search .autocomplete-suggestion:hover {
background: #f0f0f0;
}
</style>
</head>
<body>
<div class="search">
<div class="input-group">
<input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
<span class="input-group-btn">
<button type="button" name="search" id="search-btn" class="btn btn-success">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
<div class="confirm">确定</div>
<div id="container"></div>
<script charset="utf-8" src="//map.qq.com/api/js?v=2.exp&libraries=place&key={$config.tencentkey|default=''}"></script>
<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
<script src="__CDN__/assets/addons/address/js/gcoord.min.js"></script>
<script src="__CDN__/assets/addons/address/js/jquery.autocomplete.js"></script>
<script type="text/javascript">
$(function () {
var map, marker, geocoder, infoWin, searchService, keyword, address, fromtype, totype;
address = "{$address|htmlentities}";
var lng = Number("{$lng}");
var lat = Number("{$lat}");
fromtype = "GCJ02";
totype = "{$config.coordtype|default='DEFAULT'}"
totype = totype === 'DEFAULT' ? "GCJ02" : totype;
if (lng && lat && fromtype !== totype) {
var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
lng = result[0] || lng;
lat = result[1] || lat;
}
var init = function () {
var center = new qq.maps.LatLng(lat, lng);
map = new qq.maps.Map(document.getElementById('container'), {
center: center,
zoom: parseInt("{$config.zoom}")
});
//实例化信息窗口
infoWin = new qq.maps.InfoWindow({
map: map
});
geocoder = {
getAddress: function (latLng) {
$.ajax({
url: "https://apis.map.qq.com/ws/geocoder/v1/?location=" + latLng.lat + "," + latLng.lng + "&key={$config.tencentkey|default=''}&output=jsonp",
dataType: "jsonp",
type: 'GET',
cache: true,
crossDomain: true,
success: function (ret) {
console.log("getAddress:", ret)
if (ret.status === 0) {
var component = ret.result.address_component;
if (ret.result.formatted_addresses && ret.result.formatted_addresses.recommend) {
var recommend = ret.result.formatted_addresses.recommend;
var standard_address = ret.result.formatted_addresses.standard_address;
var address = component.province !== component.city ? component.province + component.city : component.province;
address = address + (recommend.indexOf(component.district) === 0 ? '' : component.district) + recommend;
} else {
address = ret.result.address;
}
showMarker(ret.result.location, address);
showInfoWin(ret.result.location, address);
}
},
error: function (e) {
console.log(e, 'error')
}
});
}
};
//初始化marker
showMarker(center);
if (address) {
showInfoWin(center, address);
} else {
geocoder.getAddress(center);
}
var place = $("#place");
place.autoComplete({
minChars: 1,
cache: 0,
menuClass: 'autocomplete-search',
source: function (term, response) {
try {
xhr.abort();
} catch (e) {
}
xhr = $.ajax({
url: "https://apis.map.qq.com/ws/place/v1/suggestion?keyword=" + term + "&key={$config.tencentkey|default=''}&output=jsonp",
dataType: "jsonp",
type: 'GET',
cache: true,
success: function (ret) {
if (ret.status === 0) {
if(ret.data.length === 0){
$(".autocomplete-suggestions.autocomplete-search").html('');
}
response(ret.data);
} else {
console.log(ret);
}
}
});
},
renderItem: function (item, search) {
search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
var regexp = new RegExp("(" + search.replace(/[\,|\u3000|\uff0c]/, ' ').split(' ').join('|') + ")", "gi");
return "<div class='autocomplete-suggestion' data-item='" + JSON.stringify(item) + "' data-title='" + item.title + "' data-val='" + item.title + "'>" + item.title.replace(regexp, "<b>$1</b>") + "</div>";
},
onSelect: function (e, term, sel) {
e.preventDefault();
var item = $(sel).data("item");
//调用获取位置方法
geocoder.getAddress(item.location);
var position = new qq.maps.LatLng(item.location.lat, item.location.lng);
map.setCenter(position);
}
});
//地图点击
qq.maps.event.addListener(map, 'click', function (event) {
try {
//调用获取位置方法
geocoder.getAddress(event.latLng);
} catch (e) {
console.log(e);
}
}
);
};
//显示info窗口
var showInfoWin = function (latLng, title) {
var position = new qq.maps.LatLng(latLng.lat, latLng.lng);
infoWin.open();
infoWin.setContent(title);
infoWin.setPosition(position);
};
//实例化marker和监听拖拽结束事件
var showMarker = function (latLng, title) {
console.log("showMarker", latLng, title)
var position = new qq.maps.LatLng(latLng.lat, latLng.lng);
marker && marker.setMap(null);
marker = new qq.maps.Marker({
map: map,
position: position,
draggable: true,
title: title || '拖动图标选择位置'
});
//监听拖拽结束
qq.maps.event.addListener(marker, 'dragend', function (event) {
//调用获取位置方法
geocoder.getAddress(event.latLng);
});
};
var close = function (data) {
var index = parent.Layer.getFrameIndex(window.name);
var callback = parent.$("#layui-layer" + index).data("callback");
//再执行关闭
parent.Layer.close(index);
//再调用回传函数
if (typeof callback === 'function') {
callback.call(undefined, data);
}
};
//点击确定后执行回调赋值
$(document).on('click', '.confirm', function () {
var zoom = map.getZoom();
var data = {lat: infoWin.position.lat.toFixed(5), lng: infoWin.position.lng.toFixed(5), zoom: zoom, address: infoWin.content};
if (fromtype !== totype) {
var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
data.lng = (result[0] || data.lng).toFixed(5);
data.lat = (result[1] || data.lat).toFixed(5);
console.log(data, result, fromtype, totype);
}
close(data);
});
//点击搜索按钮
$(document).on('click', '#search-btn', function () {
if ($("#place").val() === '')
return;
var first = $(".autocomplete-search > .autocomplete-suggestion:first");
if (!first.length) {
return;
}
var item = first.data("item");
//调用获取位置方法
geocoder.getAddress(item.location);
var position = new qq.maps.LatLng(item.location.lat, item.location.lng);
map.setCenter(position);
});
init();
});
</script>
</body>
</html>

1
addons/hwobs/.addonrc Normal file
View File

@@ -0,0 +1 @@
{"files":["public\\assets\\addons\\hwobs\\js\\spark.js"],"license":"regular","licenseto":"34485","licensekey":"kaCpXL6B57yO89Rv h7EIXhuMw6+sCcUldTSJoQ==","domains":["localhost"],"licensecodes":[],"validations":["757319b447175b6ca1882635b132a594"]}

152
addons/hwobs/Hwobs.php Normal file
View File

@@ -0,0 +1,152 @@
<?php
namespace addons\hwobs;
use addons\hwobs\library\Auth;
use Obs\ObsClient;
use think\Addons;
use think\App;
use think\Loader;
/**
* 华为云存储插件
*/
class Hwobs extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable()
{
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable()
{
return true;
}
/**
* 判断是否来源于API上传
*/
public function moduleInit($request)
{
$config = $this->getConfig();
$module = strtolower($request->module());
if ($module == 'api' && ($config['apiupload'] ?? 0) &&
strtolower($request->controller()) == 'common' &&
strtolower($request->action()) == 'upload') {
request()->param('isApi', true);
App::invokeMethod(["\\addons\\hwobs\\controller\\Index", "upload"], ['isApi' => true]);
}
}
/**
* 上传配置初始化
*/
public function uploadConfigInit(&$upload)
{
$config = $this->getConfig();
$module = request()->module();
$module = $module ? strtolower($module) : 'index';
$data = ['deadline' => time() + $config['expire']];
$signature = hash_hmac('sha1', json_encode($data), $config['secretKey'], true);
$token = '';
if (Auth::isModuleAllow()) {
$token = $config['accessKey'] . ':' . base64_encode($signature) . ':' . base64_encode(json_encode($data));
}
$multipart = [
'hwobstoken' => $token
];
$upload = array_merge($upload, [
'cdnurl' => $config['cdnurl'],
'uploadurl' => $config['uploadmode'] == 'client' ? $config['uploadurl'] : addon_url('hwobs/index/upload', [], false, true),
'uploadmode' => $config['uploadmode'],
'bucket' => $config['bucket'],
'maxsize' => $config['maxsize'],
'mimetype' => $config['mimetype'],
'savekey' => $config['savekey'],
'chunking' => (bool)($config['chunking'] ?? $upload['chunking']),
'chunksize' => (int)($config['chunksize'] ?? $upload['chunksize']),
'multipart' => $multipart,
'storage' => $this->getName(),
'multiple' => (bool)$config['multiple'],
]);
}
/**
* 附件删除后
*/
public function uploadDelete($attachment)
{
$config = $this->getConfig();
if ($attachment['storage'] == 'hwobs' && isset($config['syncdelete']) && $config['syncdelete']) {
try {
//删除云储存文件
$config = get_addon_config('hwobs');
$obs = new ObsClient([
'key' => $config['accessKey'],
'secret' => $config['secretKey'],
'endpoint' => $config['endpoint']
]);
$result = $obs->deleteObject([
'Bucket' => $config['bucket'],
'Key' => ltrim($attachment->url, '/')
]);
} catch (\Exception $e) {
return false;
}
//如果是服务端中转,还需要删除本地文件
//if ($config['uploadmode'] == 'server') {
// $filePath = ROOT_PATH . 'public' . str_replace('/', DS, $attachment->url);
// if ($filePath) {
// @unlink($filePath);
// }
//}
}
return true;
}
/**
* 渲染命名空间配置信息
*/
public function appInit()
{
if (!class_exists('\Obs\ObsClient')) {
Loader::addNamespace('Obs', $this->addons_path . str_replace('/', DS, 'library/Obs/'));
}
}
}

279
addons/hwobs/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,279 @@
if (typeof Config.upload.storage !== 'undefined' && Config.upload.storage === 'hwobs') {
require(['upload'], function (Upload) {
//获取文件MD5值
var getFileMd5 = function (file, cb) {
//如果savekey中未检测到md5则无需获取文件md5直接返回upload的uuid
if (!Config.upload.savekey.match(/\{(file)?md5\}/)) {
cb && cb(file.upload.uuid);
return;
}
require(['../addons/hwobs/js/spark'], function (SparkMD5) {
var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
chunkSize = 10 * 1024 * 1024,
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0,
spark = new SparkMD5.ArrayBuffer(),
fileReader = new FileReader();
fileReader.onload = function (e) {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
cb && cb(spark.end());
}
};
fileReader.onerror = function () {
console.warn('文件读取错误');
};
function loadNext() {
var start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
};
var _onInit = Upload.events.onInit;
//初始化中完成判断
Upload.events.onInit = function () {
_onInit.apply(this, Array.prototype.slice.apply(arguments));
//如果上传接口不是hwobs则不处理
if (this.options.url !== Config.upload.uploadurl) {
return;
}
$.extend(this.options, {
//关闭自动处理队列功能
autoQueue: false,
params: function (files, xhr, chunk) {
var params = $.extend({}, Config.upload.multipart);
if (chunk) {
return $.extend({}, params, {
filesize: chunk.file.size,
filename: chunk.file.name,
chunkid: chunk.file.upload.uuid,
chunkindex: chunk.index,
chunkcount: chunk.file.upload.totalChunkCount,
chunkfilesize: chunk.dataBlock.data.size,
chunksize: this.options.chunkSize,
width: chunk.file.width || 0,
height: chunk.file.height || 0,
type: chunk.file.type,
uploadId: chunk.file.uploadId,
key: chunk.file.key,
});
}
return params;
},
chunkSuccess: function (chunk, file, response) {
var etag = chunk.xhr.getResponseHeader("ETag").replace(/(^")|("$)/g, '');
file.etags = file.etags ? file.etags : [];
file.etags[chunk.index] = etag;
},
chunksUploaded: function (file, done) {
var that = this;
Fast.api.ajax({
url: "/addons/hwobs/index/upload",
data: {
action: 'merge',
filesize: file.size,
filename: file.name,
chunkid: file.upload.uuid,
chunkcount: file.upload.totalChunkCount,
md5: file.md5,
key: file.key,
uploadId: file.uploadId,
etags: file.etags,
category: file.category || '',
hwobstoken: Config.upload.multipart.hwobstoken,
},
}, function (data, ret) {
done(JSON.stringify(ret));
return false;
}, function (data, ret) {
file.accepted = false;
that._errorProcessing([file], ret.msg);
return false;
});
},
});
var _success = this.options.success;
//先移除已有的事件
this.off("success", _success).on("success", function (file, response) {
var ret = {code: 0, msg: response};
try {
if (response) {
ret = typeof response === 'string' ? JSON.parse(response) : response;
}
if (file.xhr.status === 200 || file.xhr.status === 204) {
if (Config.upload.uploadmode === 'client') {
ret = {code: 1, data: {url: '/' + file.key}};
}
if (ret.code == 1) {
var url = ret.data.url || '';
Fast.api.ajax({
url: "/addons/hwobs/index/notify",
data: {name: file.name, url: url, md5: file.md5, size: file.size, width: file.width || 0, height: file.height || 0, type: file.type, category: file.category || '', hwobstoken: Config.upload.multipart.hwobstoken}
}, function () {
return false;
}, function () {
return false;
});
} else {
console.error(ret);
}
} else {
console.error(file.xhr);
}
} catch (e) {
console.error(e);
}
_success.call(this, file, ret);
});
this.on("addedfile", function (file) {
var that = this;
setTimeout(function () {
if (file.status === 'error') {
return;
}
getFileMd5(file, function (md5) {
var chunk = that.options.chunking && file.size > that.options.chunkSize ? 1 : 0;
var params = $(that.element).data("params") || {};
var category = typeof params.category !== 'undefined' ? params.category : ($(that.element).data("category") || '');
category = typeof category === 'function' ? category.call(that, file) : category;
Fast.api.ajax({
url: "/addons/hwobs/index/params",
data: {method: 'POST', category: category, md5: md5, name: file.name, type: file.type, size: file.size, chunk: chunk, chunksize: that.options.chunkSize, hwobstoken: Config.upload.multipart.hwobstoken},
}, function (data) {
file.md5 = md5;
file.id = data.id;
file.key = data.key;
file.date = data.date;
file.uploadId = data.uploadId;
file.policy = data.policy;
file.signature = data.signature;
file.partsAuthorization = data.partsAuthorization;
file.headers = data.headers;
delete data.headers;
file.params = data;
file.category = category;
if (file.status != 'error') {
//开始上传
that.enqueueFile(file);
} else {
that.removeFile(file);
}
return false;
}, function () {
that.removeFile(file);
});
});
}, 0);
});
if (Config.upload.uploadmode === 'client') {
var _method = this.options.method;
var _url = this.options.url;
this.options.method = function (files) {
if (files[0].upload.chunked) {
var chunk = null;
files[0].upload.chunks.forEach(function (item) {
if (item.status === 'uploading') {
chunk = item;
}
});
if (!chunk) {
return "POST";
} else {
return "PUT";
}
} else {
return "POST";
}
return _method;
};
this.options.url = function (files) {
if (files[0].upload.chunked) {
var chunk = null;
files[0].upload.chunks.forEach(function (item) {
if (item.status === 'uploading') {
chunk = item;
}
});
var index = chunk.dataBlock.chunkIndex;
this.options.headers = {"Authorization": files[0]['partsAuthorization'][index], "x-amz-date": files[0]['date']};
if (!chunk) {
return Config.upload.uploadurl + "/" + files[0].key + "?uploadId=" + files[0].uploadId;
} else {
return Config.upload.uploadurl + "/" + files[0].key + "?partNumber=" + (index + 1) + "&uploadId=" + files[0].uploadId;
}
}
return _url;
};
this.options.params = function (files, xhr, chunk) {
var params = Config.upload.multipart;
delete params.category;
if (chunk) {
return $.extend({}, params, {
filesize: chunk.file.size,
filename: chunk.file.name,
chunkid: chunk.file.upload.uuid,
chunkindex: chunk.index,
chunkcount: chunk.file.upload.totalChunkCount,
chunkfilesize: chunk.dataBlock.data.size,
width: chunk.file.width || 0,
height: chunk.file.height || 0,
type: chunk.file.type,
});
} else {
var retParams = $.extend({}, params, files[0].params || {});
delete retParams.hwobstoken;
delete retParams.date;
delete retParams.md5;
if (Config.upload.uploadmode !== 'client') {
params.category = files[0].category || '';
}
return retParams;
}
};
this.on("sending", function (file, xhr, formData) {
var that = this;
var _send = xhr.send;
//仅允许部分字段
var allowFields = ['partNumber', 'uploadId', 'key', 'AccessKeyId', 'policy', 'signature', 'file'];
formData.forEach(function (value, key) {
if (allowFields.indexOf(key) < 0) {
formData.delete(key);
}
});
if (file.upload.chunked) {
xhr.send = function () {
if (file.upload.chunked) {
var chunk = null;
file.upload.chunks.forEach(function (item) {
if (item.status == 'uploading') {
chunk = item;
}
});
_send.call(xhr, chunk.dataBlock.data);
}
};
}
});
}
};
});
}

245
addons/hwobs/config.php Normal file
View File

@@ -0,0 +1,245 @@
<?php
return [
[
'name' => 'accessKey',
'title' => 'Access Key',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => 'required',
'msg' => '',
'tip' => '请前往华为云控制台->我的凭证->访问密钥中生成',
'ok' => '',
'extend' => '',
],
[
'name' => 'secretKey',
'title' => 'Secret Key',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => 'required',
'msg' => '',
'tip' => '请前往华为云控制台->我的凭证->访问密钥中生成',
'ok' => '',
'extend' => '',
],
[
'name' => 'bucket',
'title' => '存储桶名称',
'type' => 'string',
'content' => [],
'value' => 'yourbucket',
'rule' => 'required',
'msg' => '',
'tip' => '存储桶名称',
'ok' => '',
'extend' => '',
],
[
'name' => 'endpoint',
'title' => 'Endpoint',
'type' => 'string',
'content' => [],
'value' => 'obs.cn-south-1.myhuaweicloud.com',
'rule' => 'required;endpoint',
'msg' => '',
'tip' => '请输入你的Endpoint',
'ok' => '',
'extend' => 'data-rule-endpoint="[/^(?!http(s)?:\/\/).*$/, \'不能以http(s)://开头\']"',
],
[
'name' => 'uploadurl',
'title' => '上传接口地址',
'type' => 'string',
'content' => [],
'value' => 'https://yourbucket.obs.cn-south-1.myhuaweicloud.com',
'rule' => 'required;uploadurl',
'msg' => '',
'tip' => '请使用存储桶->基本信息->访问域名的值并在前面加上http://或https://',
'ok' => '',
'extend' => 'data-rule-uploadurl="[/^http(s)?:\/\/.*$/, \'必需以http(s)://开头\']"',
],
[
'name' => 'cdnurl',
'title' => 'CDN地址',
'type' => 'string',
'content' => [],
'value' => 'https://yourbucket.obs.cn-south-1.myhuaweicloud.com',
'rule' => 'required;cdnurl',
'msg' => '',
'tip' => '如果你的云存储有绑定自定义域名,请输入自定义域名',
'ok' => '',
'extend' => 'data-rule-cdnurl="[/^http(s)?:\/\/.*$/, \'必需以http(s)://开头\']"',
],
[
'name' => 'uploadmode',
'title' => '上传模式',
'type' => 'select',
'content' => [
'client' => '客户端直传(速度快,无备份)',
'server' => '服务器中转(占用服务器带宽,有备份)',
],
'value' => 'server',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'serverbackup',
'title' => '服务器中转模式备份',
'type' => 'radio',
'content' => [
1 => '备份(附件管理将产生2条记录)',
0 => '不备份',
],
'value' => '1',
'rule' => '',
'msg' => '',
'tip' => '服务器中转模式下是否备份文件',
'ok' => '',
'extend' => '',
],
[
'name' => 'savekey',
'title' => '保存文件名',
'type' => 'string',
'content' => [],
'value' => '/uploads/{year}{mon}{day}/{filemd5}{.suffix}',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'expire',
'title' => '上传有效时长',
'type' => 'string',
'content' => [],
'value' => '600',
'rule' => 'required',
'msg' => '',
'tip' => '用户停留页面上传有效时长,单位秒',
'ok' => '',
'extend' => '',
],
[
'name' => 'maxsize',
'title' => '最大可上传',
'type' => 'string',
'content' => [],
'value' => '10M',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'mimetype',
'title' => '可上传后缀格式',
'type' => 'string',
'content' => [],
'value' => 'jpg,png,bmp,jpeg,gif,zip,rar,xls,xlsx',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'multiple',
'title' => '多文件上传',
'type' => 'bool',
'content' => [],
'value' => '0',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'thumbstyle',
'title' => '缩略图样式',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '用于后台列表缩略图样式,可使用:?x-image-process=image/resize,m_fixed,h_90,w_120或?x-image-process=style/样式名称',
'ok' => '',
'extend' => '',
],
[
'name' => 'chunking',
'title' => '分片上传',
'type' => 'radio',
'content' => [
1 => '开启',
0 => '关闭',
],
'value' => '0',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'chunksize',
'title' => '分片大小',
'type' => 'number',
'content' => [],
'value' => '4194304',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'syncdelete',
'title' => '附件删除时是否同步删除云存储文件',
'type' => 'bool',
'content' => [],
'value' => '0',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'apiupload',
'title' => 'API接口使用云存储',
'type' => 'bool',
'content' => [],
'value' => '0',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'noneedlogin',
'title' => '免登录上传',
'type' => 'checkbox',
'content' => [
'api' => 'API',
'index' => '前台',
'admin' => '后台',
],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
];

View File

@@ -0,0 +1,325 @@
<?php
namespace addons\hwobs\controller;
use addons\hwobs\library\Auth;
use addons\hwobs\library\Signer;
use app\common\exception\UploadException;
use app\common\library\Upload;
use app\common\model\Attachment;
use Obs\Internal\Common\Model;
use Obs\Internal\Signature\DefaultSignature;
use Obs\ObsClient;
use think\addons\Controller;
use think\Config;
class Index extends Controller
{
public function _initialize()
{
//跨域检测
check_cors_request();
parent::_initialize();
Config::set('default_return_type', 'json');
}
public function index()
{
Config::set('default_return_type', 'html');
$this->error("当前插件暂无前台页面");
}
public function params()
{
$this->check();
$config = get_addon_config('hwobs');
$name = $this->request->post('name');
$md5 = $this->request->post('md5');
$chunk = $this->request->post('chunk');
$key = (new Upload())->getSavekey($config['savekey'], $name, $md5);
$key = ltrim($key, "/");
$params = [
'key' => $key,
'md5' => $md5
];
$fileSize = $this->request->post('size');
$type = $this->request->post('type');
$date = gmdate('D, d M Y H:i:s \G\M\T', time());
$headers = [];
$obs = new ObsClient([
'key' => $config['accessKey'],
'secret' => $config['secretKey'],
'endpoint' => $config['endpoint']
]);
if ($chunk) {
$partSize = $this->request->post("chunksize");
try {
$ret = $obs->initiateMultipartUpload([
'Bucket' => $config['bucket'],
'Key' => $key,
]);
} catch (\Exception $e) {
$this->error("上传失败");
}
$uploadId = $ret['UploadId'];
$params['key'] = $key;
$params['uploadId'] = $uploadId;
$params['partsAuthorization'] = Signer::getPartsAuthorization($key, $uploadId, $fileSize, $partSize, $date);
} else {
$signature = $obs->createPostSignature([
'Bucket' => $config['bucket'],
'Key' => $key,
'FormParams' => [
]
]);
$params['key'] = $key;
$params['AccessKeyId'] = $config['accessKey'];
$params['policy'] = $signature['Policy'];
$params['signature'] = $signature['Signature'];
}
$params['headers'] = $headers;
$params['date'] = $date;
$this->success('', null, $params);
return;
}
/**
* 服务器中转上传文件
* 上传分片
* 合并分片
* @param bool $isApi
*/
public function upload($isApi = false)
{
if ($isApi === true) {
if (!Auth::isModuleAllow()) {
$this->error("请登录后再进行操作");
}
} else {
$this->check();
}
$config = get_addon_config('hwobs');
$obs = new ObsClient([
'key' => $config['accessKey'],
'secret' => $config['secretKey'],
'endpoint' => $config['endpoint']
]);
//检测删除文件或附件
$checkDeleteFile = function ($attachment, $upload, $force = false) use ($config) {
//如果设定为不备份则删除文件和记录 或 强制删除
if ((isset($config['serverbackup']) && !$config['serverbackup']) || $force) {
if ($attachment && !empty($attachment['id'])) {
$attachment->delete();
}
if ($upload) {
//文件绝对路径
$filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
@unlink($filePath);
}
}
};
$chunkid = $this->request->post("chunkid");
if ($chunkid) {
$action = $this->request->post("action");
$chunkindex = $this->request->post("chunkindex/d");
$chunkcount = $this->request->post("chunkcount/d");
$filesize = $this->request->post("filesize");
$filename = $this->request->post("filename");
$method = $this->request->method(true);
$key = $this->request->post("key");
$uploadId = $this->request->post("uploadId");
if ($action == 'merge') {
$attachment = null;
$upload = null;
//合并分片
if ($config['uploadmode'] == 'server') {
//合并分片文件
try {
$upload = new Upload();
$attachment = $upload->merge($chunkid, $chunkcount, $filename);
} catch (UploadException $e) {
$this->error($e->getMessage());
}
}
$etags = $this->request->post("etags/a", []);
if (count($etags) != $chunkcount) {
$checkDeleteFile($attachment, $upload, true);
$this->error("分片数据错误");
}
$listParts = [];
for ($i = 0; $i < $chunkcount; $i++) {
$listParts[] = array("PartNumber" => $i + 1, "ETag" => $etags[$i]);
}
try {
$result = $obs->completeMultipartUpload([
'Bucket' => $config['bucket'],
'Key' => $key,
// 设置Upload ID
'UploadId' => $uploadId,
'Parts' => $listParts
]);
} catch (\Exception $e) {
$checkDeleteFile($attachment, $upload, true);
$this->error($e->getMessage());
}
if (!isset($result['Key'])) {
$checkDeleteFile($attachment, $upload, true);
$this->error("上传失败");
} else {
$checkDeleteFile($attachment, $upload);
$this->success("上传成功", '', ['url' => "/" . $key, 'fullurl' => cdnurl("/" . $key, true)]);
}
} else {
//默认普通上传文件
$file = $this->request->file('file');
try {
$upload = new Upload($file);
$file = $upload->chunk($chunkid, $chunkindex, $chunkcount);
} catch (UploadException $e) {
$this->error($e->getMessage());
}
try {
$ret = $obs->uploadPart([
'Bucket' => $config['bucket'],
'Key' => $key,
// 设置分段号范围是1~10000
'PartNumber' => $chunkindex + 1,
// 设置Upload ID
'UploadId' => $uploadId,
// 设置将要上传的大文件,localfile为上传的本地文件路径需要指定到具体的文件名
'SourceFile' => $file->getRealPath(),
// 设置分段大小
'PartSize' => $file->getSize(),
// 设置分段的起始偏移大小
'Offset' => 0
]);
} catch (\Exception $e) {
$this->error($e->getMessage());
}
$etag = isset($ret['ETag']) ? $ret['ETag'] : '';
$this->success("上传成功", "", [], 3, ['ETag' => $etag]);
}
} else {
$attachment = null;
//默认普通上传文件
$file = $this->request->file('file');
try {
$upload = new Upload($file);
$attachment = $upload->upload();
} catch (UploadException $e) {
$this->error($e->getMessage());
}
//文件绝对路径
$filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
$url = $attachment->url;
try {
$ret = $obs->putObject([
'Bucket' => $config['bucket'],
'Key' => ltrim($attachment->url, '/'),
'SourceFile' => $filePath
]);
//成功不做任何操作
} catch (\Exception $e) {
$checkDeleteFile($attachment, $upload, true);
$this->error("上传失败");
}
$checkDeleteFile($attachment, $upload);
// 记录云存储记录
$data = $attachment->toArray();
unset($data['id']);
$data['storage'] = 'hwobs';
Attachment::create($data, true);
$this->success("上传成功", '', ['url' => $url, 'fullurl' => cdnurl($url, true)]);
}
return;
}
/**
* 回调
*/
public function notify()
{
$this->check();
$config = get_addon_config('hwobs');
if ($config['uploadmode'] != 'client') {
$this->error("无需执行该操作");
}
$this->request->filter('trim,strip_tags,htmlspecialchars,xss_clean');
$size = $this->request->post('size/d');
$name = $this->request->post('name', '');
$md5 = $this->request->post('md5', '');
$type = $this->request->post('type', '');
$url = $this->request->post('url', '');
$width = $this->request->post('width/d');
$height = $this->request->post('height/d');
$category = $this->request->post('category', '');
$suffix = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
$attachment = Attachment::where('url', $url)->where('storage', 'hwobs')->find();
if (!$attachment) {
$params = array(
'category' => $category,
'admin_id' => (int)session('admin.id'),
'user_id' => (int)$this->auth->id,
'filesize' => $size,
'filename' => $name,
'imagewidth' => $width,
'imageheight' => $height,
'imagetype' => $suffix,
'imageframes' => 0,
'mimetype' => $type,
'url' => $url,
'uploadtime' => time(),
'storage' => 'hwobs',
'sha1' => $md5,
);
Attachment::create($params, true);
}
$this->success();
return;
}
/**
* 检查签名是否正确或过期
*/
protected function check()
{
$hwobstoken = $this->request->post('hwobstoken', '', 'trim');
if (!$hwobstoken) {
$this->error("参数不正确(code:1)");
}
$config = get_addon_config('hwobs');
list($accessKey, $sign, $data) = explode(':', $hwobstoken);
if (!$accessKey || !$sign || !$data) {
$this->error("参数不正确(code:2)");
}
if ($accessKey !== $config['accessKey']) {
$this->error("参数不正确(code:3)");
}
if ($sign !== base64_encode(hash_hmac('sha1', base64_decode($data), $config['secretKey'], true))) {
$this->error("签名不正确");
}
$json = json_decode(base64_decode($data), true);
if ($json['deadline'] < time()) {
$this->error("请求已经超时");
}
}
}

10
addons/hwobs/info.ini Normal file
View File

@@ -0,0 +1,10 @@
name = hwobs
title = 华为OBS云储存
intro = 使用华为OBS作为默认云储存
author = FastAdmin
website = https://www.fastadmin.net
version = 1.2.9
state = 1
url = /addons/hwobs
license = regular
licenseto = 34485

View File

@@ -0,0 +1,37 @@
<?php
namespace addons\hwobs\library;
class Auth
{
public function __construct()
{
}
public static function isModuleAllow()
{
$config = get_addon_config('hwobs');
$module = request()->module();
$module = $module ? strtolower($module) : 'index';
$noNeedLogin = array_filter(explode(',', $config['noneedlogin'] ?? ''));
$isModuleLogin = false;
$tagName = 'upload_config_checklogin';
foreach (\think\Hook::get($tagName) as $index => $name) {
if (\think\Hook::exec($name, $tagName)) {
$isModuleLogin = true;
break;
}
}
if (in_array($module, $noNeedLogin)
|| ($module == 'admin' && \app\admin\library\Auth::instance()->id)
|| ($module != 'admin' && \app\common\library\Auth::instance()->id)
|| $isModuleLogin) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Common;
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Obs\ObsException;
class CheckoutStream implements StreamInterface {
use StreamDecoratorTrait;
private $expectedLength;
private $readedCount = 0;
public function __construct(StreamInterface $stream, $expectedLength) {
$this->stream = $stream;
$this->expectedLength = $expectedLength;
}
public function getContents() {
$contents = $this->stream->getContents();
$length = strlen($contents);
if ($this->expectedLength !== null && floatval($length) !== $this->expectedLength) {
$this -> throwObsException($this->expectedLength, $length);
}
return $contents;
}
public function read($length) {
$string = $this->stream->read($length);
if ($this->expectedLength !== null) {
$this->readedCount += strlen($string);
if ($this->stream->eof()) {
if (floatval($this->readedCount) !== $this->expectedLength) {
$this -> throwObsException($this->expectedLength, $this->readedCount);
}
}
}
return $string;
}
public function throwObsException($expectedLength, $reaLength) {
$obsException = new ObsException('premature end of Content-Length delimiter message body (expected:' . $expectedLength . '; received:' . $reaLength . ')');
$obsException->setExceptionType('server');
throw $obsException;
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Common;
interface ITransform {
public function transform($sign, $para);
}

View File

@@ -0,0 +1,257 @@
<?php
/**
* Copyright (c) 2011-2018 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
namespace Obs\Internal\Common;
class Model implements \ArrayAccess, \IteratorAggregate, \Countable, ToArrayInterface
{
protected $data;
public function __construct(array $data = [])
{
$this->data = $data;
}
public function count()
{
return count($this->data);
}
public function getIterator()
{
return new \ArrayIterator($this->data);
}
public function toArray()
{
return $this->data;
}
public function clear()
{
$this->data = [];
return $this;
}
public function getAll(array $keys = null)
{
return $keys ? array_intersect_key($this->data, array_flip($keys)) : $this->data;
}
public function get($key)
{
return isset($this->data[$key]) ? $this->data[$key] : null;
}
public function set($key, $value)
{
$this->data[$key] = $value;
return $this;
}
public function add($key, $value)
{
if (!array_key_exists($key, $this->data)) {
$this->data[$key] = $value;
} elseif (is_array($this->data[$key])) {
$this->data[$key][] = $value;
} else {
$this->data[$key] = [$this->data[$key], $value];
}
return $this;
}
public function remove($key)
{
unset($this->data[$key]);
return $this;
}
public function getKeys()
{
return array_keys($this->data);
}
public function hasKey($key)
{
return array_key_exists($key, $this->data);
}
public function keySearch($key)
{
foreach (array_keys($this->data) as $k) {
if (!strcasecmp($k, $key)) {
return $k;
}
}
return false;
}
public function hasValue($value)
{
return array_search($value, $this->data);
}
public function replace(array $data)
{
$this->data = $data;
return $this;
}
public function merge($data)
{
foreach ($data as $key => $value) {
$this->add($key, $value);
}
return $this;
}
public function overwriteWith($data)
{
if (is_array($data)) {
$this->data = $data + $this->data;
} else {
foreach ($data as $key => $value) {
$this->data[$key] = $value;
}
}
return $this;
}
public function map(\Closure $closure, array $context = [], $static = true)
{
$collection = $static ? new static() : new self();
foreach ($this as $key => $value) {
$collection->add($key, $closure($key, $value, $context));
}
return $collection;
}
public function filter(\Closure $closure, $static = true)
{
$collection = ($static) ? new static() : new self();
foreach ($this->data as $key => $value) {
if ($closure($key, $value)) {
$collection->add($key, $value);
}
}
return $collection;
}
public function offsetExists($offset)
{
return isset($this->data[$offset]);
}
public function offsetGet($offset)
{
return isset($this->data[$offset]) ? $this->data[$offset] : null;
}
public function offsetSet($offset, $value)
{
$this->data[$offset] = $value;
}
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
public function setPath($path, $value)
{
$current =& $this->data;
$queue = explode('/', $path);
while (null !== ($key = array_shift($queue))) {
if (!is_array($current)) {
throw new \RuntimeException("Trying to setPath {$path}, but {$key} is set and is not an array");
} elseif (!$queue) {
$current[$key] = $value;
} elseif (isset($current[$key])) {
$current =& $current[$key];
} else {
$current[$key] = [];
$current =& $current[$key];
}
}
return $this;
}
public function getPath($path, $separator = '/', $data = null)
{
if ($data === null) {
$data =& $this->data;
}
$path = is_array($path) ? $path : explode($separator, $path);
while (null !== ($part = array_shift($path))) {
if (!is_array($data)) {
return null;
} elseif (isset($data[$part])) {
$data =& $data[$part];
} elseif ($part != '*') {
return null;
} else {
// Perform a wildcard search by diverging and merging paths
$result = [];
foreach ($data as $value) {
if (!$path) {
$result = array_merge_recursive($result, (array) $value);
} elseif (null !== ($test = $this->getPath($path, $separator, $value))) {
$result = array_merge_recursive($result, (array) $test);
}
}
return $result;
}
}
return $data;
}
public function __toString()
{
$output = 'Debug output of ';
$output .= 'model';
$output = str_repeat('=', strlen($output)) . "\n" . $output . "\n" . str_repeat('=', strlen($output)) . "\n\n";
$output .= "Model data\n-----------\n\n";
$output .= "This data can be retrieved from the model object using the get() method of the model "
. "(e.g. \$model->get(\$key)) or accessing the model like an associative array (e.g. \$model['key']).\n\n";
$lines = array_slice(explode("\n", trim(print_r($this->toArray(), true))), 2, -1);
$output .= implode("\n", $lines);
return $output . "\n";
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Common;
use Obs\ObsClient;
class ObsTransform implements ITransform {
private static $instance;
private function __construct(){}
public static function getInstance() {
if (!(self::$instance instanceof ObsTransform)) {
self::$instance = new ObsTransform();
}
return self::$instance;
}
public function transform($sign, $para) {
if ($sign === 'aclHeader') {
$para = $this->transAclHeader($para);
} else if ($sign === 'aclUri') {
$para = $this->transAclGroupUri($para);
} else if ($sign == 'event') {
$para = $this->transNotificationEvent($para);
} else if ($sign == 'storageClass') {
$para = $this->transStorageClass($para);
}
return $para;
}
private function transAclHeader($para) {
if ($para === ObsClient::AclAuthenticatedRead || $para === ObsClient::AclBucketOwnerRead ||
$para === ObsClient::AclBucketOwnerFullControl || $para === ObsClient::AclLogDeliveryWrite) {
$para = null;
}
return $para;
}
private function transAclGroupUri($para) {
if ($para === ObsClient::GroupAllUsers) {
$para = ObsClient::AllUsers;
}
return $para;
}
private function transNotificationEvent($para) {
$pos = strpos($para, 's3:');
if ($pos !== false && $pos === 0) {
$para = substr($para, 3);
}
return $para;
}
private function transStorageClass($para) {
$search = array('STANDARD', 'STANDARD_IA', 'GLACIER');
$repalce = array(ObsClient::StorageClassStandard, ObsClient::StorageClassWarm, ObsClient::StorageClassCold);
$para = str_replace($search, $repalce, $para);
return $para;
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Common;
class SchemaFormatter
{
protected static $utcTimeZone;
public static function format($fmt, $value)
{
if($fmt === 'date-time'){
return self::formatDateTime($value);
}
if($fmt === 'data-time-http'){
return self::formatDateTimeHttp($value);
}
if($fmt === 'data-time-middle'){
return self::formatDateTimeMiddle($value);
}
if($fmt === 'date'){
return self::formatDate($value);
}
if($fmt === 'timestamp'){
return self::formatTimestamp($value);
}
if($fmt === 'boolean-string'){
return self::formatBooleanAsString($value);
}
return $value;
}
public static function formatDateTimeMiddle($dateTime)
{
if (is_string($dateTime)) {
$dateTime = new \DateTime($dateTime);
}
if ($dateTime instanceof \DateTime) {
return $dateTime -> format('Y-m-d\T00:00:00\Z');
}
return null;
}
public static function formatDateTime($value)
{
return self::dateFormatter($value, 'Y-m-d\TH:i:s\Z');
}
public static function formatDateTimeHttp($value)
{
return self::dateFormatter($value, 'D, d M Y H:i:s \G\M\T');
}
public static function formatDate($value)
{
return self::dateFormatter($value, 'Y-m-d');
}
public static function formatTime($value)
{
return self::dateFormatter($value, 'H:i:s');
}
public static function formatBooleanAsString($value)
{
return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false';
}
public static function formatTimestamp($value)
{
return (int) self::dateFormatter($value, 'U');
}
private static function dateFormatter($dt, $fmt)
{
if (is_numeric($dt)) {
return gmdate($fmt, (int) $dt);
}
if (is_string($dt)) {
$dt = new \DateTime($dt);
}
if ($dt instanceof \DateTime) {
if (!self::$utcTimeZone) {
self::$utcTimeZone = new \DateTimeZone('UTC');
}
return $dt->setTimezone(self::$utcTimeZone)->format($fmt);
}
return null;
}
}

View File

@@ -0,0 +1,422 @@
<?php
/**
* Copyright (c) 2011-2018 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
namespace Obs\Internal\Common;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LazyOpenStream;
use Psr\Http\Message\RequestInterface;
use GuzzleHttp\Handler\CurlFactoryInterface;
use GuzzleHttp\Handler\EasyHandle;
class SdkCurlFactory implements CurlFactoryInterface
{
private $handles = [];
private $maxHandles;
public function __construct($maxHandles)
{
$this->maxHandles = $maxHandles;
}
public function create(RequestInterface $request, array $options): EasyHandle
{
if (isset($options['curl']['body_as_string'])) {
$options['_body_as_string'] = $options['curl']['body_as_string'];
unset($options['curl']['body_as_string']);
}
$easy = new EasyHandle;
$easy->request = $request;
$easy->options = $options;
$conf = $this->getDefaultConf($easy);
$this->applyMethod($easy, $conf);
$this->applyHandlerOptions($easy, $conf);
$this->applyHeaders($easy, $conf);
unset($conf['_headers']);
if (isset($options['curl'])) {
$conf = array_replace($conf, $options['curl']);
}
$conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
if($this->handles){
$easy->handle = array_pop($this->handles);
}else{
$easy->handle = curl_init();
}
curl_setopt_array($easy->handle, $conf);
return $easy;
}
public function close()
{
if($this->handles){
foreach ($this->handles as $handle){
curl_close($handle);
}
unset($this->handles);
$this->handles = [];
}
}
public function release(EasyHandle $easy): void
{
$resource = $easy->handle;
unset($easy->handle);
if (count($this->handles) >= $this->maxHandles) {
curl_close($resource);
} else {
curl_setopt($resource, CURLOPT_HEADERFUNCTION, null);
curl_setopt($resource, CURLOPT_READFUNCTION, null);
curl_setopt($resource, CURLOPT_WRITEFUNCTION, null);
curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null);
curl_reset($resource);
$this->handles[] = $resource;
}
}
private function getDefaultConf(EasyHandle $easy)
{
$conf = [
'_headers' => $easy->request->getHeaders(),
CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''),
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HEADER => false,
CURLOPT_CONNECTTIMEOUT => 150,
];
if (defined('CURLOPT_PROTOCOLS')) {
$conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
}
$version = $easy->request->getProtocolVersion();
if ($version == 1.1) {
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
} elseif ($version == 2.0) {
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
} else {
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
}
return $conf;
}
private function applyMethod(EasyHandle $easy, array &$conf)
{
$body = $easy->request->getBody();
$size = $body->getSize();
if ($size === null || $size > 0) {
$this->applyBody($easy->request, $easy->options, $conf);
return;
}
$method = $easy->request->getMethod();
if ($method === 'PUT' || $method === 'POST') {
if (!$easy->request->hasHeader('Content-Length')) {
$conf[CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
}
} elseif ($method === 'HEAD') {
$conf[CURLOPT_NOBODY] = true;
unset(
$conf[CURLOPT_WRITEFUNCTION],
$conf[CURLOPT_READFUNCTION],
$conf[CURLOPT_FILE],
$conf[CURLOPT_INFILE]
);
}
}
private function applyBody(RequestInterface $request, array $options, array &$conf)
{
$size = $request->hasHeader('Content-Length')
? (int) $request->getHeaderLine('Content-Length')
: $request->getBody()->getSize();
if($request->getBody()->getSize() === $size && $request -> getBody() ->tell() <= 0){
if (($size !== null && $size < 1000000) ||
!empty($options['_body_as_string'])
) {
$conf[CURLOPT_POSTFIELDS] = (string) $request->getBody();
$this->removeHeader('Content-Length', $conf);
$this->removeHeader('Transfer-Encoding', $conf);
} else {
$conf[CURLOPT_UPLOAD] = true;
if ($size !== null) {
$conf[CURLOPT_INFILESIZE] = $size;
$this->removeHeader('Content-Length', $conf);
}
$body = $request->getBody();
if ($body->isSeekable()) {
$body->rewind();
}
$conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
return $body->read($length);
};
}
}else{
$body = $request->getBody();
$conf[CURLOPT_UPLOAD] = true;
$conf[CURLOPT_INFILESIZE] = $size;
$readCount = 0;
$conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body, $readCount, $size) {
if($readCount >= $size){
$body -> close();
return '';
}
$readCountOnce = $length <= $size ? $length : $size;
$readCount += $readCountOnce;
return $body->read($readCountOnce);
};
}
if (!$request->hasHeader('Expect')) {
$conf[CURLOPT_HTTPHEADER][] = 'Expect:';
}
if (!$request->hasHeader('Content-Type')) {
$conf[CURLOPT_HTTPHEADER][] = 'Content-Type:';
}
}
private function applyHeaders(EasyHandle $easy, array &$conf)
{
foreach ($conf['_headers'] as $name => $values) {
foreach ($values as $value) {
$conf[CURLOPT_HTTPHEADER][] = "$name: $value";
}
}
// Remove the Accept header if one was not set
if (!$easy->request->hasHeader('Accept')) {
$conf[CURLOPT_HTTPHEADER][] = 'Accept:';
}
}
private function removeHeader($name, array &$options)
{
foreach (array_keys($options['_headers']) as $key) {
if (!strcasecmp($key, $name)) {
unset($options['_headers'][$key]);
return;
}
}
}
private function applyHandlerOptions(EasyHandle $easy, array &$conf)
{
$options = $easy->options;
if (isset($options['verify'])) {
$conf[CURLOPT_SSL_VERIFYHOST] = 0;
if ($options['verify'] === false) {
unset($conf[CURLOPT_CAINFO]);
$conf[CURLOPT_SSL_VERIFYPEER] = false;
} else {
$conf[CURLOPT_SSL_VERIFYPEER] = true;
if (is_string($options['verify'])) {
if (!file_exists($options['verify'])) {
throw new \InvalidArgumentException(
"SSL CA bundle not found: {$options['verify']}"
);
}
if (is_dir($options['verify']) ||
(is_link($options['verify']) && is_dir(readlink($options['verify'])))) {
$conf[CURLOPT_CAPATH] = $options['verify'];
} else {
$conf[CURLOPT_CAINFO] = $options['verify'];
}
}
}
}
if (!empty($options['decode_content'])) {
$accept = $easy->request->getHeaderLine('Accept-Encoding');
if ($accept) {
$conf[CURLOPT_ENCODING] = $accept;
} else {
$conf[CURLOPT_ENCODING] = '';
$conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
}
}
if (isset($options['sink'])) {
$sink = $options['sink'];
if (!is_string($sink)) {
$sink = \GuzzleHttp\Psr7\Utils::streamFor($sink);
} elseif (!is_dir(dirname($sink))) {
throw new \RuntimeException(sprintf(
'Directory %s does not exist for sink value of %s',
dirname($sink),
$sink
));
} else {
$sink = new LazyOpenStream($sink, 'w+');
}
$easy->sink = $sink;
$conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) {
return $sink->write($write);
};
} else {
$conf[CURLOPT_FILE] = fopen('php://temp', 'w+');
$easy->sink = \GuzzleHttp\Psr7\Utils::streamFor($conf[CURLOPT_FILE]);
}
$timeoutRequiresNoSignal = false;
if (isset($options['timeout'])) {
$timeoutRequiresNoSignal |= $options['timeout'] < 1;
$conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
}
if (isset($options['force_ip_resolve'])) {
if ('v4' === $options['force_ip_resolve']) {
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
} else if ('v6' === $options['force_ip_resolve']) {
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
}
}
if (isset($options['connect_timeout'])) {
$timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
$conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
}
if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
$conf[CURLOPT_NOSIGNAL] = true;
}
if (isset($options['proxy'])) {
if (!is_array($options['proxy'])) {
$conf[CURLOPT_PROXY] = $options['proxy'];
} else {
$scheme = $easy->request->getUri()->getScheme();
if (isset($options['proxy'][$scheme])) {
$host = $easy->request->getUri()->getHost();
if (!isset($options['proxy']['no']) ||
!\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no'])
) {
$conf[CURLOPT_PROXY] = $options['proxy'][$scheme];
}
}
}
}
if (isset($options['cert'])) {
$cert = $options['cert'];
if (is_array($cert)) {
$conf[CURLOPT_SSLCERTPASSWD] = $cert[1];
$cert = $cert[0];
}
if (!file_exists($cert)) {
throw new \InvalidArgumentException(
"SSL certificate not found: {$cert}"
);
}
$conf[CURLOPT_SSLCERT] = $cert;
}
if (isset($options['ssl_key'])) {
$sslKey = $options['ssl_key'];
if (is_array($sslKey)) {
$conf[CURLOPT_SSLKEYPASSWD] = $sslKey[1];
$sslKey = $sslKey[0];
}
if (!file_exists($sslKey)) {
throw new \InvalidArgumentException(
"SSL private key not found: {$sslKey}"
);
}
$conf[CURLOPT_SSLKEY] = $sslKey;
}
if (isset($options['progress'])) {
$progress = $options['progress'];
if (!is_callable($progress)) {
throw new \InvalidArgumentException(
'progress client option must be callable'
);
}
$conf[CURLOPT_NOPROGRESS] = false;
$conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) {
$args = func_get_args();
if (is_resource($args[0])) {
array_shift($args);
}
call_user_func_array($progress, $args);
};
}
if (!empty($options['debug'])) {
$conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']);
$conf[CURLOPT_VERBOSE] = true;
}
}
private function createHeaderFn(EasyHandle $easy)
{
if (isset($easy->options['on_headers'])) {
$onHeaders = $easy->options['on_headers'];
if (!is_callable($onHeaders)) {
throw new \InvalidArgumentException('on_headers must be callable');
}
} else {
$onHeaders = null;
}
return function ($ch, $h) use (
$onHeaders,
$easy,
&$startingResponse
) {
$value = trim($h);
if ($value === '') {
$startingResponse = true;
$easy->createResponse();
if ($onHeaders !== null) {
try {
$onHeaders($easy->response);
} catch (\Exception $e) {
$easy->onHeadersException = $e;
return -1;
}
}
} elseif ($startingResponse) {
$startingResponse = false;
$easy->headers = [$value];
} else {
$easy->headers[] = $value;
}
return strlen($h);
};
}
}

View File

@@ -0,0 +1,502 @@
<?php
/**
* Copyright (c) 2011-2018 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
namespace Obs\Internal\Common;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7;
use GuzzleHttp\TransferStats;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
class SdkStreamHandler
{
private $lastHeaders = [];
public function __invoke(RequestInterface $request, array $options)
{
if (isset($options['delay'])) {
usleep($options['delay'] * 1000);
}
$startTime = isset($options['on_stats']) ? microtime(true) : null;
try {
$request = $request->withoutHeader('Expect');
if (0 === $request->getBody()->getSize()) {
$request = $request->withHeader('Content-Length', 0);
}
return $this->createResponse(
$request,
$options,
$this->createStream($request, $options),
$startTime
);
} catch (\InvalidArgumentException $e) {
throw $e;
} catch (\Exception $e) {
$message = $e->getMessage();
if (strpos($message, 'getaddrinfo')
|| strpos($message, 'Connection refused')
|| strpos($message, "couldn't connect to host")
) {
$e = new ConnectException($e->getMessage(), $request, $e);
}
$e = RequestException::wrapException($request, $e);
$this->invokeStats($options, $request, $startTime, null, $e);
return \GuzzleHttp\Promise\rejection_for($e);
}
}
private function invokeStats(
array $options,
RequestInterface $request,
$startTime,
ResponseInterface $response = null,
$error = null
) {
if (isset($options['on_stats'])) {
$stats = new TransferStats(
$request,
$response,
microtime(true) - $startTime,
$error,
[]
);
call_user_func($options['on_stats'], $stats);
}
}
private function createResponse(
RequestInterface $request,
array $options,
$stream,
$startTime
) {
$hdrs = $this->lastHeaders;
$this->lastHeaders = [];
$parts = explode(' ', array_shift($hdrs), 3);
$ver = explode('/', $parts[0])[1];
$status = $parts[1];
$reason = isset($parts[2]) ? $parts[2] : null;
$headers = \GuzzleHttp\headers_from_lines($hdrs);
list ($stream, $headers) = $this->checkDecode($options, $headers, $stream);
$stream = \GuzzleHttp\Psr7\Utils::streamFor($stream);
$sink = $stream;
if (strcasecmp('HEAD', $request->getMethod())) {
$sink = $this->createSink($stream, $options);
}
$response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
if (isset($options['on_headers'])) {
try {
$options['on_headers']($response);
} catch (\Exception $e) {
$msg = 'An error was encountered during the on_headers event';
$ex = new RequestException($msg, $request, $response, $e);
return \GuzzleHttp\Promise\rejection_for($ex);
}
}
if ($sink !== $stream) {
$this->drain(
$stream,
$sink,
$response->getHeaderLine('Content-Length')
);
}
$this->invokeStats($options, $request, $startTime, $response, null);
return new FulfilledPromise($response);
}
private function createSink(StreamInterface $stream, array $options)
{
if (!empty($options['stream'])) {
return $stream;
}
$sink = isset($options['sink'])
? $options['sink']
: fopen('php://temp', 'r+');
return is_string($sink)
? new Psr7\LazyOpenStream($sink, 'w+')
: \GuzzleHttp\Psr7\Utils::streamFor($sink);
}
private function checkDecode(array $options, array $headers, $stream)
{
if (!empty($options['decode_content'])) {
$normalizedKeys = \GuzzleHttp\normalize_header_keys($headers);
if (isset($normalizedKeys['content-encoding'])) {
$encoding = $headers[$normalizedKeys['content-encoding']];
if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
$stream = new Psr7\InflateStream(
\GuzzleHttp\Psr7\Utils::streamFor($stream)
);
$headers['x-encoded-content-encoding']
= $headers[$normalizedKeys['content-encoding']];
unset($headers[$normalizedKeys['content-encoding']]);
if (isset($normalizedKeys['content-length'])) {
$headers['x-encoded-content-length']
= $headers[$normalizedKeys['content-length']];
$length = (int) $stream->getSize();
if ($length === 0) {
unset($headers[$normalizedKeys['content-length']]);
} else {
$headers[$normalizedKeys['content-length']] = [$length];
}
}
}
}
}
return [$stream, $headers];
}
private function drain(
StreamInterface $source,
StreamInterface $sink,
$contentLength
) {
\GuzzleHttp\Psr7\Utils::copyToStream(
$source,
$sink,
(strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1
);
$sink->seek(0);
$source->close();
return $sink;
}
private function createResource(callable $callback)
{
$errors = null;
set_error_handler(function ($_, $msg, $file, $line) use (&$errors) {
$errors[] = [
'message' => $msg,
'file' => $file,
'line' => $line
];
return true;
});
$resource = $callback();
restore_error_handler();
if (!$resource) {
$message = 'Error creating resource: ';
foreach ($errors as $err) {
foreach ($err as $key => $value) {
$message .= "[$key] $value" . PHP_EOL;
}
}
throw new \RuntimeException(trim($message));
}
return $resource;
}
private function createStream(RequestInterface $request, array $options)
{
static $methods;
if (!$methods) {
$methods = array_flip(get_class_methods(__CLASS__));
}
if ($request->getProtocolVersion() == '1.1'
&& !$request->hasHeader('Connection')
) {
$request = $request->withHeader('Connection', 'close');
}
if (!isset($options['verify'])) {
$options['verify'] = true;
}
$params = [];
$context = $this->getDefaultContext($request, $options);
if (isset($options['on_headers']) && !is_callable($options['on_headers'])) {
throw new \InvalidArgumentException('on_headers must be callable');
}
if (!empty($options)) {
foreach ($options as $key => $value) {
$method = "add_{$key}";
if (isset($methods[$method])) {
$this->{$method}($request, $context, $value, $params);
}
}
}
if (isset($options['stream_context'])) {
if (!is_array($options['stream_context'])) {
throw new \InvalidArgumentException('stream_context must be an array');
}
$context = array_replace_recursive(
$context,
$options['stream_context']
);
}
if (isset($options['auth'])
&& is_array($options['auth'])
&& isset($options['auth'][2])
&& 'ntlm' == $options['auth'][2]
) {
throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
}
$uri = $this->resolveHost($request, $options);
$context = $this->createResource(
function () use ($context, $params) {
return stream_context_create($context, $params);
}
);
return $this->createResource(
function () use ($uri, &$http_response_header, $context, $options) {
$resource = fopen((string) $uri, 'r', null, $context);
$this->lastHeaders = $http_response_header;
if (isset($options['read_timeout'])) {
$readTimeout = $options['read_timeout'];
$sec = (int) $readTimeout;
$usec = ($readTimeout - $sec) * 100000;
stream_set_timeout($resource, $sec, $usec);
}
return $resource;
}
);
}
private function resolveHost(RequestInterface $request, array $options)
{
$uri = $request->getUri();
if (isset($options['force_ip_resolve']) && !filter_var($uri->getHost(), FILTER_VALIDATE_IP)) {
if ('v4' === $options['force_ip_resolve']) {
$records = dns_get_record($uri->getHost(), DNS_A);
if (!isset($records[0]['ip'])) {
throw new ConnectException(sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
}
$uri = $uri->withHost($records[0]['ip']);
} elseif ('v6' === $options['force_ip_resolve']) {
$records = dns_get_record($uri->getHost(), DNS_AAAA);
if (!isset($records[0]['ipv6'])) {
throw new ConnectException(sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
}
$uri = $uri->withHost('[' . $records[0]['ipv6'] . ']');
}
}
return $uri;
}
private function getDefaultContext(RequestInterface $request)
{
$headers = '';
foreach ($request->getHeaders() as $name => $value) {
foreach ($value as $val) {
$headers .= "$name: $val\r\n";
}
}
$context = [
'http' => [
'method' => $request->getMethod(),
'header' => $headers,
'protocol_version' => $request->getProtocolVersion(),
'ignore_errors' => true,
'follow_location' => 0,
],
];
$body = (string) $request->getBody();
if (!empty($body)) {
$context['http']['content'] = $body;
if (!$request->hasHeader('Content-Type')) {
$context['http']['header'] .= "Content-Type:\r\n";
}
}
$context['http']['header'] = rtrim($context['http']['header']);
return $context;
}
private function add_proxy(RequestInterface $request, &$options, $value, &$params)
{
if (!is_array($value)) {
$options['http']['proxy'] = $value;
} else {
$scheme = $request->getUri()->getScheme();
if (isset($value[$scheme])) {
if (!isset($value['no'])
|| !\GuzzleHttp\is_host_in_noproxy(
$request->getUri()->getHost(),
$value['no']
)
) {
$options['http']['proxy'] = $value[$scheme];
}
}
}
}
private function add_timeout(RequestInterface $request, &$options, $value, &$params)
{
if ($value > 0) {
$options['http']['timeout'] = $value;
}
}
private function add_verify(RequestInterface $request, &$options, $value, &$params)
{
if ($value === true) {
if (PHP_VERSION_ID < 50600) {
$options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle();
}
} elseif (is_string($value)) {
$options['ssl']['cafile'] = $value;
if (!file_exists($value)) {
throw new \RuntimeException("SSL CA bundle not found: $value");
}
} elseif ($value === false) {
$options['ssl']['verify_peer'] = false;
$options['ssl']['verify_peer_name'] = false;
return;
} else {
throw new \InvalidArgumentException('Invalid verify request option');
}
$options['ssl']['verify_peer'] = true;
$options['ssl']['verify_peer_name'] = true;
$options['ssl']['allow_self_signed'] = false;
}
private function add_cert(RequestInterface $request, &$options, $value, &$params)
{
if (is_array($value)) {
$options['ssl']['passphrase'] = $value[1];
$value = $value[0];
}
if (!file_exists($value)) {
throw new \RuntimeException("SSL certificate not found: {$value}");
}
$options['ssl']['local_cert'] = $value;
}
private function add_progress(RequestInterface $request, &$options, $value, &$params)
{
$this->addNotification(
$params,
function ($code, $a, $b, $c, $transferred, $total) use ($value) {
if ($code == STREAM_NOTIFY_PROGRESS) {
$value($total, $transferred, null, null);
}
}
);
}
private function add_debug(RequestInterface $request, &$options, $value, &$params)
{
if ($value === false) {
return;
}
static $map = [
STREAM_NOTIFY_CONNECT => 'CONNECT',
STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT',
STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS',
STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS',
STREAM_NOTIFY_REDIRECTED => 'REDIRECTED',
STREAM_NOTIFY_PROGRESS => 'PROGRESS',
STREAM_NOTIFY_FAILURE => 'FAILURE',
STREAM_NOTIFY_COMPLETED => 'COMPLETED',
STREAM_NOTIFY_RESOLVE => 'RESOLVE',
];
static $args = ['severity', 'message', 'message_code',
'bytes_transferred', 'bytes_max'];
$value = \GuzzleHttp\debug_resource($value);
$ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
$this->addNotification(
$params,
function () use ($ident, $value, $map, $args) {
$passed = func_get_args();
$code = array_shift($passed);
fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
foreach (array_filter($passed) as $i => $v) {
fwrite($value, $args[$i] . ': "' . $v . '" ');
}
fwrite($value, "\n");
}
);
}
private function addNotification(array &$params, callable $notify)
{
if (!isset($params['notification'])) {
$params['notification'] = $notify;
} else {
$params['notification'] = $this->callArray([
$params['notification'],
$notify
]);
}
}
private function callArray(array $functions)
{
return function () use ($functions) {
$args = func_get_args();
foreach ($functions as $fn) {
call_user_func_array($fn, $args);
}
};
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Common;
interface ToArrayInterface
{
public function toArray();
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Common;
use Obs\ObsClient;
use Obs\Internal\Resource\V2Constants;
class V2Transform implements ITransform{
private static $instance;
private function __construct(){}
public static function getInstance() {
if (!(self::$instance instanceof V2Transform)) {
self::$instance = new V2Transform();
}
return self::$instance;
}
public function transform($sign, $para) {
if ($sign === 'storageClass') {
$para = $this->transStorageClass($para);
} else if ($sign === 'aclHeader') {
$para = $this->transAclHeader($para);
} else if ($sign === 'aclUri') {
$para = $this->transAclGroupUri($para);
} else if ($sign == 'event') {
$para = $this->transNotificationEvent($para);
}
return $para;
}
private function transStorageClass($para) {
$search = array(ObsClient::StorageClassStandard, ObsClient::StorageClassWarm, ObsClient::StorageClassCold);
$repalce = array('STANDARD', 'STANDARD_IA', 'GLACIER');
$para = str_replace($search, $repalce, $para);
return $para;
}
private function transAclHeader($para) {
if ($para === ObsClient::AclPublicReadDelivered || $para === ObsClient::AclPublicReadWriteDelivered) {
$para = null;
}
return $para;
}
private function transAclGroupUri($para) {
if ($para === ObsClient::GroupAllUsers) {
$para = V2Constants::GROUP_ALL_USERS_PREFIX . $para;
} else if ($para === ObsClient::GroupAuthenticatedUsers) {
$para = V2Constants::GROUP_AUTHENTICATED_USERS_PREFIX . $para;
} else if ($para === ObsClient::GroupLogDelivery) {
$para = V2Constants::GROUP_LOG_DELIVERY_PREFIX . $para;
} else if ($para === ObsClient::AllUsers) {
$para = V2Constants::GROUP_ALL_USERS_PREFIX . ObsClient::GroupAllUsers;
}
return $para;
}
private function transNotificationEvent($para) {
$pos = strpos($para, 's3:');
if ($pos === false || $pos !== 0) {
$para = 's3:' . $para;
}
return $para;
}
}

View File

@@ -0,0 +1,380 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Exception\RequestException;
use Obs\ObsException;
use Obs\Internal\Common\Model;
use Obs\Internal\Resource\Constants;
use Obs\Log\ObsLog;
use Psr\Http\Message\StreamInterface;
use Obs\Internal\Common\CheckoutStream;
trait GetResponseTrait
{
protected $exceptionResponseMode = true;
protected $chunkSize = 65536;
protected function isClientError(Response $response)
{
return $response -> getStatusCode() >= 400 && $response -> getStatusCode() < 500;
}
protected function parseXmlByType($searchPath, $key, &$value, $xml, $prefix)
{
$type = 'string';
if(isset($value['sentAs'])){
$key = $value['sentAs'];
}
if($searchPath === null){
$searchPath = '//'.$prefix.$key;
}
if(isset($value['type'])){
$type = $value['type'];
if($type === 'array'){
$items = $value['items'];
if(isset($value['wrapper'])){
$paths = explode('/', $searchPath);
$size = count($paths);
if ($size > 1) {
$end = $paths[$size - 1];
$paths[$size - 1] = $value['wrapper'];
$paths[] = $end;
$searchPath = implode('/', $paths) .'/' . $prefix;
}
}
$array = [];
if(!isset($value['data']['xmlFlattened'])){
$pkey = isset($items['sentAs']) ? $items['sentAs'] : $items['name'];
$_searchPath = $searchPath .'/' . $prefix .$pkey;
}else{
$pkey = $key;
$_searchPath = $searchPath;
}
if($result = $xml -> xpath($_searchPath)){
if(is_array($result)){
foreach ($result as $subXml){
$subXml = simplexml_load_string($subXml -> asXML());
$subPrefix = $this->getXpathPrefix($subXml);
$array[] = $this->parseXmlByType('//'.$subPrefix. $pkey, $pkey, $items, $subXml, $subPrefix);
}
}
}
return $array;
}else if($type === 'object'){
$properties = $value['properties'];
$array = [];
foreach ($properties as $pkey => $pvalue){
$name = isset($pvalue['sentAs']) ? $pvalue['sentAs'] : $pkey;
$array[$pkey] = $this->parseXmlByType($searchPath.'/' . $prefix .$name, $name, $pvalue, $xml, $prefix);
}
return $array;
}
}
if($result = $xml -> xpath($searchPath)){
if($type === 'boolean'){
return strval($result[0]) !== 'false';
}else if($type === 'numeric' || $type === 'float'){
return floatval($result[0]);
}else if($type === 'int' || $type === 'integer'){
return intval($result[0]);
}else{
return strval($result[0]);
}
}else{
if($type === 'boolean'){
return false;
}else if($type === 'numeric' || $type === 'float' || $type === 'int' || $type === 'integer'){
return null;
}else{
return '';
}
}
}
private function parseCommonHeaders($model, $response){
$constants = Constants::selectConstants($this -> signature);
foreach($constants::COMMON_HEADERS as $key => $value){
$model[$value] = $response -> getHeaderLine($key);
}
}
protected function parseItems($responseParameters, $model, $response, $body)
{
$prefix = '';
$this->parseCommonHeaders($model, $response);
$closeBody = false;
try{
foreach ($responseParameters as $key => $value){
if(isset($value['location'])){
$location = $value['location'];
if($location === 'header'){
$name = isset($value['sentAs']) ? $value['sentAs'] : $key;
$isSet = false;
if(isset($value['type'])){
$type = $value['type'];
if($type === 'object'){
$headers = $response -> getHeaders();
$temp = [];
foreach ($headers as $headerName => $headerValue){
if(stripos($headerName, $name) === 0){
$metaKey = rawurldecode(substr($headerName, strlen($name)));
$temp[$metaKey] = rawurldecode($response -> getHeaderLine($headerName));
}
}
$model[$key] = $temp;
$isSet = true;
}else{
if($response -> hasHeader($name)){
if($type === 'boolean'){
$model[$key] = ($response -> getHeaderLine($name)) !== 'false';
$isSet = true;
}else if($type === 'numeric' || $type === 'float'){
$model[$key] = floatval($response -> getHeaderLine($name));
$isSet = true;
}else if($type === 'int' || $type === 'integer'){
$model[$key] = intval($response -> getHeaderLine($name));
$isSet = true;
}
}
}
}
if(!$isSet){
$model[$key] = rawurldecode($response -> getHeaderLine($name));
}
}else if($location === 'xml' && $body !== null){
if(!isset($xml) && ($xml = simplexml_load_string($body -> getContents()))){
$prefix = $this ->getXpathPrefix($xml);
}
$closeBody = true;
$model[$key] = $this -> parseXmlByType(null, $key,$value, $xml, $prefix);
}else if($location === 'body' && $body !== null){
if(isset($value['type']) && $value['type'] === 'stream'){
$model[$key] = $body;
}else{
$model[$key] = $body -> getContents();
$closeBody = true;
}
}
}
}
}finally {
if($closeBody && $body !== null){
$body -> close();
}
}
}
private function writeFile($filePath, StreamInterface &$body, $contentLength)
{
$filePath = iconv('UTF-8', 'GBK', $filePath);
if(is_string($filePath) && $filePath !== '')
{
$fp = null;
$dir = dirname($filePath);
try{
if(!is_dir($dir))
{
mkdir($dir,0755,true);
}
if(($fp = fopen($filePath, 'w')))
{
while(!$body->eof())
{
$str = $body->read($this->chunkSize);
fwrite($fp, $str);
}
fflush($fp);
ObsLog::commonLog(DEBUG, "write file %s ok",$filePath);
}
else{
ObsLog::commonLog(ERROR, "open file error,file path:%s",$filePath);
}
}finally{
if($fp){
fclose($fp);
}
$body->close();
$body = null;
}
}
}
private function parseXmlToException($body, $obsException){
try{
$xmlErrorBody = trim($body -> getContents());
if($xmlErrorBody && ($xml = simplexml_load_string($xmlErrorBody))){
$prefix = $this->getXpathPrefix($xml);
if ($tempXml = $xml->xpath('//'.$prefix . 'Code')) {
$obsException-> setExceptionCode(strval($tempXml[0]));
}
if ($tempXml = $xml->xpath('//'.$prefix . 'RequestId')) {
$obsException-> setRequestId(strval($tempXml[0]));
}
if ($tempXml = $xml->xpath('//'.$prefix . 'Message')) {
$obsException-> setExceptionMessage(strval($tempXml[0]));
}
if ($tempXml = $xml->xpath('//'.$prefix . 'HostId')) {
$obsException -> setHostId(strval($tempXml[0]));
}
}
}finally{
$body -> close();
}
}
private function parseXmlToModel($body, $model){
try{
$xmlErrorBody = trim($body -> getContents());
if($xmlErrorBody && ($xml = simplexml_load_string($xmlErrorBody))){
$prefix = $this->getXpathPrefix($xml);
if ($tempXml = $xml->xpath('//'.$prefix . 'Code')) {
$model['Code'] = strval($tempXml[0]);
}
if ($tempXml = $xml->xpath('//'.$prefix . 'RequestId')) {
$model['RequestId'] = strval($tempXml[0]);
}
if ($tempXml = $xml->xpath('//'.$prefix . 'HostId')) {
$model['HostId'] = strval($tempXml[0]);
}
if ($tempXml = $xml->xpath('//'.$prefix . 'Resource')) {
$model['Resource'] = strval($tempXml[0]);
}
if ($tempXml = $xml->xpath('//'.$prefix . 'Message')) {
$model['Message'] = strval($tempXml[0]);
}
}
}finally {
$body -> close();
}
}
protected function parseResponse(Model $model, Request $request, Response $response, array $requestConfig)
{
$statusCode = $response -> getStatusCode();
$expectedLength = $response -> getHeaderLine('content-length');
$expectedLength = is_numeric($expectedLength) ? floatval($expectedLength) : null;
$body = new CheckoutStream($response->getBody(), $expectedLength);
if($statusCode >= 300){
if($this-> exceptionResponseMode){
$obsException= new ObsException();
$obsException-> setRequest($request);
$obsException-> setResponse($response);
$obsException-> setExceptionType($this->isClientError($response) ? 'client' : 'server');
$this->parseXmlToException($body, $obsException);
throw $obsException;
}else{
$this->parseCommonHeaders($model, $response);
$this->parseXmlToModel($body, $model);
}
}else{
if(!empty($model)){
foreach ($model as $key => $value){
if($key === 'method'){
continue;
}
if(isset($value['type']) && $value['type'] === 'file'){
$this->writeFile($value['value'], $body, $expectedLength);
}
$model[$key] = $value['value'];
}
}
if(isset($requestConfig['responseParameters'])){
$responseParameters = $requestConfig['responseParameters'];
if(isset($responseParameters['type']) && $responseParameters['type'] === 'object'){
$responseParameters = $responseParameters['properties'];
}
$this->parseItems($responseParameters, $model, $response, $body);
}
}
$model['HttpStatusCode'] = $statusCode;
$model['Reason'] = $response -> getReasonPhrase();
}
protected function getXpathPrefix($xml)
{
$namespaces = $xml -> getDocNamespaces();
if (isset($namespaces[''])) {
$xml->registerXPathNamespace('ns', $namespaces['']);
$prefix = 'ns:';
} else {
$prefix = '';
}
return $prefix;
}
protected function buildException(Request $request, RequestException $exception, $message)
{
$response = $exception-> hasResponse() ? $exception-> getResponse() : null;
$obsException= new ObsException($message ? $message : $exception-> getMessage());
$obsException-> setExceptionType('client');
$obsException-> setRequest($request);
if($response){
$obsException-> setResponse($response);
$obsException-> setExceptionType($this->isClientError($response) ? 'client' : 'server');
$this->parseXmlToException($response -> getBody(), $obsException);
if ($obsException->getRequestId() === null) {
$prefix = strcasecmp($this->signature, 'obs' ) === 0 ? 'x-obs-' : 'x-amz-';
$requestId = $response->getHeaderLine($prefix . 'request-id');
$obsException->setRequestId($requestId);
}
}
return $obsException;
}
protected function parseExceptionAsync(Request $request, RequestException $exception, $message=null)
{
return $this->buildException($request, $exception, $message);
}
protected function parseException(Model $model, Request $request, RequestException $exception, $message=null)
{
$response = $exception-> hasResponse() ? $exception-> getResponse() : null;
if($this-> exceptionResponseMode){
throw $this->buildException($request, $exception, $message);
}else{
if($response){
$model['HttpStatusCode'] = $response -> getStatusCode();
$model['Reason'] = $response -> getReasonPhrase();
$this->parseXmlToModel($response -> getBody(), $model);
}else{
$model['HttpStatusCode'] = -1;
$model['Message'] = $exception -> getMessage();
}
}
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Resource;
class Constants {
const ALLOWED_RESOURCE_PARAMTER_NAMES = [
'acl',
'policy',
'torrent',
'logging',
'location',
'storageinfo',
'quota',
'storagepolicy',
'requestpayment',
'versions',
'versioning',
'versionid',
'uploads',
'uploadid',
'partnumber',
'website',
'notification',
'lifecycle',
'deletebucket',
'delete',
'cors',
'restore',
'tagging',
'response-content-type',
'response-content-language',
'response-expires',
'response-cache-control',
'response-content-disposition',
'response-content-encoding',
'x-image-process',
'backtosource',
'storageclass',
'replication',
'append',
'position',
'x-oss-process'
];
const ALLOWED_REQUEST_HTTP_HEADER_METADATA_NAMES = [
'content-type',
'content-md5',
'content-length',
'content-language',
'expires',
'origin',
'cache-control',
'content-disposition',
'content-encoding',
'access-control-request-method',
'access-control-request-headers',
'x-default-storage-class',
'location',
'date',
'etag',
'range',
'host',
'if-modified-since',
'if-unmodified-since',
'if-match',
'if-none-match',
'last-modified',
'content-range',
'success-action-redirect'
];
const ALLOWED_RESPONSE_HTTP_HEADER_METADATA_NAMES = [
'content-type',
'content-md5',
'content-length',
'content-language',
'expires',
'origin',
'cache-control',
'content-disposition',
'content-encoding',
'x-default-storage-class',
'location',
'date',
'etag',
'host',
'last-modified',
'content-range',
'x-reserved',
'access-control-allow-origin',
'access-control-allow-headers',
'access-control-max-age',
'access-control-allow-methods',
'access-control-expose-headers',
'connection'
];
public static function selectConstants($signature) {
$signature = (strcasecmp ( $signature, 'obs' ) === 0) ? 'OBS' : 'V2';
return __NAMESPACE__ . '\\' . $signature . 'Constants';
}
public static function selectRequestResource($signature) {
$signature = (strcasecmp ( $signature, 'obs' ) === 0) ? 'OBS' : 'V2';
return (__NAMESPACE__ . '\\' . $signature . 'RequestResource');
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Resource;
class OBSConstants extends Constants {
const FLAG = 'OBS';
const METADATA_PREFIX = 'x-obs-meta-';
const HEADER_PREFIX = 'x-obs-';
const ALTERNATIVE_DATE_HEADER = 'x-obs-date';
const SECURITY_TOKEN_HEAD = 'x-obs-security-token';
const TEMPURL_AK_HEAD = 'AccessKeyId';
const COMMON_HEADERS = [
'content-length' => 'ContentLength',
'date' => 'Date',
'x-obs-request-id' => 'RequestId',
'x-obs-id-2' => 'Id2',
'x-reserved' => 'Reserved'
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Resource;
class V2Constants extends Constants {
const FLAG = 'AWS';
const METADATA_PREFIX = 'x-amz-meta-';
const HEADER_PREFIX = 'x-amz-';
const ALTERNATIVE_DATE_HEADER = 'x-amz-date';
const SECURITY_TOKEN_HEAD = 'x-amz-security-token';
const TEMPURL_AK_HEAD = 'AWSAccessKeyId';
const GROUP_ALL_USERS_PREFIX = 'http://acs.amazonaws.com/groups/global/';
const GROUP_AUTHENTICATED_USERS_PREFIX = 'http://acs.amazonaws.com/groups/global/';
const GROUP_LOG_DELIVERY_PREFIX = 'http://acs.amazonaws.com/groups/s3/';
const COMMON_HEADERS = [
'content-length' => 'ContentLength',
'date' => 'Date',
'x-amz-request-id' => 'RequestId',
'x-amz-id-2' => 'Id2',
'x-reserved' => 'Reserved'
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,718 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal;
use GuzzleHttp\Psr7;
use Obs\Log\ObsLog;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException;
use Obs\Internal\Common\Model;
use Obs\Internal\Resource\V2Constants;
use Obs\ObsException;
use Obs\Internal\Signature\V4Signature;
use Obs\Internal\Signature\DefaultSignature;
use GuzzleHttp\Client;
use Obs\Internal\Resource\Constants;
use Psr\Http\Message\StreamInterface;
use Obs\Internal\Resource\V2RequestResource;
trait SendRequestTrait
{
protected $ak;
protected $sk;
protected $securityToken = false;
protected $endpoint = '';
protected $pathStyle = false;
protected $region = 'region';
protected $signature = 'v2';
protected $sslVerify = false;
protected $maxRetryCount = 3;
protected $timeout = 0;
protected $socketTimeout = 60;
protected $connectTimeout = 60;
protected $isCname = false;
/** @var Client */
protected $httpClient;
public function createSignedUrl(array $args=[]){
if (strcasecmp($this -> signature, 'v4') === 0) {
return $this -> createV4SignedUrl($args);
}
return $this->createCommonSignedUrl($args, $this->signature);
}
public function createV2SignedUrl(array $args=[]) {
return $this->createCommonSignedUrl($args, 'v2');
}
private function createCommonSignedUrl(array $args=[], $signature = '') {
if(!isset($args['Method'])){
$obsException = new ObsException('Method param must be specified, allowed values: GET | PUT | HEAD | POST | DELETE | OPTIONS');
$obsException-> setExceptionType('client');
throw $obsException;
}
$method = strval($args['Method']);
$bucketName = isset($args['Bucket'])? strval($args['Bucket']): null;
$objectKey = isset($args['Key'])? strval($args['Key']): null;
$specialParam = isset($args['SpecialParam'])? strval($args['SpecialParam']): null;
$expires = isset($args['Expires']) && is_numeric($args['Expires']) ? intval($args['Expires']): 300;
$headers = [];
if(isset($args['Headers']) && is_array($args['Headers']) ){
foreach ($args['Headers'] as $key => $val){
if(is_string($key) && $key !== ''){
$headers[$key] = $val;
}
}
}
$queryParams = [];
if(isset($args['QueryParams']) && is_array($args['QueryParams']) ){
foreach ($args['QueryParams'] as $key => $val){
if(is_string($key) && $key !== ''){
$queryParams[$key] = $val;
}
}
}
$constants = Constants::selectConstants($signature);
if($this->securityToken && !isset($queryParams[$constants::SECURITY_TOKEN_HEAD])){
$queryParams[$constants::SECURITY_TOKEN_HEAD] = $this->securityToken;
}
$sign = new DefaultSignature($this->ak, $this->sk, $this->pathStyle, $this->endpoint, $method, $this->signature, $this->securityToken, $this->isCname);
$url = parse_url($this->endpoint);
$host = $url['host'];
$result = '';
if($bucketName){
if($this-> pathStyle){
$result = '/' . $bucketName;
}else{
$host = $this->isCname ? $host : $bucketName . '.' . $host;
}
}
$headers['Host'] = $host;
if($objectKey){
$objectKey = $sign ->urlencodeWithSafe($objectKey);
$result .= '/' . $objectKey;
}
$result .= '?';
if($specialParam){
$queryParams[$specialParam] = '';
}
$queryParams[$constants::TEMPURL_AK_HEAD] = $this->ak;
if(!is_numeric($expires) || $expires < 0){
$expires = 300;
}
$expires = intval($expires) + intval(microtime(true));
$queryParams['Expires'] = strval($expires);
$_queryParams = [];
foreach ($queryParams as $key => $val){
$key = $sign -> urlencodeWithSafe($key);
$val = $sign -> urlencodeWithSafe($val);
$_queryParams[$key] = $val;
$result .= $key;
if($val){
$result .= '=' . $val;
}
$result .= '&';
}
$canonicalstring = $sign ->makeCanonicalstring($method, $headers, $_queryParams, $bucketName, $objectKey, $expires);
$signatureContent = base64_encode(hash_hmac('sha1', $canonicalstring, $this->sk, true));
$result .= 'Signature=' . $sign->urlencodeWithSafe($signatureContent);
$model = new Model();
$model['ActualSignedRequestHeaders'] = $headers;
$model['SignedUrl'] = $url['scheme'] . '://' . $host . ':' . (isset($url['port']) ? $url['port'] : (strtolower($url['scheme']) === 'https' ? '443' : '80')) . $result;
return $model;
}
public function createV4SignedUrl(array $args=[]){
if(!isset($args['Method'])){
$obsException= new ObsException('Method param must be specified, allowed values: GET | PUT | HEAD | POST | DELETE | OPTIONS');
$obsException-> setExceptionType('client');
throw $obsException;
}
$method = strval($args['Method']);
$bucketName = isset($args['Bucket'])? strval($args['Bucket']): null;
$objectKey = isset($args['Key'])? strval($args['Key']): null;
$specialParam = isset($args['SpecialParam'])? strval($args['SpecialParam']): null;
$expires = isset($args['Expires']) && is_numeric($args['Expires']) ? intval($args['Expires']): 300;
$headers = [];
if(isset($args['Headers']) && is_array($args['Headers']) ){
foreach ($args['Headers'] as $key => $val){
if(is_string($key) && $key !== ''){
$headers[$key] = $val;
}
}
}
$queryParams = [];
if(isset($args['QueryParams']) && is_array($args['QueryParams']) ){
foreach ($args['QueryParams'] as $key => $val){
if(is_string($key) && $key !== ''){
$queryParams[$key] = $val;
}
}
}
if($this->securityToken && !isset($queryParams['x-amz-security-token'])){
$queryParams['x-amz-security-token'] = $this->securityToken;
}
$v4 = new V4Signature($this->ak, $this->sk, $this->pathStyle, $this->endpoint, $this->region, $method, $this->signature, $this->securityToken, $this->isCname);
$url = parse_url($this->endpoint);
$host = $url['host'];
$result = '';
if($bucketName){
if($this-> pathStyle){
$result = '/' . $bucketName;
}else{
$host = $this->isCname ? $host : $bucketName . '.' . $host;
}
}
$headers['Host'] = $host;
if($objectKey){
$objectKey = $v4 -> urlencodeWithSafe($objectKey);
$result .= '/' . $objectKey;
}
$result .= '?';
if($specialParam){
$queryParams[$specialParam] = '';
}
if(!is_numeric($expires) || $expires < 0){
$expires = 300;
}
$expires = strval($expires);
$date = isset($headers['date']) ? $headers['date'] : (isset($headers['Date']) ? $headers['Date'] : null);
$timestamp = $date ? date_create_from_format('D, d M Y H:i:s \G\M\T', $date, new \DateTimeZone ('UTC')) -> getTimestamp()
:time();
$longDate = gmdate('Ymd\THis\Z', $timestamp);
$shortDate = substr($longDate, 0, 8);
$headers['host'] = $host;
if(isset($url['port'])){
$port = $url['port'];
if($port !== 443 && $port !== 80){
$headers['host'] = $headers['host'] . ':' . $port;
}
}
$signedHeaders = $v4 -> getSignedHeaders($headers);
$queryParams['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
$queryParams['X-Amz-Credential'] = $v4 -> getCredential($shortDate);
$queryParams['X-Amz-Date'] = $longDate;
$queryParams['X-Amz-Expires'] = $expires;
$queryParams['X-Amz-SignedHeaders'] = $signedHeaders;
$_queryParams = [];
foreach ($queryParams as $key => $val){
$key = rawurlencode($key);
$val = rawurlencode($val);
$_queryParams[$key] = $val;
$result .= $key;
if($val){
$result .= '=' . $val;
}
$result .= '&';
}
$canonicalstring = $v4 -> makeCanonicalstring($method, $headers, $_queryParams, $bucketName, $objectKey, $signedHeaders, 'UNSIGNED-PAYLOAD');
$signatureContent = $v4 -> getSignature($canonicalstring, $longDate, $shortDate);
$result .= 'X-Amz-Signature=' . $v4 -> urlencodeWithSafe($signatureContent);
$model = new Model();
$model['ActualSignedRequestHeaders'] = $headers;
$model['SignedUrl'] = $url['scheme'] . '://' . $host . ':' . (isset($url['port']) ? $url['port'] : (strtolower($url['scheme']) === 'https' ? '443' : '80')) . $result;
return $model;
}
public function createPostSignature(array $args=[]) {
if (strcasecmp($this -> signature, 'v4') === 0) {
return $this -> createV4PostSignature($args);
}
$bucketName = isset($args['Bucket'])? strval($args['Bucket']): null;
$objectKey = isset($args['Key'])? strval($args['Key']): null;
$expires = isset($args['Expires']) && is_numeric($args['Expires']) ? intval($args['Expires']): 300;
$formParams = [];
if(isset($args['FormParams']) && is_array($args['FormParams'])){
foreach ($args['FormParams'] as $key => $val){
$formParams[$key] = $val;
}
}
$constants = Constants::selectConstants($this -> signature);
if($this->securityToken && !isset($formParams[$constants::SECURITY_TOKEN_HEAD])){
$formParams[$constants::SECURITY_TOKEN_HEAD] = $this->securityToken;
}
$timestamp = time();
$expires = gmdate('Y-m-d\TH:i:s\Z', $timestamp + $expires);
if($bucketName){
$formParams['bucket'] = $bucketName;
}
if($objectKey){
$formParams['key'] = $objectKey;
}
$policy = [];
$policy[] = '{"expiration":"';
$policy[] = $expires;
$policy[] = '", "conditions":[';
$matchAnyBucket = true;
$matchAnyKey = true;
$conditionAllowKeys = ['acl', 'bucket', 'key', 'success_action_redirect', 'redirect', 'success_action_status'];
foreach($formParams as $key => $val){
if($key){
$key = strtolower(strval($key));
if($key === 'bucket'){
$matchAnyBucket = false;
}else if($key === 'key'){
$matchAnyKey = false;
}
if(!in_array($key, Constants::ALLOWED_REQUEST_HTTP_HEADER_METADATA_NAMES) && strpos($key, $constants::HEADER_PREFIX) !== 0 && !in_array($key, $conditionAllowKeys)){
$key = $constants::METADATA_PREFIX . $key;
}
$policy[] = '{"';
$policy[] = $key;
$policy[] = '":"';
$policy[] = $val !== null ? strval($val) : '';
$policy[] = '"},';
}
}
if($matchAnyBucket){
$policy[] = '["starts-with", "$bucket", ""],';
}
if($matchAnyKey){
$policy[] = '["starts-with", "$key", ""],';
}
$policy[] = ']}';
$originPolicy = implode('', $policy);
$policy = base64_encode($originPolicy);
$signatureContent = base64_encode(hash_hmac('sha1', $policy, $this->sk, true));
$model = new Model();
$model['OriginPolicy'] = $originPolicy;
$model['Policy'] = $policy;
$model['Signature'] = $signatureContent;
return $model;
}
public function createV4PostSignature(array $args=[]){
$bucketName = isset($args['Bucket'])? strval($args['Bucket']): null;
$objectKey = isset($args['Key'])? strval($args['Key']): null;
$expires = isset($args['Expires']) && is_numeric($args['Expires']) ? intval($args['Expires']): 300;
$formParams = [];
if(isset($args['FormParams']) && is_array($args['FormParams'])){
foreach ($args['FormParams'] as $key => $val){
$formParams[$key] = $val;
}
}
if($this->securityToken && !isset($formParams['x-amz-security-token'])){
$formParams['x-amz-security-token'] = $this->securityToken;
}
$timestamp = time();
$longDate = gmdate('Ymd\THis\Z', $timestamp);
$shortDate = substr($longDate, 0, 8);
$credential = sprintf('%s/%s/%s/s3/aws4_request', $this->ak, $shortDate, $this->region);
$expires = gmdate('Y-m-d\TH:i:s\Z', $timestamp + $expires);
$formParams['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
$formParams['X-Amz-Date'] = $longDate;
$formParams['X-Amz-Credential'] = $credential;
if($bucketName){
$formParams['bucket'] = $bucketName;
}
if($objectKey){
$formParams['key'] = $objectKey;
}
$policy = [];
$policy[] = '{"expiration":"';
$policy[] = $expires;
$policy[] = '", "conditions":[';
$matchAnyBucket = true;
$matchAnyKey = true;
$conditionAllowKeys = ['acl', 'bucket', 'key', 'success_action_redirect', 'redirect', 'success_action_status'];
foreach($formParams as $key => $val){
if($key){
$key = strtolower(strval($key));
if($key === 'bucket'){
$matchAnyBucket = false;
}else if($key === 'key'){
$matchAnyKey = false;
}
if(!in_array($key, Constants::ALLOWED_REQUEST_HTTP_HEADER_METADATA_NAMES) && strpos($key, V2Constants::HEADER_PREFIX) !== 0 && !in_array($key, $conditionAllowKeys)){
$key = V2Constants::METADATA_PREFIX . $key;
}
$policy[] = '{"';
$policy[] = $key;
$policy[] = '":"';
$policy[] = $val !== null ? strval($val) : '';
$policy[] = '"},';
}
}
if($matchAnyBucket){
$policy[] = '["starts-with", "$bucket", ""],';
}
if($matchAnyKey){
$policy[] = '["starts-with", "$key", ""],';
}
$policy[] = ']}';
$originPolicy = implode('', $policy);
$policy = base64_encode($originPolicy);
$dateKey = hash_hmac('sha256', $shortDate, 'AWS4' . $this -> sk, true);
$regionKey = hash_hmac('sha256', $this->region, $dateKey, true);
$serviceKey = hash_hmac('sha256', 's3', $regionKey, true);
$signingKey = hash_hmac('sha256', 'aws4_request', $serviceKey, true);
$signatureContent = hash_hmac('sha256', $policy, $signingKey);
$model = new Model();
$model['OriginPolicy'] = $originPolicy;
$model['Policy'] = $policy;
$model['Algorithm'] = $formParams['X-Amz-Algorithm'];
$model['Credential'] = $formParams['X-Amz-Credential'];
$model['Date'] = $formParams['X-Amz-Date'];
$model['Signature'] = $signatureContent;
return $model;
}
public function __call($originMethod, $args)
{
$method = $originMethod;
$contents = Constants::selectRequestResource($this->signature);
$resource = &$contents::$RESOURCE_ARRAY;
$async = false;
if(strpos($method, 'Async') === (strlen($method) - 5)){
$method = substr($method, 0, strlen($method) - 5);
$async = true;
}
if(isset($resource['aliases'][$method])){
$method = $resource['aliases'][$method];
}
$method = lcfirst($method);
$operation = isset($resource['operations'][$method]) ?
$resource['operations'][$method] : null;
if(!$operation){
ObsLog::commonLog(WARNING, 'unknow method ' . $originMethod);
$obsException= new ObsException('unknow method '. $originMethod);
$obsException-> setExceptionType('client');
throw $obsException;
}
$start = microtime(true);
if(!$async){
ObsLog::commonLog(INFO, 'enter method '. $originMethod. '...');
$model = new Model();
$model['method'] = $method;
$params = empty($args) ? [] : $args[0];
$this->checkMimeType($method, $params);
$this->doRequest($model, $operation, $params);
ObsLog::commonLog(INFO, 'obsclient cost ' . round(microtime(true) - $start, 3) * 1000 . ' ms to execute '. $originMethod);
unset($model['method']);
return $model;
}else{
if(empty($args) || !(is_callable($callback = $args[count($args) -1]))){
ObsLog::commonLog(WARNING, 'async method ' . $originMethod . ' must pass a CallbackInterface as param');
$obsException= new ObsException('async method ' . $originMethod . ' must pass a CallbackInterface as param');
$obsException-> setExceptionType('client');
throw $obsException;
}
ObsLog::commonLog(INFO, 'enter method '. $originMethod. '...');
$params = count($args) === 1 ? [] : $args[0];
$this->checkMimeType($method, $params);
$model = new Model();
$model['method'] = $method;
return $this->doRequestAsync($model, $operation, $params, $callback, $start, $originMethod);
}
}
private function checkMimeType($method, &$params){
// fix bug that guzzlehttp lib will add the content-type if not set
if(($method === 'putObject' || $method === 'initiateMultipartUpload' || $method === 'uploadPart') && (!isset($params['ContentType']) || $params['ContentType'] === null)){
if(isset($params['Key'])){
$params['ContentType'] = \GuzzleHttp\Psr7\MimeType::fromFilename($params['Key']);
}
if((!isset($params['ContentType']) || $params['ContentType'] === null) && isset($params['SourceFile'])){
$params['ContentType'] = \GuzzleHttp\Psr7\MimeType::fromFilename($params['SourceFile']);
}
if(!isset($params['ContentType']) || $params['ContentType'] === null){
$params['ContentType'] = 'binary/octet-stream';
}
}
}
protected function makeRequest($model, &$operation, $params, $endpoint = null)
{
if($endpoint === null){
$endpoint = $this->endpoint;
}
$signatureInterface = strcasecmp($this-> signature, 'v4') === 0 ?
new V4Signature($this->ak, $this->sk, $this->pathStyle, $endpoint, $this->region, $model['method'], $this->signature, $this->securityToken, $this->isCname) :
new DefaultSignature($this->ak, $this->sk, $this->pathStyle, $endpoint, $model['method'], $this->signature, $this->securityToken, $this->isCname);
$authResult = $signatureInterface -> doAuth($operation, $params, $model);
$httpMethod = $authResult['method'];
ObsLog::commonLog(DEBUG, 'perform '. strtolower($httpMethod) . ' request with url ' . $authResult['requestUrl']);
ObsLog::commonLog(DEBUG, 'cannonicalRequest:' . $authResult['cannonicalRequest']);
ObsLog::commonLog(DEBUG, 'request headers ' . var_export($authResult['headers'],true));
$authResult['headers']['User-Agent'] = self::default_user_agent();
if($model['method'] === 'putObject'){
$model['ObjectURL'] = ['value' => $authResult['requestUrl']];
}
return new Request($httpMethod, $authResult['requestUrl'], $authResult['headers'], $authResult['body']);
}
protected function doRequest($model, &$operation, $params, $endpoint = null)
{
$request = $this -> makeRequest($model, $operation, $params, $endpoint);
$this->sendRequest($model, $operation, $params, $request);
}
protected function sendRequest($model, &$operation, $params, $request, $requestCount = 1)
{
$start = microtime(true);
$saveAsStream = false;
if(isset($operation['stream']) && $operation['stream']){
$saveAsStream = isset($params['SaveAsStream']) ? $params['SaveAsStream'] : false;
if(isset($params['SaveAsFile'])){
if($saveAsStream){
$obsException = new ObsException('SaveAsStream cannot be used with SaveAsFile together');
$obsException-> setExceptionType('client');
throw $obsException;
}
$saveAsStream = true;
}
if(isset($params['FilePath'])){
if($saveAsStream){
$obsException = new ObsException('SaveAsStream cannot be used with FilePath together');
$obsException-> setExceptionType('client');
throw $obsException;
}
$saveAsStream = true;
}
if(isset($params['SaveAsFile']) && isset($params['FilePath'])){
$obsException = new ObsException('SaveAsFile cannot be used with FilePath together');
$obsException-> setExceptionType('client');
throw $obsException;
}
}
$promise = $this->httpClient->sendAsync($request, ['stream' => $saveAsStream])->then(
function(Response $response) use ($model, $operation, $params, $request, $requestCount, $start){
ObsLog::commonLog(INFO, 'http request cost ' . round(microtime(true) - $start, 3) * 1000 . ' ms');
$statusCode = $response -> getStatusCode();
$readable = isset($params['Body']) && ($params['Body'] instanceof StreamInterface || is_resource($params['Body']));
if($statusCode >= 300 && $statusCode <400 && $statusCode !== 304 && !$readable && $requestCount <= $this->maxRetryCount){
if($location = $response -> getHeaderLine('location')){
$url = parse_url($this->endpoint);
$newUrl = parse_url($location);
$scheme = (isset($newUrl['scheme']) ? $newUrl['scheme'] : $url['scheme']);
$defaultPort = strtolower($scheme) === 'https' ? '443' : '80';
$this->doRequest($model, $operation, $params, $scheme. '://' . $newUrl['host'] .
':' . (isset($newUrl['port']) ? $newUrl['port'] : $defaultPort));
return;
}
}
$this -> parseResponse($model, $request, $response, $operation);
},
function ($exception) use ($model, $operation, $params, $request, $requestCount, $start) {
ObsLog::commonLog(INFO, 'http request cost ' . round(microtime(true) - $start, 3) * 1000 . ' ms');
$message = null;
if($exception instanceof ConnectException){
if($requestCount <= $this->maxRetryCount){
$this -> sendRequest($model, $operation, $params, $request, $requestCount + 1);
return;
}else{
$message = 'Exceeded retry limitation, max retry count:'. $this->maxRetryCount . ', error message:' . $exception -> getMessage();
}
}
$this -> parseException($model, $request, $exception, $message);
});
$promise -> wait();
}
protected function doRequestAsync($model, &$operation, $params, $callback, $startAsync, $originMethod, $endpoint = null){
$request = $this -> makeRequest($model, $operation, $params, $endpoint);
return $this->sendRequestAsync($model, $operation, $params, $callback, $startAsync, $originMethod, $request);
}
protected function sendRequestAsync($model, &$operation, $params, $callback, $startAsync, $originMethod, $request, $requestCount = 1)
{
$start = microtime(true);
$saveAsStream = false;
if(isset($operation['stream']) && $operation['stream']){
$saveAsStream = isset($params['SaveAsStream']) ? $params['SaveAsStream'] : false;
if($saveAsStream){
if(isset($params['SaveAsFile'])){
$obsException = new ObsException('SaveAsStream cannot be used with SaveAsFile together');
$obsException-> setExceptionType('client');
throw $obsException;
}
if(isset($params['FilePath'])){
$obsException = new ObsException('SaveAsStream cannot be used with FilePath together');
$obsException-> setExceptionType('client');
throw $obsException;
}
}
if(isset($params['SaveAsFile']) && isset($params['FilePath'])){
$obsException = new ObsException('SaveAsFile cannot be used with FilePath together');
$obsException-> setExceptionType('client');
throw $obsException;
}
}
return $this->httpClient->sendAsync($request, ['stream' => $saveAsStream])->then(
function(Response $response) use ($model, $operation, $params, $callback, $startAsync, $originMethod, $request, $start){
ObsLog::commonLog(INFO, 'http request cost ' . round(microtime(true) - $start, 3) * 1000 . ' ms');
$statusCode = $response -> getStatusCode();
$readable = isset($params['Body']) && ($params['Body'] instanceof StreamInterface || is_resource($params['Body']));
if($statusCode === 307 && !$readable){
if($location = $response -> getHeaderLine('location')){
$url = parse_url($this->endpoint);
$newUrl = parse_url($location);
$scheme = (isset($newUrl['scheme']) ? $newUrl['scheme'] : $url['scheme']);
$defaultPort = strtolower($scheme) === 'https' ? '443' : '80';
return $this->doRequestAsync($model, $operation, $params, $callback, $startAsync, $originMethod, $scheme. '://' . $newUrl['host'] .
':' . (isset($newUrl['port']) ? $newUrl['port'] : $defaultPort));
}
}
$this -> parseResponse($model, $request, $response, $operation);
ObsLog::commonLog(INFO, 'obsclient cost ' . round(microtime(true) - $startAsync, 3) * 1000 . ' ms to execute '. $originMethod);
unset($model['method']);
$callback(null, $model);
},
function ($exception) use ($model, $operation, $params, $callback, $startAsync, $originMethod, $request, $start, $requestCount){
ObsLog::commonLog(INFO, 'http request cost ' . round(microtime(true) - $start, 3) * 1000 . ' ms');
$message = null;
if($exception instanceof ConnectException){
if($requestCount <= $this->maxRetryCount){
return $this -> sendRequestAsync($model, $operation, $params, $callback, $startAsync, $originMethod, $request, $requestCount + 1);
}else{
$message = 'Exceeded retry limitation, max retry count:'. $this->maxRetryCount . ', error message:' . $exception -> getMessage();
}
}
$obsException = $this -> parseExceptionAsync($request, $exception, $message);
ObsLog::commonLog(INFO, 'obsclient cost ' . round(microtime(true) - $startAsync, 3) * 1000 . ' ms to execute '. $originMethod);
$callback($obsException, null);
}
);
}
}

View File

@@ -0,0 +1,462 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Signature;
use Obs\Log\ObsLog;
use Obs\Internal\Resource\Constants;
use Obs\ObsException;
use Obs\Internal\Common\SchemaFormatter;
use GuzzleHttp\Psr7\Stream;
use Obs\Internal\Common\Model;
use Psr\Http\Message\StreamInterface;
use Obs\Internal\Common\ObsTransform;
use Obs\Internal\Common\V2Transform;
abstract class AbstractSignature implements SignatureInterface
{
protected $ak;
protected $sk;
protected $pathStyle;
protected $endpoint;
protected $methodName;
protected $securityToken;
protected $signature;
protected $isCname;
public static function urlencodeWithSafe($val, $safe='/'){
if(($len = strlen($val)) === 0){
return '';
}
$buffer = [];
for ($index=0;$index<$len;$index++){
$str = $val[$index];
$buffer[] = !($pos = strpos($safe, $str)) && $pos !== 0 ? rawurlencode($str) : $str;
}
return implode('', $buffer);
}
protected function __construct($ak, $sk, $pathStyle, $endpoint, $methodName, $signature, $securityToken=false, $isCname=false)
{
$this -> ak = $ak;
$this -> sk = $sk;
$this -> pathStyle = $pathStyle;
$this -> endpoint = $endpoint;
$this -> methodName = $methodName;
$this -> signature = $signature;
$this -> securityToken = $securityToken;
$this -> isCname = $isCname;
}
protected function transXmlByType($key, &$value, &$subParams, $transHolder)
{
$xml = [];
$treatAsString = false;
if(isset($value['type'])){
$type = $value['type'];
if($type === 'array'){
$name = isset($value['sentAs']) ? $value['sentAs'] : $key;
$subXml = [];
foreach($subParams as $item){
$temp = $this->transXmlByType($key, $value['items'], $item, $transHolder);
if($temp !== ''){
$subXml[] = $temp;
}
}
if(!empty($subXml)){
if(!isset($value['data']['xmlFlattened'])){
$xml[] = '<' . $name . '>';
$xml[] = implode('', $subXml);
$xml[] = '</' . $name . '>';
}else{
$xml[] = implode('', $subXml);
}
}
}else if($type === 'object'){
$name = isset($value['sentAs']) ? $value['sentAs'] : (isset($value['name']) ? $value['name'] : $key);
$properties = $value['properties'];
$subXml = [];
$attr = [];
foreach ($properties as $pkey => $pvalue){
if(isset($pvalue['required']) && $pvalue['required'] && !isset($subParams[$pkey])){
$obsException= new ObsException('param:' .$pkey. ' is required');
$obsException-> setExceptionType('client');
throw $obsException;
}
if(isset($subParams[$pkey])){
if(isset($pvalue['data']) && isset($pvalue['data']['xmlAttribute']) && $pvalue['data']['xmlAttribute']){
$attrValue = $this->xml_tansfer(trim(strval($subParams[$pkey])));
$attr[$pvalue['sentAs']] = '"' . $attrValue . '"';
if(isset($pvalue['data']['xmlNamespace'])){
$ns = substr($pvalue['sentAs'], 0, strpos($pvalue['sentAs'], ':'));
$attr['xmlns:' . $ns] = '"' . $pvalue['data']['xmlNamespace'] . '"';
}
}else{
$subXml[] = $this -> transXmlByType($pkey, $pvalue, $subParams[$pkey], $transHolder);
}
}
}
$val = implode('', $subXml);
if($val !== ''){
$_name = $name;
if(!empty($attr)){
foreach ($attr as $akey => $avalue){
$_name .= ' ' . $akey . '=' . $avalue;
}
}
if(!isset($value['data']['xmlFlattened'])){
$xml[] = '<' . $_name . '>';
$xml[] = $val;
$xml[] = '</' . $name . '>';
} else {
$xml[] = $val;
}
}
}else{
$treatAsString = true;
}
}else{
$treatAsString = true;
$type = null;
}
if($treatAsString){
if($type === 'boolean'){
if(!is_bool($subParams) && strval($subParams) !== 'false' && strval($subParams) !== 'true'){
$obsException= new ObsException('param:' .$key. ' is not a boolean value');
$obsException-> setExceptionType('client');
throw $obsException;
}
}else if($type === 'numeric'){
if(!is_numeric($subParams)){
$obsException= new ObsException('param:' .$key. ' is not a numeric value');
$obsException-> setExceptionType('client');
throw $obsException;
}
}else if($type === 'float'){
if(!is_float($subParams)){
$obsException= new ObsException('param:' .$key. ' is not a float value');
$obsException-> setExceptionType('client');
throw $obsException;
}
}else if($type === 'int' || $type === 'integer'){
if(!is_int($subParams)){
$obsException= new ObsException('param:' .$key. ' is not a int value');
$obsException-> setExceptionType('client');
throw $obsException;
}
}
$name = isset($value['sentAs']) ? $value['sentAs'] : $key;
if(is_bool($subParams)){
$val = $subParams ? 'true' : 'false';
}else{
$val = strval($subParams);
}
if(isset($value['format'])){
$val = SchemaFormatter::format($value['format'], $val);
}
if (isset($value['transform'])) {
$val = $transHolder->transform($value['transform'], $val);
}
if(isset($val) && $val !== ''){
$val = $this->xml_tansfer($val);
if(!isset($value['data']['xmlFlattened'])){
$xml[] = '<' . $name . '>';
$xml[] = $val;
$xml[] = '</' . $name . '>';
} else {
$xml[] = $val;
}
}else if(isset($value['canEmpty']) && $value['canEmpty']){
$xml[] = '<' . $name . '>';
$xml[] = $val;
$xml[] = '</' . $name . '>';
}
}
$ret = implode('', $xml);
if(isset($value['wrapper'])){
$ret = '<'. $value['wrapper'] . '>' . $ret . '</'. $value['wrapper'] . '>';
}
return $ret;
}
private function xml_tansfer($tag) {
$search = array('&', '<', '>', '\'', '"');
$repalce = array('&amp;', '&lt;', '&gt;', '&apos;', '&quot;');
$transferXml = str_replace($search, $repalce, $tag);
return $transferXml;
}
protected function prepareAuth(array &$requestConfig, array &$params, Model $model)
{
$transHolder = strcasecmp($this-> signature, 'obs') === 0 ? ObsTransform::getInstance() : V2Transform::getInstance();
$method = $requestConfig['httpMethod'];
$requestUrl = $this->endpoint;
$headers = [];
$pathArgs = [];
$dnsParam = null;
$uriParam = null;
$body = [];
$xml = [];
if(isset($requestConfig['specialParam'])){
$pathArgs[$requestConfig['specialParam']] = '';
}
$result = ['body' => null];
$url = parse_url($requestUrl);
$host = $url['host'];
$fileFlag = false;
if(isset($requestConfig['requestParameters'])){
$paramsMetadata = $requestConfig['requestParameters'];
foreach ($paramsMetadata as $key => $value){
if(isset($value['required']) && $value['required'] && !isset($params[$key])){
$obsException= new ObsException('param:' .$key. ' is required');
$obsException-> setExceptionType('client');
throw $obsException;
}
if(isset($params[$key]) && isset($value['location'])){
$location = $value['location'];
$val = $params[$key];
$type = 'string';
if($val !== '' && isset($value['type'])){
$type = $value['type'];
if($type === 'boolean'){
if(!is_bool($val) && strval($val) !== 'false' && strval($val) !== 'true'){
$obsException= new ObsException('param:' .$key. ' is not a boolean value');
$obsException-> setExceptionType('client');
throw $obsException;
}
}else if($type === 'numeric'){
if(!is_numeric($val)){
$obsException= new ObsException('param:' .$key. ' is not a numeric value');
$obsException-> setExceptionType('client');
throw $obsException;
}
}else if($type === 'float'){
if(!is_float($val)){
$obsException= new ObsException('param:' .$key. ' is not a float value');
$obsException-> setExceptionType('client');
throw $obsException;
}
}else if($type === 'int' || $type === 'integer'){
if(!is_int($val)){
$obsException= new ObsException('param:' .$key. ' is not a int value');
$obsException-> setExceptionType('client');
throw $obsException;
}
}
}
if($location === 'header'){
if($type === 'object'){
if(is_array($val)){
$sentAs = strtolower($value['sentAs']);
foreach ($val as $k => $v){
$k = self::urlencodeWithSafe(strtolower($k), ' ;/?:@&=+$,');
$name = strpos($k, $sentAs) === 0 ? $k : $sentAs . $k;
$headers[$name] = self::urlencodeWithSafe($v, ' ;/?:@&=+$,\'*');
}
}
}else if($type === 'array'){
if(is_array($val)){
$name = isset($value['sentAs']) ? $value['sentAs'] : (isset($value['items']['sentAs']) ? $value['items']['sentAs'] : $key);
$temp = [];
foreach ($val as $v){
if(($v = strval($v)) !== ''){
$temp[] = self::urlencodeWithSafe($val, ' ;/?:@&=+$,\'*');
}
}
$headers[$name] = $temp;
}
}else if($type === 'password'){
if(($val = strval($val)) !== ''){
$name = isset($value['sentAs']) ? $value['sentAs'] : $key;
$pwdName = isset($value['pwdSentAs']) ? $value['pwdSentAs'] : $name . '-MD5';
$val1 = base64_encode($val);
$val2 = base64_encode(md5($val, true));
$headers[$name] = $val1;
$headers[$pwdName] = $val2;
}
}else{
if (isset($value['transform'])) {
$val = $transHolder->transform($value['transform'], strval($val));
}
if(isset($val)){
if(is_bool($val)){
$val = $val ? 'true' : 'false';
}else{
$val = strval($val);
}
if($val !== ''){
$name = isset($value['sentAs']) ? $value['sentAs'] : $key;
if(isset($value['format'])){
$val = SchemaFormatter::format($value['format'], $val);
}
$headers[$name] = self::urlencodeWithSafe($val, ' ;/?:@&=+$,\'*');
}
}
}
}else if($location === 'uri' && $uriParam === null){
$uriParam = self::urlencodeWithSafe($val);
}else if($location === 'dns' && $dnsParam === null){
$dnsParam = $val;
}else if($location === 'query'){
$name = isset($value['sentAs']) ? $value['sentAs'] : $key;
if(strval($val) !== ''){
if (strcasecmp ( $this->signature, 'v4' ) === 0) {
$pathArgs[rawurlencode($name)] = rawurlencode(strval($val));
} else {
$pathArgs[self::urlencodeWithSafe($name)] = self::urlencodeWithSafe(strval($val));
}
}
}else if($location === 'xml'){
$val = $this->transXmlByType($key, $value, $val, $transHolder);
if($val !== ''){
$xml[] = $val;
}
}else if($location === 'body'){
if(isset($result['body'])){
$obsException= new ObsException('duplicated body provided');
$obsException-> setExceptionType('client');
throw $obsException;
}
if($type === 'file'){
if(!file_exists($val)){
$obsException= new ObsException('file[' .$val. '] does not exist');
$obsException-> setExceptionType('client');
throw $obsException;
}
$result['body'] = new Stream(fopen($val, 'r'));
$fileFlag = true;
}else if($type === 'stream'){
$result['body'] = $val;
}else{
$result['body'] = strval($val);
}
}else if($location === 'response'){
$model[$key] = ['value' => $val, 'type' => $type];
}
}
}
if($dnsParam){
if($this -> pathStyle){
$requestUrl = $requestUrl . '/' . $dnsParam;
}else{
$defaultPort = strtolower($url['scheme']) === 'https' ? '443' : '80';
$host = $this -> isCname ? $host : $dnsParam. '.' . $host;
$requestUrl = $url['scheme'] . '://' . $host . ':' . (isset($url['port']) ? $url['port'] : $defaultPort);
}
}
if($uriParam){
$requestUrl = $requestUrl . '/' . $uriParam;
}
if(!empty($pathArgs)){
$requestUrl .= '?';
$_pathArgs = [];
foreach ($pathArgs as $key => $value){
$_pathArgs[] = $value === null || $value === '' ? $key : $key . '=' . $value;
}
$requestUrl .= implode('&', $_pathArgs);
}
}
if($xml || (isset($requestConfig['data']['xmlAllowEmpty']) && $requestConfig['data']['xmlAllowEmpty'])){
$body[] = '<';
$xmlRoot = $requestConfig['data']['xmlRoot']['name'];
$body[] = $xmlRoot;
$body[] = '>';
$body[] = implode('', $xml);
$body[] = '</';
$body[] = $xmlRoot;
$body[] = '>';
$headers['Content-Type'] = 'application/xml';
$result['body'] = implode('', $body);
ObsLog::commonLog(DEBUG, 'request content ' . $result['body']);
if(isset($requestConfig['data']['contentMd5']) && $requestConfig['data']['contentMd5']){
$headers['Content-MD5'] = base64_encode(md5($result['body'],true));
}
}
if($fileFlag && ($result['body'] instanceof StreamInterface)){
if($this->methodName === 'uploadPart' && (isset($model['Offset']) || isset($model['PartSize']))){
$bodySize = $result['body'] ->getSize();
if(isset($model['Offset'])){
$offset = intval($model['Offset']['value']);
$offset = $offset >= 0 && $offset < $bodySize ? $offset : 0;
}else{
$offset = 0;
}
if(isset($model['PartSize'])){
$partSize = intval($model['PartSize']['value']);
$partSize = $partSize > 0 && $partSize <= ($bodySize - $offset) ? $partSize : $bodySize - $offset;
}else{
$partSize = $bodySize - $offset;
}
$result['body'] -> rewind();
$result['body'] -> seek($offset);
$headers['Content-Length'] = $partSize;
}else if(isset($headers['Content-Length'])){
$bodySize = $result['body'] -> getSize();
if(intval($headers['Content-Length']) > $bodySize){
$headers['Content-Length'] = $bodySize;
}
}
}
$constants = Constants::selectConstants($this -> signature);
if($this->securityToken){
$headers[$constants::SECURITY_TOKEN_HEAD] = $this->securityToken;
}
$headers['Host'] = $host;
$result['host'] = $host;
$result['method'] = $method;
$result['headers'] = $headers;
$result['pathArgs'] = $pathArgs;
$result['dnsParam'] = $dnsParam;
$result['uriParam'] = $uriParam;
$result['requestUrl'] = $requestUrl;
return $result;
}
}

View File

@@ -0,0 +1,138 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Signature;
use Obs\Internal\Resource\Constants;
use Obs\Internal\Common\Model;
use Obs\Internal\Resource\V2Constants;
class DefaultSignature extends AbstractSignature
{
const INTEREST_HEADER_KEY_LIST = array('content-type', 'content-md5', 'date');
public function __construct($ak, $sk, $pathStyle, $endpoint, $methodName, $signature, $securityToken=false, $isCname=false)
{
parent::__construct($ak, $sk, $pathStyle, $endpoint, $methodName, $signature, $securityToken, $isCname);
}
public function doAuth(array &$requestConfig, array &$params, Model $model)
{
$result = $this -> prepareAuth($requestConfig, $params, $model);
$result['headers']['Date'] = gmdate('D, d M Y H:i:s \G\M\T');
$canonicalstring = $this-> makeCanonicalstring($result['method'], $result['headers'], $result['pathArgs'], $result['dnsParam'], $result['uriParam']);
$result['cannonicalRequest'] = $canonicalstring;
//print_r($result);
$signature = base64_encode(hash_hmac('sha1', $canonicalstring, $this->sk, true));
$constants = Constants::selectConstants($this -> signature);
$signatureFlag = $constants::FLAG;
$authorization = $signatureFlag . ' ' . $this->ak . ':' . $signature;
$result['headers']['Authorization'] = $authorization;
return $result;
}
public function makeCanonicalstring($method, $headers, $pathArgs, $bucketName, $objectKey, $expires = null)
{
$buffer = [];
$buffer[] = $method;
$buffer[] = "\n";
$interestHeaders = [];
$constants = Constants::selectConstants($this -> signature);
foreach ($headers as $key => $value){
$key = strtolower($key);
if(in_array($key, self::INTEREST_HEADER_KEY_LIST) || strpos($key, $constants::HEADER_PREFIX) === 0){
$interestHeaders[$key] = $value;
}
}
if(array_key_exists($constants::ALTERNATIVE_DATE_HEADER, $interestHeaders)){
$interestHeaders['date'] = '';
}
if($expires !== null){
$interestHeaders['date'] = strval($expires);
}
if(!array_key_exists('content-type', $interestHeaders)){
$interestHeaders['content-type'] = '';
}
if(!array_key_exists('content-md5', $interestHeaders)){
$interestHeaders['content-md5'] = '';
}
ksort($interestHeaders);
foreach ($interestHeaders as $key => $value){
if(strpos($key, $constants::HEADER_PREFIX) === 0){
$buffer[] = $key . ':' . $value;
}else{
$buffer[] = $value;
}
$buffer[] = "\n";
}
$uri = '';
$bucketName = $this->isCname ? $headers['Host'] : $bucketName;
if($bucketName){
$uri .= '/';
$uri .= $bucketName;
if(!$this->pathStyle){
$uri .= '/';
}
}
if($objectKey){
if(!($pos=strripos($uri, '/')) || strlen($uri)-1 !== $pos){
$uri .= '/';
}
$uri .= $objectKey;
}
$buffer[] = $uri === ''? '/' : $uri;
if(!empty($pathArgs)){
ksort($pathArgs);
$_pathArgs = [];
foreach ($pathArgs as $key => $value){
if(in_array(strtolower($key), $constants::ALLOWED_RESOURCE_PARAMTER_NAMES) || strpos($key, $constants::HEADER_PREFIX) === 0){
$_pathArgs[] = $value === null || $value === '' ? $key : $key . '=' . urldecode($value);
}
}
if(!empty($_pathArgs)){
$buffer[] = '?';
$buffer[] = implode('&', $_pathArgs);
}
}
return implode('', $buffer);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Signature;
use Obs\Internal\Common\Model;
interface SignatureInterface
{
function doAuth(array &$requestConfig, array &$params, Model $model);
}

View File

@@ -0,0 +1,199 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Internal\Signature;
use Obs\Internal\Common\Model;
class V4Signature extends AbstractSignature
{
const CONTENT_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
protected $region;
protected $utcTimeZone;
public function __construct($ak, $sk, $pathStyle, $endpoint, $region, $methodName, $signature, $securityToken=false, $isCname=false)
{
parent::__construct($ak, $sk, $pathStyle, $endpoint, $methodName, $signature, $securityToken, $isCname);
$this->region = $region;
$this->utcTimeZone = new \DateTimeZone ('UTC');
}
public function doAuth(array &$requestConfig, array &$params, Model $model)
{
$result = $this -> prepareAuth($requestConfig, $params, $model);
$result['headers']['x-amz-content-sha256'] = self::CONTENT_SHA256;
$bucketName = $result['dnsParam'];
$result['headers']['Host'] = $result['host'];
$time = null;
if(array_key_exists('x-amz-date', $result['headers'])){
$time = $result['headers']['x-amz-date'];
}else if(array_key_exists('X-Amz-Date', $result['headers'])){
$time = $result['headers']['X-Amz-Date'];
}
$timestamp = $time ? date_create_from_format('Ymd\THis\Z', $time, $this->utcTimeZone) -> getTimestamp()
:time();
$result['headers']['Date'] = gmdate('D, d M Y H:i:s \G\M\T', $timestamp);
$longDate = gmdate('Ymd\THis\Z', $timestamp);
$shortDate = substr($longDate, 0, 8);
$credential = $this-> getCredential($shortDate);
$signedHeaders = $this->getSignedHeaders($result['headers']);
$canonicalstring = $this-> makeCanonicalstring($result['method'], $result['headers'], $result['pathArgs'], $bucketName, $result['uriParam'], $signedHeaders);
$result['cannonicalRequest'] = $canonicalstring;
$signature = $this -> getSignature($canonicalstring, $longDate, $shortDate);
$authorization = 'AWS4-HMAC-SHA256 ' . 'Credential=' . $credential. ',' . 'SignedHeaders=' . $signedHeaders . ',' . 'Signature=' . $signature;
$result['headers']['Authorization'] = $authorization;
return $result;
}
public function getSignature($canonicalstring, $longDate, $shortDate)
{
$stringToSign = [];
$stringToSign[] = 'AWS4-HMAC-SHA256';
$stringToSign[] = "\n";
$stringToSign[] = $longDate;
$stringToSign[] = "\n";
$stringToSign[] = $this -> getScope($shortDate);
$stringToSign[] = "\n";
$stringToSign[] = hash('sha256', $canonicalstring);
$dateKey = hash_hmac('sha256', $shortDate, 'AWS4' . $this -> sk, true);
$regionKey = hash_hmac('sha256', $this->region, $dateKey, true);
$serviceKey = hash_hmac('sha256', 's3', $regionKey, true);
$signingKey = hash_hmac('sha256', 'aws4_request', $serviceKey, true);
$signature = hash_hmac('sha256', implode('', $stringToSign), $signingKey);
return $signature;
}
public function getCanonicalQueryString($pathArgs)
{
$queryStr = '';
ksort($pathArgs);
$index = 0;
foreach ($pathArgs as $key => $value){
$queryStr .= $key . '=' . $value;
if($index++ !== count($pathArgs) - 1){
$queryStr .= '&';
}
}
return $queryStr;
}
public function getCanonicalHeaders($headers)
{
$_headers = [];
foreach ($headers as $key => $value) {
$_headers[strtolower($key)] = $value;
}
ksort($_headers);
$canonicalHeaderStr = '';
foreach ($_headers as $key => $value){
$value = is_array($value) ? implode(',', $value) : $value;
$canonicalHeaderStr .= $key . ':' . $value;
$canonicalHeaderStr .= "\n";
}
return $canonicalHeaderStr;
}
public function getCanonicalURI($bucketName, $objectKey)
{
$uri = '';
if($this -> pathStyle && $bucketName){
$uri .= '/' . $bucketName;
}
if($objectKey){
$uri .= '/' . $objectKey;
}
if($uri === ''){
$uri = '/';
}
return $uri;
}
public function makeCanonicalstring($method, $headers, $pathArgs, $bucketName, $objectKey, $signedHeaders=null, $payload=null)
{
$buffer = [];
$buffer[] = $method;
$buffer[] = "\n";
$buffer[] = $this->getCanonicalURI($bucketName, $objectKey);
$buffer[] = "\n";
$buffer[] = $this->getCanonicalQueryString($pathArgs);
$buffer[] = "\n";
$buffer[] = $this->getCanonicalHeaders($headers);
$buffer[] = "\n";
$buffer[] = $signedHeaders ? $signedHeaders : $this->getSignedHeaders($headers);
$buffer[] = "\n";
$buffer[] = $payload ? strval($payload) : self::CONTENT_SHA256;
return implode('', $buffer);
}
public function getSignedHeaders($headers)
{
$_headers = [];
foreach ($headers as $key => $value) {
$_headers[] = strtolower($key);
}
sort($_headers);
$signedHeaders = '';
foreach ($_headers as $key => $value){
$signedHeaders .= $value;
if($key !== count($_headers) - 1){
$signedHeaders .= ';';
}
}
return $signedHeaders;
}
public function getScope($shortDate)
{
return $shortDate . '/' . $this->region . '/s3/aws4_request';
}
public function getCredential($shortDate)
{
return $this->ak . '/' . $this->getScope($shortDate);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Log;
class ObsConfig
{
const LOG_FILE_CONFIG = [
'FilePath'=>'./logs',
'FileName'=>'eSDK-OBS-PHP.log',
'MaxFiles'=>10,
'Level'=>INFO
];
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs\Log;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
class ObsLog extends Logger
{
public static $log = null;
protected $log_path = './';
protected $log_name = null;
protected $log_level = Logger::DEBUG;
protected $log_maxFiles = 0;
private $formatter = null;
private $handler = null;
private $filepath = '';
public static function initLog($logConfig= [])
{
$s3log = new ObsLog('');
$s3log->setConfig($logConfig);
$s3log->cheakDir();
$s3log->setFilePath();
$s3log->setFormat();
$s3log->setHande();
}
private function setFormat()
{
$output = '[%datetime%][%level_name%]'.'%message%' . "\n";
$this->formatter = new LineFormatter($output);
}
private function setHande()
{
self::$log = new Logger('obs_logger');
$rotating = new RotatingFileHandler($this->filepath, $this->log_maxFiles, $this->log_level);
$rotating->setFormatter($this->formatter);
self::$log->pushHandler($rotating);
}
private function setConfig($logConfig= [])
{
$arr = empty($logConfig) ? ObsConfig::LOG_FILE_CONFIG : $logConfig;
$this->log_path = iconv('UTF-8', 'GBK',$arr['FilePath']);
$this->log_name = iconv('UTF-8', 'GBK',$arr['FileName']);
$this->log_maxFiles = is_numeric($arr['MaxFiles']) ? 0 : intval($arr['MaxFiles']);
$this->log_level = $arr['Level'];
}
private function cheakDir()
{
if (!is_dir($this->log_path)){
mkdir($this->log_path, 0755, true);
}
}
private function setFilePath()
{
$this->filepath = $this->log_path.'/'.$this->log_name;
}
private static function writeLog($level, $msg)
{
switch ($level) {
case DEBUG:
self::$log->debug($msg);
break;
case INFO:
self::$log->info($msg);
break;
case NOTICE:
self::$log->notice($msg);
break;
case WARNING:
self::$log->warning($msg);
break;
case ERROR:
self::$log->error($msg);
break;
case CRITICAL:
self::$log->critical($msg);
break;
case ALERT:
self::$log->alert($msg);
break;
case EMERGENCY:
self::$log->emergency($msg);
break;
default:
break;
}
}
public static function commonLog($level, $format, $args1 = null, $arg2 = null)
{
if(ObsLog::$log){
if ($args1 === null && $arg2 === null) {
$msg = urldecode($format);
} else {
$msg = sprintf($format, $args1, $arg2);
}
$back = debug_backtrace();
$line = $back[0]['line'];
$funcname = $back[1]['function'];
$filename = basename($back[0]['file']);
$message = '['.$filename.':'.$line.']: '.$msg;
ObsLog::writeLog($level, $message);
}
}
}

View File

@@ -0,0 +1,405 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs;
use Obs\Log\ObsLog;
use Obs\Internal\Common\SdkCurlFactory;
use Obs\Internal\Common\SdkStreamHandler;
use Obs\Internal\Common\Model;
use Monolog\Logger;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\Handler\CurlMultiHandler;
use GuzzleHttp\Handler\Proxy;
use GuzzleHttp\Promise\Promise;
use Obs\Internal\Resource\Constants;
define('DEBUG', Logger::DEBUG);
define('INFO', Logger::INFO);
define('NOTICE', Logger::NOTICE);
define('WARNING', Logger::WARNING);
define('WARN', Logger::WARNING);
define('ERROR', Logger::ERROR);
define('CRITICAL', Logger::CRITICAL);
define('ALERT', Logger::ALERT);
define('EMERGENCY', Logger::EMERGENCY);
/**
* @method Model createPostSignature(array $args=[]);
* @method Model createSignedUrl(array $args=[]);
* @method Model createBucket(array $args = []);
* @method Model listBuckets();
* @method Model deleteBucket(array $args = []);
* @method Model listObjects(array $args = []);
* @method Model listVersions(array $args = []);
* @method Model headBucket(array $args = []);
* @method Model getBucketMetadata(array $args = []);
* @method Model getBucketLocation(array $args = []);
* @method Model getBucketStorageInfo(array $args = []);
* @method Model setBucketQuota(array $args = []);
* @method Model getBucketQuota(array $args = []);
* @method Model setBucketStoragePolicy(array $args = []);
* @method Model getBucketStoragePolicy(array $args = []);
* @method Model setBucketAcl(array $args = []);
* @method Model getBucketAcl(array $args = []);
* @method Model setBucketLogging(array $args = []);
* @method Model getBucketLogging(array $args = []);
* @method Model setBucketPolicy(array $args = []);
* @method Model getBucketPolicy(array $args = []);
* @method Model deleteBucketPolicy(array $args = []);
* @method Model setBucketLifecycle(array $args = []);
* @method Model getBucketLifecycle(array $args = []);
* @method Model deleteBucketLifecycle(array $args = []);
* @method Model setBucketWebsite(array $args = []);
* @method Model getBucketWebsite(array $args = []);
* @method Model deleteBucketWebsite(array $args = []);
* @method Model setBucketVersioning(array $args = []);
* @method Model getBucketVersioning(array $args = []);
* @method Model setBucketCors(array $args = []);
* @method Model getBucketCors(array $args = []);
* @method Model deleteBucketCors(array $args = []);
* @method Model setBucketNotification(array $args = []);
* @method Model getBucketNotification(array $args = []);
* @method Model setBucketTagging(array $args = []);
* @method Model getBucketTagging(array $args = []);
* @method Model deleteBucketTagging(array $args = []);
* @method Model optionsBucket(array $args = []);
*
* @method Model putObject(array $args = []);
* @method Model getObject(array $args = []);
* @method Model copyObject(array $args = []);
* @method Model deleteObject(array $args = []);
* @method Model deleteObjects(array $args = []);
* @method Model getObjectMetadata(array $args = []);
* @method Model setObjectAcl(array $args = []);
* @method Model getObjectAcl(array $args = []);
* @method Model initiateMultipartUpload(array $args = []);
* @method Model uploadPart(array $args = []);
* @method Model copyPart(array $args = []);
* @method Model listParts(array $args = []);
* @method Model completeMultipartUpload(array $args = []);
* @method Model abortMultipartUpload(array $args = []);
* @method Model listMultipartUploads(array $args = []);
* @method Model optionsObject(array $args = []);
* @method Model restoreObject(array $args = []);
*
* @method Promise createBucketAsync(array $args = [], callable $callback);
* @method Promise listBucketsAsync(callable $callback);
* @method Promise deleteBucketAsync(array $args = [], callable $callback);
* @method Promise listObjectsAsync(array $args = [], callable $callback);
* @method Promise listVersionsAsync(array $args = [], callable $callback);
* @method Promise headBucketAsync(array $args = [], callable $callback);
* @method Promise getBucketMetadataAsync(array $args = [], callable $callback);
* @method Promise getBucketLocationAsync(array $args = [], callable $callback);
* @method Promise getBucketStorageInfoAsync(array $args = [], callable $callback);
* @method Promise setBucketQuotaAsync(array $args = [], callable $callback);
* @method Promise getBucketQuotaAsync(array $args = [], callable $callback);
* @method Promise setBucketStoragePolicyAsync(array $args = [], callable $callback);
* @method Promise getBucketStoragePolicyAsync(array $args = [], callable $callback);
* @method Promise setBucketAclAsync(array $args = [], callable $callback);
* @method Promise getBucketAclAsync(array $args = [], callable $callback);
* @method Promise setBucketLoggingAsync(array $args = [], callable $callback);
* @method Promise getBucketLoggingAsync(array $args = [], callable $callback);
* @method Promise setBucketPolicyAsync(array $args = [], callable $callback);
* @method Promise getBucketPolicyAsync(array $args = [], callable $callback);
* @method Promise deleteBucketPolicyAsync(array $args = [], callable $callback);
* @method Promise setBucketLifecycleAsync(array $args = [], callable $callback);
* @method Promise getBucketLifecycleAsync(array $args = [], callable $callback);
* @method Promise deleteBucketLifecycleAsync(array $args = [], callable $callback);
* @method Promise setBucketWebsiteAsync(array $args = [], callable $callback);
* @method Promise getBucketWebsiteAsync(array $args = [], callable $callback);
* @method Promise deleteBucketWebsiteAsync(array $args = [], callable $callback);
* @method Promise setBucketVersioningAsync(array $args = [], callable $callback);
* @method Promise getBucketVersioningAsync(array $args = [], callable $callback);
* @method Promise setBucketCorsAsync(array $args = [], callable $callback);
* @method Promise getBucketCorsAsync(array $args = [], callable $callback);
* @method Promise deleteBucketCorsAsync(array $args = [], callable $callback);
* @method Promise setBucketNotificationAsync(array $args = [], callable $callback);
* @method Promise getBucketNotificationAsync(array $args = [], callable $callback);
* @method Promise setBucketTaggingAsync(array $args = [], callable $callback);
* @method Promise getBucketTaggingAsync(array $args = [], callable $callback);
* @method Promise deleteBucketTaggingAsync(array $args = [], callable $callback);
* @method Promise optionsBucketAsync(array $args = [], callable $callback);
*
* @method Promise putObjectAsync(array $args = [], callable $callback);
* @method Promise getObjectAsync(array $args = [], callable $callback);
* @method Promise copyObjectAsync(array $args = [], callable $callback);
* @method Promise deleteObjectAsync(array $args = [], callable $callback);
* @method Promise deleteObjectsAsync(array $args = [], callable $callback);
* @method Promise getObjectMetadataAsync(array $args = [], callable $callback);
* @method Promise setObjectAclAsync(array $args = [], callable $callback);
* @method Promise getObjectAclAsync(array $args = [], callable $callback);
* @method Promise initiateMultipartUploadAsync(array $args = [], callable $callback);
* @method Promise uploadPartAsync(array $args = [], callable $callback);
* @method Promise copyPartAsync(array $args = [], callable $callback);
* @method Promise listPartsAsync(array $args = [], callable $callback);
* @method Promise completeMultipartUploadAsync(array $args = [], callable $callback);
* @method Promise abortMultipartUploadAsync(array $args = [], callable $callback);
* @method Promise listMultipartUploadsAsync(array $args = [], callable $callback);
* @method Promise optionsObjectAsync(array $args = [], callable $callback);
* @method Promise restoreObjectAsync(array $args = [], callable $callback);
*
*/
class ObsClient
{
const SDK_VERSION = '3.20.5';
const AclPrivate = 'private';
const AclPublicRead = 'public-read';
const AclPublicReadWrite = 'public-read-write';
const AclPublicReadDelivered = 'public-read-delivered';
const AclPublicReadWriteDelivered = 'public-read-write-delivered';
const AclAuthenticatedRead = 'authenticated-read';
const AclBucketOwnerRead = 'bucket-owner-read';
const AclBucketOwnerFullControl = 'bucket-owner-full-control';
const AclLogDeliveryWrite = 'log-delivery-write';
const StorageClassStandard = 'STANDARD';
const StorageClassWarm = 'WARM';
const StorageClassCold = 'COLD';
const PermissionRead = 'READ';
const PermissionWrite = 'WRITE';
const PermissionReadAcp = 'READ_ACP';
const PermissionWriteAcp = 'WRITE_ACP';
const PermissionFullControl = 'FULL_CONTROL';
const AllUsers = 'Everyone';
const GroupAllUsers = 'AllUsers';
const GroupAuthenticatedUsers = 'AuthenticatedUsers';
const GroupLogDelivery = 'LogDelivery';
const RestoreTierExpedited = 'Expedited';
const RestoreTierStandard = 'Standard';
const RestoreTierBulk = 'Bulk';
const GranteeGroup = 'Group';
const GranteeUser = 'CanonicalUser';
const CopyMetadata = 'COPY';
const ReplaceMetadata = 'REPLACE';
const SignatureV2 = 'v2';
const SignatureV4 = 'v4';
const SigantureObs = 'obs';
const ObjectCreatedAll = 'ObjectCreated:*';
const ObjectCreatedPut = 'ObjectCreated:Put';
const ObjectCreatedPost = 'ObjectCreated:Post';
const ObjectCreatedCopy = 'ObjectCreated:Copy';
const ObjectCreatedCompleteMultipartUpload = 'ObjectCreated:CompleteMultipartUpload';
const ObjectRemovedAll = 'ObjectRemoved:*';
const ObjectRemovedDelete = 'ObjectRemoved:Delete';
const ObjectRemovedDeleteMarkerCreated = 'ObjectRemoved:DeleteMarkerCreated';
use Internal\SendRequestTrait;
use Internal\GetResponseTrait;
private $factorys;
public function __construct(array $config = []){
$this ->factorys = [];
$this -> ak = strval($config['key']);
$this -> sk = strval($config['secret']);
if(isset($config['security_token'])){
$this -> securityToken = strval($config['security_token']);
}
if(isset($config['endpoint'])){
$this -> endpoint = trim(strval($config['endpoint']));
}
if($this -> endpoint === ''){
throw new \RuntimeException('endpoint is not set');
}
while($this -> endpoint[strlen($this -> endpoint)-1] === '/'){
$this -> endpoint = substr($this -> endpoint, 0, strlen($this -> endpoint)-1);
}
if(strpos($this-> endpoint, 'http') !== 0){
$this -> endpoint = 'https://' . $this -> endpoint;
}
if(isset($config['signature'])){
$this -> signature = strval($config['signature']);
}
if(isset($config['path_style'])){
$this -> pathStyle = $config['path_style'];
}
if(isset($config['region'])){
$this -> region = strval($config['region']);
}
if(isset($config['ssl_verify'])){
$this -> sslVerify = $config['ssl_verify'];
}else if(isset($config['ssl.certificate_authority'])){
$this -> sslVerify = $config['ssl.certificate_authority'];
}
if(isset($config['max_retry_count'])){
$this -> maxRetryCount = intval($config['max_retry_count']);
}
if(isset($config['timeout'])){
$this -> timeout = intval($config['timeout']);
}
if(isset($config['socket_timeout'])){
$this -> socketTimeout = intval($config['socket_timeout']);
}
if(isset($config['connect_timeout'])){
$this -> connectTimeout = intval($config['connect_timeout']);
}
if(isset($config['chunk_size'])){
$this -> chunkSize = intval($config['chunk_size']);
}
if(isset($config['exception_response_mode'])){
$this -> exceptionResponseMode = $config['exception_response_mode'];
}
if (isset($config['is_cname'])) {
$this -> isCname = $config['is_cname'];
}
$host = parse_url($this -> endpoint)['host'];
if(filter_var($host, FILTER_VALIDATE_IP) !== false) {
$this -> pathStyle = true;
}
$handler = self::choose_handler($this);
$this -> httpClient = new Client(
[
'timeout' => 0,
'read_timeout' => $this -> socketTimeout,
'connect_timeout' => $this -> connectTimeout,
'allow_redirects' => false,
'verify' => $this -> sslVerify,
'expect' => false,
'handler' => HandlerStack::create($handler),
'curl' => [
CURLOPT_BUFFERSIZE => $this -> chunkSize
]
]
);
}
public function __destruct(){
$this-> close();
}
public function refresh($key, $secret, $security_token=false){
$this -> ak = strval($key);
$this -> sk = strval($secret);
if($security_token){
$this -> securityToken = strval($security_token);
}
}
/**
* Get the default User-Agent string to use with Guzzle
*
* @return string
*/
private static function default_user_agent()
{
static $defaultAgent = '';
if (!$defaultAgent) {
$defaultAgent = 'obs-sdk-php/' . self::SDK_VERSION;
}
return $defaultAgent;
}
/**
* Factory method to create a new Obs client using an array of configuration options.
*
* @param array $config Client configuration data
*
* @return ObsClient
*/
public static function factory(array $config = [])
{
return new ObsClient($config);
}
public function close(){
if($this->factorys){
foreach ($this->factorys as $factory){
$factory->close();
}
}
}
public function initLog(array $logConfig= [])
{
ObsLog::initLog($logConfig);
$msg = [];
$msg[] = '[OBS SDK Version=' . self::SDK_VERSION;
$msg[] = 'Endpoint=' . $this->endpoint;
$msg[] = 'Access Mode=' . ($this->pathStyle ? 'Path' : 'Virtual Hosting').']';
ObsLog::commonLog(WARNING, implode("];[", $msg));
}
private static function choose_handler($obsclient)
{
$handler = null;
if (function_exists('curl_multi_exec') && function_exists('curl_exec')) {
$f1 = new SdkCurlFactory(50);
$f2 = new SdkCurlFactory(3);
$obsclient->factorys[] = $f1;
$obsclient->factorys[] = $f2;
$handler = Proxy::wrapSync(new CurlMultiHandler(['handle_factory' => $f1]), new CurlHandler(['handle_factory' => $f2]));
} elseif (function_exists('curl_exec')) {
$f = new SdkCurlFactory(3);
$obsclient->factorys[] = $f;
$handler = new CurlHandler(['handle_factory' => $f]);
} elseif (function_exists('curl_multi_exec')) {
$f = new SdkCurlFactory(50);
$obsclient->factorys[] = $f;
$handler = new CurlMultiHandler(['handle_factory' => $f]);
}
if (ini_get('allow_url_fopen')) {
$handler = $handler
? Proxy::wrapStreaming($handler, new SdkStreamHandler())
: new SdkStreamHandler();
} elseif (!$handler) {
throw new \RuntimeException('GuzzleHttp requires cURL, the '
. 'allow_url_fopen ini setting, or a custom HTTP handler.');
}
return $handler;
}
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* Copyright 2019 Huawei Technologies Co.,Ltd.
* 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.
*
*/
namespace Obs;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Obs\Log\ObsLog;
class ObsException extends \RuntimeException
{
const CLIENT = 'client';
const SERVER = 'server';
private $response;
private $request;
private $requestId;
private $exceptionType;
private $exceptionCode;
private $exceptionMessage;
private $hostId;
public function __construct ($message = null, $code = null, $previous = null)
{
parent::__construct($message, $code, $previous);
}
public function setExceptionCode($exceptionCode)
{
$this->exceptionCode = $exceptionCode;
}
public function getExceptionCode()
{
return $this->exceptionCode;
}
public function setExceptionMessage($exceptionMessage)
{
$this->exceptionMessage = $exceptionMessage;
}
public function getExceptionMessage()
{
return $this->exceptionMessage ? $this->exceptionMessage : $this->message;
}
public function setExceptionType($exceptionType)
{
$this->exceptionType = $exceptionType;
}
public function getExceptionType()
{
return $this->exceptionType;
}
public function setRequestId($requestId)
{
$this->requestId = $requestId;
}
public function getRequestId()
{
return $this->requestId;
}
public function setResponse(Response $response)
{
$this->response = $response;
}
public function getResponse()
{
return $this->response;
}
public function setRequest(Request $request)
{
$this->request = $request;
}
public function getRequest()
{
return $this->request;
}
public function getStatusCode()
{
return $this->response ? $this->response->getStatusCode() : -1;
}
public function setHostId($hostId){
$this->hostId = $hostId;
}
public function getHostId(){
return $this->hostId;
}
public function __toString()
{
$message = get_class($this) . ': '
. 'OBS Error Code: ' . $this->getExceptionCode() . ', '
. 'Status Code: ' . $this->getStatusCode() . ', '
. 'OBS Error Type: ' . $this->getExceptionType() . ', '
. 'OBS Error Message: ' . ($this->getExceptionMessage() ? $this->getExceptionMessage():$this->getMessage());
// Add the User-Agent if available
if ($this->request) {
$message .= ', ' . 'User-Agent: ' . $this->request->getHeaderLine('User-Agent');
}
$message .= "\n";
ObsLog::commonLog(INFO, "http request:status:%d, %s",$this->getStatusCode(),"code:".$this->getExceptionCode().", message:".$this->getMessage());
return $message;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace addons\hwobs\library;
use Obs\Internal\Common\Model;
use Obs\Internal\Signature\DefaultSignature;
class Signer
{
public function __construct()
{
}
/**
* 获取分片文件的签名
* @param string $url
* @param string $uploadId
* @param int $fileSize
* @param int $partSize
* @param string $date
* @return array
*/
public static function getPartsAuthorization($url, $uploadId, $fileSize, $partSize, $date)
{
$config = get_addon_config('hwobs');
$i = 0;
$size_count = $fileSize;
$values = array();
while ($size_count > 0) {
$size_count -= $partSize;
$values[] = array(
$partSize * $i,
($size_count > 0) ? $partSize : ($size_count + $partSize),
);
$i++;
}
$httpMethod = "PUT";
$headers = [
"Host" => str_replace(['http://', 'https://'], '', $config['uploadurl']),
"Content-Length" => 0,
"x-amz-date" => $date,
];
$result = [];
foreach ($values as $index => $value) {
$headers['Content-Length'] = $value[1];
$params = ['partNumber' => $index + 1, 'uploadId' => $uploadId, 'uriParam' => $url, 'dnsParam' => $config['bucket'], 'x-amz-date' => $date];
$model = new Model($params);
$sign = new DefaultSignature($config['accessKey'], $config['secretKey'], false, $config['uploadurl'], $httpMethod, 'v2', false, false);
$requestConfig = [
'httpMethod' => $httpMethod,
'requestParameters' => [
'x-amz-date' => ['location' => 'header'],
'partNumber' => ['location' => 'query'],
'uploadId' => ['location' => 'query'],
'uriParam' => ['location' => 'uri'],
'dnsParam' => ['location' => 'dns'],
]
];
$sig = $sign->doAuth($requestConfig, $params, $model);
$result[] = $sig['headers']['Authorization'];
}
return $result;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace addons\shopro\controller\zy;
use think\Db;
use think\Exception;
use app\admin\model\zy\Stadium;
use think\exception\PDOException;
use addons\shopro\controller\Common;
use think\exception\ValidateException;
class Gym extends Common
{
// protected $noNeedLogin = ['index'];
protected $noNeedLogin = ['*'];
protected $noNeedRight = ['*'];
public function index()
{
$params = $this->request->param();
$model = Stadium::where('status', 1);
if (isset($params['name'])) {
$model->where('name', 'like', '%' . $params['name'] . '%');
}
$res = $model->select();
$this->success('获取成功', $res);
}
public function add()
{
$result = false;
$params = $this->request->param();
Db::startTrans();
try {
$result = (new Stadium)->allowField(true)->save($params);
Db::commit();
} catch (ValidateException | PDOException | Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if ($result === false) {
$this->error(__('No rows were inserted'));
}
$this->success();
}
public function update()
{
$result = false;
$params = $this->request->param();
$model = Stadium::get($params['id']);
if (empty($model)) {
$this->error(__('No rows were found'));
}
Db::startTrans();
try {
$result = $model->allowField(true)->save($params);
Db::commit();
} catch (ValidateException | PDOException | Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if ($result === false) {
$this->error(__('No rows were inserted'));
}
$this->success();
}
public function view()
{
$model = Stadium::get($this->request->param('id'));
if (empty($model)) {
$this->error(__('No rows were found'));
}
$this->success($model);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace addons\shopro\controller\zy;
use app\admin\model\zy\Tags as TagsModel;
use addons\shopro\controller\Common;
class Tags extends Common
{
// protected $noNeedLogin = ['index'];
protected $noNeedLogin = ['*'];
protected $noNeedRight = ['*'];
public function index()
{
$params = $this->request->param();
$model = new TagsModel();
if (isset($params['type'])) {
$model->where('type', $params['type']);
}
if (isset($params['group'])) {
$model->where('group', $params['group']);
}
$res = $model->select();
foreach ($res as &$value) {
$value['content_json'] = array_values(json_decode($value['content_json'] ?? '[]', true));
}
$this->success('获取成功', $res);
}
}

View File

@@ -20,7 +20,7 @@ class GoodsService
protected $is_activity = false; // 是否处理活动 protected $is_activity = false; // 是否处理活动
protected $show_score_shop = false; // 查询积分商城商品 protected $show_score_shop = false; // 查询积分商城商品
public function __construct(\Closure $format = null, \think\db\Query $query = null) public function __construct(?\Closure $format = null, ?\think\db\Query $query = null)
{ {
$this->query = $query ?: new Goods(); $this->query = $query ?: new Goods();
$this->format = $format; $this->format = $format;

View File

@@ -0,0 +1,168 @@
<?php
namespace app\admin\controller\zy;
use app\common\controller\Backend;
use Exception;
use think\Db;
use think\exception\DbException;
use think\exception\PDOException;
use think\exception\ValidateException;
/**
* 球馆
*
* @icon fa fa-circle-o
*/
class Stadium extends Backend
{
/**
* Stadium模型对象
* @var \app\admin\model\zy\Stadium
*/
protected $model = null;
public function _initialize()
{
parent::_initialize();
$this->model = new \app\admin\model\zy\Stadium;
}
/**
* 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
* 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
* 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
*/
/**
* 查看
*/
public function index()
{
//当前是否为关联查询
$this->relationSearch = true;
//设置过滤方法
$this->request->filter(['strip_tags', 'trim']);
if ($this->request->isAjax()) {
//如果发送的来源是Selectpage则转发到Selectpage
if ($this->request->request('keyField')) {
return $this->selectpage();
}
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
$list = $this->model
->with(['user'])
->where($where)
->order($sort, $order)
->paginate($limit);
foreach ($list as $row) {
$row->getRelation('user')->visible(['id', 'username', 'nickname', 'avatar', 'gender']);
}
$result = array("total" => $list->total(), "rows" => $list->items());
return json($result);
}
return $this->view->fetch();
}
/**
* 添加
*
* @return string
* @throws \think\Exception
*/
public function add()
{
if (false === $this->request->isPost()) {
return $this->view->fetch();
}
$params = $this->request->post('row/a');
if (empty($params)) {
$this->error(__('Parameter %s can not be empty', ''));
}
$params = $this->preExcludeFields($params);
$params['position'] = sprintf("POINT(%s %s)", $params['lng'], $params['lat']);
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
$params[$this->dataLimitField] = $this->auth->id;
}
$result = false;
Db::startTrans();
try {
//是否采用模型验证
if ($this->modelValidate) {
$name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
$validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate;
$this->model->validateFailException()->validate($validate);
}
$result = $this->model->allowField(true)->save($params);
Db::commit();
} catch (ValidateException | PDOException | Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if ($result === false) {
$this->error(__('No rows were inserted'));
}
$this->success();
}
/**
* 编辑
*
* @param $ids
* @return string
* @throws DbException
* @throws \think\Exception
*/
public function edit($ids = null)
{
$row = $this->model->get($ids);
if (!$row) {
$this->error(__('No Results were found'));
}
$adminIds = $this->getDataLimitAdminIds();
if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) {
$this->error(__('You have no permission'));
}
if (false === $this->request->isPost()) {
$point = empty($row['position']) ? [0, 0] : explode(' ', substr($row['position'], 6, -1));
$row['lng'] = $point[0];
$row['lat'] = $point[1];
$this->view->assign('row', $row);
return $this->view->fetch();
}
$params = $this->request->post('row/a');
if (empty($params)) {
$this->error(__('Parameter %s can not be empty', ''));
}
$params = $this->preExcludeFields($params);
$params['position'] = sprintf("POINT(%s %s)", $params['lng'], $params['lat']);
$result = false;
Db::startTrans();
try {
//是否采用模型验证
if ($this->modelValidate) {
$name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
$validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
$row->validateFailException()->validate($validate);
}
$result = $row->allowField(true)->save($params);
Db::commit();
} catch (ValidateException | PDOException | Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if (false === $result) {
$this->error(__('No rows were updated'));
}
$this->success();
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace app\admin\controller\zy;
use app\common\controller\Backend;
/**
* 标签
*
* @icon fa fa-circle-o
*/
class Tags extends Backend
{
/**
* Tags模型对象
* @var \app\admin\model\zy\Tags
*/
protected $model = null;
public function _initialize()
{
parent::_initialize();
$this->model = new \app\admin\model\zy\Tags;
}
/**
* 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
* 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
* 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
*/
}

View File

@@ -0,0 +1,25 @@
<?php
return [
'Name' => '球馆名称',
'Sub_name' => '简称',
'Position' => '位置',
'Address' => '地址',
'City' => '城市',
'Contact' => '联系方式',
'Blurb' => '简介',
'Imgs' => '图片',
'Admin_ids' => '管理员',
'Tags' => '标签',
'Status' => '状态',
'Create_time' => '创建时间',
'Update_time' => '修改时间',
'User.id' => 'ID',
'User.username' => '用户名',
'User.nickname' => '昵称',
'User.avatar' => '头像',
'User.gender' => '性别',
'Status0' => '禁用',
'Status1' => '正常',
];

View File

@@ -0,0 +1,21 @@
<?php
return [
'Type' => '标签类型',
'Group' => '标签分组',
'Content_json' => '标签内容',
'Choose' => '选择方式',
'Create_time' => '创建时间',
'Update_time' => '修改时间',
'Single' => '仅单选',
'Multiple' => '可多选',
'General' => '通用标签',
'Ability' => '能力标签',
'Role' => '角色标签',
'Gym' => '球馆标签',
'Club' => '俱乐部标签',
'User' => '用户标签',
];

View File

@@ -0,0 +1,44 @@
<?php
namespace app\admin\model\zy;
use think\Model;
class Stadium extends Model
{
// 表名
protected $table = 'zy_stadium';
// 自动写入时间戳字段
protected $autoWriteTimestamp = false;
// 定义时间戳字段名
protected $createTime = false;
protected $updateTime = false;
protected $deleteTime = false;
// 追加属性
protected $append = [
];
public function user()
{
return $this->belongsTo('app\admin\model\User', 'admin_ids', 'id', [], 'LEFT')->setEagerlyType(0);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace app\admin\model\zy;
use think\Model;
class Tags extends Model
{
// 表名
protected $table = 'zy_tags';
// 自动写入时间戳字段
protected $autoWriteTimestamp = false;
// 定义时间戳字段名
protected $createTime = false;
protected $updateTime = false;
protected $deleteTime = false;
// 追加属性
protected $append = [
];
}

View File

@@ -0,0 +1,27 @@
<?php
namespace app\admin\validate\zy;
use think\Validate;
class Stadium extends Validate
{
/**
* 验证规则
*/
protected $rule = [
];
/**
* 提示消息
*/
protected $message = [
];
/**
* 验证场景
*/
protected $scene = [
'add' => [],
'edit' => [],
];
}

View File

@@ -0,0 +1,27 @@
<?php
namespace app\admin\validate\zy;
use think\Validate;
class Tags extends Validate
{
/**
* 验证规则
*/
protected $rule = [
];
/**
* 提示消息
*/
protected $message = [
];
/**
* 验证场景
*/
protected $scene = [
'add' => [],
'edit' => [],
];
}

View File

@@ -0,0 +1,96 @@
<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Sub_name')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-sub_name" data-rule="required" class="form-control" name="row[sub_name]" type="text" value="">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Position')}:</label>
<div class="col-xs-4 col-sm-3">
<input id="c-lng" data-rule="required" class="form-control" name="row[lng]" type="text" value="" placeholder="经度">
</div>
<div class="col-xs-4 col-sm-3">
<input id="c-lat" data-rule="required" class="form-control" name="row[lat]" type="text" value="" placeholder="经度">
</div>
<div class="col-xs-4 col-sm-2">
<button type="button" class="btn btn-primary" data-toggle='addresspicker'
data-lng-id="c-lng" data-lat-id="c-lat" data-zoom-id="zoom">地图选择</button>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-6 col-sm-2">{:__('Address')}:</label>
<div class="col-xs-6 col-sm-4">
<div class='control-relative'><input id="c-city" class="form-control" data-toggle="city-picker"
name="row[city]" type="text" value="NULL"></div>
</div>
<div class="col-xs-6 col-sm-4">
<input id="c-address" class="form-control" name="row[address]" type="text" value="">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Contact')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-contact" class="form-control" name="row[contact]" type="text" value="">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Blurb')}:</label>
<div class="col-xs-12 col-sm-8">
<textarea id="c-blurb" class="form-control " rows="5" name="row[blurb]" cols="50">NULL</textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Imgs')}:</label>
<div class="col-xs-12 col-sm-8">
<div class="input-group">
<input id="c-imgs" class="form-control" size="50" name="row[imgs]" type="textarea" value="">
<div class="input-group-addon no-border no-padding">
<span><button type="button" id="faupload-imgs" class="btn btn-danger faupload"
data-input-id="c-imgs"
data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp"
data-multiple="true" data-preview-id="p-imgs"><i class="fa fa-upload"></i>
{:__('Upload')}</button></span>
<span><button type="button" id="fachoose-imgs" class="btn btn-primary fachoose"
data-input-id="c-imgs" data-mimetype="image/*" data-multiple="true"><i
class="fa fa-list"></i> {:__('Choose')}</button></span>
</div>
<span class="msg-box n-right" for="c-imgs"></span>
</div>
<ul class="row list-inline faupload-preview" id="p-imgs"></ul>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Admin_ids')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-admin_ids" data-rule="required" data-source="user/user/index" data-field="nickname"
data-multiple="true" class="form-control selectpage" name="row[admin_ids]" type="text" value="">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Tags')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-tags" class="form-control" data-role="tagsinput" name="row[tags]" type="text" value="NULL">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
<div class="col-xs-12 col-sm-8">
{:build_radios('row[status]', ['1'=>__('Status1'), '0'=>__('Status0')],1)}
</div>
</div>
<div class="form-group layer-footer">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,106 @@
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text"
value="{$row.name|htmlentities}">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Sub_name')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-sub_name" data-rule="required" class="form-control" name="row[sub_name]" type="text"
value="{$row.sub_name|htmlentities}">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Position')}:</label>
<div class="col-xs-4 col-sm-3">
<input id="c-lng" data-rule="required" class="form-control" name="row[lng]" type="text"
value="{$row.lng|htmlentities}" placeholder="经度">
</div>
<div class="col-xs-4 col-sm-3">
<input id="c-lat" data-rule="required" class="form-control" name="row[lat]" type="text"
value="{$row.lat|htmlentities}" placeholder="经度">
</div>
<div class="col-xs-4 col-sm-2">
<button type="button" class="btn btn-primary" data-toggle='addresspicker' data-lng-id="c-lng"
data-lat-id="c-lat" data-zoom-id="zoom">地图选择</button>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Address')}:</label>
<div class="col-xs-6 col-sm-4">
<div class='control-relative'><input id="c-city" class="form-control" data-toggle="city-picker"
name="row[city]" type="text" value="{$row.city|htmlentities}"></div>
</div>
<div class="col-xs-6 col-sm-4">
<input id="c-address" class="form-control" name="row[address]" type="text"
value="{$row.address|htmlentities}">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Contact')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-contact" class="form-control" name="row[contact]" type="text"
value="{$row.contact|htmlentities}">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Blurb')}:</label>
<div class="col-xs-12 col-sm-8">
<textarea id="c-blurb" class="form-control " rows="5" name="row[blurb]"
cols="50">{$row.blurb|htmlentities}</textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Imgs')}:</label>
<div class="col-xs-12 col-sm-8">
<div class="input-group">
<input id="c-imgs" class="form-control" size="50" name="row[imgs]" type="textarea"
value="{$row.imgs|htmlentities}">
<div class="input-group-addon no-border no-padding">
<span><button type="button" id="faupload-imgs" class="btn btn-danger faupload"
data-input-id="c-imgs"
data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp"
data-multiple="true" data-preview-id="p-imgs"><i class="fa fa-upload"></i>
{:__('Upload')}</button></span>
<span><button type="button" id="fachoose-imgs" class="btn btn-primary fachoose"
data-input-id="c-imgs" data-mimetype="image/*" data-multiple="true"><i
class="fa fa-list"></i> {:__('Choose')}</button></span>
</div>
<span class="msg-box n-right" for="c-imgs"></span>
</div>
<ul class="row list-inline faupload-preview" id="p-imgs"></ul>
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Admin_ids')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-admin_ids" data-rule="required" data-source="user/user/index" data-field="nickname"
data-multiple="true" class="form-control selectpage" name="row[admin_ids]" type="text"
value="{$row.admin_ids|htmlentities}">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Tags')}:</label>
<div class="col-xs-12 col-sm-8">
<input id="c-tags" class="form-control" data-role="tagsinput" name="row[tags]" type="text"
value="{$row.tags|htmlentities}">
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
<div class="col-xs-12 col-sm-8">
{:build_radios('row[status]', ['1'=>__('Status0'), '0'=>__('Status1')], $row['status'])}
</div>
</div>
<div class="form-group layer-footer">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,29 @@
<div class="panel panel-default panel-intro">
{:build_heading()}
<div class="panel-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in" id="one">
<div class="widget-body no-padding">
<div id="toolbar" class="toolbar">
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('zy/stadium/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('zy/stadium/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('zy/stadium/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
</div>
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
data-operate-edit="{:$auth->check('zy/stadium/edit')}"
data-operate-del="{:$auth->check('zy/stadium/del')}"
width="100%">
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,46 @@
<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
<div class="col-xs-12 col-sm-8">
{:build_radios('row[type]', ['0'=>__('Gym'), '1'=>__('Club'), '2'=>__('User')])}
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Group')}:</label>
<div class="col-xs-12 col-sm-8">
{:build_radios('row[group]', ['0'=>__('General'), '1'=>__('Ability'), '2'=>__('Role')])}
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Choose')}:</label>
<div class="col-xs-12 col-sm-8">
{:build_radios('row[choose]', ['0'=>__('Multiple'), '1'=>__('Single')])}
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Content_json')}:</label>
<div class="col-xs-12 col-sm-8">
<dl class="fieldlist" data-name="row[content_json]">
<dd>
<ins>{:__('Key')}</ins>
<ins>{:__('Value')}</ins>
</dd>
<dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
<textarea name="row[content_json]" class="form-control hide" cols="30" rows="5">''</textarea>
</dl>
</div>
</div>
<div class="form-group layer-footer">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,43 @@
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
<div class="col-xs-12 col-sm-8">
{:build_radios('row[type]', ['0'=>__('Gym'), '1'=>__('Club'), '2'=>__('User')], $row['type'])}
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Group')}:</label>
<div class="col-xs-12 col-sm-8">
{:build_radios('row[group]', ['0'=>__('General'), '1'=>__('Ability'), '2'=>__('Role')], $row['group'])}
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Choose')}:</label>
<div class="col-xs-12 col-sm-8">
{:build_radios('row[choose]', ['0'=>__('Multiple'), '1'=>__('Single')], $row['choose'])}
</div>
</div>
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Content_json')}:</label>
<div class="col-xs-12 col-sm-8">
<dl class="fieldlist" data-name="row[content_json]">
<dd>
<ins>{:__('Key')}</ins>
<ins>{:__('Value')}</ins>
</dd>
<dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
<textarea name="row[content_json]" class="form-control hide" cols="30" rows="5">{$row.content_json|htmlentities}</textarea>
</dl>
</div>
</div>
<div class="form-group layer-footer">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8">
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,29 @@
<div class="panel panel-default panel-intro">
{:build_heading()}
<div class="panel-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in" id="one">
<div class="widget-body no-padding">
<div id="toolbar" class="toolbar">
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('zy/tags/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('zy/tags/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('zy/tags/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
</div>
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
data-operate-edit="{:$auth->check('zy/tags/edit')}"
data-operate-del="{:$auth->check('zy/tags/del')}"
width="100%">
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -21,10 +21,20 @@ return [
'action_begin' => [ 'action_begin' => [
'epay', 'epay',
], ],
'upgrade' => [ 'module_init' => [
'shopro', 'hwobs',
],
'upload_config_init' => [
'hwobs',
],
'upload_delete' => [
'hwobs',
], ],
'app_init' => [ 'app_init' => [
'hwobs',
'shopro',
],
'upgrade' => [
'shopro', 'shopro',
], ],
'config_init' => [ 'config_init' => [

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,215 @@
/*
jQuery autoComplete v1.0.7
Copyright (c) 2014 Simon Steinberger / Pixabay
GitHub: https://github.com/Pixabay/jQuery-autoComplete
License: http://www.opensource.org/licenses/mit-license.php
*/
(function ($) {
$.fn.autoComplete = function (options) {
var o = $.extend({}, $.fn.autoComplete.defaults, options);
// public methods
if (typeof options == 'string') {
this.each(function () {
var that = $(this);
if (options == 'destroy') {
$(window).off('resize.autocomplete', that.updateSC);
that.off('blur.autocomplete focus.autocomplete keydown.autocomplete keyup.autocomplete');
if (that.data('autocomplete'))
that.attr('autocomplete', that.data('autocomplete'));
else
that.removeAttr('autocomplete');
$(that.data('sc')).remove();
that.removeData('sc').removeData('autocomplete');
}
});
return this;
}
return this.each(function () {
var that = $(this);
// sc = 'suggestions container'
that.sc = $('<div class="autocomplete-suggestions ' + o.menuClass + '"></div>');
that.data('sc', that.sc).data('autocomplete', that.attr('autocomplete'));
that.attr('autocomplete', 'off');
that.cache = {};
that.last_val = '';
that.updateSC = function (resize, next) {
that.sc.css({
top: that.offset().top + that.outerHeight() - (that.sc.css("position") == "fixed" ? $(window).scrollTop() : 0),
left: that.offset().left,
width: that.outerWidth()
});
if (!resize) {
that.sc.show();
if (!that.sc.maxHeight) that.sc.maxHeight = parseInt(that.sc.css('max-height'));
if (!that.sc.suggestionHeight) that.sc.suggestionHeight = $('.autocomplete-suggestion', that.sc).first().outerHeight();
if (that.sc.suggestionHeight)
if (!next) that.sc.scrollTop(0);
else {
var scrTop = that.sc.scrollTop(), selTop = next.offset().top - that.sc.offset().top;
if (selTop + that.sc.suggestionHeight - that.sc.maxHeight > 0)
that.sc.scrollTop(selTop + that.sc.suggestionHeight + scrTop - that.sc.maxHeight);
else if (selTop < 0)
that.sc.scrollTop(selTop + scrTop);
}
}
}
$(window).on('resize.autocomplete', that.updateSC);
that.sc.appendTo('body');
that.on('click', function () {
if ($(this).val().length > 0 && that.sc.is(":hidden")) {
setTimeout(function () {
that.sc.show();
}, 100);
}
});
that.sc.on('mouseleave', '.autocomplete-suggestion', function () {
$('.autocomplete-suggestion.selected').removeClass('selected');
});
that.sc.on('mouseenter', '.autocomplete-suggestion', function () {
$('.autocomplete-suggestion.selected').removeClass('selected');
$(this).addClass('selected');
});
that.sc.on('mousedown click', '.autocomplete-suggestion', function (e) {
var item = $(this), v = item.data('val');
if (v || item.hasClass('autocomplete-suggestion')) { // else outside click
that.val(v);
o.onSelect(e, v, item);
that.sc.hide();
}
return false;
});
that.on('blur.autocomplete', function () {
try {
over_sb = $('.autocomplete-suggestions:hover').length;
} catch (e) {
over_sb = 0;
} // IE7 fix :hover
if (!over_sb) {
that.last_val = that.val();
that.sc.hide();
setTimeout(function () {
that.sc.hide();
}, 350); // hide suggestions on fast input
} else if (!that.is(':focus')) setTimeout(function () {
that.focus();
}, 20);
});
if (!o.minChars) that.on('focus.autocomplete', function () {
that.last_val = '\n';
that.trigger('keyup.autocomplete');
});
function suggest(data) {
var val = that.val();
that.cache[val] = data;
if (data.length && val.length >= o.minChars) {
var s = '';
if (data.length > 0) {
s += typeof o.header === 'function' ? o.header.call(data, o, that) : o.header;
for (var i = 0; i < data.length; i++) s += o.renderItem(data[i], val);
s += typeof o.footer === 'function' ? o.footer.call(data, o, that) : o.footer;
}
that.sc.html(s);
that.updateSC(0);
} else
that.sc.hide();
}
that.on('keydown.autocomplete', function (e) {
// down (40), up (38)
if ((e.which == 40 || e.which == 38) && that.sc.html()) {
var next, sel = $('.autocomplete-suggestion.selected', that.sc);
if (!sel.length) {
next = (e.which == 40) ? $('.autocomplete-suggestion', that.sc).first() : $('.autocomplete-suggestion', that.sc).last();
that.val(next.addClass('selected').data('val'));
} else {
next = (e.which == 40) ? sel.next('.autocomplete-suggestion') : sel.prev('.autocomplete-suggestion');
if (next.length) {
sel.removeClass('selected');
that.val(next.addClass('selected').data('val'));
} else {
sel.removeClass('selected');
that.val(that.last_val);
next = 0;
}
}
that.updateSC(0, next);
return false;
}
// esc
else if (e.which == 27) that.val(that.last_val).sc.hide();
// enter or tab
else if (e.which == 13 || e.which == 9) {
var sel = $('.autocomplete-suggestion.selected', that.sc);
if (sel.length && that.sc.is(':visible')) {
o.onSelect(e, sel.data('val'), sel);
setTimeout(function () {
that.sc.hide();
}, 20);
}
}
});
that.on('keyup.autocomplete', function (e) {
if (!~$.inArray(e.which, [13, 27, 35, 36, 37, 38, 39, 40])) {
var val = that.val();
if (val.length >= o.minChars) {
if (val != that.last_val) {
that.last_val = val;
clearTimeout(that.timer);
if (o.cache) {
if (val in that.cache) {
suggest(that.cache[val]);
return;
}
// no requests if previous suggestions were empty
for (var i = 1; i < val.length - o.minChars; i++) {
var part = val.slice(0, val.length - i);
if (part in that.cache && !that.cache[part].length) {
suggest([]);
return;
}
}
}
that.timer = setTimeout(function () {
o.source(val, suggest)
}, o.delay);
}
} else {
that.last_val = val;
that.sc.hide();
}
}
});
});
}
$.fn.autoComplete.defaults = {
source: 0,
minChars: 3,
delay: 150,
cache: 1,
menuClass: '',
header: '',
footer: '',
renderItem: function (item, search) {
// escape special characters
search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
var re = new RegExp("(" + search.split(' ').join('|') + ")", "gi");
return '<div class="autocomplete-suggestion" data-val="' + item + '">' + item.replace(re, "<b>$1</b>") + '</div>';
},
onSelect: function (e, term, item) {
}
};
}(jQuery));

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,319 @@
define([], function () { define([], function () {
require([], function () {
//绑定data-toggle=addresspicker属性点击事件
$(document).on('click', "[data-toggle='addresspicker']", function () {
var that = this;
var callback = $(that).data('callback');
var input_id = $(that).data("input-id") ? $(that).data("input-id") : "";
var lat_id = $(that).data("lat-id") ? $(that).data("lat-id") : "";
var lng_id = $(that).data("lng-id") ? $(that).data("lng-id") : "";
var zoom_id = $(that).data("zoom-id") ? $(that).data("zoom-id") : "";
var lat = lat_id ? $("#" + lat_id).val() : '';
var lng = lng_id ? $("#" + lng_id).val() : '';
var zoom = zoom_id ? $("#" + zoom_id).val() : '';
var url = "/addons/address/index/select";
url += (lat && lng) ? '?lat=' + lat + '&lng=' + lng + (input_id ? "&address=" + $("#" + input_id).val() : "") + (zoom ? "&zoom=" + zoom : "") : '';
Fast.api.open(url, '位置选择', {
callback: function (res) {
input_id && $("#" + input_id).val(res.address).trigger("change");
lat_id && $("#" + lat_id).val(res.lat).trigger("change");
lng_id && $("#" + lng_id).val(res.lng).trigger("change");
zoom_id && $("#" + zoom_id).val(res.zoom).trigger("change");
try {
//执行回调函数
if (typeof callback === 'function') {
callback.call(that, res);
}
} catch (e) {
}
}
});
});
});
if (typeof Config.upload.storage !== 'undefined' && Config.upload.storage === 'hwobs') {
require(['upload'], function (Upload) {
//获取文件MD5值
var getFileMd5 = function (file, cb) {
//如果savekey中未检测到md5则无需获取文件md5直接返回upload的uuid
if (!Config.upload.savekey.match(/\{(file)?md5\}/)) {
cb && cb(file.upload.uuid);
return;
}
require(['../addons/hwobs/js/spark'], function (SparkMD5) {
var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
chunkSize = 10 * 1024 * 1024,
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0,
spark = new SparkMD5.ArrayBuffer(),
fileReader = new FileReader();
fileReader.onload = function (e) {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
cb && cb(spark.end());
}
};
fileReader.onerror = function () {
console.warn('文件读取错误');
};
function loadNext() {
var start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
};
var _onInit = Upload.events.onInit;
//初始化中完成判断
Upload.events.onInit = function () {
_onInit.apply(this, Array.prototype.slice.apply(arguments));
//如果上传接口不是hwobs则不处理
if (this.options.url !== Config.upload.uploadurl) {
return;
}
$.extend(this.options, {
//关闭自动处理队列功能
autoQueue: false,
params: function (files, xhr, chunk) {
var params = $.extend({}, Config.upload.multipart);
if (chunk) {
return $.extend({}, params, {
filesize: chunk.file.size,
filename: chunk.file.name,
chunkid: chunk.file.upload.uuid,
chunkindex: chunk.index,
chunkcount: chunk.file.upload.totalChunkCount,
chunkfilesize: chunk.dataBlock.data.size,
chunksize: this.options.chunkSize,
width: chunk.file.width || 0,
height: chunk.file.height || 0,
type: chunk.file.type,
uploadId: chunk.file.uploadId,
key: chunk.file.key,
});
}
return params;
},
chunkSuccess: function (chunk, file, response) {
var etag = chunk.xhr.getResponseHeader("ETag").replace(/(^")|("$)/g, '');
file.etags = file.etags ? file.etags : [];
file.etags[chunk.index] = etag;
},
chunksUploaded: function (file, done) {
var that = this;
Fast.api.ajax({
url: "/addons/hwobs/index/upload",
data: {
action: 'merge',
filesize: file.size,
filename: file.name,
chunkid: file.upload.uuid,
chunkcount: file.upload.totalChunkCount,
md5: file.md5,
key: file.key,
uploadId: file.uploadId,
etags: file.etags,
category: file.category || '',
hwobstoken: Config.upload.multipart.hwobstoken,
},
}, function (data, ret) {
done(JSON.stringify(ret));
return false;
}, function (data, ret) {
file.accepted = false;
that._errorProcessing([file], ret.msg);
return false;
});
},
});
var _success = this.options.success;
//先移除已有的事件
this.off("success", _success).on("success", function (file, response) {
var ret = {code: 0, msg: response};
try {
if (response) {
ret = typeof response === 'string' ? JSON.parse(response) : response;
}
if (file.xhr.status === 200 || file.xhr.status === 204) {
if (Config.upload.uploadmode === 'client') {
ret = {code: 1, data: {url: '/' + file.key}};
}
if (ret.code == 1) {
var url = ret.data.url || '';
Fast.api.ajax({
url: "/addons/hwobs/index/notify",
data: {name: file.name, url: url, md5: file.md5, size: file.size, width: file.width || 0, height: file.height || 0, type: file.type, category: file.category || '', hwobstoken: Config.upload.multipart.hwobstoken}
}, function () {
return false;
}, function () {
return false;
});
} else {
console.error(ret);
}
} else {
console.error(file.xhr);
}
} catch (e) {
console.error(e);
}
_success.call(this, file, ret);
});
this.on("addedfile", function (file) {
var that = this;
setTimeout(function () {
if (file.status === 'error') {
return;
}
getFileMd5(file, function (md5) {
var chunk = that.options.chunking && file.size > that.options.chunkSize ? 1 : 0;
var params = $(that.element).data("params") || {};
var category = typeof params.category !== 'undefined' ? params.category : ($(that.element).data("category") || '');
category = typeof category === 'function' ? category.call(that, file) : category;
Fast.api.ajax({
url: "/addons/hwobs/index/params",
data: {method: 'POST', category: category, md5: md5, name: file.name, type: file.type, size: file.size, chunk: chunk, chunksize: that.options.chunkSize, hwobstoken: Config.upload.multipart.hwobstoken},
}, function (data) {
file.md5 = md5;
file.id = data.id;
file.key = data.key;
file.date = data.date;
file.uploadId = data.uploadId;
file.policy = data.policy;
file.signature = data.signature;
file.partsAuthorization = data.partsAuthorization;
file.headers = data.headers;
delete data.headers;
file.params = data;
file.category = category;
if (file.status != 'error') {
//开始上传
that.enqueueFile(file);
} else {
that.removeFile(file);
}
return false;
}, function () {
that.removeFile(file);
});
});
}, 0);
});
if (Config.upload.uploadmode === 'client') {
var _method = this.options.method;
var _url = this.options.url;
this.options.method = function (files) {
if (files[0].upload.chunked) {
var chunk = null;
files[0].upload.chunks.forEach(function (item) {
if (item.status === 'uploading') {
chunk = item;
}
});
if (!chunk) {
return "POST";
} else {
return "PUT";
}
} else {
return "POST";
}
return _method;
};
this.options.url = function (files) {
if (files[0].upload.chunked) {
var chunk = null;
files[0].upload.chunks.forEach(function (item) {
if (item.status === 'uploading') {
chunk = item;
}
});
var index = chunk.dataBlock.chunkIndex;
this.options.headers = {"Authorization": files[0]['partsAuthorization'][index], "x-amz-date": files[0]['date']};
if (!chunk) {
return Config.upload.uploadurl + "/" + files[0].key + "?uploadId=" + files[0].uploadId;
} else {
return Config.upload.uploadurl + "/" + files[0].key + "?partNumber=" + (index + 1) + "&uploadId=" + files[0].uploadId;
}
}
return _url;
};
this.options.params = function (files, xhr, chunk) {
var params = Config.upload.multipart;
delete params.category;
if (chunk) {
return $.extend({}, params, {
filesize: chunk.file.size,
filename: chunk.file.name,
chunkid: chunk.file.upload.uuid,
chunkindex: chunk.index,
chunkcount: chunk.file.upload.totalChunkCount,
chunkfilesize: chunk.dataBlock.data.size,
width: chunk.file.width || 0,
height: chunk.file.height || 0,
type: chunk.file.type,
});
} else {
var retParams = $.extend({}, params, files[0].params || {});
delete retParams.hwobstoken;
delete retParams.date;
delete retParams.md5;
if (Config.upload.uploadmode !== 'client') {
params.category = files[0].category || '';
}
return retParams;
}
};
this.on("sending", function (file, xhr, formData) {
var that = this;
var _send = xhr.send;
//仅允许部分字段
var allowFields = ['partNumber', 'uploadId', 'key', 'AccessKeyId', 'policy', 'signature', 'file'];
formData.forEach(function (value, key) {
if (allowFields.indexOf(key) < 0) {
formData.delete(key);
}
});
if (file.upload.chunked) {
xhr.send = function () {
if (file.upload.chunked) {
var chunk = null;
file.upload.chunks.forEach(function (item) {
if (item.status == 'uploading') {
chunk = item;
}
});
_send.call(xhr, chunk.dataBlock.data);
}
};
}
});
}
};
});
}
if (Config.modulename == 'admin' && Config.controllername == 'index' && Config.actionname == 'index') { if (Config.modulename == 'admin' && Config.controllername == 'index' && Config.actionname == 'index') {
require.config({ require.config({
paths: { paths: {

View File

@@ -0,0 +1,61 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
var Controller = {
index: function () {
// 初始化表格参数配置
Table.api.init({
extend: {
index_url: 'zy/stadium/index' + location.search,
add_url: 'zy/stadium/add',
edit_url: 'zy/stadium/edit',
del_url: 'zy/stadium/del',
multi_url: 'zy/stadium/multi',
import_url: 'zy/stadium/import',
table: 'zy_stadium',
}
});
var table = $("#table");
// 初始化表格
table.bootstrapTable({
url: $.fn.bootstrapTable.defaults.extend.index_url,
pk: 'id',
sortName: 'id',
fixedColumns: true,
fixedRightNumber: 1,
columns: [
[
{ checkbox: true },
{ field: 'id', title: __('Id') },
{ field: 'name', title: __('Name'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content },
{ field: 'sub_name', title: __('Sub_name'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content },
{ field: 'position', title: __('Position') },
{ field: 'address', title: __('Address'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content },
{ field: 'city', title: __('City'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content },
{ field: 'contact', title: __('Contact'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content },
{ field: 'status', title: __('Status'), formatter: Table.api.formatter.label, searchList: { 0: __('Status0'), 1: __('Status1') } },
{ field: 'create_time', title: __('Create_time'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false },
{ field: 'update_time', title: __('Update_time'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false },
{ field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate }
]
]
});
// 为表格绑定事件
Table.api.bindevent(table);
},
add: function () {
Controller.api.bindevent();
},
edit: function () {
Controller.api.bindevent();
},
api: {
bindevent: function () {
Form.api.bindevent($("form[role=form]"));
}
}
};
return Controller;
});

View File

@@ -0,0 +1,56 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
var Controller = {
index: function () {
// 初始化表格参数配置
Table.api.init({
extend: {
index_url: 'zy/tags/index' + location.search,
add_url: 'zy/tags/add',
edit_url: 'zy/tags/edit',
del_url: 'zy/tags/del',
multi_url: 'zy/tags/multi',
import_url: 'zy/tags/import',
table: 'zy_tags',
}
});
var table = $("#table");
// 初始化表格
table.bootstrapTable({
url: $.fn.bootstrapTable.defaults.extend.index_url,
pk: 'id',
sortName: 'id',
columns: [
[
{ checkbox: true },
{ field: 'id', title: __('Id') },
{ field: 'type', title: __('Type'), formatter: Table.api.formatter.label, searchList: { 0: __('Gym'), 1: __('Club'), 2: __('User') } },
{ field: 'group', title: __('Group'), formatter: Table.api.formatter.label, searchList: { 0: __('General'), 1: __('Ability'), 2: __('Role') } },
{ field: 'choose', title: __('Choose'), formatter: Table.api.formatter.label, searchList: { 0: __('Multiple'), 1: __('Single') } },
{ field: 'content_json', title: __('Content_json') },
{ field: 'create_time', title: __('Create_time'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false },
{ field: 'update_time', title: __('Update_time'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false },
{ field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate }
]
]
});
// 为表格绑定事件
Table.api.bindevent(table);
},
add: function () {
Controller.api.bindevent();
},
edit: function () {
Controller.api.bindevent();
},
api: {
bindevent: function () {
Form.api.bindevent($("form[role=form]"));
}
}
};
return Controller;
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff