创建版本
This commit is contained in:
commit
af555f49c3
|
@ -0,0 +1,75 @@
|
|||
APP_NAME=gdoo
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=laravel
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailhog
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS=null
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
# 即时通讯
|
||||
REALTIME_KEY=fsmDyhOBxjUo8Nksd0EAlyJWT4jtfSAO
|
||||
REALTIME_URL=ws://192.168.0.2:6002/realtime
|
||||
REALTIME_API=http://192.168.0.2:6002/api
|
||||
|
||||
# 二次验证开关
|
||||
AUTH_TOTP_STATUS=false
|
||||
|
||||
# 微信推送
|
||||
WECHAT_MESSAGE_PUSH_STATUS=false
|
||||
|
||||
# 外部接口
|
||||
PLUGIN_SYNC_API_STATUS=false
|
||||
PLUGIN_SYNC_API_URL=
|
||||
|
||||
# PRINCE转换
|
||||
PRINCE_DIR=E:/develop/Prince/engine/bin/prince.exe
|
||||
|
||||
# 添加ag-grid授权
|
||||
AGGRID_LICENSE=
|
||||
|
||||
# 演示版本
|
||||
DEMO_VERSION=false
|
||||
|
||||
DEMO_DATA="agGrid.LicenseManager.prototype.validateLicense = function() {}"
|
|
@ -0,0 +1,19 @@
|
|||
/node_modules
|
||||
/public/workflow
|
||||
/public/storage
|
||||
/public/deploy.php
|
||||
/vendor
|
||||
/.idea
|
||||
/.vagrant
|
||||
/.vscode
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
/src
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
_ide_helper.php
|
||||
.phpstorm.meta.php
|
||||
.env
|
||||
/nbproject/private/
|
|
@ -0,0 +1,20 @@
|
|||
# 良讯办公系统 - 免费开源
|
||||
|
||||
### 项目介绍
|
||||
良讯办公是给企业定制开发的,基于laravel 8.x框架开发。因为是企业定制的原因,目前很多模块还需要完善。
|
||||
##### 演示地址: http://demo.gdoo.net 帐号:<code>admin</code>, 密码:<code>123456</code>
|
||||
##### 交流QQ群: 79446405
|
||||
|
||||
### 软件架构
|
||||
基于PHP框架Laravel 8.x + MySQL 8.x
|
||||
|
||||
### 安装教程
|
||||
1. 上传压缩包到目录,这里推荐使用宝塔面板,安装php-8.x、mysql-8.x、redis、nginx
|
||||
2. 然后使用<code>composer install --no-dev</code>安装依赖
|
||||
3. 如果要修改前端文件请执行<code>yarn install</code>安装依赖
|
||||
4. 最后导入<code>database/gdoo-2.2.sql</code>
|
||||
5. 然后执行<code>php artisan key:generate</code>
|
||||
6. 修改.env相关配置
|
||||
|
||||
### 使用说明
|
||||
1. 等待添加
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* The Artisan commands provided by your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<?php namespace App\Exceptions;
|
||||
|
||||
class AbortException extends \Exception {}
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use ErrorException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dontReport = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed for validation exceptions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $exception
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function render($request, Throwable $e)
|
||||
{
|
||||
// 自定义错误
|
||||
if ($e instanceof AbortException) {
|
||||
$code = $e->getCode();
|
||||
$msg = $e->getMessage();
|
||||
$result = [
|
||||
'success' => false,
|
||||
'status' => false,
|
||||
'code' => $code,
|
||||
'data' => $msg,
|
||||
'msg' => $msg,
|
||||
];
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json($result);
|
||||
} else {
|
||||
return response()->view('errors.abort', $result);
|
||||
}
|
||||
} else {
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,368 @@
|
|||
<?php namespace Gdoo\Approach\Controllers;
|
||||
|
||||
use DB;
|
||||
use Request;
|
||||
use Validator;
|
||||
|
||||
use Gdoo\Approach\Models\Approach;
|
||||
use Gdoo\Model\Grid;
|
||||
use Gdoo\Model\Form;
|
||||
|
||||
use Gdoo\Index\Controllers\WorkflowController;
|
||||
|
||||
class ApproachController extends WorkflowController
|
||||
{
|
||||
public $permission = ['dialog', 'reference', 'useCount', 'serviceReview', 'serviceCostList', 'serviceCostDetail', 'product'];
|
||||
|
||||
public function indexAction()
|
||||
{
|
||||
// 客户权限
|
||||
$region = regionCustomer('customer_id_customer');
|
||||
|
||||
$header = Grid::header([
|
||||
'code' => 'approach',
|
||||
'referer' => 1,
|
||||
'search' => ['by' => ''],
|
||||
]);
|
||||
|
||||
$cols = $header['cols'];
|
||||
|
||||
$cols['actions']['options'] = [[
|
||||
'name' => '显示',
|
||||
'action' => 'show',
|
||||
'display' => $this->access['show'],
|
||||
]];
|
||||
|
||||
$cols['master_product']['cellRenderer'] = 'htmlCellRenderer';
|
||||
$cols['master_cash_amount']['cellRenderer'] = 'htmlCellRenderer';
|
||||
|
||||
$search = $header['search_form'];
|
||||
$query = $search['query'];
|
||||
|
||||
if (Request::method() == 'POST') {
|
||||
|
||||
$model = DB::table($header['table'])->setBy($header);
|
||||
foreach ($header['join'] as $join) {
|
||||
$model->leftJoin($join[0], $join[1], $join[2], $join[3]);
|
||||
}
|
||||
|
||||
$model->leftJoin(DB::raw('(SELECT sum(verification_cost) as master_cash_amount, min(date) as master_cash_date, apply_id FROM approach_review where status = 1 group by apply_id) as b'), 'approach.id', '=', 'b.apply_id');
|
||||
$header['select'][] = 'b.master_cash_amount';
|
||||
$header['select'][] = 'b.master_cash_date';
|
||||
|
||||
$model->orderBy($header['sort'], $header['order']);
|
||||
|
||||
foreach ($search['where'] as $where) {
|
||||
if ($where['active']) {
|
||||
$model->search($where);
|
||||
}
|
||||
}
|
||||
|
||||
if ($region['authorise']) {
|
||||
foreach ($region['whereIn'] as $key => $where) {
|
||||
$model->whereIn($key, $where);
|
||||
}
|
||||
}
|
||||
|
||||
$model->select($header['select']);
|
||||
$rows = $model->paginate($query['limit'])->appends($query);
|
||||
|
||||
$items = Grid::dataFilters($rows, $header, function($item) {
|
||||
$item['master_cash_amount'] = '<a href="javascript:;" data-toggle="event" data-action="fee_detail" data-master_id="'.$item['master_id'].'" class="option">'.$item['master_cash_amount'].'</a>';
|
||||
$item['master_product'] = '<a href="javascript:;" data-toggle="event" data-action="product" data-master_id="'.$item['master_id'].'" class="option">明细</a>';
|
||||
return $item;
|
||||
});
|
||||
return $items->toJson();
|
||||
}
|
||||
|
||||
$header['buttons'] = [
|
||||
// ['name' => '删除', 'icon' => 'fa-remove', 'action' => 'delete', 'display' => $this->access['delete']],
|
||||
['name' => '导出', 'icon' => 'fa-share', 'action' => 'export', 'display' => 1],
|
||||
];
|
||||
|
||||
$header['left_buttons'] = [
|
||||
['name' => '批量编辑', 'color' => 'default', 'icon' => 'fa-pencil-square-o', 'action' => 'batchEdit', 'display' => $this->access['batchEdit']],
|
||||
];
|
||||
|
||||
$header['cols'] = $cols;
|
||||
$header['tabs'] = Approach::$tabs;
|
||||
$header['bys'] = Approach::$bys;
|
||||
$header['js'] = Grid::js($header);
|
||||
|
||||
return $this->display([
|
||||
'header' => $header,
|
||||
]);
|
||||
}
|
||||
|
||||
// 新建促销
|
||||
public function createAction($action = 'edit')
|
||||
{
|
||||
$id = (int) Request::get('id');
|
||||
|
||||
// 客户权限
|
||||
$header['region'] = ['field' => 'customer_id'];
|
||||
$header['authorise'] = ['action' => 'index', 'field' => 'created_id'];
|
||||
|
||||
$header['action'] = $action;
|
||||
$header['code'] = 'approach';
|
||||
$header['id'] = $id;
|
||||
|
||||
$form = Form::make($header);
|
||||
$tpl = $action == 'print' ? 'print' : 'create';
|
||||
return $this->display([
|
||||
'form' => $form,
|
||||
], $tpl);
|
||||
}
|
||||
|
||||
// 编辑进店
|
||||
public function editAction()
|
||||
{
|
||||
return $this->createAction();
|
||||
}
|
||||
|
||||
// 审核进店
|
||||
public function auditAction()
|
||||
{
|
||||
return $this->createAction('audit');
|
||||
}
|
||||
|
||||
// 显示进店
|
||||
public function showAction()
|
||||
{
|
||||
return $this->createAction('show');
|
||||
}
|
||||
|
||||
// 显示进店
|
||||
public function printAction()
|
||||
{
|
||||
$this->layout = 'layouts.print2';
|
||||
print_prince($this->createAction('print'));
|
||||
}
|
||||
|
||||
// 关闭操作
|
||||
public function closeAction()
|
||||
{
|
||||
$gets = Request::all();
|
||||
if (Request::method() == 'POST') {
|
||||
$row = DB::table('approach')->where('id', $gets['id'])->first();
|
||||
DB::table('approach')->where('id', $gets['id'])->update([
|
||||
'is_close' => !$row['is_close']
|
||||
]);
|
||||
return $this->json('操作成功。', true);
|
||||
}
|
||||
}
|
||||
|
||||
// 产品明细
|
||||
public function productAction()
|
||||
{
|
||||
$query = Request::all();
|
||||
if (Request::method() == 'POST') {
|
||||
$rows = DB::table('approach_data')
|
||||
->leftJoin('product', 'product.id', '=', 'approach_data.product_id')
|
||||
->where('approach_id', $query['id'])
|
||||
->orderBy('product.code', 'asc')
|
||||
->get(['product.*']);
|
||||
return $this->json($rows, true);
|
||||
}
|
||||
return $this->render(['query' => $query]);
|
||||
}
|
||||
|
||||
// 核销单选择
|
||||
public function serviceReviewAction()
|
||||
{
|
||||
$header = Grid::header([
|
||||
'code' => 'approach',
|
||||
'prefix' => '',
|
||||
]);
|
||||
$search = $header['search_form'];
|
||||
$query = $search['query'];
|
||||
|
||||
if (Request::method() == 'POST') {
|
||||
if ($query['master']) {
|
||||
$model = DB::table('approach');
|
||||
foreach ($header['join'] as $join) {
|
||||
$model->leftJoin($join[0], $join[1], $join[2], $join[3]);
|
||||
}
|
||||
$model->leftJoin('approach_market', 'approach_market.id', '=', 'approach.market_id')
|
||||
->where('approach.status', 1)
|
||||
->orderBy('approach.id', 'desc');
|
||||
|
||||
foreach ($search['where'] as $where) {
|
||||
if ($where['active']) {
|
||||
$model->search($where);
|
||||
}
|
||||
}
|
||||
$model->selectRaw('
|
||||
distinct(approach.id),
|
||||
approach.id,
|
||||
approach.sn,
|
||||
approach.status,
|
||||
approach.barcode_cast,
|
||||
approach.apply2_money,
|
||||
approach.created_at,
|
||||
approach.customer_id,
|
||||
customer_id_customer.region_id,
|
||||
approach_market.name as market_name,
|
||||
customer_id_region_id_customer_region.name as region_name,
|
||||
customer_id_customer.code as customer_code,
|
||||
customer_id_customer.name as customer_name,
|
||||
customer_id_customer.warehouse_contact,
|
||||
customer_id_customer.warehouse_phone,
|
||||
customer_id_customer.warehouse_address
|
||||
');
|
||||
$rows = $model->get();
|
||||
$rows = Grid::dataFilters($rows, $header);
|
||||
} else {
|
||||
|
||||
$model = DB::table('approach_data')
|
||||
->leftJoin('product', 'product.id', '=', 'approach_data.product_id')
|
||||
->leftJoin('approach', 'approach.id', '=', 'approach_data.approach_id')
|
||||
->leftJoin('product_unit', 'product_unit.id', '=', 'product.unit_id')
|
||||
->whereIn('approach.id', (array)$query['ids']);
|
||||
if ($query['sort'] && $query['order']) {
|
||||
$model->orderBy($query['sort'], $query['order']);
|
||||
}
|
||||
|
||||
foreach ($search['where'] as $where) {
|
||||
if ($where['active']) {
|
||||
$model->search($where);
|
||||
}
|
||||
}
|
||||
$model->selectRaw("
|
||||
approach_data.*,
|
||||
approach.id as approach_id,
|
||||
approach.sn as approach_sn,
|
||||
product.code as product_code,
|
||||
product.name as product_name,
|
||||
product.spec as product_spec,
|
||||
product.barcode as product_barcode,
|
||||
product.unit_id as unit_id,
|
||||
product_unit.name as product_unit,
|
||||
product.weight
|
||||
");
|
||||
$rows = $model->get();
|
||||
}
|
||||
return $this->json($rows, true);
|
||||
}
|
||||
|
||||
return $this->render([
|
||||
'search' => $search,
|
||||
'query' => $query,
|
||||
]);
|
||||
}
|
||||
|
||||
// 费用申请明细
|
||||
public function serviceCostDetailAction()
|
||||
{
|
||||
$query = Request::all();
|
||||
$customer_id = (int)$query['customer_id'];
|
||||
$date = empty($query['date']) ? date('Y-m-d') : $query['date'];
|
||||
$year = date('Y', strtotime($date));
|
||||
|
||||
if (Request::method() == 'POST') {
|
||||
|
||||
if ($query['type'] == 'promotion') {
|
||||
$rows = DB::table('promotion')
|
||||
->where('type_id', 2)
|
||||
->whereRaw('customer_id=? and '.sql_year('actived_dt').'=? and isnull(is_close, 0) = 0 and isnull(status, 0) <> 0', [$customer_id, $year])
|
||||
->get();
|
||||
}
|
||||
if ($query['type'] == 'approach') {
|
||||
$rows = DB::table('approach')
|
||||
->whereRaw('customer_id=? and '.sql_year('actived_dt').'=? and isnull(is_close, 0) = 0 and isnull(status, 0) <> 0', [$customer_id, $year])
|
||||
->get();
|
||||
}
|
||||
if ($query['type'] == 'material') {
|
||||
$rows = DB::table('promotion')
|
||||
->where('type_id', 1)
|
||||
->whereRaw('customer_id=? and '.sql_year('actived_dt').'=? and isnull(is_close, 0) = 0 and isnull(status, 0) <> 0', [$customer_id, $year])
|
||||
->get();
|
||||
}
|
||||
return $this->json($rows, true);
|
||||
}
|
||||
|
||||
$approach = DB::table('approach')
|
||||
->whereRaw('customer_id=? and '.sql_year('actived_dt').'=? and isnull(is_close, 0) = 0 and isnull(status, 0) <> 0', [$customer_id, $year])
|
||||
->selectRaw('sum(barcode_cast) as apply_money,sum(apply2_money) as support_money')
|
||||
->first();
|
||||
|
||||
$promotion = DB::table('promotion')
|
||||
->whereRaw('customer_id=? and '.sql_year('actived_dt').'=? and isnull(is_close, 0) = 0 and isnull(status, 0) <> 0', [$customer_id, $year])
|
||||
->selectRaw('sum(apply_fee) as apply_money,sum(undertake_money) as support_money')
|
||||
->first();
|
||||
|
||||
$apply_money = $approach['apply_money'] + $promotion['apply_money'];
|
||||
$support_money = $approach['support_money'] + $promotion['support_money'];
|
||||
|
||||
|
||||
// 发货
|
||||
$delivery = DB::table('stock_delivery_data as d')
|
||||
->leftJoin('stock_delivery as m', 'm.id', '=', 'd.delivery_id')
|
||||
->leftJoin('product', 'product.id', '=', 'd.product_id')
|
||||
->whereRaw('m.customer_id=? and '.sql_year('m.invoice_dt').'=? and d.product_id <> 20226 and isnull(product.product_type, 0) = 1', [$customer_id, $year])
|
||||
->selectRaw('sum(isnull(d.money, 0) - isnull(d.other_money, 0)) money');
|
||||
// 退货
|
||||
$cancel = DB::table('stock_cancel_data as d')
|
||||
->leftJoin('stock_cancel as m', 'm.id', '=', 'd.cancel_id')
|
||||
->leftJoin('product', 'product.id', '=', 'd.product_id')
|
||||
->whereRaw('m.customer_id=? and '.sql_year('m.invoice_dt').'=? and d.product_id <> 20226 and isnull(product.product_type, 0) = 1', [$customer_id, $year])
|
||||
->selectRaw('sum(isnull(d.money, 0) - isnull(d.other_money, 0)) money');
|
||||
// 直营
|
||||
$direct = DB::table('stock_direct_data as d')
|
||||
->leftJoin('stock_direct as m', 'm.id', '=', 'd.direct_id')
|
||||
->leftJoin('product', 'product.id', '=', 'd.product_id')
|
||||
->whereRaw('m.customer_id=? and '.sql_year('m.invoice_dt').'=? and d.product_id <> 20226 and isnull(product.product_type, 0) = 1', [$customer_id, $year])
|
||||
->selectRaw('sum(isnull(d.money, 0) - isnull(d.other_money, 0)) money');
|
||||
$rows = $cancel->unionAll($delivery)->unionAll($direct)->get();
|
||||
$money = $rows->sum('money');
|
||||
|
||||
$apply_percent = $support_percent = 0;
|
||||
if ($money > 0) {
|
||||
$apply_percent = ($apply_money / $money) * 100;
|
||||
$support_percent = ($support_money / $money) * 100;
|
||||
}
|
||||
|
||||
$all = [
|
||||
'money' => $money,
|
||||
'apply_money' => $apply_money,
|
||||
'support_money' => $support_money,
|
||||
'apply_percent' => $apply_percent,
|
||||
'support_percent' => $support_percent,
|
||||
];
|
||||
|
||||
return $this->render([
|
||||
'all' => $all,
|
||||
'query' => $query,
|
||||
]);
|
||||
}
|
||||
|
||||
// 批量编辑
|
||||
public function batchEditAction()
|
||||
{
|
||||
$gets = Request::all();
|
||||
if (Request::method() == 'POST') {
|
||||
$ids = explode(',', $gets['ids']);
|
||||
DB::table('approach')->whereIn('id', $ids)->update([
|
||||
$gets['field'] => $gets['search_0'],
|
||||
]);
|
||||
return $this->json('修改完成。', true);
|
||||
}
|
||||
$header = Grid::batchEdit([
|
||||
'code' => 'approach',
|
||||
'columns' => ['customer_id', 'tax_id'],
|
||||
]);
|
||||
return view('batchEdit', [
|
||||
'gets' => $gets,
|
||||
'header' => $header
|
||||
]);
|
||||
}
|
||||
|
||||
// 删除进店申请
|
||||
public function deleteAction()
|
||||
{
|
||||
if (Request::method() == 'POST') {
|
||||
$ids = Request::get('id');
|
||||
return Form::remove(['code' => 'approach', 'ids' => $ids]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
<?php namespace Gdoo\Approach\Controllers;
|
||||
|
||||
use DB;
|
||||
use Request;
|
||||
use Validator;
|
||||
|
||||
use Gdoo\Approach\Models\ApproachMarket;
|
||||
use Gdoo\Model\Grid;
|
||||
use Gdoo\Model\Form;
|
||||
|
||||
use Gdoo\Index\Controllers\DefaultController;
|
||||
|
||||
class MarketController extends DefaultController
|
||||
{
|
||||
public $permission = ['dialog'];
|
||||
|
||||
public function indexAction()
|
||||
{
|
||||
// 客户权限
|
||||
$region = regionCustomer('customer_id_customer');
|
||||
|
||||
$header = Grid::header([
|
||||
'code' => 'approach_market',
|
||||
'referer' => 1,
|
||||
'search' => ['by' => ''],
|
||||
]);
|
||||
|
||||
$cols = $header['cols'];
|
||||
|
||||
$cols['actions']['options'] = [[
|
||||
'name' => '编辑',
|
||||
'action' => 'edit',
|
||||
'display' => $this->access['edit'],
|
||||
]];
|
||||
|
||||
$search = $header['search_form'];
|
||||
$query = $search['query'];
|
||||
|
||||
if (Request::method() == 'POST') {
|
||||
$model = DB::table($header['table'])->setBy($header);
|
||||
foreach ($header['join'] as $join) {
|
||||
$model->leftJoin($join[0], $join[1], $join[2], $join[3]);
|
||||
}
|
||||
$model->orderBy($header['sort'], $header['order']);
|
||||
|
||||
foreach ($search['where'] as $where) {
|
||||
if ($where['active']) {
|
||||
$model->search($where);
|
||||
}
|
||||
}
|
||||
|
||||
if ($region['authorise']) {
|
||||
foreach ($region['whereIn'] as $key => $where) {
|
||||
$model->whereIn($key, $where);
|
||||
}
|
||||
}
|
||||
|
||||
$model->select($header['select']);
|
||||
$rows = $model->paginate($query['limit'])->appends($query);
|
||||
$items = Grid::dataFilters($rows, $header);
|
||||
return $items->toJson();
|
||||
}
|
||||
|
||||
$header['buttons'] = [
|
||||
['name' => '删除', 'icon' => 'fa-remove', 'action' => 'delete', 'display' => $this->access['delete']],
|
||||
['name' => '导出', 'icon' => 'fa-share', 'action' => 'export', 'display' => 1],
|
||||
];
|
||||
|
||||
$header['cols'] = $cols;
|
||||
$header['tabs'] = ApproachMarket::$tabs;
|
||||
$header['bys'] = ApproachMarket::$bys;
|
||||
$header['js'] = Grid::js($header);
|
||||
|
||||
return $this->display([
|
||||
'header' => $header,
|
||||
]);
|
||||
}
|
||||
|
||||
// 新建促销
|
||||
public function createAction($action = 'edit')
|
||||
{
|
||||
$id = (int) Request::get('id');
|
||||
$header['action'] = $action;
|
||||
$header['code'] = 'approach_market';
|
||||
$header['id'] = $id;
|
||||
|
||||
$form = Form::make($header);
|
||||
return $this->render([
|
||||
'form' => $form,
|
||||
], 'create');
|
||||
}
|
||||
|
||||
// 编辑促销
|
||||
public function editAction()
|
||||
{
|
||||
return $this->createAction();
|
||||
}
|
||||
|
||||
// 审核促销
|
||||
public function auditAction()
|
||||
{
|
||||
return $this->createAction('audit');
|
||||
}
|
||||
|
||||
// 显示促销
|
||||
public function showAction()
|
||||
{
|
||||
return $this->createAction('show');
|
||||
}
|
||||
|
||||
// 删除促销
|
||||
public function deleteAction()
|
||||
{
|
||||
if (Request::method() == 'POST') {
|
||||
$ids = Request::get('id');
|
||||
return Form::remove(['code' => 'approach_market', 'ids' => $ids]);
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框
|
||||
public function dialogAction()
|
||||
{
|
||||
$header = Grid::header([
|
||||
'code' => 'approach_market',
|
||||
]);
|
||||
$search = $header['search_form'];
|
||||
$query = $search['query'];
|
||||
|
||||
if (Request::method() == 'POST') {
|
||||
$model = DB::table('approach_market');
|
||||
/*
|
||||
foreach ($header['join'] as $join) {
|
||||
$model->leftJoin($join[0], $join[1], $join[2], $join[3]);
|
||||
}
|
||||
*/
|
||||
if ($query['sort'] && $query['order']) {
|
||||
$model->orderBy($query['sort'], $query['order']);
|
||||
}
|
||||
|
||||
if (isset($query['customer_id'])) {
|
||||
$model->where('approach_market.customer_id', $query['customer_id']);
|
||||
}
|
||||
|
||||
if ($query['q']) {
|
||||
$model->where('approach_market.name', 'like', '%'.$query['q'].'%');
|
||||
}
|
||||
|
||||
foreach ($search['where'] as $where) {
|
||||
if ($where['active']) {
|
||||
$model->search($where);
|
||||
}
|
||||
}
|
||||
|
||||
$sql = 'name as text,code,name,customer_id,market_count,type_id,single_cast,total_cast,fax,market_address,market_area,market_person_name,market_person_phone,status';
|
||||
|
||||
if ($query['related'] == '0') {
|
||||
$sql = $sql.',name as id';
|
||||
} else {
|
||||
$sql = $sql.',id as id';
|
||||
}
|
||||
$model->selectRaw($sql);
|
||||
$rows = $model->paginate();
|
||||
|
||||
$items = Grid::dataFilters($rows, $header);
|
||||
|
||||
return response()->json($items);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
<?php namespace Gdoo\Approach\Controllers;
|
||||
|
||||
use DB;
|
||||
use Request;
|
||||
use Validator;
|
||||
|
||||
use Gdoo\User\Models\User;
|
||||
use Gdoo\Approach\Models\ApproachReview;
|
||||
use Gdoo\Model\Grid;
|
||||
use Gdoo\Model\Form;
|
||||
|
||||
use Gdoo\Index\Controllers\WorkflowController;
|
||||
|
||||
class ReviewController extends WorkflowController
|
||||
{
|
||||
public $permission = ['dialog', 'reference', 'useCount', 'feeDetail'];
|
||||
|
||||
public function indexAction()
|
||||
{
|
||||
// 客户权限
|
||||
$region = regionCustomer('customer_id_customer');
|
||||
|
||||
$header = Grid::header([
|
||||
'code' => 'approach_review',
|
||||
'referer' => 1,
|
||||
'search' => ['by' => ''],
|
||||
]);
|
||||
|
||||
$cols = $header['cols'];
|
||||
|
||||
$cols['actions']['options'] = [[
|
||||
'name' => '显示',
|
||||
'action' => 'show',
|
||||
'display' => $this->access['show'],
|
||||
]];
|
||||
|
||||
$search = $header['search_form'];
|
||||
$query = $search['query'];
|
||||
|
||||
if (Request::method() == 'POST') {
|
||||
$model = DB::table($header['table'])->setBy($header);
|
||||
foreach ($header['join'] as $join) {
|
||||
$model->leftJoin($join[0], $join[1], $join[2], $join[3]);
|
||||
}
|
||||
$model->orderBy($header['sort'], $header['order']);
|
||||
|
||||
foreach ($search['where'] as $where) {
|
||||
if ($where['active']) {
|
||||
$model->search($where);
|
||||
}
|
||||
}
|
||||
|
||||
if ($region['authorise']) {
|
||||
foreach ($region['whereIn'] as $key => $where) {
|
||||
$model->whereIn($key, $where);
|
||||
}
|
||||
}
|
||||
|
||||
$model->select($header['select']);
|
||||
$rows = $model->paginate($query['limit'])->appends($query);
|
||||
$items = Grid::dataFilters($rows, $header);
|
||||
return $items->toJson();
|
||||
}
|
||||
|
||||
$header['buttons'] = [
|
||||
//['name' => '删除', 'icon' => 'fa-remove', 'action' => 'delete', 'display' => $this->access['delete']],
|
||||
['name' => '导出', 'icon' => 'fa-share', 'action' => 'export', 'display' => 1],
|
||||
];
|
||||
|
||||
$header['left_buttons'] = [
|
||||
['name' => '批量编辑', 'color' => 'default', 'icon' => 'fa-pencil-square-o', 'action' => 'batchEdit', 'display' => $this->access['batchEdit']],
|
||||
];
|
||||
|
||||
$header['cols'] = $cols;
|
||||
$header['tabs'] = ApproachReview::$tabs;
|
||||
$header['bys'] = ApproachReview::$bys;
|
||||
$header['js'] = Grid::js($header);
|
||||
|
||||
return $this->display([
|
||||
'header' => $header,
|
||||
]);
|
||||
}
|
||||
|
||||
// 新建促销
|
||||
public function createAction($action = 'edit')
|
||||
{
|
||||
$id = (int) Request::get('id');
|
||||
|
||||
// 客户权限
|
||||
$header['region'] = ['field' => 'customer_id'];
|
||||
$header['authorise'] = ['action' => 'index', 'field' => 'created_id'];
|
||||
|
||||
$header['action'] = $action;
|
||||
$header['code'] = 'approach_review';
|
||||
$header['id'] = $id;
|
||||
|
||||
$header['joint'] = [
|
||||
['name' => '申请单', 'action' => 'apply', 'field' => 'apply_id'],
|
||||
['name' => '兑现明细', 'action' => 'cash_detail', 'field' => 'apply_id'],
|
||||
];
|
||||
|
||||
$form = Form::make($header);
|
||||
$tpl = $action == 'print' ? 'print' : 'create';
|
||||
return $this->display([
|
||||
'form' => $form,
|
||||
], $tpl);
|
||||
}
|
||||
|
||||
// 编辑促销
|
||||
public function editAction()
|
||||
{
|
||||
return $this->createAction();
|
||||
}
|
||||
|
||||
// 审核促销
|
||||
public function auditAction()
|
||||
{
|
||||
return $this->createAction('audit');
|
||||
}
|
||||
|
||||
// 显示促销
|
||||
public function showAction()
|
||||
{
|
||||
return $this->createAction('show');
|
||||
}
|
||||
|
||||
// 显示促销
|
||||
public function printAction()
|
||||
{
|
||||
$this->layout = 'layouts.print2';
|
||||
print_prince($this->createAction('print'));
|
||||
}
|
||||
|
||||
// 批量编辑
|
||||
public function batchEditAction()
|
||||
{
|
||||
$gets = Request::all();
|
||||
if (Request::method() == 'POST') {
|
||||
$ids = explode(',', $gets['ids']);
|
||||
DB::table('approach_review')->whereIn('id', $ids)->update([
|
||||
$gets['field'] => $gets['search_0'],
|
||||
]);
|
||||
return $this->json('修改完成。', true);
|
||||
}
|
||||
$header = Grid::batchEdit([
|
||||
'code' => 'approach_review',
|
||||
'columns' => ['customer_id', 'tax_id'],
|
||||
]);
|
||||
return view('batchEdit', [
|
||||
'gets' => $gets,
|
||||
'header' => $header
|
||||
]);
|
||||
}
|
||||
|
||||
// 兑现明细
|
||||
public function feeDetailAction()
|
||||
{
|
||||
$query = Request::all();
|
||||
if (Request::method() == 'POST') {
|
||||
$rows = DB::table('approach_review')->where('apply_id', $query['id'])->orderBy('id', 'desc')->get();
|
||||
return $this->json($rows, true);
|
||||
}
|
||||
return $this->render(['query' => $query]);
|
||||
}
|
||||
|
||||
// 删除促销
|
||||
public function deleteAction()
|
||||
{
|
||||
if (Request::method() == 'POST') {
|
||||
$ids = Request::get('id');
|
||||
return Form::remove(['code' => 'approach_review', 'ids' => $ids]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
<?php namespace Gdoo\Approach\Hooks;
|
||||
|
||||
use DB;
|
||||
use Gdoo\User\Models\User;
|
||||
|
||||
class ApproachHook
|
||||
{
|
||||
public function onBeforeForm($params) {
|
||||
$views = $params['views'];
|
||||
$view = $views[3];
|
||||
|
||||
$view['fields'][] = [
|
||||
'field' => 'apply_percentage',
|
||||
'hidden' => 1,
|
||||
'width' => 40,
|
||||
'readonly' => 0,
|
||||
'hide_title' => 1,
|
||||
'type' => 0,
|
||||
'name' => '按回款核销(%)',
|
||||
];
|
||||
$view['fields'][] = [
|
||||
'field' => 'order_payment_scale',
|
||||
'hidden' => 1,
|
||||
'width' => 40,
|
||||
'readonly' => 0,
|
||||
'hide_title' => 1,
|
||||
'type' => 0,
|
||||
'name' => '按订单进行兑付(%)',
|
||||
];
|
||||
$views[3] = $view;
|
||||
$params['views'] = $views;
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onFieldFilter($params) {
|
||||
$values = $params['values'];
|
||||
$field = $params['field'];
|
||||
$f = $field['field'];
|
||||
$value = $values[$f];
|
||||
if ($f == 'market_name') {
|
||||
if (strpos($value, 'draft_') === 0) {
|
||||
$name = str_replace('draft_', '', $value);
|
||||
$market = [
|
||||
'customer_id' => $values['customer_id'],
|
||||
'name' => $name,
|
||||
'market_count' => $values['market_totol'],
|
||||
'type_id' => $values['type_id'],
|
||||
'single_cast' => $values['single_cast'],
|
||||
'total_cast' => $values['totol_cast'],
|
||||
'fax' => $values['fax'],
|
||||
'market_address' => $values['market_address'],
|
||||
'market_area' => $values['market_size'],
|
||||
'market_person_name' => $values['market_contact'],
|
||||
'market_person_phone' => $values['market_contact_phone'],
|
||||
];
|
||||
$values['market_name'] = $name;
|
||||
$values['market_id'] = DB::table('approach_market')->insertGetId($market);
|
||||
}
|
||||
}
|
||||
$params['values'] = $values;
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onFormFieldFilter($params) {
|
||||
$_replace = $params['_replace'];
|
||||
|
||||
$verification_info = $_replace['{verification_info}'];
|
||||
if ($verification_info) {
|
||||
$verification_info = $verification_info.'
|
||||
<div class="m-xs">
|
||||
贵司出具发票:按回款(回款以我司批复之日起算) '.$_replace['{apply_percentage}'].'% 核销;
|
||||
贵司未出具发票:按订单(订单以提交审核资料核销后) '.$_replace['{order_payment_scale}'].'% 进行兑付。直到核完我司支持费用为止。
|
||||
</div>
|
||||
<div class="red">
|
||||
客户进场后必须在2个月内提交资料核销,否则不予受理。开始核销后,超过一年未核完的,将不再核销。
|
||||
</div>
|
||||
';
|
||||
$_replace['{verification_info}'] = $verification_info;
|
||||
unset($_replace['{apply_percentage}']);
|
||||
unset($_replace['{order_payment_scale}']);
|
||||
}
|
||||
|
||||
$params['_replace'] = $_replace;
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onAfterForm($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeAudit($params) {
|
||||
// 流程结束写入生效日期
|
||||
$master = $params['master'];
|
||||
$master['actived_dt'] = date('Y-m-d');
|
||||
$params['master'] = $master;
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeStore($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onAfterStore($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeDelete($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeImport($params) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php namespace Gdoo\Approach\Hooks;
|
||||
|
||||
use DB;
|
||||
use Gdoo\User\Models\User;
|
||||
|
||||
class ReviewHook
|
||||
{
|
||||
public function onBeforeForm($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onFieldFilter($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onFormFieldFilter($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onAfterForm($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeStore($params)
|
||||
{
|
||||
$gets = $params['gets'];
|
||||
$approach_review = $gets['approach_review'];
|
||||
$count = count($gets['approach_review_data']['rows']);
|
||||
$apply_id = $approach_review['apply_id'];
|
||||
$count2 = DB::table('approach_data')->where('approach_id', $apply_id)->count();
|
||||
if ($count <> $count2) {
|
||||
abort_error('进店申请条码数量和核销条码数量不一致。');
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeAudit($params) {
|
||||
$id = $params['id'];
|
||||
// 生效费用
|
||||
$row = DB::table('approach_review')->where('id', $id)->first();
|
||||
if ($row['use_order'] == 1) {
|
||||
// 生成费用类型
|
||||
$categorys = [1 => 4, 3 => 5];
|
||||
$master = [
|
||||
'sn' => $row['sn'],
|
||||
'date' => $row['date'],
|
||||
'category_id' => $categorys[$row['pay_type']],
|
||||
'type_id' => 57,
|
||||
'remark' => $row['remark'],
|
||||
'status' => 1,
|
||||
];
|
||||
$cost_id = DB::table('customer_cost')->insertGetId($master);
|
||||
DB::table('customer_cost_data')->insert([
|
||||
'cost_id' => $cost_id,
|
||||
'customer_id' => $row['customer_id'],
|
||||
'money' => $row['fact_verification_cost'],
|
||||
'remain_money' => $row['fact_verification_cost'],
|
||||
'src_id' => $row['id'],
|
||||
'src_sn' => $row['sn'],
|
||||
'src_type_id' => 57,
|
||||
'status' => 1,
|
||||
]);
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeAbort($params) {
|
||||
$id = $params['id'];
|
||||
$review = DB::table('approach_review')->where('id', $id)->first();
|
||||
$cost_count = DB::table('customer_cost')->where('sn', $review['sn'])->count();
|
||||
if ($cost_count > 0) {
|
||||
abort_error('客户费用单号['.$review['sn'].']已经存在无法弃审。');
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onAfterStore($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeDelete($params) {
|
||||
// 删除生成的费用
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeImport($params) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php namespace Gdoo\Approach\Models;
|
||||
|
||||
use Gdoo\Index\Models\BaseModel;
|
||||
|
||||
class Approach extends BaseModel
|
||||
{
|
||||
protected $table = 'approach';
|
||||
|
||||
public static $tabs = [
|
||||
'name' => 'tab',
|
||||
'items' => [
|
||||
['value' => 'approach', 'url' => 'approach/approach/index', 'name' => '进店列表'],
|
||||
]
|
||||
];
|
||||
|
||||
public static $bys = [
|
||||
'name' => 'by',
|
||||
'items' => [
|
||||
['value' => '', 'name' => '全部'],
|
||||
['value' => 'divider'],
|
||||
['value' => 'day', 'name' => '今日创建'],
|
||||
['value' => 'week', 'name' => '本周创建'],
|
||||
['value' => 'month', 'name' => '本月创建'],
|
||||
]
|
||||
];
|
||||
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo('Gdoo\Customer\Models\Customer');
|
||||
}
|
||||
|
||||
public function datas()
|
||||
{
|
||||
return $this->hasMany('Gdoo\Promotion\Models\PromotionData');
|
||||
}
|
||||
|
||||
public function scopeDialog($q, $value)
|
||||
{
|
||||
return $q->whereIn('id', $value)->pluck('sn', 'id');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php namespace Gdoo\Approach\Models;
|
||||
|
||||
use Gdoo\Index\Models\BaseModel;
|
||||
|
||||
class ApproachData extends BaseModel
|
||||
{
|
||||
protected $table = 'approach_data';
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php namespace Gdoo\Approach\Models;
|
||||
|
||||
use Gdoo\Index\Models\BaseModel;
|
||||
|
||||
class ApproachMarket extends BaseModel
|
||||
{
|
||||
protected $table = 'approach_market';
|
||||
|
||||
public static $tabs = [
|
||||
'name' => 'tab',
|
||||
'items' => [
|
||||
['value' => 'market', 'url' => 'approach/market/index', 'name' => '超市列表'],
|
||||
]
|
||||
];
|
||||
|
||||
public static $bys = [
|
||||
'name' => 'by',
|
||||
'items' => [
|
||||
['value' => '', 'name' => '全部'],
|
||||
['value' => 'divider'],
|
||||
['value' => 'day', 'name' => '今日创建'],
|
||||
['value' => 'week', 'name' => '本周创建'],
|
||||
['value' => 'month', 'name' => '本月创建'],
|
||||
]
|
||||
];
|
||||
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo('Gdoo\Customer\Models\Customer');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php namespace Gdoo\Approach\Models;
|
||||
|
||||
use Gdoo\Index\Models\BaseModel;
|
||||
|
||||
class ApproachReview extends BaseModel
|
||||
{
|
||||
protected $table = 'approach_review';
|
||||
|
||||
public static $tabs = [
|
||||
'name' => 'tab',
|
||||
'items' => [
|
||||
['value' => 'review', 'url' => 'approach/review/index', 'name' => '进店核销'],
|
||||
]
|
||||
];
|
||||
|
||||
public static $bys = [
|
||||
'name' => 'by',
|
||||
'items' => [
|
||||
['value' => '', 'name' => '全部'],
|
||||
['value' => 'divider'],
|
||||
['value' => 'day', 'name' => '今日创建'],
|
||||
['value' => 'week', 'name' => '本周创建'],
|
||||
['value' => 'month', 'name' => '本月创建'],
|
||||
]
|
||||
];
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php namespace Gdoo\Approach\Services;
|
||||
|
||||
use DB;
|
||||
use Gdoo\Index\Services\BadgeService;
|
||||
|
||||
class ApproachService
|
||||
{
|
||||
/**
|
||||
* 获取待办的进店申请
|
||||
*/
|
||||
public static function getBadge()
|
||||
{
|
||||
return BadgeService::getModelTodo('approach');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
return [
|
||||
"name" => "进店管理",
|
||||
"version" => "1.0",
|
||||
"description" => "条码进店。",
|
||||
"listens" => [
|
||||
'approach' => 'Gdoo\Approach\Hooks\ApproachHook',
|
||||
'approach_review' => 'Gdoo\Approach\Hooks\ReviewHook',
|
||||
],
|
||||
'dialogs' => [
|
||||
'approach' => [
|
||||
'name' => '进店申请',
|
||||
'model' => 'Gdoo\Approach\Models\Approach::Dialog',
|
||||
'url' => 'approach/approach/dialog',
|
||||
],
|
||||
'approach_market' => [
|
||||
'name' => '进店超市',
|
||||
'model' => 'Gdoo\Approach\Models\ApproachMarket::Dialog',
|
||||
'url' => 'approach/market/dialog',
|
||||
],
|
||||
],
|
||||
'badges' => [
|
||||
'approach_approach_index' => 'Gdoo\Approach\Services\ApproachService::getBadge',
|
||||
],
|
||||
"controllers" => [
|
||||
"approach" => [
|
||||
"name" => "进店申请",
|
||||
"actions" => [
|
||||
"index" => [
|
||||
"name" => "列表"
|
||||
],
|
||||
"show" => [
|
||||
"name" => "查看"
|
||||
],
|
||||
"create" => [
|
||||
"name" => "新建"
|
||||
],
|
||||
"edit" => [
|
||||
"name" => "编辑"
|
||||
],
|
||||
"audit" => [
|
||||
"name" => "审核"
|
||||
],
|
||||
"recall" => [
|
||||
"name" => "撤回"
|
||||
],
|
||||
"abort" => [
|
||||
"name" => "弃审"
|
||||
],
|
||||
"print" => [
|
||||
"name" => "打印"
|
||||
],
|
||||
"delete" => [
|
||||
"name" => "删除"
|
||||
],
|
||||
"close" => [
|
||||
"name" => "关闭"
|
||||
],
|
||||
"batchEdit" => [
|
||||
"name" => "批量编辑"
|
||||
],
|
||||
]
|
||||
],
|
||||
"review" => [
|
||||
"name" => "进店核销",
|
||||
"actions" => [
|
||||
"index" => [
|
||||
"name" => "列表"
|
||||
],
|
||||
"show" => [
|
||||
"name" => "查看"
|
||||
],
|
||||
"create" => [
|
||||
"name" => "新建"
|
||||
],
|
||||
"edit" => [
|
||||
"name" => "编辑"
|
||||
],
|
||||
"audit" => [
|
||||
"name" => "审核"
|
||||
],
|
||||
"recall" => [
|
||||
"name" => "撤回"
|
||||
],
|
||||
"abort" => [
|
||||
"name" => "弃审"
|
||||
],
|
||||
"print" => [
|
||||
"name" => "打印"
|
||||
],
|
||||
"delete" => [
|
||||
"name" => "删除"
|
||||
],
|
||||
"batchEdit" => [
|
||||
"name" => "批量编辑"
|
||||
],
|
||||
]
|
||||
],
|
||||
"market" => [
|
||||
"name" => "进店超市",
|
||||
"actions" => [
|
||||
"index" => [
|
||||
"name" => "列表"
|
||||
],
|
||||
"create" => [
|
||||
"name" => "新建"
|
||||
],
|
||||
"edit" => [
|
||||
"name" => "编辑"
|
||||
],
|
||||
"delete" => [
|
||||
"name" => "删除"
|
||||
],
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
|
@ -0,0 +1,176 @@
|
|||
<div class="form-panel">
|
||||
<div class="form-panel-header">
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
{{$form['btn']}}
|
||||
|
||||
<a href="javascript:costDetailDialog();" class="btn btn-sm btn-default">
|
||||
费用申请明细
|
||||
</a>
|
||||
|
||||
@if($form['access']['close'])
|
||||
<a href="javascript:closeDialog();" class="btn btn-sm btn-default">
|
||||
关闭(打开)
|
||||
</a>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
<div class="form-panel-body panel-form-{{$form['action']}}">
|
||||
<form class="form-horizontal form-controller" method="post" id="{{$form['table']}}" name="{{$form['table']}}">
|
||||
{{$form['tpl']}}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var table = '{{$form["table"]}}';
|
||||
var id = '{{$form["row"]["id"]}}';
|
||||
var actived_dt = '{{$form["row"]["actived_dt"]}}';
|
||||
var customer_id = '{{$form["row"]["customer_id"]}}';
|
||||
|
||||
function costDetailDialog() {
|
||||
viewDialog({
|
||||
title: '费用申请明细',
|
||||
dialogClass: 'modal-md',
|
||||
url: app.url('approach/approach/serviceCostDetail', {date: actived_dt, customer_id: customer_id}),
|
||||
close: function() {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
$.post(app.url('approach/approach/close'), {id: id}, function(res) {
|
||||
toastrSuccess(res.data);
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function get_customer_id() {
|
||||
var customer_id = $('#approach_customer_id').val();
|
||||
return customer_id;
|
||||
}
|
||||
|
||||
function undertake_ratio() {
|
||||
var v1 = $('#approach_barcode_cast').val();
|
||||
var v2 = $('#approach_market_cast').val();
|
||||
var v3 = (toNumber(v2) / toNumber(v1)) * 100;
|
||||
$('#approach_barcode_cast_ratio').val(v3 > 100 ? 100 : v3.toFixed(2));
|
||||
}
|
||||
|
||||
function undertake_ratio2() {
|
||||
var v1 = $('#approach_barcode_cast').val();
|
||||
var v2 = $('#approach_apply2_money').val();
|
||||
var v3 = (toNumber(v2) / toNumber(v1)) * 100;
|
||||
$('#approach_apply2_ratio').val(v3 > 100 ? 100 : v3.toFixed(2));
|
||||
|
||||
var v4 = $('#approach_apply_bccount').val();
|
||||
var v5 = $('#approach_apply_market_count').val();
|
||||
var v6 = (toNumber(v2) / toNumber(v4)) / toNumber(v5);
|
||||
$('#approach_apply2_single_cast').val(toNumber(v6).toFixed(2));
|
||||
}
|
||||
|
||||
function undertake_ratio3() {
|
||||
var v1 = $('#approach_barcode_cast').val();
|
||||
var v2 = $('#approach_apply_money').val();
|
||||
var v3 = (toNumber(v2) / toNumber(v1)) * 100;
|
||||
$('#approach_fee_support_ratio').val(v3 > 100 ? 100 : v3.toFixed(2));
|
||||
}
|
||||
|
||||
(function($) {
|
||||
|
||||
$('#approach_barcode_cast,#approach_market_cast').bind('input propertychange', function() {
|
||||
undertake_ratio();
|
||||
undertake_ratio2();
|
||||
});
|
||||
|
||||
$('#approach_apply2_money,#approach_apply_bccount,#approach_apply_market_count').bind('input propertychange', function() {
|
||||
undertake_ratio();
|
||||
undertake_ratio2();
|
||||
});
|
||||
|
||||
$('#approach_apply_money').bind('input propertychange', function() {
|
||||
undertake_ratio3();
|
||||
});
|
||||
|
||||
$('#approach_field001').prop('checked', true);
|
||||
$('#approach_field004').prop('checked', true);
|
||||
|
||||
})(jQuery);
|
||||
|
||||
// grid初始化事件
|
||||
gdoo.event.set('grid.approach_data', {
|
||||
ready(me) {
|
||||
grid = me;
|
||||
grid.dataKey = 'product_id';
|
||||
},
|
||||
editable: {
|
||||
product_name(params) {
|
||||
var customer_id = $('#approach_customer_id').val();
|
||||
if (customer_id.trim() == '') {
|
||||
toastrError('请先选择客户');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 子表对话框
|
||||
gdoo.event.set('approach_data.product_id', {
|
||||
open(params) {
|
||||
params.url = 'product/product/serviceCustomer';
|
||||
},
|
||||
query(query) {
|
||||
var customer_id = $('#approach_customer_id').val();
|
||||
query.customer_id = customer_id;
|
||||
},
|
||||
onSelect(row, selectedRow) {
|
||||
row.price = selectedRow.price;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// 选择客户事件
|
||||
gdoo.event.set('approach.customer_id', {
|
||||
onSelect(row) {
|
||||
if (row.id) {
|
||||
$('#customer_region_region_id').val(row.region_id);
|
||||
$('#customer_region_region_id_text').val(row.region_id_name || '');
|
||||
$('#approach_phone').val(row.tel);
|
||||
$('#approach_fax').val(row.fax);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 选择超市事件
|
||||
gdoo.event.set('approach.market_name', {
|
||||
init(params) {
|
||||
params.ajax.url = app.url('approach/market/dialog');
|
||||
params.resultCache = false;
|
||||
},
|
||||
query(query) {
|
||||
query.field_0 = 'approach_market.name';
|
||||
var customer_id = $('#approach_customer_id').val();
|
||||
query.customer_id = customer_id;
|
||||
},
|
||||
onSelect(row) {
|
||||
if (row.id) {
|
||||
$('#approach_market_fax').val(row.fax);
|
||||
$('#approach_market_totol').val(row.market_count);
|
||||
$('#approach_single_cast').val(row.single_cast);
|
||||
$('#approach_totol_cast').val(row.total_cast);
|
||||
|
||||
$('#approach_market_size').val(row.market_area);
|
||||
$('#approach_market_address').val(row.market_address);
|
||||
$('#approach_market_contact').val(row.market_person_name);
|
||||
$('#approach_market_contact_phone').val(row.market_person_phone);
|
||||
$('#approach_market_type_id_select').val(row.type_id);
|
||||
$('#approach_market_type_id').val(row.type_id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,188 @@
|
|||
<style>
|
||||
.modal-body { overflow:hidden; }
|
||||
</style>
|
||||
|
||||
<div class="wrapper-sm" style="padding-bottom:0;">
|
||||
<div id="dialog-approach-toolbar">
|
||||
<form id="dialog-approach-search-form" name="dialog_approach_search_form" class="form-inline" method="get">
|
||||
@include('searchForm3')
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-t-sm">
|
||||
<div id="ref_approach" class="ag-theme-balham" style="width:100%;height:140px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="m-t-sm">
|
||||
<div id="ref_approach_data" class="ag-theme-balham" style="width:100%;height:240px;"></div>
|
||||
</div>
|
||||
<script>
|
||||
var $ref_approach = null;
|
||||
var $ref_approach_data = null;
|
||||
var params = JSON.parse('{{json_encode($query)}}');
|
||||
(function($) {
|
||||
params['master'] = 1;
|
||||
var mGridDiv = document.querySelector("#ref_approach");
|
||||
var mGrid = new agGridOptions();
|
||||
mGrid.remoteDataUrl = '{{url()}}';
|
||||
|
||||
var option = gdoo.formKey(params);
|
||||
var event = gdoo.event.get(option.key);
|
||||
event.trigger('query', params);
|
||||
|
||||
mGrid.remoteParams = params;
|
||||
mGrid.rowMultiSelectWithClick = false;
|
||||
mGrid.rowSelection = 'multiple';
|
||||
mGrid.autoColumnsToFit = false;
|
||||
mGrid.defaultColDef.suppressMenu = true;
|
||||
mGrid.defaultColDef.sortable = false;
|
||||
mGrid.columnDefs = [
|
||||
{cellClass:'text-center', checkboxSelection: true, headerCheckboxSelection: true, suppressSizeToFit: true, width: 40},
|
||||
{cellClass:'text-center', field: 'sn', headerName: '单据编号', minWidth: 160},
|
||||
{cellClass:'text-center', field: 'created_at', headerName: '单据日期', width: 120},
|
||||
{cellClass:'text-center', field: 'status', cellRenderer: 'htmlCellRenderer', headerName: '状态', width: 160},
|
||||
{cellClass:'text-center', field: 'customer_code', headerName: '客户编码', width: 120},
|
||||
{field:'customer_name', headerName: '客户名称', width: 160},
|
||||
{cellClass:'text-center',field:'region_name', headerName: '销售团队', width: 120},
|
||||
{cellClass:'text-right',field:'barcode_cast', headerName: '申请费用', width: 100},
|
||||
{cellClass:'text-right',field:'apply2_money', headerName: '批复费用', width: 100},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
|
||||
mGrid.onSelectionChanged = function() {
|
||||
var rows = mGrid.api.getSelectedRows();
|
||||
var ids = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
ids.push(rows[i].id);
|
||||
}
|
||||
params.ids = ids;
|
||||
sGrid.remoteData(params);
|
||||
};
|
||||
|
||||
mGrid.onRowClicked = function(row) {
|
||||
var selected = row.node.isSelected();
|
||||
if (selected === false) {
|
||||
row.node.setSelected(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
mGrid.onRowDoubleClicked = function (row) {
|
||||
var ret = writeSelected();
|
||||
if (ret === true) {
|
||||
$('#gdoo-dialog-' + params.dialog_index).dialog('close');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化选择
|
||||
*/
|
||||
function initSelected() {
|
||||
if (params.is_grid) {
|
||||
} else {
|
||||
var rows = {};
|
||||
var id = $('#'+option.id).val();
|
||||
if (id) {
|
||||
var ids = id.split(',');
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
rows[ids[i]] = ids[i];
|
||||
}
|
||||
}
|
||||
mGrid.api.forEachNode(function(node) {
|
||||
var key = node.data['id'];
|
||||
if (rows[key] != undefined) {
|
||||
node.setSelected(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入选中
|
||||
*/
|
||||
function writeSelected() {
|
||||
var rows = mGrid.api.getSelectedRows();
|
||||
if (params.is_grid) {
|
||||
var list = gdoo.forms[params.form_id];
|
||||
list.api.dialogSelected(params);
|
||||
} else {
|
||||
var id = [];
|
||||
var text = [];
|
||||
$.each(rows, function(k, row) {
|
||||
id.push(row['id']);
|
||||
text.push(row.name);
|
||||
});
|
||||
$('#'+option.id).val(id.join(','));
|
||||
$('#'+option.id+'_text').val(text.join(','));
|
||||
|
||||
if (event.exist('onSelect')) {
|
||||
return event.trigger('onSelect', multiple ? rows : rows[0]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
mGrid.writeSelected = writeSelected;
|
||||
gdoo.dialogs[option.id] = mGrid;
|
||||
|
||||
new agGrid.Grid(mGridDiv, mGrid);
|
||||
// 读取数据
|
||||
mGrid.remoteData();
|
||||
$ref_approach = mGrid;
|
||||
|
||||
params['master'] = 0;
|
||||
var sGridDiv = document.querySelector("#ref_approach_data");
|
||||
var sGrid = new agGridOptions();
|
||||
sGrid.remoteDataUrl = '{{url()}}';
|
||||
sGrid.remoteParams = params;
|
||||
sGrid.rowSelection = 'multiple';
|
||||
sGrid.defaultColDef.suppressMenu = true;
|
||||
sGrid.defaultColDef.sortable = false;
|
||||
sGrid.suppressRowClickSelection = true;
|
||||
sGrid.getRowClass = function(params) {
|
||||
params.node.setSelected(true);
|
||||
};
|
||||
sGrid.columnDefs = [
|
||||
{cellClass:'text-center', checkboxSelection: true, headerCheckboxSelection: true, suppressSizeToFit: true, width: 40},
|
||||
{cellClass:'text-center', field: 'product_code', headerName: '产品编码', width: 100},
|
||||
{field: 'product_name', headerName: '商品名称', minWidth: 180},
|
||||
{cellClass:'text-center', field: 'product_spec', headerName: '商品规格', width: 140},
|
||||
{cellClass:'text-center', field: 'product_barcode', headerName: '商品条码', width: 120},
|
||||
{cellClass:'text-center', field: 'product_unit', headerName: '计量单位', width: 80},
|
||||
{cellClass:'text-right', field: 'price1', headerName: '报价', width: 80},
|
||||
{cellClass:'text-right', field: 'price2', headerName: '售价', width: 80},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
|
||||
sGrid.onRowClicked = function(row) {
|
||||
var selected = row.node.isSelected();
|
||||
if (selected === false) {
|
||||
row.node.setSelected(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
new agGrid.Grid(sGridDiv, sGrid);
|
||||
// 读取数据
|
||||
sGrid.remoteData();
|
||||
$ref_approach_data = sGrid;
|
||||
|
||||
var data = JSON.parse('{{json_encode($search["forms"])}}');
|
||||
var search = $('#dialog-approach-search-form').searchForm({
|
||||
data: data,
|
||||
init:function(e) {}
|
||||
});
|
||||
|
||||
search.find('#search-submit').on('click', function() {
|
||||
var query = search.serializeArray();
|
||||
$.map(query, function(row) {
|
||||
params[row.name] = row.value;
|
||||
});
|
||||
|
||||
params['master'] = 1;
|
||||
mGrid.remoteData(params);
|
||||
|
||||
params['master'] = 0;
|
||||
sGrid.remoteData(params);
|
||||
return false;
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
|
@ -0,0 +1,83 @@
|
|||
{{$header["js"]}}
|
||||
|
||||
<div class="panel no-border" id="{{$header['master_table']}}-controller">
|
||||
@include('headers')
|
||||
<div class='list-jqgrid'>
|
||||
<div id="{{$header['master_table']}}-grid" style="width:100%;" class="ag-theme-balham"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function ($) {
|
||||
var table = '{{$header["master_table"]}}';
|
||||
var config = gdoo.grids[table];
|
||||
var action = config.action;
|
||||
var search = config.search;
|
||||
|
||||
action.dialogType = 'layer';
|
||||
|
||||
// 自定义搜索方法
|
||||
search.searchInit = function (e) {
|
||||
var self = this;
|
||||
}
|
||||
|
||||
action.fee_detail = function(data) {
|
||||
viewDialog({
|
||||
title: '兑现明细',
|
||||
dialogClass: 'modal-md',
|
||||
url: app.url('approach/review/feeDetail', {id: data.master_id}),
|
||||
close: function() {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
action.product = function(data) {
|
||||
viewDialog({
|
||||
title: '产品明细',
|
||||
dialogClass: 'modal-md',
|
||||
url: app.url('approach/approach/product', {id: data.master_id}),
|
||||
close: function() {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var options = new agGridOptions();
|
||||
var gridDiv = document.querySelector("#{{$header['master_table']}}-grid");
|
||||
gridDiv.style.height = getPanelHeight(48);
|
||||
|
||||
options.remoteDataUrl = '{{url()}}';
|
||||
options.autoColumnsToFit = false;
|
||||
options.remoteParams = search.advanced.query;
|
||||
options.columnDefs = config.cols;
|
||||
options.onRowDoubleClicked = function (params) {
|
||||
if (params.node.rowPinned) {
|
||||
return;
|
||||
}
|
||||
if (params.data == undefined) {
|
||||
return;
|
||||
}
|
||||
if (params.data.master_id > 0) {
|
||||
action.show(params.data);
|
||||
}
|
||||
};
|
||||
|
||||
new agGrid.Grid(gridDiv, options);
|
||||
|
||||
// 读取数据
|
||||
options.remoteData({page: 1});
|
||||
|
||||
// 绑定自定义事件
|
||||
var $gridDiv = $(gridDiv);
|
||||
$gridDiv.on('click', '[data-toggle="event"]', function () {
|
||||
var data = $(this).data();
|
||||
if (data.master_id > 0) {
|
||||
action[data.action](data);
|
||||
}
|
||||
});
|
||||
config.grid = options;
|
||||
|
||||
})(jQuery);
|
||||
|
||||
</script>
|
||||
@include('footers')
|
|
@ -0,0 +1 @@
|
|||
{{$form['tpl']}}
|
|
@ -0,0 +1,33 @@
|
|||
<style>
|
||||
.modal-body { overflow:hidden; }
|
||||
</style>
|
||||
|
||||
<div id="approach_product" class="ag-theme-balham" style="width:100%;height:320px;"></div>
|
||||
|
||||
<script>
|
||||
(function($) {
|
||||
var gridDiv = document.querySelector("#approach_product");
|
||||
var grid = new agGridOptions();
|
||||
grid.remoteDataUrl = '{{url()}}';
|
||||
|
||||
var params = JSON.parse('{{json_encode($query)}}');
|
||||
|
||||
grid.remoteParams = params;
|
||||
grid.rowMultiSelectWithClick = false;
|
||||
grid.rowSelection = 'multiple';
|
||||
// grid.autoColumnsToFit = false;
|
||||
grid.defaultColDef.suppressMenu = true;
|
||||
grid.defaultColDef.sortable = false;
|
||||
grid.columnDefs = [
|
||||
{cellClass:'text-center', headerName: '', type: 'sn', width: 40},
|
||||
{cellClass:'text-left', field: 'name', headerName: '产品名称', minWidth: 140},
|
||||
{cellClass:'text-center', field: 'spec', headerName: '规格型号', width: 100},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
|
||||
new agGrid.Grid(gridDiv, grid);
|
||||
// 读取数据
|
||||
grid.remoteData();
|
||||
|
||||
})(jQuery);
|
||||
</script>
|
|
@ -0,0 +1,121 @@
|
|||
<style>
|
||||
.modal-body { overflow:hidden; }
|
||||
</style>
|
||||
|
||||
<div class="wrapper-sm" style="padding-bottom:0;">
|
||||
<div id="dialog-approach-toolbar">
|
||||
<form id="dialog-approach-search-form" name="dialog_approach_search_form" class="form-inline" method="get">
|
||||
@include('searchForm3')
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-t-sm">
|
||||
<div id="ref_approach" class="ag-theme-balham" style="width:100%;height:140px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="m-t-sm">
|
||||
<div id="ref_approach_data" class="ag-theme-balham" style="width:100%;height:240px;"></div>
|
||||
</div>
|
||||
<script>
|
||||
var $ref_approach = null;
|
||||
var $ref_approach_data = null;
|
||||
var params = JSON.parse('{{json_encode($query)}}');
|
||||
(function($) {
|
||||
params['master'] = 1;
|
||||
var mGridDiv = document.querySelector("#ref_approach");
|
||||
var mGrid = new agGridOptions();
|
||||
mGrid.remoteDataUrl = '{{url()}}';
|
||||
mGrid.remoteParams = params;
|
||||
mGrid.rowSelection = 'multiple';
|
||||
mGrid.autoColumnsToFit = false;
|
||||
mGrid.rowMultiSelectWithClick = false;
|
||||
mGrid.defaultColDef.suppressMenu = true;
|
||||
mGrid.defaultColDef.sortable = false;
|
||||
mGrid.columnDefs = [
|
||||
{cellClass:'text-center', checkboxSelection: true, headerCheckboxSelection: true, suppressSizeToFit: true, width: 40},
|
||||
{cellClass:'text-center', field: 'sn', headerName: '促销编号', minWidth: 160},
|
||||
{cellClass:'text-center', field: 'created_at', type: 'datetime', headerName: '单据日期', width: 120},
|
||||
{cellClass:'text-center', field: 'status', headerName: '状态', width: 160},
|
||||
{cellClass:'text-center', field: 'customer_code', headerName: '客户编码', width: 160},
|
||||
{field:'customer_name', headerName: '客户名称', width: 160},
|
||||
{field:'warehouse_contact', headerName: '收货人', width: 160},
|
||||
{field:'warehouse_phone', headerName: '收货人电话', width: 160},
|
||||
{field:'warehouse_address', headerName: '收货地址', width: 260},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
|
||||
mGrid.onSelectionChanged = function() {
|
||||
var rows = mGrid.api.getSelectedRows();
|
||||
var ids = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
ids.push(rows[i].id);
|
||||
}
|
||||
params.ids = ids;
|
||||
sGrid.remoteData(params);
|
||||
};
|
||||
new agGrid.Grid(mGridDiv, mGrid);
|
||||
// 读取数据
|
||||
mGrid.remoteData();
|
||||
$ref_approach = mGrid;
|
||||
|
||||
params['master'] = 0;
|
||||
var sGridDiv = document.querySelector("#ref_approach_data");
|
||||
var sGrid = new agGridOptions();
|
||||
sGrid.remoteDataUrl = '{{url()}}';
|
||||
sGrid.remoteParams = params;
|
||||
sGrid.rowSelection = 'multiple';
|
||||
sGrid.autoColumnsToFit = false;
|
||||
sGrid.defaultColDef.suppressMenu = true;
|
||||
sGrid.defaultColDef.sortable = false;
|
||||
sGrid.suppressRowClickSelection = true;
|
||||
sGrid.getRowClass = function(params) {
|
||||
var data = params.data;
|
||||
params.node.setSelected(true);
|
||||
};
|
||||
sGrid.columnDefs = [
|
||||
{cellClass:'text-center', checkboxSelection: true, headerCheckboxSelection: true, suppressSizeToFit: true, width: 40},
|
||||
{cellClass:'text-center', field: 'product_code', headerName: '存货编码', width: 100},
|
||||
{field: 'product_name', headerName: '商品名称', minWidth: 180},
|
||||
{cellClass:'text-center', field: 'product_spec', headerName: '商品规格', width: 140},
|
||||
{cellClass:'text-center', field: 'unit_name', headerName: '计量单位', width: 80},
|
||||
{cellClass:'text-center', field: 'discount_rate', headerName: '现存量', width: 80},
|
||||
{cellClass:'text-right', field: 'total_quantity', headerName: '促销数量', width: 80},
|
||||
{cellClass:'text-right', field: 'use_quantity', headerName: '已发数量', width: 80},
|
||||
{cellClass:'text-right', field: 'quantity', headerName: '可用数量', width: 80},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
|
||||
sGrid.onRowClicked = function(row) {
|
||||
var selected = row.node.isSelected();
|
||||
if (selected === false) {
|
||||
row.node.setSelected(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
new agGrid.Grid(sGridDiv, sGrid);
|
||||
// 读取数据
|
||||
sGrid.remoteData();
|
||||
$ref_approach_data = sGrid;
|
||||
|
||||
var data = JSON.parse('{{json_encode($search["forms"])}}');
|
||||
var search = $('#dialog-approach-search-form').searchForm({
|
||||
data: data,
|
||||
init:function(e) {}
|
||||
});
|
||||
|
||||
search.find('#search-submit').on('click', function() {
|
||||
var query = search.serializeArray();
|
||||
$.map(query, function(row) {
|
||||
params[row.name] = row.value;
|
||||
});
|
||||
|
||||
params['master'] = 1;
|
||||
mGrid.remoteData(params);
|
||||
|
||||
params['master'] = 0;
|
||||
sGrid.remoteData(params);
|
||||
return false;
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
|
@ -0,0 +1,124 @@
|
|||
<style>
|
||||
.modal-body { overflow:hidden; }
|
||||
.service-cost-detail .control-label {
|
||||
padding-top: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
.service-cost-detail .form-group:first-child .col-xs-3:nth-child(3),
|
||||
.service-cost-detail .form-group:first-child .col-xs-3:nth-child(4) {
|
||||
border-top: 0 !important;
|
||||
}
|
||||
|
||||
.service-cost-detail .ag-theme-balham .ag-root-wrapper {
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="service-cost-detail">
|
||||
|
||||
<div class="form-controller">
|
||||
<div class="form-group">
|
||||
<div class="col-xs-3 col-sm-2 control-label">费用合计</div>
|
||||
<div class="col-xs-3 col-sm-2 control-text"><div class="form-control input-sm">@number($all['apply_money'], 2)</div></div>
|
||||
<div class="col-xs-3 col-sm-2 control-label">支持合计</div>
|
||||
<div class="col-xs-3 col-sm-2 control-text"><div class="form-control input-sm">@number($all['support_money'], 2)</div></div>
|
||||
<div class="col-xs-3 col-sm-2 control-label">年度销售额</div>
|
||||
<div class="col-xs-3 col-sm-2 control-text"><div class="form-control input-sm">@number($all['money'], 2)</div></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-xs-3 col-sm-2 control-label">申请产出比(%)</div>
|
||||
<div class="col-xs-3 col-sm-2 control-text"><div class="form-control input-sm">@number($all['support_percent'], 2)</div></div>
|
||||
<div class="col-xs-3 col-sm-2 control-label">兑现产出比(%)</div>
|
||||
<div class="col-xs-3 col-sm-2 control-text"><div class="form-control input-sm">@number($all['apply_percent'], 2)</div></div>
|
||||
<div class="col-xs-3 col-sm-2 control-label"></div>
|
||||
<div class="col-xs-3 col-sm-2 control-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="tabs-box">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a class="text-sm" href="#tab_a" data-toggle="tab">促销</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-sm" href="#tab_b" data-toggle="tab">进店</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-sm" href="#tab_c" data-toggle="tab">物资</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="tab_ontent" class="tab-content" style="padding:5px;">
|
||||
<div class="tab-pane active" id="tab_a">
|
||||
<div id="ref_tab_a" class="ag-theme-balham" style="width:100%;height:280px;"></div>
|
||||
</div>
|
||||
<div class="tab-pane" id="tab_b">
|
||||
<div id="ref_tab_b" class="ag-theme-balham" style="width:100%;height:280px;"></div>
|
||||
</div>
|
||||
<div class="tab-pane" id="tab_c">
|
||||
<div id="ref_tab_c" class="ag-theme-balham" style="width:100%;height:280px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var params = JSON.parse('{{json_encode($query)}}');
|
||||
(function($) {
|
||||
var tabDiv = document.querySelector("#ref_tab_a");
|
||||
var grid = new agGridOptions();
|
||||
grid.remoteDataUrl = '{{url()}}';
|
||||
grid.remoteParams = params;
|
||||
grid.rowSelection = 'single';
|
||||
grid.defaultColDef.suppressMenu = true;
|
||||
grid.defaultColDef.sortable = false;
|
||||
grid.columnDefs = [
|
||||
{cellClass:'text-center', field: 'start_dt', headerName: '开始日期', width: 100},
|
||||
{cellClass:'text-center', field: 'end_dt', headerName: '结束日期', width: 100},
|
||||
{cellClass:'text-center', field: 'promote_scope', headerName: '超市', minWidth: 160},
|
||||
//{cellClass:'text-center', field: 'created_at', headerName: '促销方式', width: 100},
|
||||
{cellClass:'text-right',field:'pro_total_cost', headerName: '总费用', width: 100},
|
||||
{cellClass:'text-right',field:'undertake_money', headerName: '支持费用', width: 100}
|
||||
];
|
||||
new agGrid.Grid(tabDiv, grid);
|
||||
// 读取数据
|
||||
grid.remoteData({type: 'promotion'});
|
||||
|
||||
var tabDiv2 = document.querySelector("#ref_tab_b");
|
||||
var grid2 = new agGridOptions();
|
||||
grid2.remoteDataUrl = '{{url()}}';
|
||||
grid2.remoteParams = params;
|
||||
grid2.rowSelection = 'single';
|
||||
grid2.defaultColDef.suppressMenu = true;
|
||||
grid2.defaultColDef.sortable = false;
|
||||
grid2.columnDefs = [
|
||||
{cellClass:'text-center', field: 'created_at', type: 'date', headerName: '申请日期', width: 120},
|
||||
{cellClass:'text-center', field: 'market_name', headerName: '超市名称', minWidth: 160},
|
||||
{cellClass:'text-right',field:'barcode_cast', headerName: '总费用', width: 140},
|
||||
{cellClass:'text-right',field:'apply2_money', headerName: '支持费用', width: 140}
|
||||
];
|
||||
new agGrid.Grid(tabDiv2, grid2);
|
||||
// 读取数据
|
||||
grid2.remoteData({type: 'approach'});
|
||||
|
||||
var tabDiv3 = document.querySelector("#ref_tab_c");
|
||||
var grid3 = new agGridOptions();
|
||||
grid3.remoteDataUrl = '{{url()}}';
|
||||
grid3.remoteParams = params;
|
||||
grid3.rowSelection = 'single';
|
||||
grid3.defaultColDef.suppressMenu = true;
|
||||
grid3.defaultColDef.sortable = false;
|
||||
grid3.columnDefs = [
|
||||
{cellClass:'text-center', field: 'created_at', type: 'date', headerName: '申请日期', minWidth: 120},
|
||||
{cellClass:'text-right',field:'pro_total_cost', headerName: '总费用', width: 140},
|
||||
{cellClass:'text-right',field:'undertake_money', headerName: '支持费用', width: 140}
|
||||
];
|
||||
new agGrid.Grid(tabDiv3, grid3);
|
||||
// 读取数据
|
||||
grid3.remoteData({type: 'material'});
|
||||
})(jQuery);
|
||||
</script>
|
|
@ -0,0 +1,179 @@
|
|||
<style>
|
||||
.modal-body { overflow:hidden; }
|
||||
</style>
|
||||
<div class="wrapper-xs">
|
||||
<div id="dialog-approach-toolbar">
|
||||
<form id="dialog-approach-search-form" name="dialog_approach_search_form" class="form-inline" method="get">
|
||||
@include('searchForm3')
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ref_approach" class="ag-theme-balham" style="width:100%;height:140px;"></div>
|
||||
<div class="m-t-xs">
|
||||
<div id="ref_approach_data" class="ag-theme-balham" style="width:100%;height:240px;"></div>
|
||||
</div>
|
||||
<script>
|
||||
var $ref_approach = null;
|
||||
var $ref_approach_data = null;
|
||||
var params = JSON.parse('{{json_encode($query)}}');
|
||||
(function($) {
|
||||
params['master'] = 1;
|
||||
var mGridDiv = document.querySelector("#ref_approach");
|
||||
var mGrid = new agGridOptions();
|
||||
mGrid.remoteDataUrl = '{{url()}}';
|
||||
|
||||
var option = gdoo.formKey(params);
|
||||
var event = gdoo.event.get(option.key);
|
||||
event.trigger('query', params);
|
||||
|
||||
mGrid.remoteParams = params;
|
||||
mGrid.rowSelection = 'single';
|
||||
mGrid.autoColumnsToFit = false;
|
||||
mGrid.defaultColDef.suppressMenu = true;
|
||||
mGrid.defaultColDef.sortable = false;
|
||||
mGrid.columnDefs = [
|
||||
{cellClass:'text-center', checkboxSelection: true, headerCheckboxSelection: true, suppressSizeToFit: true, width: 40},
|
||||
{cellClass:'text-center', field: 'sn', headerName: '单据编号', minWidth: 160},
|
||||
{cellClass:'text-center', field: 'created_at', headerName: '单据日期', width: 120},
|
||||
{cellClass:'text-center', field: 'status', cellRenderer: 'htmlCellRenderer', headerName: '状态', width: 160},
|
||||
{cellClass:'text-center', field: 'customer_code', headerName: '客户编码', width: 120},
|
||||
{field:'customer_name', headerName: '客户名称', width: 160},
|
||||
{cellClass:'text-center',field:'region_name', headerName: '销售团队', width: 120},
|
||||
{cellClass:'text-right',field:'barcode_cast', headerName: '申请费用', width: 100},
|
||||
{cellClass:'text-right',field:'apply2_money', headerName: '批复费用', width: 100},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
|
||||
mGrid.onRowClicked = function(row) {
|
||||
var selected = row.node.isSelected();
|
||||
if (selected === false) {
|
||||
row.node.setSelected(true, true);
|
||||
}
|
||||
var rows = mGrid.api.getSelectedRows();
|
||||
var ids = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
ids.push(rows[i].id);
|
||||
}
|
||||
params.ids = ids;
|
||||
sGrid.remoteData(params);
|
||||
};
|
||||
|
||||
mGrid.onRowDoubleClicked = function (row) {
|
||||
var ret = writeSelected();
|
||||
if (ret == true) {
|
||||
$('#gdoo-dialog-' + params.dialog_index).dialog('close');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化选择
|
||||
*/
|
||||
function initSelected() {
|
||||
if (params.is_grid) {
|
||||
} else {
|
||||
var rows = {};
|
||||
var id = $('#'+option.id).val();
|
||||
if (id) {
|
||||
var ids = id.split(',');
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
rows[ids[i]] = ids[i];
|
||||
}
|
||||
}
|
||||
mGrid.api.forEachNode(function(node) {
|
||||
var key = node.data['id'];
|
||||
if (rows[key] != undefined) {
|
||||
node.setSelected(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入选中
|
||||
*/
|
||||
function writeSelected() {
|
||||
var rows = mGrid.api.getSelectedRows();
|
||||
if (params.is_grid) {
|
||||
var list = gdoo.forms[params.form_id];
|
||||
list.api.dialogSelected(params);
|
||||
} else {
|
||||
var id = [];
|
||||
var text = [];
|
||||
$.each(rows, function(k, row) {
|
||||
id.push(row['id']);
|
||||
text.push(row.name);
|
||||
});
|
||||
$('#'+option.id).val(id.join(','));
|
||||
$('#'+option.id+'_text').val(text.join(','));
|
||||
|
||||
if (event.exist('onSelect')) {
|
||||
return event.trigger('onSelect', multiple ? rows : rows[0]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
mGrid.writeSelected = writeSelected;
|
||||
gdoo.dialogs[option.id] = mGrid;
|
||||
|
||||
new agGrid.Grid(mGridDiv, mGrid);
|
||||
// 读取数据
|
||||
mGrid.remoteData();
|
||||
$ref_approach = mGrid;
|
||||
|
||||
params['master'] = 0;
|
||||
var sGridDiv = document.querySelector("#ref_approach_data");
|
||||
var sGrid = new agGridOptions();
|
||||
sGrid.remoteDataUrl = '{{url()}}';
|
||||
sGrid.remoteParams = params;
|
||||
sGrid.rowSelection = 'multiple';
|
||||
sGrid.defaultColDef.suppressMenu = true;
|
||||
sGrid.defaultColDef.sortable = false;
|
||||
sGrid.suppressRowClickSelection = true;
|
||||
sGrid.getRowClass = function(params) {
|
||||
params.node.setSelected(true);
|
||||
};
|
||||
sGrid.columnDefs = [
|
||||
{cellClass:'text-center', checkboxSelection: true, headerCheckboxSelection: true, suppressSizeToFit: true, width: 40},
|
||||
{cellClass:'text-center', field: 'product_code', headerName: '产品编码', width: 100},
|
||||
{field: 'product_name', headerName: '商品名称', minWidth: 180},
|
||||
{cellClass:'text-center', field: 'product_spec', headerName: '商品规格', width: 140},
|
||||
{cellClass:'text-center', field: 'product_barcode', headerName: '商品条码', width: 120},
|
||||
{cellClass:'text-center', field: 'product_unit', headerName: '计量单位', width: 80},
|
||||
{cellClass:'text-right', field: 'price1', headerName: '报价', width: 80},
|
||||
{cellClass:'text-right', field: 'price2', headerName: '售价', width: 80},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
|
||||
sGrid.onRowClicked = function(row) {
|
||||
var selected = row.node.isSelected();
|
||||
if (selected === false) {
|
||||
row.node.setSelected(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
new agGrid.Grid(sGridDiv, sGrid);
|
||||
// 读取数据
|
||||
sGrid.remoteData();
|
||||
$ref_approach_data = sGrid;
|
||||
|
||||
var data = JSON.parse('{{json_encode($search["forms"])}}');
|
||||
var search = $('#dialog-approach-search-form').searchForm({
|
||||
data: data,
|
||||
init:function(e) {}
|
||||
});
|
||||
|
||||
search.find('#search-submit').on('click', function() {
|
||||
var query = search.serializeArray();
|
||||
$.map(query, function(row) {
|
||||
params[row.name] = row.value;
|
||||
});
|
||||
|
||||
params['master'] = 1;
|
||||
mGrid.remoteData(params);
|
||||
|
||||
params['master'] = 0;
|
||||
sGrid.remoteData(params);
|
||||
return false;
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
<form class="form-horizontal form-controller" method="post" id="{{$form['table']}}" name="{{$form['table']}}">
|
||||
{{$form['tpl']}}
|
||||
</form>
|
|
@ -0,0 +1,109 @@
|
|||
<style>
|
||||
.modal-body { overflow:hidden; }
|
||||
</style>
|
||||
|
||||
<div class="wrapper-sm" style="padding-bottom:0;">
|
||||
<div id="dialog-approach-toolbar">
|
||||
<form id="dialog-approach-search-form" name="dialog_approach_search_form" class="form-inline" method="get">
|
||||
@include('searchForm3')
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-t-sm">
|
||||
<div id="ref_approach" class="ag-theme-balham" style="width:100%;height:140px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="m-t-sm">
|
||||
<div id="ref_approach_data" class="ag-theme-balham" style="width:100%;height:240px;"></div>
|
||||
</div>
|
||||
<script>
|
||||
var $ref_approach = null;
|
||||
var $ref_approach_data = null;
|
||||
var params = JSON.parse('{{json_encode($query)}}');
|
||||
(function($) {
|
||||
params['master'] = 1;
|
||||
var mGridDiv = document.querySelector("#ref_approach");
|
||||
var mGrid = new agGridOptions();
|
||||
mGrid.remoteDataUrl = '{{url()}}';
|
||||
mGrid.remoteParams = params;
|
||||
mGrid.rowMultiSelectWithClick = false;
|
||||
mGrid.rowSelection = 'multiple';
|
||||
mGrid.autoColumnsToFit = false;
|
||||
mGrid.defaultColDef.suppressMenu = true;
|
||||
mGrid.defaultColDef.sortable = false;
|
||||
mGrid.columnDefs = [
|
||||
{cellClass:'text-center', checkboxSelection: true, headerCheckboxSelection: true, suppressSizeToFit: true, width: 40},
|
||||
{cellClass:'text-center', field: 'sn', headerName: '促销编号', minWidth: 160},
|
||||
{cellClass:'text-center', field: 'created_at', type: 'datetime', headerName: '单据日期', width: 120},
|
||||
{cellClass:'text-center', field: 'status', headerName: '状态', width: 160},
|
||||
{cellClass:'text-center', field: 'customer_code', headerName: '客户编码', width: 160},
|
||||
{field:'customer_name', headerName: '客户名称', width: 160},
|
||||
{field:'warehouse_contact', headerName: '收货人', width: 160},
|
||||
{field:'warehouse_phone', headerName: '收货人电话', width: 160},
|
||||
{field:'warehouse_address', headerName: '收货地址', width: 260},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
|
||||
mGrid.onSelectionChanged = function() {
|
||||
var rows = mGrid.api.getSelectedRows();
|
||||
var ids = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
ids.push(rows[i].id);
|
||||
}
|
||||
params.approach_ids = ids;
|
||||
sGrid.remoteData(params);
|
||||
};
|
||||
new agGrid.Grid(mGridDiv, mGrid);
|
||||
// 读取数据
|
||||
mGrid.remoteData();
|
||||
$ref_approach = mGrid;
|
||||
|
||||
params['master'] = 0;
|
||||
var sGridDiv = document.querySelector("#ref_approach_data");
|
||||
var sGrid = new agGridOptions();
|
||||
sGrid.remoteDataUrl = '{{url()}}';
|
||||
sGrid.remoteParams = params;
|
||||
//sGrid.rowMultiSelectWithClick = true;
|
||||
sGrid.rowSelection = 'multiple';
|
||||
sGrid.autoColumnsToFit = false;
|
||||
sGrid.defaultColDef.suppressMenu = true;
|
||||
sGrid.defaultColDef.sortable = false;
|
||||
sGrid.columnDefs = [
|
||||
{cellClass:'text-center', checkboxSelection: true, headerCheckboxSelection: true, suppressSizeToFit: true, width: 40},
|
||||
{cellClass:'text-center', field: 'product_code', headerName: '存货编码', width: 100},
|
||||
{field: 'product_name', headerName: '商品名称', minWidth: 180},
|
||||
{cellClass:'text-center', field: 'product_spec', headerName: '商品规格', width: 140},
|
||||
{cellClass:'text-center', field: 'unit_name', headerName: '计量单位', width: 80},
|
||||
{cellClass:'text-center', field: 'discount_rate', headerName: '现存量', width: 80},
|
||||
{cellClass:'text-right', field: 'total_quantity', headerName: '促销数量', width: 80},
|
||||
{cellClass:'text-right', field: 'use_quantity', headerName: '已发数量', width: 80},
|
||||
{cellClass:'text-right', field: 'quantity', headerName: '可用数量', width: 80},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
new agGrid.Grid(sGridDiv, sGrid);
|
||||
// 读取数据
|
||||
sGrid.remoteData();
|
||||
$ref_approach_data = sGrid;
|
||||
|
||||
var data = JSON.parse('{{json_encode($search["forms"])}}');
|
||||
var search = $('#dialog-approach-search-form').searchForm({
|
||||
data: data,
|
||||
init:function(e) {}
|
||||
});
|
||||
|
||||
search.find('#search-submit').on('click', function() {
|
||||
var query = search.serializeArray();
|
||||
$.map(query, function(row) {
|
||||
params[row.name] = row.value;
|
||||
});
|
||||
|
||||
params['master'] = 1;
|
||||
mGrid.remoteData(params);
|
||||
|
||||
params['master'] = 0;
|
||||
sGrid.remoteData(params);
|
||||
return false;
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
|
@ -0,0 +1,59 @@
|
|||
{{$header["js"]}}
|
||||
|
||||
<div class="panel no-border" id="{{$header['master_table']}}-controller">
|
||||
@include('headers')
|
||||
<div class='list-jqgrid'>
|
||||
<div id="{{$header['master_table']}}-grid" style="width:100%;" class="ag-theme-balham"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function ($) {
|
||||
var table = '{{$header["master_table"]}}';
|
||||
var config = gdoo.grids[table];
|
||||
var action = config.action;
|
||||
var search = config.search;
|
||||
|
||||
//action.dialogType = 'layer';
|
||||
|
||||
// 自定义搜索方法
|
||||
search.searchInit = function (e) {
|
||||
var self = this;
|
||||
}
|
||||
|
||||
var options = new agGridOptions();
|
||||
var gridDiv = document.querySelector("#{{$header['master_table']}}-grid");
|
||||
gridDiv.style.height = getPanelHeight(48);
|
||||
|
||||
options.remoteDataUrl = '{{url()}}';
|
||||
options.remoteParams = search.advanced.query;
|
||||
options.columnDefs = config.cols;
|
||||
options.onRowDoubleClicked = function (params) {
|
||||
if (params.node.rowPinned) {
|
||||
return;
|
||||
}
|
||||
if (params.data == undefined) {
|
||||
return;
|
||||
}
|
||||
if (params.data.master_id > 0) {
|
||||
action.edit(params.data);
|
||||
}
|
||||
};
|
||||
|
||||
new agGrid.Grid(gridDiv, options);
|
||||
|
||||
// 读取数据
|
||||
options.remoteData({page: 1});
|
||||
|
||||
// 绑定自定义事件
|
||||
var $gridDiv = $(gridDiv);
|
||||
$gridDiv.on('click', '[data-toggle="event"]', function () {
|
||||
var data = $(this).data();
|
||||
if (data.master_id > 0) {
|
||||
action[data.action](data);
|
||||
}
|
||||
});
|
||||
config.grid = options;
|
||||
})(jQuery);
|
||||
|
||||
</script>
|
||||
@include('footers')
|
|
@ -0,0 +1,120 @@
|
|||
<div class="form-panel">
|
||||
<div class="form-panel-header">
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
{{$form['btn']}}
|
||||
</div>
|
||||
<div class="form-panel-body panel-form-{{$form['action']}}">
|
||||
<form class="form-horizontal form-controller" method="post" id="{{$form['table']}}" name="{{$form['table']}}">
|
||||
{{$form['tpl']}}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var table = '{{$form["table"]}}';
|
||||
var grid = null;
|
||||
|
||||
function get_customer_id() {
|
||||
var customer_id = $('#approach_review_customer_id').val();
|
||||
return customer_id;
|
||||
}
|
||||
|
||||
$(function($) {
|
||||
|
||||
$(document).on('click', '[data-toggle="joint"]', function(event) {
|
||||
var data = $(this).data();
|
||||
// 联查进店申请
|
||||
if (data.action == 'apply') {
|
||||
top.addTab('approach/approach/show?id=' + data.id, 'approach_approach_show', '进店申请(联查)');
|
||||
}
|
||||
// 联查费用明细
|
||||
if (data.action == 'cash_detail') {
|
||||
viewDialog({
|
||||
title: '兑现明细',
|
||||
dialogClass: 'modal-md',
|
||||
url: app.url('approach/review/feeDetail', {id: data.id}),
|
||||
close: function() {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// grid初始化事件
|
||||
gdoo.event.set('grid.approach_review_data', {
|
||||
ready(me) {
|
||||
grid = me;
|
||||
grid.dataKey = 'product_id';
|
||||
},
|
||||
editable: {
|
||||
product_name(params) {
|
||||
var approach_id = $('#approach_review_approach_id').val();
|
||||
if (approach_id.trim() == '') {
|
||||
toastrError('请先选择申请编号');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 子表对话框
|
||||
gdoo.event.set('approach_review_data.product_id', {
|
||||
open(params) {
|
||||
params.url = 'product/product/serviceCustomer';
|
||||
},
|
||||
query(query) {
|
||||
var customer_id = $('#approach_review_customer_id').val();
|
||||
query.customer_id = customer_id;
|
||||
},
|
||||
onSelect(row, selectedRow) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
$(function() {
|
||||
$('#approach_review_pay_type').on('change', function() {
|
||||
if (this.value == 1 || this.value == 3) {
|
||||
$('#approach_review_use_order').val(1);
|
||||
} else {
|
||||
$('#approach_review_use_order').val(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 进场核销申请编号
|
||||
gdoo.event.set('approach_review.apply_id', {
|
||||
open(params) {
|
||||
params.url = 'approach/approach/serviceReview';
|
||||
},
|
||||
query(query) {
|
||||
},
|
||||
onSelect() {
|
||||
var approach = $ref_approach.api.getSelectedRows()[0];
|
||||
var rows = $ref_approach_data.api.getSelectedRows();
|
||||
$('#approach_review_apply_id').val(approach.id);
|
||||
$('#approach_review_apply_id_text').val(approach.sn);
|
||||
$('#approach_review_apply_dt').val(approach.created_at);
|
||||
$('#approach_review_apply_money').val(approach.apply2_money);
|
||||
$('#approach_review_verification_cost').val(approach.apply2_money);
|
||||
$('#approach_review_fact_verification_cost').val(approach.apply2_money);
|
||||
$('#approach_review_market_name').val(approach.market_name);
|
||||
$('#approach_review_customer_id').val(approach.customer_id);
|
||||
$('#approach_review_customer_id_text').val(approach.customer_name);
|
||||
$('#customer_region_region_id').val(approach.region_id);
|
||||
$('#customer_region_region_id_text').val(approach.region_name);
|
||||
|
||||
grid.api.setRowData([]);
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
var row = rows[i];
|
||||
row.is_store = 1;
|
||||
grid.api.memoryStore.create(row);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
|
@ -0,0 +1,34 @@
|
|||
<style>
|
||||
.modal-body { overflow:hidden; }
|
||||
</style>
|
||||
|
||||
<div id="approach_fee_detail" class="ag-theme-balham" style="width:100%;height:280px;"></div>
|
||||
|
||||
<script>
|
||||
(function($) {
|
||||
var gridDiv = document.querySelector("#approach_fee_detail");
|
||||
var grid = new agGridOptions();
|
||||
grid.remoteDataUrl = '{{url()}}';
|
||||
|
||||
var params = JSON.parse('{{json_encode($query)}}');
|
||||
|
||||
grid.remoteParams = params;
|
||||
grid.rowMultiSelectWithClick = false;
|
||||
grid.rowSelection = 'multiple';
|
||||
// grid.autoColumnsToFit = false;
|
||||
grid.defaultColDef.suppressMenu = true;
|
||||
grid.defaultColDef.sortable = false;
|
||||
grid.columnDefs = [
|
||||
{cellClass:'text-center', headerName: '', type: 'sn', width: 40},
|
||||
{cellClass:'text-center', field: 'sn', headerName: '单据编号', minWidth: 140},
|
||||
{cellClass:'text-center', field: 'date', headerName: '兑现日期', width: 100},
|
||||
{cellClass:'text-right', field:'verification_cost', headerName: '兑现金额', width: 100},
|
||||
{cellClass:'text-center', field: 'id', headerName: 'ID', width: 60}
|
||||
];
|
||||
|
||||
new agGrid.Grid(gridDiv, grid);
|
||||
// 读取数据
|
||||
grid.remoteData();
|
||||
|
||||
})(jQuery);
|
||||
</script>
|
|
@ -0,0 +1,59 @@
|
|||
{{$header["js"]}}
|
||||
|
||||
<div class="panel no-border" id="{{$header['master_table']}}-controller">
|
||||
@include('headers')
|
||||
<div class='list-jqgrid'>
|
||||
<div id="{{$header['master_table']}}-grid" style="width:100%;" class="ag-theme-balham"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function ($) {
|
||||
var table = '{{$header["master_table"]}}';
|
||||
var config = gdoo.grids[table];
|
||||
var action = config.action;
|
||||
var search = config.search;
|
||||
|
||||
action.dialogType = 'layer';
|
||||
|
||||
// 自定义搜索方法
|
||||
search.searchInit = function (e) {
|
||||
var self = this;
|
||||
}
|
||||
|
||||
var options = new agGridOptions();
|
||||
var gridDiv = document.querySelector("#{{$header['master_table']}}-grid");
|
||||
gridDiv.style.height = getPanelHeight(48);
|
||||
|
||||
options.remoteDataUrl = '{{url()}}';
|
||||
options.remoteParams = search.advanced.query;
|
||||
options.columnDefs = config.cols;
|
||||
options.onRowDoubleClicked = function (params) {
|
||||
if (params.node.rowPinned) {
|
||||
return;
|
||||
}
|
||||
if (params.data == undefined) {
|
||||
return;
|
||||
}
|
||||
if (params.data.master_id > 0) {
|
||||
action.show(params.data);
|
||||
}
|
||||
};
|
||||
|
||||
new agGrid.Grid(gridDiv, options);
|
||||
|
||||
// 读取数据
|
||||
options.remoteData({page: 1});
|
||||
|
||||
// 绑定自定义事件
|
||||
var $gridDiv = $(gridDiv);
|
||||
$gridDiv.on('click', '[data-toggle="event"]', function () {
|
||||
var data = $(this).data();
|
||||
if (data.master_id > 0) {
|
||||
action[data.action](data);
|
||||
}
|
||||
});
|
||||
config.grid = options;
|
||||
})(jQuery);
|
||||
|
||||
</script>
|
||||
@include('footers')
|
|
@ -0,0 +1 @@
|
|||
{{$form['tpl']}}
|
|
@ -0,0 +1,202 @@
|
|||
<?php namespace Gdoo\Article\Controllers;
|
||||
|
||||
use Arr;
|
||||
use DB;
|
||||
use Auth;
|
||||
use Request;
|
||||
use Validator;
|
||||
|
||||
use Gdoo\Model\Form;
|
||||
use Gdoo\Model\Grid;
|
||||
|
||||
use Gdoo\User\Models\User;
|
||||
use Gdoo\User\Models\Role;
|
||||
use Gdoo\Article\Models\Article;
|
||||
|
||||
use Gdoo\Index\Services\AttachmentService;
|
||||
|
||||
use Gdoo\Index\Controllers\DefaultController;
|
||||
use Gdoo\User\Models\Department;
|
||||
use Gdoo\User\Services\UserService;
|
||||
|
||||
class ArticleController extends DefaultController
|
||||
{
|
||||
public $permission = [];
|
||||
|
||||
public function indexAction()
|
||||
{
|
||||
$header = Grid::header([
|
||||
'code' => 'article',
|
||||
'referer' => 1,
|
||||
'search' => ['by' => '', 'tab' => 'all'],
|
||||
]);
|
||||
|
||||
$cols = $header['cols'];
|
||||
|
||||
$cols['actions']['options'] = [[
|
||||
'name' => '显示',
|
||||
'action' => 'show',
|
||||
'display' => $this->access['show'],
|
||||
],[
|
||||
'name' => '编辑',
|
||||
'action' => 'edit',
|
||||
'display' => $this->access['edit'],
|
||||
]];
|
||||
|
||||
$search = $header['search_form'];
|
||||
$query = $search['query'];
|
||||
|
||||
if (Request::method() == 'POST') {
|
||||
$model = DB::table($header['table'])->setBy($header);
|
||||
foreach ($header['join'] as $join) {
|
||||
$model->leftJoin($join[0], $join[1], $join[2], $join[3]);
|
||||
}
|
||||
$model->orderBy($header['sort'], $header['order']);
|
||||
|
||||
foreach ($search['where'] as $where) {
|
||||
if ($where['active']) {
|
||||
$model->search($where);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->access['index'] < 4) {
|
||||
// 这里需要包括创建者权限
|
||||
$model->permission('receive_id', null, false, true, false, 'created_id');
|
||||
}
|
||||
|
||||
// 查询是否已经阅读
|
||||
$reader = function ($q) {
|
||||
$q->selectRaw('1')
|
||||
->from('article_reader')
|
||||
->whereRaw('article_reader.article_id = article.id')
|
||||
->where('article_reader.created_id', auth()->id());
|
||||
};
|
||||
if ($query['tab'] == 'done') {
|
||||
$model->whereExists($reader);
|
||||
}
|
||||
if ($query['tab'] == 'unread') {
|
||||
$model->whereNotExists($reader);
|
||||
}
|
||||
|
||||
$model->select($header['select']);
|
||||
$rows = $model->paginate($query['limit'])->appends($query);
|
||||
$items = Grid::dataFilters($rows, $header, function($item) {
|
||||
return $item;
|
||||
});
|
||||
return $items->toJson();
|
||||
}
|
||||
|
||||
$header['buttons'] = [
|
||||
['name' => '删除', 'icon' => 'fa-remove', 'action' => 'delete', 'display' => $this->access['delete']],
|
||||
['name' => '导出', 'icon' => 'fa-share', 'action' => 'export', 'display' => 1],
|
||||
];
|
||||
|
||||
$header['cols'] = $cols;
|
||||
$header['tabs'] = Article::$tabs;
|
||||
$header['bys'] = Article::$bys;
|
||||
$header['js'] = Grid::js($header);
|
||||
|
||||
return $this->display([
|
||||
'header' => $header,
|
||||
]);
|
||||
}
|
||||
|
||||
// 新建客户联系人
|
||||
public function createAction()
|
||||
{
|
||||
$id = (int)Request::get('id');
|
||||
$form = Form::make(['code' => 'article', 'id' => $id]);
|
||||
return $this->display([
|
||||
'form' => $form,
|
||||
], 'create');
|
||||
}
|
||||
|
||||
// 创建客户联系人
|
||||
public function editAction()
|
||||
{
|
||||
return $this->createAction();
|
||||
}
|
||||
|
||||
public function showAction()
|
||||
{
|
||||
$id = (int)Request::get('id');
|
||||
|
||||
$res = Article::withAt('user', ['id','username','name'])
|
||||
->where('id', $id)->first();
|
||||
|
||||
// 发布人
|
||||
$from = DB::table('user')->where('id', $res['created_id'])->first();
|
||||
|
||||
// 附件
|
||||
$attachment = AttachmentService::show($res['attachment']);
|
||||
|
||||
// 已读记录
|
||||
$reads = DB::table('article_reader')->where('article_id', $id)->get();
|
||||
$reads = array_by($reads, 'created_id');
|
||||
|
||||
// 更新阅读记录
|
||||
if (empty($reads[Auth::id()])) {
|
||||
DB::table('article_reader')->insert([
|
||||
'article_id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
// 返回json
|
||||
if (Request::wantsJson()) {
|
||||
return $res->toJson();
|
||||
}
|
||||
|
||||
$form = Form::make(['code' => 'article', 'id' => $id, 'action' => 'show']);
|
||||
return $this->display([
|
||||
'attachment' => $attachment,
|
||||
'res' => $res,
|
||||
'from' => $from,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 阅读记录
|
||||
*/
|
||||
public function readerAction()
|
||||
{
|
||||
$id = Request::get('id', 0);
|
||||
|
||||
// 取得当前项目阅读情况
|
||||
$reads = DB::table('article_reader')->where('article_id', $id)->get();
|
||||
$reads = array_by($reads, 'created_id');
|
||||
$row = DB::table('article')->where('id', $id)->first();
|
||||
$scopes = UserService::getDRU($row['receive_id']);
|
||||
if ($scopes->count()) {
|
||||
$rows = [];
|
||||
$departments = Department::orderBy('lft', 'asc')->pluck('name', 'id');
|
||||
foreach ($scopes as $scope) {
|
||||
$read = isset($reads[$scope['id']]) ? 1 : 0;
|
||||
$rows['total'][$read]++;
|
||||
$rows['data'][] = [
|
||||
'read' => $read,
|
||||
'department_id' => $scope['department_id'],
|
||||
'department' => $departments[$scope['department_id']],
|
||||
'name' => $scope['name'],
|
||||
'created_at' => $reads[$scope['id']]['created_at'],
|
||||
];
|
||||
}
|
||||
|
||||
$rows['data'] = Arr::sort($rows['data'], function ($value) {
|
||||
return $value['created_at'];
|
||||
});
|
||||
}
|
||||
|
||||
return $this->render([
|
||||
'rows' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteAction()
|
||||
{
|
||||
if (Request::method() == 'POST') {
|
||||
$ids = Request::get('id');
|
||||
return Form::remove(['code' => 'article', 'ids' => $ids]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php namespace Gdoo\Article\Controllers;
|
||||
|
||||
use DB;
|
||||
use Auth;
|
||||
use Request;
|
||||
use Gdoo\Index\Controllers\DefaultController;
|
||||
use Gdoo\Index\Services\InfoService;
|
||||
use Gdoo\User\Models\UserWidget;
|
||||
|
||||
class WidgetController extends DefaultController
|
||||
{
|
||||
public $permission = ['index', 'info'];
|
||||
|
||||
/**
|
||||
* 公告部件
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
if (Request::isJson()) {
|
||||
$model = DB::table('article')
|
||||
->permission('receive_id')
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
// 查询是否已经阅读
|
||||
$reader = function ($q) {
|
||||
$q->selectRaw('1')
|
||||
->from('article_reader')
|
||||
->whereRaw('article_reader.article_id = article.id')
|
||||
->where('article_reader.created_id', auth()->id());
|
||||
};
|
||||
$model->whereNotExists($reader);
|
||||
|
||||
$rows = $model->get(['id', 'title', 'created_at']);
|
||||
|
||||
$json['total'] = sizeof($rows);
|
||||
$json['data'] = $rows;
|
||||
return response()->json($json);
|
||||
}
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* 公告信息
|
||||
*/
|
||||
public function infoAction()
|
||||
{
|
||||
$config = InfoService::getInfo('article');
|
||||
|
||||
$count = DB::table('article')
|
||||
->permission('receive_id')
|
||||
->whereNotExists(function ($q) {
|
||||
$q->selectRaw('1')
|
||||
->from('article_reader')
|
||||
->whereRaw('article_reader.article_id = article.id')
|
||||
->where('article_reader.created_id', auth()->id());
|
||||
})->whereRaw('('.$config['sql'].')')->count();
|
||||
|
||||
$count2 = DB::table('article')
|
||||
->permission('receive_id')
|
||||
->whereNotExists(function ($q) {
|
||||
$q->selectRaw('1')
|
||||
->from('article_reader')
|
||||
->whereRaw('article_reader.article_id = article.id')
|
||||
->where('article_reader.created_id', auth()->id());
|
||||
})->whereRaw('('.$config['sql2'].')')->count();
|
||||
|
||||
$rate = 0;
|
||||
if ($count2 > 0) {
|
||||
$rate = $count / $count2 * 100;
|
||||
}
|
||||
$res = [
|
||||
'count' => $count,
|
||||
'count2' => $count2,
|
||||
'rate' => $rate,
|
||||
];
|
||||
return $this->render([
|
||||
'dates' => $config['dates'],
|
||||
'info' => $config['info'],
|
||||
'res' => $res,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php namespace Gdoo\Article\Hooks;
|
||||
|
||||
use DB;
|
||||
use Gdoo\Index\Services\AttachmentService;
|
||||
|
||||
class ArticleHook
|
||||
{
|
||||
public function onBeforeForm($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onAfterForm($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeStore($params)
|
||||
{
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onAfterStore($params) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeDelete($params) {
|
||||
$ids = $params['ids'];
|
||||
$masters = $params['masters'];
|
||||
// 新删除附件
|
||||
foreach($masters as $master) {
|
||||
AttachmentService::remove($master['attachment']);
|
||||
}
|
||||
// 删除阅读积累
|
||||
DB::table('article_reader')->whereIn('article_id', $ids)->delete();
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onBeforeImport($params)
|
||||
{
|
||||
return $params;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php namespace Gdoo\Article\Models;
|
||||
|
||||
use Gdoo\Index\Models\BaseModel;
|
||||
|
||||
class Article extends BaseModel
|
||||
{
|
||||
protected $table = 'article';
|
||||
|
||||
public static $tabs = [
|
||||
'name' => 'tab',
|
||||
'items' => [
|
||||
['url' => 'article/article/index', 'name' => '未读', 'value' => 'unread'],
|
||||
['url' => 'article/article/index', 'name' => '已读', 'value' => 'done'],
|
||||
['url' => 'article/article/index', 'name' => '全部', 'value' => 'all'],
|
||||
]
|
||||
];
|
||||
|
||||
public static $bys = [
|
||||
'name' => 'by',
|
||||
'items' => [
|
||||
['value' => '', 'name' => '全部'],
|
||||
['value' => 'divider'],
|
||||
['value' => 'day', 'name' => '今日创建'],
|
||||
['value' => 'week', 'name' => '本周创建'],
|
||||
['value' => 'month', 'name' => '本月创建'],
|
||||
]
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('Gdoo\User\Models\User', 'created_id');
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo('Gdoo\Article\Models\ArticleCategory');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php namespace Gdoo\Article\Models;
|
||||
|
||||
use Gdoo\Index\Models\BaseModel;
|
||||
|
||||
class ArticleCategory extends BaseModel
|
||||
{
|
||||
protected $table = 'article_category';
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php namespace Gdoo\Article\Services;
|
||||
|
||||
use DB;
|
||||
|
||||
class ArticleService
|
||||
{
|
||||
/**
|
||||
* 获取未读公告
|
||||
*/
|
||||
public static function getBadge()
|
||||
{
|
||||
$rows = DB::table('article')
|
||||
->permission('receive_id')
|
||||
->whereNotExists(function ($q) {
|
||||
$q->selectRaw('1')
|
||||
->from('article_reader')
|
||||
->whereRaw('article_reader.article_id = article.id')
|
||||
->where('article_reader.created_id', auth()->id());
|
||||
})->get();
|
||||
$ret['total'] = sizeof($rows);
|
||||
$ret['data'] = $rows;
|
||||
return $ret;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
return [
|
||||
"name" => "公告",
|
||||
"version" => "1.0",
|
||||
"description" => "公告。",
|
||||
"listens" => [
|
||||
'article' => 'Gdoo\Article\Hooks\ArticleHook',
|
||||
],
|
||||
'widgets' => [
|
||||
'widget_article_index' => [
|
||||
'name' => '最新公告',
|
||||
'type' => 1,
|
||||
'url' => 'article/widget/index',
|
||||
'more_url' => 'article/article/index',
|
||||
],
|
||||
'info_article_index' => [
|
||||
'name' => '新增公告',
|
||||
'type' => 2,
|
||||
'url' => 'article/widget/info',
|
||||
'more_url' => 'article/article/index',
|
||||
],
|
||||
],
|
||||
'badges' => [
|
||||
'article_article_index' => 'Gdoo\Article\Services\ArticleService::getBadge',
|
||||
],
|
||||
'menus' => [
|
||||
['name' => '资讯', 'id' => 'article'],
|
||||
['name' => '公告列表', 'id' => 'article_article_index', 'parent' => 'article', 'url' => 'article/article/index', 'badge' => 'article_article_index'],
|
||||
],
|
||||
"controllers" => [
|
||||
"article" => [
|
||||
"name" => "公告",
|
||||
"actions" => [
|
||||
"index" => [
|
||||
"name" => "列表"
|
||||
],
|
||||
"create" => [
|
||||
"name" => "新建"
|
||||
],
|
||||
"edit" => [
|
||||
"name" => "编辑"
|
||||
],
|
||||
"show" => [
|
||||
"name" => "查看"
|
||||
],
|
||||
"reader" => [
|
||||
"name" => "阅读记录"
|
||||
],
|
||||
"delete" => [
|
||||
"name" => "删除"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
|
@ -0,0 +1,16 @@
|
|||
<div class="form-panel">
|
||||
<div class="form-panel-header">
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
{{$form['btn']}}
|
||||
</div>
|
||||
<div class="form-panel-body">
|
||||
<form class="form-horizontal form-controller" method="post" id="{{$form['table']}}" name="{{$form['table']}}">
|
||||
{{$form['tpl']}}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var table = '{{$form["table"]}}';
|
||||
</script>
|
|
@ -0,0 +1,59 @@
|
|||
{{$header["js"]}}
|
||||
|
||||
<div class="panel no-border" id="{{$header['master_table']}}-controller">
|
||||
@include('headers')
|
||||
<div class='list-jqgrid'>
|
||||
<div id="{{$header['master_table']}}-grid" style="width:100%;" class="ag-theme-balham"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function ($) {
|
||||
var table = '{{$header["master_table"]}}';
|
||||
var config = gdoo.grids[table];
|
||||
var action = config.action;
|
||||
var search = config.search;
|
||||
|
||||
action.dialogType = 'layer';
|
||||
|
||||
// 自定义搜索方法
|
||||
search.searchInit = function (e) {
|
||||
var self = this;
|
||||
}
|
||||
|
||||
var options = new agGridOptions();
|
||||
var gridDiv = document.querySelector("#{{$header['master_table']}}-grid");
|
||||
gridDiv.style.height = getPanelHeight(48);
|
||||
|
||||
options.remoteDataUrl = '{{url()}}';
|
||||
options.remoteParams = search.advanced.query;
|
||||
options.columnDefs = config.cols;
|
||||
options.onRowDoubleClicked = function (params) {
|
||||
if (params.node.rowPinned) {
|
||||
return;
|
||||
}
|
||||
if (params.data == undefined) {
|
||||
return;
|
||||
}
|
||||
if (params.data.master_id > 0) {
|
||||
action.show(params.data);
|
||||
}
|
||||
};
|
||||
|
||||
new agGrid.Grid(gridDiv, options);
|
||||
|
||||
// 读取数据
|
||||
options.remoteData({page: 1});
|
||||
|
||||
// 绑定自定义事件
|
||||
var $gridDiv = $(gridDiv);
|
||||
$gridDiv.on('click', '[data-toggle="event"]', function () {
|
||||
var data = $(this).data();
|
||||
if (data.master_id > 0) {
|
||||
action[data.action](data);
|
||||
}
|
||||
});
|
||||
config.grid = options;
|
||||
})(jQuery);
|
||||
|
||||
</script>
|
||||
@include('footers')
|
|
@ -0,0 +1,23 @@
|
|||
<form id="search-form" class="form-inline" name="mysearch" action="{{url()}}" method="get">
|
||||
<div class="pull-right">
|
||||
@if(isset($access['delete']))
|
||||
<a class="btn btn-sm btn-danger" href="javascript:optionDelete('#myform','{{url('delete')}}');"><i class="icon icon-remove"></i> 删除</a>
|
||||
@endif
|
||||
</div>
|
||||
@if(isset($access['create']))
|
||||
<a href="{{url('create')}}" class="btn btn-sm btn-info"><i class="icon icon-plus"></i> 新建</a>
|
||||
@endif
|
||||
@include('searchForm')
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
$('#search-form').searchForm({
|
||||
data: {{json_encode($search['forms'])}},
|
||||
init: function(e) {
|
||||
var self = this;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,43 @@
|
|||
<table class="table m-b-none">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" colspan="4">
|
||||
已读 <span class="badge bg-info">{{(int)$rows['total'][1]}}</span>
|
||||
|
||||
未读 <span class="badge">{{(int)$rows['total'][0]}}</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th align="center">
|
||||
序号
|
||||
</th>
|
||||
<th align="left">
|
||||
阅读人
|
||||
</th>
|
||||
<th align="center">
|
||||
部门
|
||||
</th>
|
||||
<th align="center">
|
||||
阅读时间
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@if($rows['data'])
|
||||
@foreach($rows['data'] as $row)
|
||||
<tr>
|
||||
<td align="center">
|
||||
{{$loop->index + 1}}
|
||||
</td>
|
||||
<td align="left">
|
||||
{{$row['name']}}
|
||||
</td>
|
||||
<td align="center">
|
||||
{{$row['department']}}
|
||||
</td>
|
||||
<td align="center">
|
||||
@datetime($row['created_at'])
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
</table>
|
|
@ -0,0 +1,38 @@
|
|||
<style>
|
||||
.content-body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="form-panel">
|
||||
<div class="form-panel-header">
|
||||
<div class="pull-right"></div>
|
||||
{{$form['btn']}}
|
||||
@if(isset($access['reader']))
|
||||
<button type="button" onclick="viewBox('reader', '阅读记录', '{{url('reader',['id' => $res['id']])}}')" class="btn btn-sm btn-default"><i class="icon icon-eye-open"></i> 阅读记录</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="form-panel-body">
|
||||
<div class="wrapper-sm">
|
||||
<div class="panel">
|
||||
<div class="panel-heading b-b b-light text-center">
|
||||
<h3 class="m-xs m-l-none">
|
||||
{{$res['title']}}
|
||||
</h3>
|
||||
<small class="text-muted">
|
||||
发布人: {{get_user($res['created_id'], 'name')}}
|
||||
|
||||
发布时间: @datetime($res['created_at'])
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="panel-body text-base">
|
||||
{{$res['content']}}
|
||||
</div>
|
||||
|
||||
<div class="wrapper-sm">
|
||||
@include('attachment/view')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,32 @@
|
|||
<table id="widget-article-index">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field="title" data-formatter="titleFormatter" data-align="left">标题</th>
|
||||
<th data-field="created_at" data-width="200" data-formatter="datetimeFormatter" data-sortable="true" data-align="center">发布时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
|
||||
function datetimeFormatter(value, row) {
|
||||
return format_datetime(value);
|
||||
}
|
||||
|
||||
function titleFormatter(value, row) {
|
||||
return '<a href="'+app.url('article/article/view', {id: row.id})+'">' + value + '</a>';
|
||||
}
|
||||
|
||||
(function($) {
|
||||
var $table = $('#widget-article-index');
|
||||
$table.bootstrapTable({
|
||||
sidePagination: 'server',
|
||||
showColumns: false,
|
||||
showHeader: false,
|
||||
height: 200,
|
||||
pagination: false,
|
||||
url: '{{url("article/widget/index")}}',
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
</script>
|
|
@ -0,0 +1,15 @@
|
|||
<div class="panel panel-shadow info-skin1">
|
||||
<div class="info-l hidden-xs" style="background-color:{{$info['color']}}">
|
||||
<i class="fa fa-2x {{$info['icon']}}"></i>
|
||||
</div>
|
||||
<div class="info-c">
|
||||
<div class="info-name">{{$info['name']}}</div>
|
||||
<a href="javascript:;" data-toggle="addtab" data-url="{{$info['more_url']}}" data-id="{{str_replace(['/', '?', '='], ['_', '_', '_'], $info['more_url'])}}" data-name="{{$info['name']}}">
|
||||
<div class="text-info info-item" data-id="{{$info['id']}}" data-more_url="{{$info['more_url']}}">{{$res['count']}}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="info-r">
|
||||
<div>较{{$dates[$info['params']['date']]}}</div>
|
||||
<div class="rate @if($res['rate'] > 100) red @endif">{{$res['rate']}}%</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,135 @@
|
|||
<?php namespace Gdoo\Calendar\Controllers;
|
||||
|
||||
use Auth;
|
||||
use Request;
|
||||
|
||||
use Gdoo\Calendar\Services\CalendarService;
|
||||
|
||||
use Gdoo\User\Models\Department;
|
||||
use Gdoo\User\Models\User;
|
||||
|
||||
use Gdoo\Index\Controllers\DefaultController;
|
||||
|
||||
class CalendarController extends DefaultController
|
||||
{
|
||||
public $permission = ['calendars', 'help'];
|
||||
|
||||
/**
|
||||
* 显示日历
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$user_id = Request::get('user_id', Auth::id());
|
||||
|
||||
// 获取下属用户列表
|
||||
$users = User::where('status', 1)->where('leader_id', $user_id)->get(['id', 'department_id', 'name']);
|
||||
$departments = Department::orderBy('lft', 'asc')->get()->toNested()->toArray();
|
||||
$underling = array();
|
||||
foreach ($users as $row) {
|
||||
$underling['role'][$row['department_id']] = $departments[$row['department_id']];
|
||||
$underling['user'][$row['department_id']][$row['id']] = $row;
|
||||
}
|
||||
$user = User::find($user_id);
|
||||
|
||||
return $this->display([
|
||||
'user' => $user,
|
||||
'underling' => $underling,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日历列表
|
||||
*/
|
||||
public function calendarsAction()
|
||||
{
|
||||
$user_id = Request::get('user_id', Auth::id());
|
||||
$calendars = CalendarService::getCalendars($user_id);
|
||||
|
||||
$calendars[] = [
|
||||
'id' => 'shared',
|
||||
'displayname' => '共享事件',
|
||||
'calendarcolor' => '#999',
|
||||
];
|
||||
$sources = [];
|
||||
foreach ($calendars as $calendar) {
|
||||
if ($calendar['id'] == 'shared') {
|
||||
$url = url('event/share', ['user_id'=>$user_id]);
|
||||
} else {
|
||||
$url = url('event/index', ['calendar_id'=>$calendar['id']]);
|
||||
}
|
||||
$sources[] = [
|
||||
'url' => $url,
|
||||
'id' => $calendar['id'],
|
||||
'userid' => $calendar['userid'],
|
||||
'backgroundColor' => $calendar['calendarcolor'],
|
||||
"borderColor" => $calendar['calendarcolor'],
|
||||
];
|
||||
}
|
||||
return $this->json([
|
||||
'calendars' => $calendars,
|
||||
'sources' => $sources,
|
||||
], true);
|
||||
}
|
||||
|
||||
public function activeAction()
|
||||
{
|
||||
if (Request::method() == 'POST') {
|
||||
$gets = Request::all();
|
||||
$calendar = CalendarService::getCalendar($gets['id'], true);
|
||||
if ($calendar) {
|
||||
try {
|
||||
CalendarService::setCalendarActive($gets['id'], $gets['active']);
|
||||
} catch (\Exception $e) {
|
||||
return $this->json($e->getMessage());
|
||||
}
|
||||
/*} else {
|
||||
return $this->json('permission denied');
|
||||
*/
|
||||
}
|
||||
$calendar = CalendarService::getCalendar($gets['id'], false);
|
||||
return $this->json([
|
||||
'active' => $gets['active'],
|
||||
'eventSource' => array(
|
||||
'id' => $calendar['id'],
|
||||
'url' => url('event/index', ['calendar_id' => $calendar['id']]),
|
||||
'backgroundColor' => $calendar['calendarcolor'],
|
||||
"borderColor" => $calendar['calendarcolor'],
|
||||
)
|
||||
], true);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加日历
|
||||
public function addAction()
|
||||
{
|
||||
$gets = Request::all();
|
||||
if (Request::method() == 'POST') {
|
||||
if ($gets['id'] > 0) {
|
||||
$id = CalendarService::editCalendar($gets['id'], $gets['displayname'], null, null, null, $gets['calendarcolor']);
|
||||
} else {
|
||||
$id = CalendarService::addCalendar(Auth::id(), $gets['displayname'], 'VEVENT,VTODO,VJOURNAL', null, 0, $gets['calendarcolor']);
|
||||
}
|
||||
return $this->json(['id' => $id], true);
|
||||
}
|
||||
$calendar = CalendarService::getCalendar((int)$gets['id']);
|
||||
return $this->render(array(
|
||||
'calendar' => $calendar,
|
||||
));
|
||||
}
|
||||
|
||||
// 帮助信息
|
||||
public function helpAction()
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
// 删除日历
|
||||
public function deleteAction()
|
||||
{
|
||||
$id = Request::get('id');
|
||||
if ($id > 0) {
|
||||
CalendarService::deleteCalendar($id);
|
||||
return $this->json(['id'=>$id], true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,749 @@
|
|||
<?php namespace Gdoo\Calendar\Controllers;
|
||||
|
||||
use Auth;
|
||||
use DB;
|
||||
use Request;
|
||||
|
||||
use App\Support\VObject;
|
||||
|
||||
use Gdoo\Calendar\Services\CalendarService;
|
||||
use Gdoo\Calendar\Services\CalendarObjectService;
|
||||
|
||||
use Gdoo\Index\Services\ShareService;
|
||||
use Gdoo\Index\Services\AttachmentService;
|
||||
|
||||
use Gdoo\Index\Controllers\DefaultController;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class EventController extends DefaultController
|
||||
{
|
||||
public $permission = ['share', 'items'];
|
||||
|
||||
// 事件对象
|
||||
public function indexAction()
|
||||
{
|
||||
$gets = Request::all();
|
||||
$calendars = CalendarService::getCalendars($gets['user_id']);
|
||||
$ids = [];
|
||||
foreach ($calendars as $calendar) {
|
||||
$ids[] = $calendar['id'];
|
||||
}
|
||||
|
||||
$cals = $calendars->keyBy('id');
|
||||
|
||||
// 普通事件
|
||||
$rows = CalendarService::getRangeEvents($ids, $gets['start'], $gets['end']);
|
||||
foreach ($rows as $row) {
|
||||
$calendar = $cals[$row['calendarid']];
|
||||
if ($calendar['active'] == 1) {
|
||||
$row['backgroundColor'] = $calendar['calendarcolor'];
|
||||
$row['borderColor'] = $calendar['calendarcolor'];
|
||||
$row['userid'] = $calendar['userid'];
|
||||
$events[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取共享事件
|
||||
$shared = ShareService::getItemsSourceBy(['event'], $gets['user_id']);
|
||||
$share_id = Arr::pluck($shared, 'source_id');
|
||||
if (count($share_id)) {
|
||||
$share = Arr::pluck($shared, 'name', 'source_id');
|
||||
$rows = CalendarService::getRangeEvents($share_id, $gets['start'], $gets['end'], true);
|
||||
foreach ($rows as $row) {
|
||||
$row['title'] = '['.$share[$row['id']].']'.$row['title'];
|
||||
$row['backgroundColor'] = '#666666';
|
||||
$row['borderColor'] = '#666666';
|
||||
$row['shared'] = true;
|
||||
$events[] = $row;
|
||||
}
|
||||
}
|
||||
return response()->json($events);
|
||||
}
|
||||
|
||||
// 客户端显示事件列表
|
||||
public function itemsAction()
|
||||
{
|
||||
$gets = Request::all();
|
||||
$start = strtotime($gets['start']);
|
||||
$end = strtotime($gets['end']) + 86400;
|
||||
|
||||
$items = [];
|
||||
|
||||
// 读取共享事件
|
||||
$shared = ShareService::getItemsSourceBy(['event'], auth()->id());
|
||||
$share_id = Arr::pluck($shared, 'source_id');
|
||||
$share = Arr::pluck($shared, 'name', 'source_id');
|
||||
|
||||
$events = CalendarService::getRangeEvents($share_id, $gets['start'], $gets['end'], true);
|
||||
|
||||
foreach ($events as $key => $row) {
|
||||
$master = [
|
||||
'id' => $row['id'],
|
||||
'title' => '['.$share[$row['id']].']'.$row['title'],
|
||||
'start' => $row['start'],
|
||||
'end' => $row['end'],
|
||||
'allday' => $row['allDay'],
|
||||
'calendar' => [
|
||||
'color' => '#666666',
|
||||
'name' => '共享事件',
|
||||
],
|
||||
];
|
||||
|
||||
$repeat = CalendarObjectService::getEventRepeat($master, '1D', 'Y-n-j');
|
||||
$items = array_merge($items, $repeat);
|
||||
}
|
||||
|
||||
// 读取正常事件
|
||||
$rows = DB::table('calendar_object')
|
||||
->leftJoin('calendar', 'calendar_object.calendarid', '=', 'calendar.id')
|
||||
->where('calendar.userid', auth()->id())
|
||||
->whereRaw('(
|
||||
(calendar_object.firstoccurence between '.$start.' and '.$end.' or calendar_object.lastoccurence between '.$start.' and '.$end.')
|
||||
or (calendar_object.rrule = 1 and calendar_object.firstoccurence <= '.$end.')
|
||||
)')
|
||||
->get(['calendar_object.*','calendar.calendarcolor','calendar.displayname']);
|
||||
|
||||
foreach ($rows as $key => $row) {
|
||||
$vcalendar = \Sabre\VObject\Reader::read($row['calendardata']);
|
||||
$allday = ($vcalendar->VEVENT->DTSTART->getDateType() == \Sabre\VObject\Property\DateTime::DATE) ? true : false;
|
||||
$master = [
|
||||
'id' => $row['id'],
|
||||
'title' => $vcalendar->VEVENT->SUMMARY->value,
|
||||
'start' => $vcalendar->VEVENT->DTSTART->value,
|
||||
'end' => $vcalendar->VEVENT->DTEND->value,
|
||||
'allday' => $allday,
|
||||
'calendar' => [
|
||||
'color' => $row['calendarcolor'],
|
||||
'name' => $row['displayname'],
|
||||
],
|
||||
];
|
||||
$repeat = CalendarObjectService::getEventRepeat($master, '1D', 'Y-n-j');
|
||||
$items = array_merge($items, $repeat);
|
||||
}
|
||||
return response()->json($items);
|
||||
}
|
||||
|
||||
// 调整事件
|
||||
public function resizeAction()
|
||||
{
|
||||
if (Request::method() == 'POST') {
|
||||
$gets = Request::all();
|
||||
$event = CalendarService::getEvent($gets['id']);
|
||||
|
||||
if ($event['lastmodified'] != $gets['lastmodified']) {
|
||||
return $this->json('事件已被修改。');
|
||||
}
|
||||
|
||||
$vcalendar = VObject::parse($event['calendardata']);
|
||||
$vevent = $vcalendar->VEVENT;
|
||||
|
||||
/*
|
||||
$accessclass = $vevent->getAsString('CLASS');
|
||||
$permissions = CalendarService::getPermissions($id, Calendar::EVENT, $accessclass);
|
||||
if(!$permissions & OCP\PERMISSION_UPDATE) {
|
||||
return $this->json('permission denied');
|
||||
}
|
||||
*/
|
||||
|
||||
$delta = new \DateInterval('P0D');
|
||||
$delta->s = $gets['delta'];
|
||||
|
||||
$dtend = CalendarService::getDTEndFromVEvent($vevent);
|
||||
$end_type = $dtend->getDateType();
|
||||
$dtend->setDateTime($dtend->getDateTime()->add($delta), $end_type);
|
||||
unset($vevent->DURATION);
|
||||
|
||||
$vevent->setDateTime('LAST-MODIFIED', 'now', \Sabre\VObject\Property\DateTime::UTC);
|
||||
$vevent->setDateTime('DTSTAMP', 'now', \Sabre\VObject\Property\DateTime::UTC);
|
||||
|
||||
CalendarService::edit($gets['id'], $vcalendar->serialize());
|
||||
|
||||
$lastmodified = $vevent->__get('LAST-MODIFIED')->getDateTime();
|
||||
return $this->json(['lastmodified' => $lastmodified->format('U')], true);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动事件
|
||||
public function moveAction()
|
||||
{
|
||||
if (Request::method() == 'POST') {
|
||||
$gets = Request::all();
|
||||
$event = CalendarService::getEvent($gets['id']);
|
||||
|
||||
if ($event['lastmodified'] != $gets['lastmodified']) {
|
||||
return $this->json('事件已被修改。');
|
||||
}
|
||||
|
||||
$vcalendar = VObject::parse($event['calendardata']);
|
||||
$vevent = $vcalendar->VEVENT;
|
||||
|
||||
$allday = $gets['allday'] == 'true' ? 1 : 0;
|
||||
$delta = new \DateInterval('P0D');
|
||||
$delta->s = $gets['delta'];
|
||||
|
||||
$dtstart = $vevent->DTSTART;
|
||||
$dtend = CalendarService::getDTEndFromVEvent($vevent);
|
||||
$start_type = $dtstart->getDateType();
|
||||
$end_type = $dtend->getDateType();
|
||||
|
||||
if ($allday && $start_type != \Sabre\VObject\Property\DateTime::DATE) {
|
||||
$start_type = $end_type = \Sabre\VObject\Property\DateTime::DATE;
|
||||
$dtend->setDateTime($dtend->getDateTime()->modify('+1 day'), $end_type);
|
||||
}
|
||||
|
||||
if (!$allday && $start_type == \Sabre\VObject\Property\DateTime::DATE) {
|
||||
$start_type = $end_type = \Sabre\VObject\Property\DateTime::LOCALTZ;
|
||||
}
|
||||
|
||||
$dtstart->setDateTime($dtstart->getDateTime()->add($delta), $start_type);
|
||||
$dtend->setDateTime($dtend->getDateTime()->add($delta), $end_type);
|
||||
unset($vevent->DURATION);
|
||||
|
||||
$vevent->setDateTime('LAST-MODIFIED', 'now', \Sabre\VObject\Property\DateTime::UTC);
|
||||
$vevent->setDateTime('DTSTAMP', 'now', \Sabre\VObject\Property\DateTime::UTC);
|
||||
|
||||
try {
|
||||
CalendarService::edit($gets['id'], $vcalendar->serialize());
|
||||
} catch (\Exception $e) {
|
||||
return $this->json($e->getMessage());
|
||||
}
|
||||
$lastmodified = $vevent->__get('LAST-MODIFIED')->getDateTime();
|
||||
return $this->json(['lastmodified' => $lastmodified->format('U')], true);
|
||||
}
|
||||
}
|
||||
|
||||
public function addAction()
|
||||
{
|
||||
$gets = Request::all();
|
||||
// 更新数据
|
||||
if (Request::method() == 'POST') {
|
||||
$error = CalendarService::validateRequest($gets);
|
||||
if ($error) {
|
||||
return $this->json($error);
|
||||
}
|
||||
$vcalendar = CalendarService::createVCalendarFromRequest($gets);
|
||||
try {
|
||||
$attachment = join(',', array_filter((array)$gets['attachment']));
|
||||
$id = CalendarService::add($gets['calendarid'], $vcalendar->serialize(), $attachment);
|
||||
AttachmentService::publish($gets['attachment']);
|
||||
|
||||
// 写入共享数据
|
||||
ShareService::addItem(array(
|
||||
'source_id' => $id,
|
||||
'source_type' => 'event',
|
||||
'receive_id' => $gets['receive_id'],
|
||||
'receive_name' => $gets['receive_name'],
|
||||
));
|
||||
|
||||
return $this->json(['id'=>$id], true);
|
||||
} catch (\Exception $e) {
|
||||
return $this->json($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 新增表单
|
||||
if (Request::method() == 'GET') {
|
||||
$start = $gets['start'];
|
||||
$end = $gets['end'];
|
||||
$allday = $gets['allDay'];
|
||||
|
||||
/*
|
||||
if (!$end) {
|
||||
$duration = 60;
|
||||
$end = $start + ($duration * 60);
|
||||
}
|
||||
*/
|
||||
|
||||
$start = new \DateTime('@'.strtotime($start));
|
||||
$end = new \DateTime('@'.strtotime($end));
|
||||
|
||||
$timezone = CalendarService::getTimezone();
|
||||
$start->setTimezone(new \DateTimeZone($timezone));
|
||||
$end->setTimezone(new \DateTimeZone($timezone));
|
||||
|
||||
if ($allday == 'true') {
|
||||
$end->modify('-1 day');
|
||||
}
|
||||
|
||||
$calendar_options = CalendarService::getCalendars(Auth::id(), false);
|
||||
|
||||
/*
|
||||
分享日历暂时未实现
|
||||
foreach($calendars as $calendar)
|
||||
{
|
||||
if($calendar['userid'] != OCP\User::getUser()) {
|
||||
$sharedCalendar = OCP\Share::getItemSharedWithBySource('calendar', $calendar['id']);
|
||||
if ($sharedCalendar && ($sharedCalendar['permissions'] & OCP\PERMISSION_UPDATE)) {
|
||||
array_push($calendar_options, $calendar);
|
||||
}
|
||||
} else {
|
||||
array_push($calendar_options, $calendar);
|
||||
}
|
||||
}*/
|
||||
|
||||
$options['calendar_options'] = $calendar_options;
|
||||
|
||||
$options['access_class_options'] = CalendarService::getAccessClassOptions();
|
||||
$options['valarm_options'] = CalendarService::getValarmOptions();
|
||||
$options['repeat_options'] = CalendarService::getRepeatOptions();
|
||||
$options['repeat_end_options'] = CalendarService::getEndOptions();
|
||||
$options['repeat_month_options'] = CalendarService::getMonthOptions();
|
||||
$options['repeat_year_options'] = CalendarService::getYearOptions();
|
||||
$options['repeat_weekly_options'] = CalendarService::getWeeklyOptions();
|
||||
$options['repeat_weekofmonth_options'] = CalendarService::getWeekofMonth();
|
||||
$options['repeat_byyearday_options'] = CalendarService::getByYearDayOptions();
|
||||
$options['repeat_bymonth_options'] = CalendarService::getByMonthOptions();
|
||||
$options['repeat_byweekno_options'] = CalendarService::getByWeekNoOptions();
|
||||
$options['repeat_bymonthday_options'] = CalendarService::getByMonthDayOptions();
|
||||
|
||||
$options['access'] = 'owner';
|
||||
$options['accessclass'] = 'PUBLIC';
|
||||
$options['startdate'] = $start->format('Y-m-d');
|
||||
$options['starttime'] = $start->format('H:i');
|
||||
$options['enddate'] = $end->format('Y-m-d');
|
||||
$options['endtime'] = $end->format('H:i');
|
||||
$options['allday'] = $allday;
|
||||
$options['valarm'] = '';
|
||||
|
||||
$repeats['repeat'] = 'doesnotrepeat';
|
||||
$repeats['repeat_month'] = 'monthday';
|
||||
$repeats['repeat_weekdays'] = array();
|
||||
$repeats['repeat_interval'] = 1;
|
||||
$repeats['repeat_end'] = 'never';
|
||||
$repeats['repeat_count'] = 10;
|
||||
$repeats['repeat_weekofmonth'] = 'auto';
|
||||
$repeats['repeat_date'] = '';
|
||||
$repeats['repeat_year'] = 'bydate';
|
||||
|
||||
$attachment['model'] = 'calendar_attachment';
|
||||
$attachment['path'] = 'calendar';
|
||||
$attachment['draft'] = AttachmentService::draft(Auth::id());
|
||||
|
||||
return $this->render(array(
|
||||
'attachList' => $attachment,
|
||||
'options' => $options,
|
||||
'repeats' => $repeats,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑事件
|
||||
public function editAction()
|
||||
{
|
||||
$gets = Request::all();
|
||||
|
||||
// 更新数据
|
||||
if (Request::method() == 'POST') {
|
||||
$error = CalendarService::validateRequest($gets);
|
||||
if ($error) {
|
||||
return $this->json($error);
|
||||
}
|
||||
|
||||
$event = CalendarService::getEvent($gets['id']);
|
||||
|
||||
if ($event['lastmodified'] != $gets['lastmodified']) {
|
||||
return $this->json('事件已被修改。', true);
|
||||
}
|
||||
|
||||
$vcalendar = VObject::parse($event['calendardata']);
|
||||
CalendarService::updateVCalendarFromRequest($gets, $vcalendar);
|
||||
try {
|
||||
$attachment = join(',', array_filter((array)$gets['attachment']));
|
||||
CalendarService::edit($gets['id'], $vcalendar->serialize(), $attachment);
|
||||
AttachmentService::publish($gets['attachment']);
|
||||
|
||||
$start_at = strtotime($gets['from'].' '.$gets['fromtime']);
|
||||
$end_at = strtotime($gets['to'].' '.$gets['totime']);
|
||||
|
||||
$share_data = array(
|
||||
'source_id' => $gets['id'],
|
||||
'source_type' => 'event',
|
||||
'receive_id' => $gets['receive_id'],
|
||||
'receive_name' => $gets['receive_name'],
|
||||
'start_at' => $start_at,
|
||||
'end_at' => $end_at,
|
||||
);
|
||||
|
||||
$share = ShareService::getItem('event', $gets['id']);
|
||||
if (empty($share)) {
|
||||
ShareService::addItem($share_data);
|
||||
} else {
|
||||
ShareService::editItem('event', $gets['id'], $share_data);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->json($e->getMessage());
|
||||
}
|
||||
|
||||
if ($data['calendarid'] != $gets['calendarid']) {
|
||||
try {
|
||||
CalendarService::moveToCalendar($gets['id'], $gets['calendarid']);
|
||||
} catch (\Exception $e) {
|
||||
return $this->json($e->getMessage());
|
||||
}
|
||||
}
|
||||
return $this->json('事件编辑完成。', true);
|
||||
}
|
||||
|
||||
// 新增表单
|
||||
if (Request::method() == 'GET') {
|
||||
$event = CalendarService::getEvent($gets['id']);
|
||||
|
||||
if (empty($event)) {
|
||||
return $this->json('事件数据不正确。');
|
||||
}
|
||||
|
||||
$object = VObject::parse($event['calendardata']);
|
||||
$vevent = $object->VEVENT;
|
||||
|
||||
/*
|
||||
$object = Sabre_Calendar_Object::cleanByAccessClass($id, $object);
|
||||
$accessclass = $vevent->getAsString('CLASS');
|
||||
$permissions = CalendarService::getPermissions($id, Calendar::EVENT, $accessclass);
|
||||
*/
|
||||
|
||||
$dtstart = $vevent->DTSTART;
|
||||
$dtend = CalendarService::getDTEndFromVEvent($vevent);
|
||||
|
||||
switch ($dtstart->getDateType()) {
|
||||
case \Sabre\VObject\Property\DateTime::UTC:
|
||||
$timezone = new \DateTimeZone(CalendarService::getTimezone());
|
||||
$newDT = $dtstart->getDateTime();
|
||||
$newDT->setTimezone($timezone);
|
||||
$dtstart->setDateTime($newDT);
|
||||
$newDT = $dtend->getDateTime();
|
||||
$newDT->setTimezone($timezone);
|
||||
$dtend->setDateTime($newDT);
|
||||
// no break
|
||||
case \Sabre\VObject\Property\DateTime::LOCALTZ:
|
||||
case \Sabre\VObject\Property\DateTime::LOCAL:
|
||||
$startdate = $dtstart->getDateTime()->format('Y-m-d');
|
||||
$starttime = $dtstart->getDateTime()->format('H:i');
|
||||
$enddate = $dtend->getDateTime()->format('Y-m-d');
|
||||
$endtime = $dtend->getDateTime()->format('H:i');
|
||||
$allday = false;
|
||||
break;
|
||||
case \Sabre\VObject\Property\DateTime::DATE:
|
||||
$startdate = $dtstart->getDateTime()->format('Y-m-d');
|
||||
$starttime = '';
|
||||
$dtend->getDateTime()->modify('-1 day');
|
||||
$enddate = $dtend->getDateTime()->format('Y-m-d');
|
||||
$endtime = '';
|
||||
$allday = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$summary = strtr($vevent->getAsString('SUMMARY'), array('\,' => ',', '\;' => ';'));
|
||||
$location = strtr($vevent->getAsString('LOCATION'), array('\,' => ',', '\;' => ';'));
|
||||
$description = strtr($vevent->getAsString('DESCRIPTION'), array('\,' => ',', '\;' => ';'));
|
||||
$categories = $vevent->getAsString('CATEGORIES');
|
||||
|
||||
if ($vevent->VALARM) {
|
||||
$valarm = $vevent->VALARM->getAsString('TRIGGER');
|
||||
}
|
||||
|
||||
if ($vevent->RRULE) {
|
||||
$rrule = explode(';', $vevent->getAsString('RRULE'));
|
||||
$rrulearr = array();
|
||||
foreach ($rrule as $rule) {
|
||||
list($attr, $val) = explode('=', $rule);
|
||||
$rrulearr[$attr] = $val;
|
||||
}
|
||||
if (!isset($rrulearr['INTERVAL']) || $rrulearr['INTERVAL'] == '') {
|
||||
$rrulearr['INTERVAL'] = 1;
|
||||
}
|
||||
if (array_key_exists('BYDAY', $rrulearr)) {
|
||||
if (substr_count($rrulearr['BYDAY'], ',') == 0) {
|
||||
if (strlen($rrulearr['BYDAY']) == 2) {
|
||||
$repeat['weekdays'] = array($rrulearr['BYDAY']);
|
||||
} elseif (strlen($rrulearr['BYDAY']) == 3) {
|
||||
$repeat['weekofmonth'] = substr($rrulearr['BYDAY'], 0, 1);
|
||||
$repeat['weekdays'] = array(substr($rrulearr['BYDAY'], 1, 2));
|
||||
} elseif (strlen($rrulearr['BYDAY']) == 4) {
|
||||
$repeat['weekofmonth'] = substr($rrulearr['BYDAY'], 0, 2);
|
||||
$repeat['weekdays'] = array(substr($rrulearr['BYDAY'], 2, 2));
|
||||
}
|
||||
} else {
|
||||
$byday_days = explode(',', $rrulearr['BYDAY']);
|
||||
foreach ($byday_days as $byday_day) {
|
||||
if (strlen($byday_day) == 2) {
|
||||
$repeat['weekdays'][] = $byday_day;
|
||||
} elseif (strlen($byday_day) == 3) {
|
||||
$repeat['weekofmonth'] = substr($byday_day, 0, 1);
|
||||
$repeat['weekdays'][] = substr($byday_day, 1, 2);
|
||||
} elseif (strlen($byday_day) == 4) {
|
||||
$repeat['weekofmonth'] = substr($byday_day, 0, 2);
|
||||
$repeat['weekdays'][] = substr($byday_day, 2, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (array_key_exists('BYMONTHDAY', $rrulearr)) {
|
||||
if (substr_count($rrulearr['BYMONTHDAY'], ',') == 0) {
|
||||
$repeat['bymonthday'][] = $rrulearr['BYMONTHDAY'];
|
||||
} else {
|
||||
$bymonthdays = explode(',', $rrulearr['BYMONTHDAY']);
|
||||
foreach ($bymonthdays as $bymonthday) {
|
||||
$repeat['bymonthday'][] = $bymonthday;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (array_key_exists('BYYEARDAY', $rrulearr)) {
|
||||
if (substr_count($rrulearr['BYYEARDAY'], ',') == 0) {
|
||||
$repeat['byyearday'][] = $rrulearr['BYYEARDAY'];
|
||||
} else {
|
||||
$byyeardays = explode(',', $rrulearr['BYYEARDAY']);
|
||||
foreach ($byyeardays as $yearday) {
|
||||
$repeat['byyearday'][] = $yearday;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (array_key_exists('BYWEEKNO', $rrulearr)) {
|
||||
if (substr_count($rrulearr['BYWEEKNO'], ',') == 0) {
|
||||
$repeat['byweekno'][] = (string) $rrulearr['BYWEEKNO'];
|
||||
} else {
|
||||
$byweekno = explode(',', $rrulearr['BYWEEKNO']);
|
||||
foreach ($byweekno as $weekno) {
|
||||
$repeat['byweekno'][] = (string) $weekno;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (array_key_exists('BYMONTH', $rrulearr)) {
|
||||
if (substr_count($rrulearr['BYMONTH'], ',') == 0) {
|
||||
$repeat['bymonth'][] = $month;
|
||||
} else {
|
||||
$bymonth = explode(',', $rrulearr['BYMONTH']);
|
||||
foreach ($bymonth as $month) {
|
||||
$repeat['bymonth'][] = $month;
|
||||
}
|
||||
}
|
||||
}
|
||||
switch ($rrulearr['FREQ']) {
|
||||
case 'DAILY':
|
||||
$repeat['repeat'] = 'daily';
|
||||
break;
|
||||
case 'WEEKLY':
|
||||
if ($rrulearr['INTERVAL'] % 2 == 0) {
|
||||
$repeat['repeat'] = 'biweekly';
|
||||
$rrulearr['INTERVAL'] = $rrulearr['INTERVAL'] / 2;
|
||||
} elseif ($rrulearr['BYDAY'] == 'MO,TU,WE,TH,FR') {
|
||||
$repeat['repeat'] = 'weekday';
|
||||
} else {
|
||||
$repeat['repeat'] = 'weekly';
|
||||
}
|
||||
break;
|
||||
case 'MONTHLY':
|
||||
$repeat['repeat'] = 'monthly';
|
||||
if (array_key_exists('BYDAY', $rrulearr)) {
|
||||
$repeat['month'] = 'weekday';
|
||||
} else {
|
||||
$repeat['month'] = 'monthday';
|
||||
}
|
||||
break;
|
||||
case 'YEARLY':
|
||||
$repeat['repeat'] = 'yearly';
|
||||
if (array_key_exists('BYMONTH', $rrulearr)) {
|
||||
$repeat['year'] = 'bydaymonth';
|
||||
} elseif (array_key_exists('BYWEEKNO', $rrulearr)) {
|
||||
$repeat['year'] = 'byweekno';
|
||||
} else {
|
||||
$repeat['year'] = 'byyearday';
|
||||
}
|
||||
}
|
||||
$repeat['interval'] = $rrulearr['INTERVAL'];
|
||||
if (array_key_exists('COUNT', $rrulearr)) {
|
||||
$repeat['end'] = 'count';
|
||||
$repeat['count'] = $rrulearr['COUNT'];
|
||||
} elseif (array_key_exists('UNTIL', $rrulearr)) {
|
||||
$repeat['end'] = 'date';
|
||||
$endbydate_year = substr($rrulearr['UNTIL'], 0, 4);
|
||||
$endbydate_month = substr($rrulearr['UNTIL'], 4, 2);
|
||||
$endbydate_day = substr($rrulearr['UNTIL'], 6, 2);
|
||||
$repeat['date'] = $endbydate_year . '-' . $endbydate_month . '-' . $endbydate_day;
|
||||
} else {
|
||||
$repeat['end'] = 'never';
|
||||
}
|
||||
if (array_key_exists('weekdays', $repeat)) {
|
||||
$repeat_weekdays_ = array();
|
||||
foreach ($repeat['weekdays'] as $weekday) {
|
||||
$repeat_weekdays_[] = $weekday;
|
||||
}
|
||||
$repeat['weekdays'] = $repeat_weekdays_;
|
||||
}
|
||||
} else {
|
||||
$repeat['repeat'] = 'doesnotrepeat';
|
||||
}
|
||||
|
||||
// $options['category_options'] = CalendarService::getCategoryOptions();
|
||||
$options['calendar_options']= CalendarService::getCalendars(Auth::id(), false);
|
||||
$options['access_class_options'] = CalendarService::getAccessClassOptions();
|
||||
$options['valarm_options'] = CalendarService::getValarmOptions();
|
||||
$options['repeat_options'] = CalendarService::getRepeatOptions();
|
||||
$options['repeat_end_options'] = CalendarService::getEndOptions();
|
||||
$options['repeat_month_options'] = CalendarService::getMonthOptions();
|
||||
$options['repeat_year_options'] = CalendarService::getYearOptions();
|
||||
$options['repeat_weekly_options'] = CalendarService::getWeeklyOptions();
|
||||
$options['repeat_weekofmonth_options'] = CalendarService::getWeekofMonth();
|
||||
$options['repeat_byyearday_options'] = CalendarService::getByYearDayOptions();
|
||||
$options['repeat_bymonth_options'] = CalendarService::getByMonthOptions();
|
||||
$options['repeat_byweekno_options'] = CalendarService::getByWeekNoOptions();
|
||||
$options['repeat_bymonthday_options'] = CalendarService::getByMonthDayOptions();
|
||||
|
||||
/*
|
||||
if($permissions & OCP\PERMISSION_UPDATE) {
|
||||
$tmpl = new OCP\Template('calendar', 'part.editevent');
|
||||
} elseif($permissions & OCP\PERMISSION_READ) {
|
||||
$tmpl = new OCP\Template('calendar', 'part.showevent');
|
||||
} elseif($permissions === 0) {
|
||||
OCP\JSON::error(array('data' => array('message' => CalendarService::$l10n->t('You do not have the permissions to edit this event.'))));
|
||||
exit;
|
||||
}*/
|
||||
|
||||
$options['id'] = $gets['id'];
|
||||
$options['permissions'] = $permissions;
|
||||
$options['lastmodified'] = $event['lastmodified'];
|
||||
$options['title'] = $summary;
|
||||
$options['accessclass'] = $accessclass;
|
||||
$options['location'] = $location;
|
||||
$options['categories'] = $categories;
|
||||
$options['calendarid'] = $event['calendarid'];
|
||||
$options['allday'] = $allday;
|
||||
$options['valarm'] = $valarm;
|
||||
$options['startdate'] = $startdate;
|
||||
$options['starttime'] = $starttime;
|
||||
$options['enddate'] = $enddate;
|
||||
$options['endtime'] = $endtime;
|
||||
$options['description'] = $description;
|
||||
|
||||
$repeats['repeat'] = $repeat['repeat'];
|
||||
|
||||
if ($repeat['repeat'] != 'doesnotrepeat') {
|
||||
$repeats['repeat_month'] = isset($repeat['month']) ? $repeat['month'] : 'monthday';
|
||||
$repeats['repeat_weekdays'] = isset($repeat['weekdays']) ? $repeat['weekdays'] : array();
|
||||
$repeats['repeat_interval'] = isset($repeat['interval']) ? $repeat['interval'] : '1';
|
||||
$repeats['repeat_end'] = isset($repeat['end']) ? $repeat['end'] : 'never';
|
||||
$repeats['repeat_count'] = isset($repeat['count']) ? $repeat['count'] : '10';
|
||||
$repeats['repeat_weekofmonth'] = $repeat['weekofmonth'];
|
||||
$repeats['repeat_date'] = isset($repeat['date']) ? $repeat['date'] : '';
|
||||
$repeats['repeat_year'] = isset($repeat['year']) ? $repeat['year'] : array();
|
||||
$repeats['repeat_byyearday'] = isset($repeat['byyearday']) ? $repeat['byyearday'] : array();
|
||||
$repeats['repeat_bymonthday'] = isset($repeat['bymonthday']) ? $repeat['bymonthday'] : array();
|
||||
$repeats['repeat_bymonth'] = isset($repeat['bymonth']) ? $repeat['bymonth'] : array();
|
||||
$repeats['repeat_byweekno'] = isset($repeat['byweekno']) ? $repeat['byweekno'] : array();
|
||||
} else {
|
||||
$repeats['repeat_month'] = 'monthday';
|
||||
$repeats['repeat_weekdays'] = array();
|
||||
$repeats['repeat_byyearday'] = array();
|
||||
$repeats['repeat_bymonthday'] = array();
|
||||
$repeats['repeat_bymonth'] = array();
|
||||
$repeats['repeat_byweekno'] = array();
|
||||
$repeats['repeat_interval'] = '1';
|
||||
$repeats['repeat_end'] = 'never';
|
||||
$repeats['repeat_count'] = '10';
|
||||
$repeats['repeat_weekofmonth'] = 'auto';
|
||||
$repeats['repeat_date'] = '';
|
||||
$repeats['repeat_year'] = 'bydate';
|
||||
}
|
||||
|
||||
$attachment['model'] = 'calendar_attachment';
|
||||
$attachment['path'] = 'calendar';
|
||||
$attachment['draft'] = AttachmentService::draft(Auth::id());
|
||||
$attachment['queue'] = AttachmentService::get($event['attachment']);
|
||||
|
||||
$share = ShareService::getItem('event', $gets['id']);
|
||||
return $this->render(array(
|
||||
'attachList' => $attachment,
|
||||
'repeats' => $repeats,
|
||||
'options' => $options,
|
||||
'share' => $share,
|
||||
), 'add');
|
||||
}
|
||||
}
|
||||
|
||||
public function viewAction()
|
||||
{
|
||||
$id = (int)Request::get('id');
|
||||
$event = CalendarService::getEvent($id);
|
||||
|
||||
if (empty($event)) {
|
||||
return $this->json('事件数据不正确。');
|
||||
}
|
||||
|
||||
$object = VObject::parse($event['calendardata']);
|
||||
$vevent = $object->VEVENT;
|
||||
|
||||
$dtstart = $vevent->DTSTART;
|
||||
$dtend = CalendarService::getDTEndFromVEvent($vevent);
|
||||
switch ($dtstart->getDateType()) {
|
||||
case \Sabre\VObject\Property\DateTime::UTC:
|
||||
$timezone = new \DateTimeZone(CalendarService::getTimezone());
|
||||
$newDT = $dtstart->getDateTime();
|
||||
$newDT->setTimezone($timezone);
|
||||
$dtstart->setDateTime($newDT);
|
||||
$newDT = $dtend->getDateTime();
|
||||
$newDT->setTimezone($timezone);
|
||||
$dtend->setDateTime($newDT);
|
||||
// no break
|
||||
case \Sabre\VObject\Property\DateTime::LOCALTZ:
|
||||
case \Sabre\VObject\Property\DateTime::LOCAL:
|
||||
$startdate = $dtstart->getDateTime()->format('Y-m-d');
|
||||
$starttime = $dtstart->getDateTime()->format('H:i');
|
||||
$enddate = $dtend->getDateTime()->format('Y-m-d');
|
||||
$endtime = $dtend->getDateTime()->format('H:i');
|
||||
$allday = false;
|
||||
break;
|
||||
case \Sabre\VObject\Property\DateTime::DATE:
|
||||
$startdate = $dtstart->getDateTime()->format('Y-m-d');
|
||||
$starttime = '';
|
||||
$dtend->getDateTime()->modify('-1 day');
|
||||
$enddate = $dtend->getDateTime()->format('Y-m-d');
|
||||
$endtime = '';
|
||||
$allday = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$summary = strtr($vevent->getAsString('SUMMARY'), array('\,' => ',', '\;' => ';'));
|
||||
$location = strtr($vevent->getAsString('LOCATION'), array('\,' => ',', '\;' => ';'));
|
||||
$categories = $vevent->getAsString('CATEGORIES');
|
||||
$description = strtr($vevent->getAsString('DESCRIPTION'), array('\,' => ',', '\;' => ';'));
|
||||
|
||||
$options['id'] = $id;
|
||||
$options['permissions'] = $permissions;
|
||||
$options['lastmodified'] = $event['lastmodified'];
|
||||
$options['title'] = $summary;
|
||||
$options['location'] = $location;
|
||||
$options['categories'] = $categories;
|
||||
$options['calendarid'] = $event['calendarid'];
|
||||
$options['allday'] = $allday;
|
||||
$options['startdate'] = $startdate;
|
||||
$options['starttime'] = $starttime;
|
||||
$options['enddate'] = $enddate;
|
||||
$options['endtime'] = $endtime;
|
||||
$options['description'] = $description;
|
||||
$options['accessclass'] = 'PUBLIC';
|
||||
$options['access_class_options'] = CalendarService::getAccessClassOptions();
|
||||
|
||||
$attach['model'] = 'calendar_attachment';
|
||||
$attach['path'] = 'calendar';
|
||||
$attach['main'] = AttachmentService::get($event['attachment']);
|
||||
|
||||
$calendar = CalendarService::getCalendar($event['calendarid'], false);
|
||||
$share = ShareService::getItem('event', $id);
|
||||
|
||||
return $this->render(array(
|
||||
'attachList' => $attach,
|
||||
'options' => $options,
|
||||
'calendar' => $calendar,
|
||||
'share' => $share,
|
||||
));
|
||||
}
|
||||
|
||||
public function deleteAction()
|
||||
{
|
||||
$id = Request::get('id');
|
||||
if ($id > 0) {
|
||||
CalendarService::remove($id);
|
||||
ShareService::removeItem('event', $id);
|
||||
return $this->json('删除成功。', true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php namespace Gdoo\Calendar\Models;
|
||||
|
||||
use Gdoo\Index\Models\BaseModel;
|
||||
|
||||
class Calendar extends BaseModel
|
||||
{
|
||||
public $table = 'calendar';
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php namespace Gdoo\Calendar\Models;
|
||||
|
||||
use Gdoo\Index\Models\BaseModel;
|
||||
|
||||
class CalendarObject extends BaseModel
|
||||
{
|
||||
public $table = 'calendar_object';
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Backend;
|
||||
|
||||
use Sabre\VObject;
|
||||
use Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* Abstract Calendaring backend. Extend this class to create your own backends.
|
||||
*
|
||||
* Checkout the BackendInterface for all the methods that must be implemented.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
abstract class AbstractBackend implements BackendInterface {
|
||||
|
||||
/**
|
||||
* Updates properties for a calendar.
|
||||
*
|
||||
* The mutations array uses the propertyName in clark-notation as key,
|
||||
* and the array value for the property value. In the case a property
|
||||
* should be deleted, the property value will be null.
|
||||
*
|
||||
* This method must be atomic. If one property cannot be changed, the
|
||||
* entire operation must fail.
|
||||
*
|
||||
* If the operation was successful, true can be returned.
|
||||
* If the operation failed, false can be returned.
|
||||
*
|
||||
* Deletion of a non-existent property is always successful.
|
||||
*
|
||||
* Lastly, it is optional to return detailed information about any
|
||||
* failures. In this case an array should be returned with the following
|
||||
* structure:
|
||||
*
|
||||
* array(
|
||||
* 403 => array(
|
||||
* '{DAV:}displayname' => null,
|
||||
* ),
|
||||
* 424 => array(
|
||||
* '{DAV:}owner' => null,
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* In this example it was forbidden to update {DAV:}displayname.
|
||||
* (403 Forbidden), which in turn also caused {DAV:}owner to fail
|
||||
* (424 Failed Dependency) because the request needs to be atomic.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param array $mutations
|
||||
* @return bool|array
|
||||
*/
|
||||
public function updateCalendar($calendarId, array $mutations) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a calendar-query on the contents of this calendar.
|
||||
*
|
||||
* The calendar-query is defined in RFC4791 : CalDAV. Using the
|
||||
* calendar-query it is possible for a client to request a specific set of
|
||||
* object, based on contents of iCalendar properties, date-ranges and
|
||||
* iCalendar component types (VTODO, VEVENT).
|
||||
*
|
||||
* This method should just return a list of (relative) urls that match this
|
||||
* query.
|
||||
*
|
||||
* The list of filters are specified as an array. The exact array is
|
||||
* documented by \Sabre\CalDAV\CalendarQueryParser.
|
||||
*
|
||||
* Note that it is extremely likely that getCalendarObject for every path
|
||||
* returned from this method will be called almost immediately after. You
|
||||
* may want to anticipate this to speed up these requests.
|
||||
*
|
||||
* This method provides a default implementation, which parses *all* the
|
||||
* iCalendar objects in the specified calendar.
|
||||
*
|
||||
* This default may well be good enough for personal use, and calendars
|
||||
* that aren't very large. But if you anticipate high usage, big calendars
|
||||
* or high loads, you are strongly adviced to optimize certain paths.
|
||||
*
|
||||
* The best way to do so is override this method and to optimize
|
||||
* specifically for 'common filters'.
|
||||
*
|
||||
* Requests that are extremely common are:
|
||||
* * requests for just VEVENTS
|
||||
* * requests for just VTODO
|
||||
* * requests with a time-range-filter on either VEVENT or VTODO.
|
||||
*
|
||||
* ..and combinations of these requests. It may not be worth it to try to
|
||||
* handle every possible situation and just rely on the (relatively
|
||||
* easy to use) CalendarQueryValidator to handle the rest.
|
||||
*
|
||||
* Note that especially time-range-filters may be difficult to parse. A
|
||||
* time-range filter specified on a VEVENT must for instance also handle
|
||||
* recurrence rules correctly.
|
||||
* A good example of how to interprete all these filters can also simply
|
||||
* be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
|
||||
* as possible, so it gives you a good idea on what type of stuff you need
|
||||
* to think of.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param array $filters
|
||||
* @return array
|
||||
*/
|
||||
public function calendarQuery($calendarId, array $filters) {
|
||||
|
||||
$result = array();
|
||||
$objects = $this->getCalendarObjects($calendarId);
|
||||
|
||||
$validator = new \Sabre\CalDAV\CalendarQueryValidator();
|
||||
|
||||
foreach($objects as $object) {
|
||||
|
||||
if ($this->validateFilterForObject($object, $filters)) {
|
||||
$result[] = $object['uri'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method validates if a filters (as passed to calendarQuery) matches
|
||||
* the given object.
|
||||
*
|
||||
* @param array $object
|
||||
* @param array $filters
|
||||
* @return bool
|
||||
*/
|
||||
protected function validateFilterForObject(array $object, array $filters) {
|
||||
|
||||
// Unfortunately, setting the 'calendardata' here is optional. If
|
||||
// it was excluded, we actually need another call to get this as
|
||||
// well.
|
||||
if (!isset($object['calendardata'])) {
|
||||
$object = $this->getCalendarObject($object['calendarid'], $object['uri']);
|
||||
}
|
||||
|
||||
$data = is_resource($object['calendardata'])?stream_get_contents($object['calendardata']):$object['calendardata'];
|
||||
$vObject = VObject\Reader::read($data);
|
||||
|
||||
$validator = new CalDAV\CalendarQueryValidator();
|
||||
return $validator->validate($vObject, $filters);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Backend;
|
||||
|
||||
/**
|
||||
* Every CalDAV backend must at least implement this interface.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface BackendInterface {
|
||||
|
||||
/**
|
||||
* Returns a list of calendars for a principal.
|
||||
*
|
||||
* Every project is an array with the following keys:
|
||||
* * id, a unique id that will be used by other functions to modify the
|
||||
* calendar. This can be the same as the uri or a database key.
|
||||
* * uri, which the basename of the uri with which the calendar is
|
||||
* accessed.
|
||||
* * principaluri. The owner of the calendar. Almost always the same as
|
||||
* principalUri passed to this method.
|
||||
*
|
||||
* Furthermore it can contain webdav properties in clark notation. A very
|
||||
* common one is '{DAV:}displayname'.
|
||||
*
|
||||
* @param string $principalUri
|
||||
* @return array
|
||||
*/
|
||||
public function getCalendarsForUser($principalUri);
|
||||
|
||||
/**
|
||||
* Creates a new calendar for a principal.
|
||||
*
|
||||
* If the creation was a success, an id must be returned that can be used to reference
|
||||
* this calendar in other methods, such as updateCalendar.
|
||||
*
|
||||
* @param string $principalUri
|
||||
* @param string $calendarUri
|
||||
* @param array $properties
|
||||
* @return void
|
||||
*/
|
||||
public function createCalendar($principalUri,$calendarUri,array $properties);
|
||||
|
||||
/**
|
||||
* Updates properties for a calendar.
|
||||
*
|
||||
* The mutations array uses the propertyName in clark-notation as key,
|
||||
* and the array value for the property value. In the case a property
|
||||
* should be deleted, the property value will be null.
|
||||
*
|
||||
* This method must be atomic. If one property cannot be changed, the
|
||||
* entire operation must fail.
|
||||
*
|
||||
* If the operation was successful, true can be returned.
|
||||
* If the operation failed, false can be returned.
|
||||
*
|
||||
* Deletion of a non-existent property is always successful.
|
||||
*
|
||||
* Lastly, it is optional to return detailed information about any
|
||||
* failures. In this case an array should be returned with the following
|
||||
* structure:
|
||||
*
|
||||
* array(
|
||||
* 403 => array(
|
||||
* '{DAV:}displayname' => null,
|
||||
* ),
|
||||
* 424 => array(
|
||||
* '{DAV:}owner' => null,
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* In this example it was forbidden to update {DAV:}displayname.
|
||||
* (403 Forbidden), which in turn also caused {DAV:}owner to fail
|
||||
* (424 Failed Dependency) because the request needs to be atomic.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param array $mutations
|
||||
* @return bool|array
|
||||
*/
|
||||
public function updateCalendar($calendarId, array $mutations);
|
||||
|
||||
/**
|
||||
* Delete a calendar and all it's objects
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @return void
|
||||
*/
|
||||
public function deleteCalendar($calendarId);
|
||||
|
||||
/**
|
||||
* Returns all calendar objects within a calendar.
|
||||
*
|
||||
* Every item contains an array with the following keys:
|
||||
* * id - unique identifier which will be used for subsequent updates
|
||||
* * calendardata - The iCalendar-compatible calendar data
|
||||
* * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
|
||||
* * lastmodified - a timestamp of the last modification time
|
||||
* * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
|
||||
* ' "abcdef"')
|
||||
* * calendarid - The calendarid as it was passed to this function.
|
||||
* * size - The size of the calendar objects, in bytes.
|
||||
*
|
||||
* Note that the etag is optional, but it's highly encouraged to return for
|
||||
* speed reasons.
|
||||
*
|
||||
* The calendardata is also optional. If it's not returned
|
||||
* 'getCalendarObject' will be called later, which *is* expected to return
|
||||
* calendardata.
|
||||
*
|
||||
* If neither etag or size are specified, the calendardata will be
|
||||
* used/fetched to determine these numbers. If both are specified the
|
||||
* amount of times this is needed is reduced by a great degree.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @return array
|
||||
*/
|
||||
public function getCalendarObjects($calendarId);
|
||||
|
||||
/**
|
||||
* Returns information from a single calendar object, based on it's object
|
||||
* uri.
|
||||
*
|
||||
* The returned array must have the same keys as getCalendarObjects. The
|
||||
* 'calendardata' object is required here though, while it's not required
|
||||
* for getCalendarObjects.
|
||||
*
|
||||
* This method must return null if the object did not exist.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param string $objectUri
|
||||
* @return array|null
|
||||
*/
|
||||
public function getCalendarObject($calendarId,$objectUri);
|
||||
|
||||
/**
|
||||
* Creates a new calendar object.
|
||||
*
|
||||
* It is possible return an etag from this function, which will be used in
|
||||
* the response to this PUT request. Note that the ETag must be surrounded
|
||||
* by double-quotes.
|
||||
*
|
||||
* However, you should only really return this ETag if you don't mangle the
|
||||
* calendar-data. If the result of a subsequent GET to this object is not
|
||||
* the exact same as this request body, you should omit the ETag.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param string $objectUri
|
||||
* @param string $calendarData
|
||||
* @return string|null
|
||||
*/
|
||||
public function createCalendarObject($calendarId,$objectUri,$calendarData);
|
||||
|
||||
/**
|
||||
* Updates an existing calendarobject, based on it's uri.
|
||||
*
|
||||
* It is possible return an etag from this function, which will be used in
|
||||
* the response to this PUT request. Note that the ETag must be surrounded
|
||||
* by double-quotes.
|
||||
*
|
||||
* However, you should only really return this ETag if you don't mangle the
|
||||
* calendar-data. If the result of a subsequent GET to this object is not
|
||||
* the exact same as this request body, you should omit the ETag.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param string $objectUri
|
||||
* @param string $calendarData
|
||||
* @return string|null
|
||||
*/
|
||||
public function updateCalendarObject($calendarId,$objectUri,$calendarData);
|
||||
|
||||
/**
|
||||
* Deletes an existing calendar object.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param string $objectUri
|
||||
* @return void
|
||||
*/
|
||||
public function deleteCalendarObject($calendarId,$objectUri);
|
||||
|
||||
/**
|
||||
* Performs a calendar-query on the contents of this calendar.
|
||||
*
|
||||
* The calendar-query is defined in RFC4791 : CalDAV. Using the
|
||||
* calendar-query it is possible for a client to request a specific set of
|
||||
* object, based on contents of iCalendar properties, date-ranges and
|
||||
* iCalendar component types (VTODO, VEVENT).
|
||||
*
|
||||
* This method should just return a list of (relative) urls that match this
|
||||
* query.
|
||||
*
|
||||
* The list of filters are specified as an array. The exact array is
|
||||
* documented by Sabre\CalDAV\CalendarQueryParser.
|
||||
*
|
||||
* Note that it is extremely likely that getCalendarObject for every path
|
||||
* returned from this method will be called almost immediately after. You
|
||||
* may want to anticipate this to speed up these requests.
|
||||
*
|
||||
* This method provides a default implementation, which parses *all* the
|
||||
* iCalendar objects in the specified calendar.
|
||||
*
|
||||
* This default may well be good enough for personal use, and calendars
|
||||
* that aren't very large. But if you anticipate high usage, big calendars
|
||||
* or high loads, you are strongly adviced to optimize certain paths.
|
||||
*
|
||||
* The best way to do so is override this method and to optimize
|
||||
* specifically for 'common filters'.
|
||||
*
|
||||
* Requests that are extremely common are:
|
||||
* * requests for just VEVENTS
|
||||
* * requests for just VTODO
|
||||
* * requests with a time-range-filter on either VEVENT or VTODO.
|
||||
*
|
||||
* ..and combinations of these requests. It may not be worth it to try to
|
||||
* handle every possible situation and just rely on the (relatively
|
||||
* easy to use) CalendarQueryValidator to handle the rest.
|
||||
*
|
||||
* Note that especially time-range-filters may be difficult to parse. A
|
||||
* time-range filter specified on a VEVENT must for instance also handle
|
||||
* recurrence rules correctly.
|
||||
* A good example of how to interprete all these filters can also simply
|
||||
* be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
|
||||
* as possible, so it gives you a good idea on what type of stuff you need
|
||||
* to think of.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param array $filters
|
||||
* @return array
|
||||
*/
|
||||
public function calendarQuery($calendarId, array $filters);
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Backend;
|
||||
|
||||
/**
|
||||
* Adds caldav notification support to a backend.
|
||||
*
|
||||
* Note: This feature is experimental, and may change in between different
|
||||
* SabreDAV versions.
|
||||
*
|
||||
* Notifications are defined at:
|
||||
* http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-notifications.txt
|
||||
*
|
||||
* These notifications are basically a list of server-generated notifications
|
||||
* displayed to the user. Users can dismiss notifications by deleting them.
|
||||
*
|
||||
* The primary usecase is to allow for calendar-sharing.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface NotificationSupport extends BackendInterface {
|
||||
|
||||
/**
|
||||
* Returns a list of notifications for a given principal url.
|
||||
*
|
||||
* The returned array should only consist of implementations of
|
||||
* \Sabre\CalDAV\Notifications\INotificationType.
|
||||
*
|
||||
* @param string $principalUri
|
||||
* @return array
|
||||
*/
|
||||
public function getNotificationsForPrincipal($principalUri);
|
||||
|
||||
/**
|
||||
* This deletes a specific notifcation.
|
||||
*
|
||||
* This may be called by a client once it deems a notification handled.
|
||||
*
|
||||
* @param string $principalUri
|
||||
* @param \Sabre\CalDAV\Notifications\INotificationType $notification
|
||||
* @return void
|
||||
*/
|
||||
public function deleteNotification($principalUri, \Sabre\CalDAV\Notifications\INotificationType $notification);
|
||||
|
||||
}
|
|
@ -0,0 +1,691 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Backend;
|
||||
|
||||
use Sabre\VObject;
|
||||
use Sabre\CalDAV;
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* PDO CalDAV backend
|
||||
*
|
||||
* This backend is used to store calendar-data in a PDO database, such as
|
||||
* sqlite or MySQL
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class PDO extends AbstractBackend {
|
||||
|
||||
/**
|
||||
* We need to specify a max date, because we need to stop *somewhere*
|
||||
*
|
||||
* On 32 bit system the maximum for a signed integer is 2147483647, so
|
||||
* MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
|
||||
* in 2038-01-19 to avoid problems when the date is converted
|
||||
* to a unix timestamp.
|
||||
*/
|
||||
const MAX_DATE = '2038-01-01';
|
||||
|
||||
/**
|
||||
* pdo
|
||||
*
|
||||
* @var \PDO
|
||||
*/
|
||||
protected $pdo;
|
||||
|
||||
/**
|
||||
* The table name that will be used for calendars
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $calendarTableName;
|
||||
|
||||
/**
|
||||
* The table name that will be used for calendar objects
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $calendarObjectTableName;
|
||||
|
||||
/**
|
||||
* List of CalDAV properties, and how they map to database fieldnames
|
||||
* Add your own properties by simply adding on to this array.
|
||||
*
|
||||
* Note that only string-based properties are supported here.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $propertyMap = array(
|
||||
'{DAV:}displayname' => 'displayname',
|
||||
'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
|
||||
'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
|
||||
'{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
|
||||
'{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates the backend
|
||||
*
|
||||
* @param \PDO $pdo
|
||||
* @param string $calendarTableName
|
||||
* @param string $calendarObjectTableName
|
||||
*/
|
||||
public function __construct(\PDO $pdo, $calendarTableName = 'calendars', $calendarObjectTableName = 'calendarobjects') {
|
||||
|
||||
$this->pdo = $pdo;
|
||||
$this->calendarTableName = $calendarTableName;
|
||||
$this->calendarObjectTableName = $calendarObjectTableName;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of calendars for a principal.
|
||||
*
|
||||
* Every project is an array with the following keys:
|
||||
* * id, a unique id that will be used by other functions to modify the
|
||||
* calendar. This can be the same as the uri or a database key.
|
||||
* * uri, which the basename of the uri with which the calendar is
|
||||
* accessed.
|
||||
* * principaluri. The owner of the calendar. Almost always the same as
|
||||
* principalUri passed to this method.
|
||||
*
|
||||
* Furthermore it can contain webdav properties in clark notation. A very
|
||||
* common one is '{DAV:}displayname'.
|
||||
*
|
||||
* @param string $principalUri
|
||||
* @return array
|
||||
*/
|
||||
public function getCalendarsForUser($principalUri) {
|
||||
|
||||
$fields = array_values($this->propertyMap);
|
||||
$fields[] = 'id';
|
||||
$fields[] = 'uri';
|
||||
$fields[] = 'ctag';
|
||||
$fields[] = 'components';
|
||||
$fields[] = 'principaluri';
|
||||
$fields[] = 'transparent';
|
||||
|
||||
// Making fields a comma-delimited list
|
||||
$fields = implode(', ', $fields);
|
||||
$stmt = $this->pdo->prepare("SELECT " . $fields . " FROM ".$this->calendarTableName." WHERE principaluri = ? ORDER BY calendarorder ASC");
|
||||
$stmt->execute(array($principalUri));
|
||||
|
||||
$calendars = array();
|
||||
while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
|
||||
$components = array();
|
||||
if ($row['components']) {
|
||||
$components = explode(',',$row['components']);
|
||||
}
|
||||
|
||||
$calendar = array(
|
||||
'id' => $row['id'],
|
||||
'uri' => $row['uri'],
|
||||
'principaluri' => $row['principaluri'],
|
||||
'{' . CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $row['ctag']?$row['ctag']:'0',
|
||||
'{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Property\SupportedCalendarComponentSet($components),
|
||||
'{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new CalDAV\Property\ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
|
||||
);
|
||||
|
||||
|
||||
foreach($this->propertyMap as $xmlName=>$dbName) {
|
||||
$calendar[$xmlName] = $row[$dbName];
|
||||
}
|
||||
|
||||
$calendars[] = $calendar;
|
||||
|
||||
}
|
||||
|
||||
return $calendars;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new calendar for a principal.
|
||||
*
|
||||
* If the creation was a success, an id must be returned that can be used to reference
|
||||
* this calendar in other methods, such as updateCalendar
|
||||
*
|
||||
* @param string $principalUri
|
||||
* @param string $calendarUri
|
||||
* @param array $properties
|
||||
* @return string
|
||||
*/
|
||||
public function createCalendar($principalUri, $calendarUri, array $properties) {
|
||||
|
||||
$fieldNames = array(
|
||||
'principaluri',
|
||||
'uri',
|
||||
'ctag',
|
||||
'transparent',
|
||||
);
|
||||
$values = array(
|
||||
':principaluri' => $principalUri,
|
||||
':uri' => $calendarUri,
|
||||
':ctag' => 1,
|
||||
':transparent' => 0,
|
||||
);
|
||||
|
||||
// Default value
|
||||
$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
|
||||
$fieldNames[] = 'components';
|
||||
if (!isset($properties[$sccs])) {
|
||||
$values[':components'] = 'VEVENT,VTODO';
|
||||
} else {
|
||||
if (!($properties[$sccs] instanceof CalDAV\Property\SupportedCalendarComponentSet)) {
|
||||
throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
|
||||
}
|
||||
$values[':components'] = implode(',',$properties[$sccs]->getValue());
|
||||
}
|
||||
$transp = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp';
|
||||
if (isset($properties[$transp])) {
|
||||
$values[':transparent'] = $properties[$transp]->getValue()==='transparent';
|
||||
}
|
||||
|
||||
foreach($this->propertyMap as $xmlName=>$dbName) {
|
||||
if (isset($properties[$xmlName])) {
|
||||
|
||||
$values[':' . $dbName] = $properties[$xmlName];
|
||||
$fieldNames[] = $dbName;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare("INSERT INTO ".$this->calendarTableName." (".implode(', ', $fieldNames).") VALUES (".implode(', ',array_keys($values)).")");
|
||||
$stmt->execute($values);
|
||||
|
||||
return $this->pdo->lastInsertId();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates properties for a calendar.
|
||||
*
|
||||
* The mutations array uses the propertyName in clark-notation as key,
|
||||
* and the array value for the property value. In the case a property
|
||||
* should be deleted, the property value will be null.
|
||||
*
|
||||
* This method must be atomic. If one property cannot be changed, the
|
||||
* entire operation must fail.
|
||||
*
|
||||
* If the operation was successful, true can be returned.
|
||||
* If the operation failed, false can be returned.
|
||||
*
|
||||
* Deletion of a non-existent property is always successful.
|
||||
*
|
||||
* Lastly, it is optional to return detailed information about any
|
||||
* failures. In this case an array should be returned with the following
|
||||
* structure:
|
||||
*
|
||||
* array(
|
||||
* 403 => array(
|
||||
* '{DAV:}displayname' => null,
|
||||
* ),
|
||||
* 424 => array(
|
||||
* '{DAV:}owner' => null,
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* In this example it was forbidden to update {DAV:}displayname.
|
||||
* (403 Forbidden), which in turn also caused {DAV:}owner to fail
|
||||
* (424 Failed Dependency) because the request needs to be atomic.
|
||||
*
|
||||
* @param string $calendarId
|
||||
* @param array $mutations
|
||||
* @return bool|array
|
||||
*/
|
||||
public function updateCalendar($calendarId, array $mutations) {
|
||||
|
||||
$newValues = array();
|
||||
$result = array(
|
||||
200 => array(), // Ok
|
||||
403 => array(), // Forbidden
|
||||
424 => array(), // Failed Dependency
|
||||
);
|
||||
|
||||
$hasError = false;
|
||||
|
||||
foreach($mutations as $propertyName=>$propertyValue) {
|
||||
|
||||
switch($propertyName) {
|
||||
case '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' :
|
||||
$fieldName = 'transparent';
|
||||
$newValues[$fieldName] = $propertyValue->getValue()==='transparent';
|
||||
break;
|
||||
default :
|
||||
// Checking the property map
|
||||
if (!isset($this->propertyMap[$propertyName])) {
|
||||
// We don't know about this property.
|
||||
$hasError = true;
|
||||
$result[403][$propertyName] = null;
|
||||
unset($mutations[$propertyName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldName = $this->propertyMap[$propertyName];
|
||||
$newValues[$fieldName] = $propertyValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If there were any errors we need to fail the request
|
||||
if ($hasError) {
|
||||
// Properties has the remaining properties
|
||||
foreach($mutations as $propertyName=>$propertyValue) {
|
||||
$result[424][$propertyName] = null;
|
||||
}
|
||||
|
||||
// Removing unused statuscodes for cleanliness
|
||||
foreach($result as $status=>$properties) {
|
||||
if (is_array($properties) && count($properties)===0) unset($result[$status]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
// Success
|
||||
|
||||
// Now we're generating the sql query.
|
||||
$valuesSql = array();
|
||||
foreach($newValues as $fieldName=>$value) {
|
||||
$valuesSql[] = $fieldName . ' = ?';
|
||||
}
|
||||
$valuesSql[] = 'ctag = ctag + 1';
|
||||
|
||||
$stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ',$valuesSql) . " WHERE id = ?");
|
||||
$newValues['id'] = $calendarId;
|
||||
$stmt->execute(array_values($newValues));
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a calendar and all it's objects
|
||||
*
|
||||
* @param string $calendarId
|
||||
* @return void
|
||||
*/
|
||||
public function deleteCalendar($calendarId) {
|
||||
|
||||
$stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
|
||||
$stmt->execute(array($calendarId));
|
||||
|
||||
$stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarTableName.' WHERE id = ?');
|
||||
$stmt->execute(array($calendarId));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all calendar objects within a calendar.
|
||||
*
|
||||
* Every item contains an array with the following keys:
|
||||
* * id - unique identifier which will be used for subsequent updates
|
||||
* * calendardata - The iCalendar-compatible calendar data
|
||||
* * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
|
||||
* * lastmodified - a timestamp of the last modification time
|
||||
* * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
|
||||
* ' "abcdef"')
|
||||
* * calendarid - The calendarid as it was passed to this function.
|
||||
* * size - The size of the calendar objects, in bytes.
|
||||
*
|
||||
* Note that the etag is optional, but it's highly encouraged to return for
|
||||
* speed reasons.
|
||||
*
|
||||
* The calendardata is also optional. If it's not returned
|
||||
* 'getCalendarObject' will be called later, which *is* expected to return
|
||||
* calendardata.
|
||||
*
|
||||
* If neither etag or size are specified, the calendardata will be
|
||||
* used/fetched to determine these numbers. If both are specified the
|
||||
* amount of times this is needed is reduced by a great degree.
|
||||
*
|
||||
* @param string $calendarId
|
||||
* @return array
|
||||
*/
|
||||
public function getCalendarObjects($calendarId) {
|
||||
|
||||
$stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
|
||||
$stmt->execute(array($calendarId));
|
||||
|
||||
$result = array();
|
||||
foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
|
||||
$result[] = array(
|
||||
'id' => $row['id'],
|
||||
'uri' => $row['uri'],
|
||||
'lastmodified' => $row['lastmodified'],
|
||||
'etag' => '"' . $row['etag'] . '"',
|
||||
'calendarid' => $row['calendarid'],
|
||||
'size' => (int)$row['size'],
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information from a single calendar object, based on it's object
|
||||
* uri.
|
||||
*
|
||||
* The returned array must have the same keys as getCalendarObjects. The
|
||||
* 'calendardata' object is required here though, while it's not required
|
||||
* for getCalendarObjects.
|
||||
*
|
||||
* This method must return null if the object did not exist.
|
||||
*
|
||||
* @param string $calendarId
|
||||
* @param string $objectUri
|
||||
* @return array|null
|
||||
*/
|
||||
public function getCalendarObject($calendarId,$objectUri) {
|
||||
|
||||
$stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
|
||||
$stmt->execute(array($calendarId, $objectUri));
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if(!$row) return null;
|
||||
|
||||
return array(
|
||||
'id' => $row['id'],
|
||||
'uri' => $row['uri'],
|
||||
'lastmodified' => $row['lastmodified'],
|
||||
'etag' => '"' . $row['etag'] . '"',
|
||||
'calendarid' => $row['calendarid'],
|
||||
'size' => (int)$row['size'],
|
||||
'calendardata' => $row['calendardata'],
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new calendar object.
|
||||
*
|
||||
* It is possible return an etag from this function, which will be used in
|
||||
* the response to this PUT request. Note that the ETag must be surrounded
|
||||
* by double-quotes.
|
||||
*
|
||||
* However, you should only really return this ETag if you don't mangle the
|
||||
* calendar-data. If the result of a subsequent GET to this object is not
|
||||
* the exact same as this request body, you should omit the ETag.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param string $objectUri
|
||||
* @param string $calendarData
|
||||
* @return string|null
|
||||
*/
|
||||
public function createCalendarObject($calendarId,$objectUri,$calendarData) {
|
||||
|
||||
$extraData = $this->getDenormalizedData($calendarData);
|
||||
|
||||
$stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarObjectTableName.' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence) VALUES (?,?,?,?,?,?,?,?,?)');
|
||||
$stmt->execute(array(
|
||||
$calendarId,
|
||||
$objectUri,
|
||||
$calendarData,
|
||||
time(),
|
||||
$extraData['etag'],
|
||||
$extraData['size'],
|
||||
$extraData['componentType'],
|
||||
$extraData['firstOccurence'],
|
||||
$extraData['lastOccurence'],
|
||||
));
|
||||
$stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET ctag = ctag + 1 WHERE id = ?');
|
||||
$stmt->execute(array($calendarId));
|
||||
|
||||
return '"' . $extraData['etag'] . '"';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing calendarobject, based on it's uri.
|
||||
*
|
||||
* It is possible return an etag from this function, which will be used in
|
||||
* the response to this PUT request. Note that the ETag must be surrounded
|
||||
* by double-quotes.
|
||||
*
|
||||
* However, you should only really return this ETag if you don't mangle the
|
||||
* calendar-data. If the result of a subsequent GET to this object is not
|
||||
* the exact same as this request body, you should omit the ETag.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param string $objectUri
|
||||
* @param string $calendarData
|
||||
* @return string|null
|
||||
*/
|
||||
public function updateCalendarObject($calendarId,$objectUri,$calendarData) {
|
||||
|
||||
$extraData = $this->getDenormalizedData($calendarData);
|
||||
|
||||
$stmt = $this->pdo->prepare('UPDATE '.$this->calendarObjectTableName.' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ? WHERE calendarid = ? AND uri = ?');
|
||||
$stmt->execute(array($calendarData,time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'] ,$calendarId,$objectUri));
|
||||
$stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET ctag = ctag + 1 WHERE id = ?');
|
||||
$stmt->execute(array($calendarId));
|
||||
|
||||
return '"' . $extraData['etag'] . '"';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses some information from calendar objects, used for optimized
|
||||
* calendar-queries.
|
||||
*
|
||||
* Returns an array with the following keys:
|
||||
* * etag
|
||||
* * size
|
||||
* * componentType
|
||||
* * firstOccurence
|
||||
* * lastOccurence
|
||||
*
|
||||
* @param string $calendarData
|
||||
* @return array
|
||||
*/
|
||||
protected function getDenormalizedData($calendarData) {
|
||||
|
||||
$vObject = VObject\Reader::read($calendarData);
|
||||
$componentType = null;
|
||||
$component = null;
|
||||
$firstOccurence = null;
|
||||
$lastOccurence = null;
|
||||
foreach($vObject->getComponents() as $component) {
|
||||
if ($component->name!=='VTIMEZONE') {
|
||||
$componentType = $component->name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$componentType) {
|
||||
throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
|
||||
}
|
||||
if ($componentType === 'VEVENT') {
|
||||
$firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
|
||||
// Finding the last occurence is a bit harder
|
||||
if (!isset($component->RRULE)) {
|
||||
if (isset($component->DTEND)) {
|
||||
$lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
|
||||
} elseif (isset($component->DURATION)) {
|
||||
$endDate = clone $component->DTSTART->getDateTime();
|
||||
$endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
|
||||
$lastOccurence = $endDate->getTimeStamp();
|
||||
} elseif (!$component->DTSTART->hasTime()) {
|
||||
$endDate = clone $component->DTSTART->getDateTime();
|
||||
$endDate->modify('+1 day');
|
||||
$lastOccurence = $endDate->getTimeStamp();
|
||||
} else {
|
||||
$lastOccurence = $firstOccurence;
|
||||
}
|
||||
} else {
|
||||
$it = new VObject\RecurrenceIterator($vObject, (string)$component->UID);
|
||||
$maxDate = new \DateTime(self::MAX_DATE);
|
||||
if ($it->isInfinite()) {
|
||||
$lastOccurence = $maxDate->getTimeStamp();
|
||||
} else {
|
||||
$end = $it->getDtEnd();
|
||||
while($it->valid() && $end < $maxDate) {
|
||||
$end = $it->getDtEnd();
|
||||
$it->next();
|
||||
|
||||
}
|
||||
$lastOccurence = $end->getTimeStamp();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'etag' => md5($calendarData),
|
||||
'size' => strlen($calendarData),
|
||||
'componentType' => $componentType,
|
||||
'firstOccurence' => $firstOccurence,
|
||||
'lastOccurence' => $lastOccurence,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing calendar object.
|
||||
*
|
||||
* @param string $calendarId
|
||||
* @param string $objectUri
|
||||
* @return void
|
||||
*/
|
||||
public function deleteCalendarObject($calendarId,$objectUri) {
|
||||
|
||||
$stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
|
||||
$stmt->execute(array($calendarId,$objectUri));
|
||||
$stmt = $this->pdo->prepare('UPDATE '. $this->calendarTableName .' SET ctag = ctag + 1 WHERE id = ?');
|
||||
$stmt->execute(array($calendarId));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a calendar-query on the contents of this calendar.
|
||||
*
|
||||
* The calendar-query is defined in RFC4791 : CalDAV. Using the
|
||||
* calendar-query it is possible for a client to request a specific set of
|
||||
* object, based on contents of iCalendar properties, date-ranges and
|
||||
* iCalendar component types (VTODO, VEVENT).
|
||||
*
|
||||
* This method should just return a list of (relative) urls that match this
|
||||
* query.
|
||||
*
|
||||
* The list of filters are specified as an array. The exact array is
|
||||
* documented by \Sabre\CalDAV\CalendarQueryParser.
|
||||
*
|
||||
* Note that it is extremely likely that getCalendarObject for every path
|
||||
* returned from this method will be called almost immediately after. You
|
||||
* may want to anticipate this to speed up these requests.
|
||||
*
|
||||
* This method provides a default implementation, which parses *all* the
|
||||
* iCalendar objects in the specified calendar.
|
||||
*
|
||||
* This default may well be good enough for personal use, and calendars
|
||||
* that aren't very large. But if you anticipate high usage, big calendars
|
||||
* or high loads, you are strongly adviced to optimize certain paths.
|
||||
*
|
||||
* The best way to do so is override this method and to optimize
|
||||
* specifically for 'common filters'.
|
||||
*
|
||||
* Requests that are extremely common are:
|
||||
* * requests for just VEVENTS
|
||||
* * requests for just VTODO
|
||||
* * requests with a time-range-filter on a VEVENT.
|
||||
*
|
||||
* ..and combinations of these requests. It may not be worth it to try to
|
||||
* handle every possible situation and just rely on the (relatively
|
||||
* easy to use) CalendarQueryValidator to handle the rest.
|
||||
*
|
||||
* Note that especially time-range-filters may be difficult to parse. A
|
||||
* time-range filter specified on a VEVENT must for instance also handle
|
||||
* recurrence rules correctly.
|
||||
* A good example of how to interprete all these filters can also simply
|
||||
* be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
|
||||
* as possible, so it gives you a good idea on what type of stuff you need
|
||||
* to think of.
|
||||
*
|
||||
* This specific implementation (for the PDO) backend optimizes filters on
|
||||
* specific components, and VEVENT time-ranges.
|
||||
*
|
||||
* @param string $calendarId
|
||||
* @param array $filters
|
||||
* @return array
|
||||
*/
|
||||
public function calendarQuery($calendarId, array $filters) {
|
||||
|
||||
$result = array();
|
||||
$validator = new \Sabre\CalDAV\CalendarQueryValidator();
|
||||
|
||||
$componentType = null;
|
||||
$requirePostFilter = true;
|
||||
$timeRange = null;
|
||||
|
||||
// if no filters were specified, we don't need to filter after a query
|
||||
if (!$filters['prop-filters'] && !$filters['comp-filters']) {
|
||||
$requirePostFilter = false;
|
||||
}
|
||||
|
||||
// Figuring out if there's a component filter
|
||||
if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
|
||||
$componentType = $filters['comp-filters'][0]['name'];
|
||||
|
||||
// Checking if we need post-filters
|
||||
if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
|
||||
$requirePostFilter = false;
|
||||
}
|
||||
// There was a time-range filter
|
||||
if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
|
||||
$timeRange = $filters['comp-filters'][0]['time-range'];
|
||||
|
||||
// If start time OR the end time is not specified, we can do a
|
||||
// 100% accurate mysql query.
|
||||
if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
|
||||
$requirePostFilter = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($requirePostFilter) {
|
||||
$query = "SELECT uri, calendardata FROM ".$this->calendarObjectTableName." WHERE calendarid = :calendarid";
|
||||
} else {
|
||||
$query = "SELECT uri FROM ".$this->calendarObjectTableName." WHERE calendarid = :calendarid";
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'calendarid' => $calendarId,
|
||||
);
|
||||
|
||||
if ($componentType) {
|
||||
$query.=" AND componenttype = :componenttype";
|
||||
$values['componenttype'] = $componentType;
|
||||
}
|
||||
|
||||
if ($timeRange && $timeRange['start']) {
|
||||
$query.=" AND lastoccurence > :startdate";
|
||||
$values['startdate'] = $timeRange['start']->getTimeStamp();
|
||||
}
|
||||
if ($timeRange && $timeRange['end']) {
|
||||
$query.=" AND firstoccurence < :enddate";
|
||||
$values['enddate'] = $timeRange['end']->getTimeStamp();
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare($query);
|
||||
$stmt->execute($values);
|
||||
|
||||
$result = array();
|
||||
while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
if ($requirePostFilter) {
|
||||
if (!$this->validateFilterForObject($row, $filters)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$result[] = $row['uri'];
|
||||
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Backend;
|
||||
|
||||
/**
|
||||
* Adds support for sharing features to a CalDAV server.
|
||||
*
|
||||
* Note: This feature is experimental, and may change in between different
|
||||
* SabreDAV versions.
|
||||
*
|
||||
* Early warning: Currently SabreDAV provides no implementation for this. This
|
||||
* is, because in it's current state there is no elegant way to do this.
|
||||
* The problem lies in the fact that a real CalDAV server with sharing support
|
||||
* would first need email support (with invite notifications), and really also
|
||||
* a browser-frontend that allows people to accept or reject these shares.
|
||||
*
|
||||
* In addition, the CalDAV backends are currently kept as independent as
|
||||
* possible, and should not be aware of principals, email addresses or
|
||||
* accounts.
|
||||
*
|
||||
* Adding an implementation for Sharing to standard-sabredav would contradict
|
||||
* these goals, so for this reason this is currently not implemented, although
|
||||
* it may very well in the future; but probably not before SabreDAV 2.0.
|
||||
*
|
||||
* The interface works however, so if you implement all this, and do it
|
||||
* correctly sharing _will_ work. It's not particularly easy, and I _urge you_
|
||||
* to make yourself acquainted with the following document first:
|
||||
*
|
||||
* https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
|
||||
*
|
||||
* An overview
|
||||
* ===========
|
||||
*
|
||||
* Implementing this interface will allow a user to share his or her calendars
|
||||
* to other users. Effectively, when a calendar is shared the calendar will
|
||||
* show up in both the Sharer's and Sharee's calendar-home root.
|
||||
* This interface adds a few methods that ensure that this happens, and there
|
||||
* are also a number of new requirements in the base-class you must now follow.
|
||||
*
|
||||
*
|
||||
* How it works
|
||||
* ============
|
||||
*
|
||||
* When a user shares a calendar, the updateShares() method will be called with
|
||||
* a list of sharees that are now added, and a list of sharees that have been
|
||||
* removed.
|
||||
* Removal is instant, but when a sharee is added the sharee first gets a
|
||||
* chance to accept or reject the invitation for a share.
|
||||
*
|
||||
* After a share is accepted, the calendar will be returned from
|
||||
* getUserCalendars for both the sharer, and the sharee.
|
||||
*
|
||||
* If the sharee deletes the calendar, only their share gets deleted. When the
|
||||
* owner deletes a calendar, it will be removed for everybody.
|
||||
*
|
||||
*
|
||||
* Notifications
|
||||
* =============
|
||||
*
|
||||
* During all these sharing operations, a lot of notifications are sent back
|
||||
* and forward.
|
||||
*
|
||||
* Whenever the list of sharees for a calendar has been changed (they have been
|
||||
* added, removed or modified) all sharees should get a notification for this
|
||||
* change.
|
||||
* This notification is always represented by:
|
||||
*
|
||||
* Sabre\CalDAV\Notifications\Notification\Invite
|
||||
*
|
||||
* In the case of an invite, the sharee may reply with an 'accept' or
|
||||
* 'decline'. These are always represented by:
|
||||
*
|
||||
* Sabre\CalDAV\Notifications\Notification\Invite
|
||||
*
|
||||
*
|
||||
* Calendar access by sharees
|
||||
* ==========================
|
||||
*
|
||||
* As mentioned earlier, shared calendars must now also be returned for
|
||||
* getCalendarsForUser for sharees. A few things change though.
|
||||
*
|
||||
* The following properties must be specified:
|
||||
*
|
||||
* 1. {http://calendarserver.org/ns/}shared-url
|
||||
*
|
||||
* This property MUST contain the url to the original calendar, that is.. the
|
||||
* path to the calendar from the owner.
|
||||
*
|
||||
* 2. {http://sabredav.org/ns}owner-principal
|
||||
*
|
||||
* This is a url to to the principal who is sharing the calendar.
|
||||
*
|
||||
* 3. {http://sabredav.org/ns}read-only
|
||||
*
|
||||
* This should be either 0 or 1, depending on if the user has read-only or
|
||||
* read-write access to the calendar.
|
||||
*
|
||||
* Only when this is done, the calendar will correctly be marked as a calendar
|
||||
* that's shared to him, thus allowing clients to display the correct interface
|
||||
* and ACL enforcement.
|
||||
*
|
||||
* If a sharee deletes their calendar, only their instance of the calendar
|
||||
* should be deleted, the original should still exists.
|
||||
* Pretty much any 'dead' WebDAV properties on these shared calendars should be
|
||||
* specific to a user. This means that if the displayname is changed by a
|
||||
* sharee, the original is not affected. This is also true for:
|
||||
* * The description
|
||||
* * The color
|
||||
* * The order
|
||||
* * And any other dead properties.
|
||||
*
|
||||
* Properties like a ctag should not be different for multiple instances of the
|
||||
* calendar.
|
||||
*
|
||||
* Lastly, objects *within* calendars should also have user-specific data. The
|
||||
* two things that are user-specific are:
|
||||
* * VALARM objects
|
||||
* * The TRANSP property
|
||||
*
|
||||
* This _also_ implies that if a VALARM is deleted by a sharee for some event,
|
||||
* this has no effect on the original VALARM.
|
||||
*
|
||||
* Understandably, the this last requirement is one of the hardest.
|
||||
* Realisticly, I can see people ignoring this part of the spec, but that could
|
||||
* cause a different set of issues.
|
||||
*
|
||||
*
|
||||
* Publishing
|
||||
* ==========
|
||||
*
|
||||
* When a user publishes a url, the server should generate a 'publish url'.
|
||||
* This is a read-only url, anybody can use to consume the calendar feed.
|
||||
*
|
||||
* Calendars are in one of two states:
|
||||
* * published
|
||||
* * unpublished
|
||||
*
|
||||
* If a calendar is published, the following property should be returned
|
||||
* for each calendar in getCalendarsForPrincipal.
|
||||
*
|
||||
* {http://calendarserver.org/ns/}publish-url
|
||||
*
|
||||
* This element should contain a {DAV:}href element, which points to the
|
||||
* public url that does not require authentication. Unlike every other href,
|
||||
* this url must be absolute.
|
||||
*
|
||||
* Ideally, the following property is always returned
|
||||
*
|
||||
* {http://calendarserver.org/ns/}pre-publish-url
|
||||
*
|
||||
* This property should contain the url that the calendar _would_ have, if it
|
||||
* were to be published. iCal uses this to display the url, before the user
|
||||
* will actually publish it.
|
||||
*
|
||||
*
|
||||
* Selectively disabling publish or share feature
|
||||
* ==============================================
|
||||
*
|
||||
* If Sabre\CalDAV\Property\AllowedSharingModes is returned from
|
||||
* getCalendarsByUser, this allows the server to specify whether either sharing,
|
||||
* or publishing is supported.
|
||||
*
|
||||
* This allows a client to determine in advance which features are available,
|
||||
* and update the interface appropriately. If this property is not returned by
|
||||
* the backend, the SharingPlugin automatically injects it and assumes both
|
||||
* features are available.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface SharingSupport extends NotificationSupport {
|
||||
|
||||
/**
|
||||
* Updates the list of shares.
|
||||
*
|
||||
* The first array is a list of people that are to be added to the
|
||||
* calendar.
|
||||
*
|
||||
* Every element in the add array has the following properties:
|
||||
* * href - A url. Usually a mailto: address
|
||||
* * commonName - Usually a first and last name, or false
|
||||
* * summary - A description of the share, can also be false
|
||||
* * readOnly - A boolean value
|
||||
*
|
||||
* Every element in the remove array is just the address string.
|
||||
*
|
||||
* Note that if the calendar is currently marked as 'not shared' by and
|
||||
* this method is called, the calendar should be 'upgraded' to a shared
|
||||
* calendar.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param array $add
|
||||
* @param array $remove
|
||||
* @return void
|
||||
*/
|
||||
function updateShares($calendarId, array $add, array $remove);
|
||||
|
||||
/**
|
||||
* Returns the list of people whom this calendar is shared with.
|
||||
*
|
||||
* Every element in this array should have the following properties:
|
||||
* * href - Often a mailto: address
|
||||
* * commonName - Optional, for example a first + last name
|
||||
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
|
||||
* * readOnly - boolean
|
||||
* * summary - Optional, a description for the share
|
||||
*
|
||||
* This method may be called by either the original instance of the
|
||||
* calendar, as well as the shared instances. In the case of the shared
|
||||
* instances, it is perfectly acceptable to return an empty array in case
|
||||
* there are privacy concerns.
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @return array
|
||||
*/
|
||||
function getShares($calendarId);
|
||||
|
||||
/**
|
||||
* This method is called when a user replied to a request to share.
|
||||
*
|
||||
* If the user chose to accept the share, this method should return the
|
||||
* newly created calendar url.
|
||||
*
|
||||
* @param string href The sharee who is replying (often a mailto: address)
|
||||
* @param int status One of the SharingPlugin::STATUS_* constants
|
||||
* @param string $calendarUri The url to the calendar thats being shared
|
||||
* @param string $inReplyTo The unique id this message is a response to
|
||||
* @param string $summary A description of the reply
|
||||
* @return null|string
|
||||
*/
|
||||
function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null);
|
||||
|
||||
/**
|
||||
* Publishes a calendar
|
||||
*
|
||||
* @param mixed $calendarId
|
||||
* @param bool $value
|
||||
* @return void
|
||||
*/
|
||||
function setPublishStatus($calendarId, $value);
|
||||
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* This object represents a CalDAV calendar.
|
||||
*
|
||||
* A calendar can contain multiple TODO and or Events. These are represented
|
||||
* as \Sabre\CalDAV\CalendarObject objects.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Calendar implements ICalendar, DAV\IProperties, DAVACL\IACL {
|
||||
|
||||
/**
|
||||
* This is an array with calendar information
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $calendarInfo;
|
||||
|
||||
/**
|
||||
* CalDAV backend
|
||||
*
|
||||
* @var Backend\BackendInterface
|
||||
*/
|
||||
protected $caldavBackend;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Backend\BackendInterface $caldavBackend
|
||||
* @param array $calendarInfo
|
||||
*/
|
||||
public function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) {
|
||||
|
||||
$this->caldavBackend = $caldavBackend;
|
||||
$this->calendarInfo = $calendarInfo;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the calendar
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return $this->calendarInfo['uri'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates properties such as the display name and description
|
||||
*
|
||||
* @param array $mutations
|
||||
* @return array
|
||||
*/
|
||||
public function updateProperties($mutations) {
|
||||
|
||||
return $this->caldavBackend->updateCalendar($this->calendarInfo['id'],$mutations);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of properties
|
||||
*
|
||||
* @param array $requestedProperties
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties($requestedProperties) {
|
||||
|
||||
$response = array();
|
||||
|
||||
foreach($requestedProperties as $prop) switch($prop) {
|
||||
|
||||
case '{urn:ietf:params:xml:ns:caldav}supported-calendar-data' :
|
||||
$response[$prop] = new Property\SupportedCalendarData();
|
||||
break;
|
||||
case '{urn:ietf:params:xml:ns:caldav}supported-collation-set' :
|
||||
$response[$prop] = new Property\SupportedCollationSet();
|
||||
break;
|
||||
case '{DAV:}owner' :
|
||||
$response[$prop] = new DAVACL\Property\Principal(DAVACL\Property\Principal::HREF,$this->calendarInfo['principaluri']);
|
||||
break;
|
||||
default :
|
||||
if (isset($this->calendarInfo[$prop])) $response[$prop] = $this->calendarInfo[$prop];
|
||||
break;
|
||||
|
||||
}
|
||||
return $response;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a calendar object
|
||||
*
|
||||
* The contained calendar objects are for example Events or Todo's.
|
||||
*
|
||||
* @param string $name
|
||||
* @return \Sabre\CalDAV\ICalendarObject
|
||||
*/
|
||||
public function getChild($name) {
|
||||
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name);
|
||||
|
||||
if (!$obj) throw new DAV\Exception\NotFound('Calendar object not found');
|
||||
|
||||
$obj['acl'] = $this->getACL();
|
||||
// Removing the irrelivant
|
||||
foreach($obj['acl'] as $key=>$acl) {
|
||||
if ($acl['privilege'] === '{' . Plugin::NS_CALDAV . '}read-free-busy') {
|
||||
unset($obj['acl'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return new CalendarObject($this->caldavBackend,$this->calendarInfo,$obj);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full list of calendar objects
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getChildren() {
|
||||
|
||||
$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
|
||||
$children = array();
|
||||
foreach($objs as $obj) {
|
||||
$obj['acl'] = $this->getACL();
|
||||
// Removing the irrelivant
|
||||
foreach($obj['acl'] as $key=>$acl) {
|
||||
if ($acl['privilege'] === '{' . Plugin::NS_CALDAV . '}read-free-busy') {
|
||||
unset($obj['acl'][$key]);
|
||||
}
|
||||
}
|
||||
$children[] = new CalendarObject($this->caldavBackend,$this->calendarInfo,$obj);
|
||||
}
|
||||
return $children;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a child-node exists.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function childExists($name) {
|
||||
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name);
|
||||
if (!$obj)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new directory
|
||||
*
|
||||
* We actually block this, as subdirectories are not allowed in calendars.
|
||||
*
|
||||
* @param string $name
|
||||
* @return void
|
||||
*/
|
||||
public function createDirectory($name) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file
|
||||
*
|
||||
* The contents of the new file must be a valid ICalendar string.
|
||||
*
|
||||
* @param string $name
|
||||
* @param resource $calendarData
|
||||
* @return string|null
|
||||
*/
|
||||
public function createFile($name,$calendarData = null) {
|
||||
|
||||
if (is_resource($calendarData)) {
|
||||
$calendarData = stream_get_contents($calendarData);
|
||||
}
|
||||
return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'],$name,$calendarData);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the calendar.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete() {
|
||||
|
||||
$this->caldavBackend->deleteCalendar($this->calendarInfo['id']);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the calendar. Note that most calendars use the
|
||||
* {DAV:}displayname to display a name to display a name.
|
||||
*
|
||||
* @param string $newName
|
||||
* @return void
|
||||
*/
|
||||
public function setName($newName) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modification date as a unix timestamp.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getLastModified() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the owner principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOwner() {
|
||||
|
||||
return $this->calendarInfo['principaluri'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a group principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGroup() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ACE's for this node.
|
||||
*
|
||||
* Each ACE has the following properties:
|
||||
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
|
||||
* currently the only supported privileges
|
||||
* * 'principal', a url to the principal who owns the node
|
||||
* * 'protected' (optional), indicating that this ACE is not allowed to
|
||||
* be updated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getACL() {
|
||||
|
||||
return array(
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->getOwner(),
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}write',
|
||||
'principal' => $this->getOwner(),
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->getOwner() . '/calendar-proxy-write',
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}write',
|
||||
'principal' => $this->getOwner() . '/calendar-proxy-write',
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->getOwner() . '/calendar-proxy-read',
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
|
||||
'principal' => '{DAV:}authenticated',
|
||||
'protected' => true,
|
||||
),
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ACL
|
||||
*
|
||||
* This method will receive a list of new ACE's.
|
||||
*
|
||||
* @param array $acl
|
||||
* @return void
|
||||
*/
|
||||
public function setACL(array $acl) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of supported privileges for this node.
|
||||
*
|
||||
* The returned data structure is a list of nested privileges.
|
||||
* See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
|
||||
* standard structure.
|
||||
*
|
||||
* If null is returned from this method, the default privilege set is used,
|
||||
* which is fine for most common usecases.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getSupportedPrivilegeSet() {
|
||||
|
||||
$default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
|
||||
|
||||
// We need to inject 'read-free-busy' in the tree, aggregated under
|
||||
// {DAV:}read.
|
||||
foreach($default['aggregates'] as &$agg) {
|
||||
|
||||
if ($agg['privilege'] !== '{DAV:}read') continue;
|
||||
|
||||
$agg['aggregates'][] = array(
|
||||
'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
|
||||
);
|
||||
|
||||
}
|
||||
return $default;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a calendar-query on the contents of this calendar.
|
||||
*
|
||||
* The calendar-query is defined in RFC4791 : CalDAV. Using the
|
||||
* calendar-query it is possible for a client to request a specific set of
|
||||
* object, based on contents of iCalendar properties, date-ranges and
|
||||
* iCalendar component types (VTODO, VEVENT).
|
||||
*
|
||||
* This method should just return a list of (relative) urls that match this
|
||||
* query.
|
||||
*
|
||||
* The list of filters are specified as an array. The exact array is
|
||||
* documented by Sabre\CalDAV\CalendarQueryParser.
|
||||
*
|
||||
* @param array $filters
|
||||
* @return array
|
||||
*/
|
||||
public function calendarQuery(array $filters) {
|
||||
|
||||
return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* The CalendarObject represents a single VEVENT or VTODO within a Calendar.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class CalendarObject extends \Sabre\DAV\File implements ICalendarObject, \Sabre\DAVACL\IACL {
|
||||
|
||||
/**
|
||||
* Sabre\CalDAV\Backend\BackendInterface
|
||||
*
|
||||
* @var Sabre\CalDAV\Backend\AbstractBackend
|
||||
*/
|
||||
protected $caldavBackend;
|
||||
|
||||
/**
|
||||
* Array with information about this CalendarObject
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $objectData;
|
||||
|
||||
/**
|
||||
* Array with information about the containing calendar
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $calendarInfo;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Backend\BackendInterface $caldavBackend
|
||||
* @param array $calendarInfo
|
||||
* @param array $objectData
|
||||
*/
|
||||
public function __construct(Backend\BackendInterface $caldavBackend,array $calendarInfo,array $objectData) {
|
||||
|
||||
$this->caldavBackend = $caldavBackend;
|
||||
|
||||
if (!isset($objectData['calendarid'])) {
|
||||
throw new \InvalidArgumentException('The objectData argument must contain a \'calendarid\' property');
|
||||
}
|
||||
if (!isset($objectData['uri'])) {
|
||||
throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
|
||||
}
|
||||
|
||||
$this->calendarInfo = $calendarInfo;
|
||||
$this->objectData = $objectData;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uri for this object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return $this->objectData['uri'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ICalendar-formatted object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get() {
|
||||
|
||||
// Pre-populating the 'calendardata' is optional, if we don't have it
|
||||
// already we fetch it from the backend.
|
||||
if (!isset($this->objectData['calendardata'])) {
|
||||
$this->objectData = $this->caldavBackend->getCalendarObject($this->objectData['calendarid'], $this->objectData['uri']);
|
||||
}
|
||||
return $this->objectData['calendardata'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ICalendar-formatted object
|
||||
*
|
||||
* @param string|resource $calendarData
|
||||
* @return string
|
||||
*/
|
||||
public function put($calendarData) {
|
||||
|
||||
if (is_resource($calendarData)) {
|
||||
$calendarData = stream_get_contents($calendarData);
|
||||
}
|
||||
$etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'],$this->objectData['uri'],$calendarData);
|
||||
$this->objectData['calendardata'] = $calendarData;
|
||||
$this->objectData['etag'] = $etag;
|
||||
|
||||
return $etag;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the calendar object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete() {
|
||||
|
||||
$this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'],$this->objectData['uri']);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime content-type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContentType() {
|
||||
|
||||
return 'text/calendar; charset=utf-8';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ETag for this object.
|
||||
*
|
||||
* The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getETag() {
|
||||
|
||||
if (isset($this->objectData['etag'])) {
|
||||
return $this->objectData['etag'];
|
||||
} else {
|
||||
return '"' . md5($this->get()). '"';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modification date as a unix timestamp
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLastModified() {
|
||||
|
||||
return $this->objectData['lastmodified'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of this object in bytes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSize() {
|
||||
|
||||
if (array_key_exists('size',$this->objectData)) {
|
||||
return $this->objectData['size'];
|
||||
} else {
|
||||
return strlen($this->get());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the owner principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOwner() {
|
||||
|
||||
return $this->calendarInfo['principaluri'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a group principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGroup() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ACE's for this node.
|
||||
*
|
||||
* Each ACE has the following properties:
|
||||
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
|
||||
* currently the only supported privileges
|
||||
* * 'principal', a url to the principal who owns the node
|
||||
* * 'protected' (optional), indicating that this ACE is not allowed to
|
||||
* be updated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getACL() {
|
||||
|
||||
// An alternative acl may be specified in the object data.
|
||||
if (isset($this->objectData['acl'])) {
|
||||
return $this->objectData['acl'];
|
||||
}
|
||||
|
||||
// The default ACL
|
||||
return array(
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->calendarInfo['principaluri'],
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}write',
|
||||
'principal' => $this->calendarInfo['principaluri'],
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}write',
|
||||
'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-read',
|
||||
'protected' => true,
|
||||
),
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ACL
|
||||
*
|
||||
* This method will receive a list of new ACE's.
|
||||
*
|
||||
* @param array $acl
|
||||
* @return void
|
||||
*/
|
||||
public function setACL(array $acl) {
|
||||
|
||||
throw new \Sabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of supported privileges for this node.
|
||||
*
|
||||
* The returned data structure is a list of nested privileges.
|
||||
* See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
|
||||
* standard structure.
|
||||
*
|
||||
* If null is returned from this method, the default privilege set is used,
|
||||
* which is fine for most common usecases.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getSupportedPrivilegeSet() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
use Sabre\VObject;
|
||||
|
||||
/**
|
||||
* Parses the calendar-query report request body.
|
||||
*
|
||||
* Whoever designed this format, and the CalDAV equivalent even more so,
|
||||
* has no feel for design.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class CalendarQueryParser {
|
||||
|
||||
/**
|
||||
* List of requested properties the client wanted
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $requestedProperties;
|
||||
|
||||
/**
|
||||
* List of property/component filters.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $filters;
|
||||
|
||||
/**
|
||||
* This property will contain null if CALDAV:expand was not specified,
|
||||
* otherwise it will contain an array with 2 elements (start, end). Each
|
||||
* contain a DateTime object.
|
||||
*
|
||||
* If expand is specified, recurring calendar objects are to be expanded
|
||||
* into their individual components, and only the components that fall
|
||||
* within the specified time-range are to be returned.
|
||||
*
|
||||
* For more details, see rfc4791, section 9.6.5.
|
||||
*
|
||||
* @var null|array
|
||||
*/
|
||||
public $expand;
|
||||
|
||||
/**
|
||||
* DOM Document
|
||||
*
|
||||
* @var DOMDocument
|
||||
*/
|
||||
protected $dom;
|
||||
|
||||
/**
|
||||
* DOM XPath object
|
||||
*
|
||||
* @var DOMXPath
|
||||
*/
|
||||
protected $xpath;
|
||||
|
||||
/**
|
||||
* Creates the parser
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
*/
|
||||
public function __construct(\DOMDocument $dom) {
|
||||
|
||||
$this->dom = $dom;
|
||||
$this->xpath = new \DOMXPath($dom);
|
||||
$this->xpath->registerNameSpace('cal',Plugin::NS_CALDAV);
|
||||
$this->xpath->registerNameSpace('dav','urn:DAV');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function parse() {
|
||||
|
||||
$filterNode = null;
|
||||
|
||||
$filter = $this->xpath->query('/cal:calendar-query/cal:filter');
|
||||
if ($filter->length !== 1) {
|
||||
throw new \Sabre\DAV\Exception\BadRequest('Only one filter element is allowed');
|
||||
}
|
||||
|
||||
$compFilters = $this->parseCompFilters($filter->item(0));
|
||||
if (count($compFilters)!==1) {
|
||||
throw new \Sabre\DAV\Exception\BadRequest('There must be exactly 1 top-level comp-filter.');
|
||||
}
|
||||
|
||||
$this->filters = $compFilters[0];
|
||||
$this->requestedProperties = array_keys(\Sabre\DAV\XMLUtil::parseProperties($this->dom->firstChild));
|
||||
|
||||
$expand = $this->xpath->query('/cal:calendar-query/dav:prop/cal:calendar-data/cal:expand');
|
||||
if ($expand->length>0) {
|
||||
$this->expand = $this->parseExpand($expand->item(0));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all the 'comp-filter' elements from a node
|
||||
*
|
||||
* @param \DOMElement $parentNode
|
||||
* @return array
|
||||
*/
|
||||
protected function parseCompFilters(\DOMElement $parentNode) {
|
||||
|
||||
$compFilterNodes = $this->xpath->query('cal:comp-filter', $parentNode);
|
||||
$result = array();
|
||||
|
||||
for($ii=0; $ii < $compFilterNodes->length; $ii++) {
|
||||
|
||||
$compFilterNode = $compFilterNodes->item($ii);
|
||||
|
||||
$compFilter = array();
|
||||
$compFilter['name'] = $compFilterNode->getAttribute('name');
|
||||
$compFilter['is-not-defined'] = $this->xpath->query('cal:is-not-defined', $compFilterNode)->length>0;
|
||||
$compFilter['comp-filters'] = $this->parseCompFilters($compFilterNode);
|
||||
$compFilter['prop-filters'] = $this->parsePropFilters($compFilterNode);
|
||||
$compFilter['time-range'] = $this->parseTimeRange($compFilterNode);
|
||||
|
||||
if ($compFilter['time-range'] && !in_array($compFilter['name'],array(
|
||||
'VEVENT',
|
||||
'VTODO',
|
||||
'VJOURNAL',
|
||||
'VFREEBUSY',
|
||||
'VALARM',
|
||||
))) {
|
||||
throw new \Sabre\DAV\Exception\BadRequest('The time-range filter is not defined for the ' . $compFilter['name'] . ' component');
|
||||
};
|
||||
|
||||
$result[] = $compFilter;
|
||||
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all the prop-filter elements from a node
|
||||
*
|
||||
* @param \DOMElement $parentNode
|
||||
* @return array
|
||||
*/
|
||||
protected function parsePropFilters(\DOMElement $parentNode) {
|
||||
|
||||
$propFilterNodes = $this->xpath->query('cal:prop-filter', $parentNode);
|
||||
$result = array();
|
||||
|
||||
for ($ii=0; $ii < $propFilterNodes->length; $ii++) {
|
||||
|
||||
$propFilterNode = $propFilterNodes->item($ii);
|
||||
$propFilter = array();
|
||||
$propFilter['name'] = $propFilterNode->getAttribute('name');
|
||||
$propFilter['is-not-defined'] = $this->xpath->query('cal:is-not-defined', $propFilterNode)->length>0;
|
||||
$propFilter['param-filters'] = $this->parseParamFilters($propFilterNode);
|
||||
$propFilter['text-match'] = $this->parseTextMatch($propFilterNode);
|
||||
$propFilter['time-range'] = $this->parseTimeRange($propFilterNode);
|
||||
|
||||
$result[] = $propFilter;
|
||||
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the param-filter element
|
||||
*
|
||||
* @param \DOMElement $parentNode
|
||||
* @return array
|
||||
*/
|
||||
protected function parseParamFilters(\DOMElement $parentNode) {
|
||||
|
||||
$paramFilterNodes = $this->xpath->query('cal:param-filter', $parentNode);
|
||||
$result = array();
|
||||
|
||||
for($ii=0;$ii<$paramFilterNodes->length;$ii++) {
|
||||
|
||||
$paramFilterNode = $paramFilterNodes->item($ii);
|
||||
$paramFilter = array();
|
||||
$paramFilter['name'] = $paramFilterNode->getAttribute('name');
|
||||
$paramFilter['is-not-defined'] = $this->xpath->query('cal:is-not-defined', $paramFilterNode)->length>0;
|
||||
$paramFilter['text-match'] = $this->parseTextMatch($paramFilterNode);
|
||||
|
||||
$result[] = $paramFilter;
|
||||
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the text-match element
|
||||
*
|
||||
* @param \DOMElement $parentNode
|
||||
* @return array|null
|
||||
*/
|
||||
protected function parseTextMatch(\DOMElement $parentNode) {
|
||||
|
||||
$textMatchNodes = $this->xpath->query('cal:text-match', $parentNode);
|
||||
|
||||
if ($textMatchNodes->length === 0)
|
||||
return null;
|
||||
|
||||
$textMatchNode = $textMatchNodes->item(0);
|
||||
$negateCondition = $textMatchNode->getAttribute('negate-condition');
|
||||
$negateCondition = $negateCondition==='yes';
|
||||
$collation = $textMatchNode->getAttribute('collation');
|
||||
if (!$collation) $collation = 'i;ascii-casemap';
|
||||
|
||||
return array(
|
||||
'negate-condition' => $negateCondition,
|
||||
'collation' => $collation,
|
||||
'value' => $textMatchNode->nodeValue
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the time-range element
|
||||
*
|
||||
* @param \DOMElement $parentNode
|
||||
* @return array|null
|
||||
*/
|
||||
protected function parseTimeRange(\DOMElement $parentNode) {
|
||||
|
||||
$timeRangeNodes = $this->xpath->query('cal:time-range', $parentNode);
|
||||
if ($timeRangeNodes->length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timeRangeNode = $timeRangeNodes->item(0);
|
||||
|
||||
if ($start = $timeRangeNode->getAttribute('start')) {
|
||||
$start = VObject\DateTimeParser::parseDateTime($start);
|
||||
} else {
|
||||
$start = null;
|
||||
}
|
||||
if ($end = $timeRangeNode->getAttribute('end')) {
|
||||
$end = VObject\DateTimeParser::parseDateTime($end);
|
||||
} else {
|
||||
$end = null;
|
||||
}
|
||||
|
||||
if (!is_null($start) && !is_null($end) && $end <= $start) {
|
||||
throw new \Sabre\DAV\Exception\BadRequest('The end-date must be larger than the start-date in the time-range filter');
|
||||
}
|
||||
|
||||
return array(
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the CALDAV:expand element
|
||||
*
|
||||
* @param \DOMElement $parentNode
|
||||
* @return void
|
||||
*/
|
||||
protected function parseExpand(\DOMElement $parentNode) {
|
||||
|
||||
$start = $parentNode->getAttribute('start');
|
||||
if(!$start) {
|
||||
throw new \Sabre\DAV\Exception\BadRequest('The "start" attribute is required for the CALDAV:expand element');
|
||||
}
|
||||
$start = VObject\DateTimeParser::parseDateTime($start);
|
||||
|
||||
$end = $parentNode->getAttribute('end');
|
||||
if(!$end) {
|
||||
throw new \Sabre\DAV\Exception\BadRequest('The "end" attribute is required for the CALDAV:expand element');
|
||||
}
|
||||
|
||||
$end = VObject\DateTimeParser::parseDateTime($end);
|
||||
|
||||
if ($end <= $start) {
|
||||
throw new \Sabre\DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.');
|
||||
}
|
||||
|
||||
return array(
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,392 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
use Sabre\VObject;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* CalendarQuery Validator
|
||||
*
|
||||
* This class is responsible for checking if an iCalendar object matches a set
|
||||
* of filters. The main function to do this is 'validate'.
|
||||
*
|
||||
* This is used to determine which icalendar objects should be returned for a
|
||||
* calendar-query REPORT request.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class CalendarQueryValidator {
|
||||
|
||||
/**
|
||||
* Verify if a list of filters applies to the calendar data object
|
||||
*
|
||||
* The list of filters must be formatted as parsed by \Sabre\CalDAV\CalendarQueryParser
|
||||
*
|
||||
* @param VObject\Component $vObject
|
||||
* @param array $filters
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(VObject\Component $vObject,array $filters) {
|
||||
|
||||
// The top level object is always a component filter.
|
||||
// We'll parse it manually, as it's pretty simple.
|
||||
if ($vObject->name !== $filters['name']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return
|
||||
$this->validateCompFilters($vObject, $filters['comp-filters']) &&
|
||||
$this->validatePropFilters($vObject, $filters['prop-filters']);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks the validity of comp-filters.
|
||||
*
|
||||
* A list of comp-filters needs to be specified. Also the parent of the
|
||||
* component we're checking should be specified, not the component to check
|
||||
* itself.
|
||||
*
|
||||
* @param VObject\Component $parent
|
||||
* @param array $filters
|
||||
* @return bool
|
||||
*/
|
||||
protected function validateCompFilters(VObject\Component $parent, array $filters) {
|
||||
|
||||
foreach($filters as $filter) {
|
||||
|
||||
$isDefined = isset($parent->{$filter['name']});
|
||||
|
||||
if ($filter['is-not-defined']) {
|
||||
|
||||
if ($isDefined) {
|
||||
return false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
if (!$isDefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($filter['time-range']) {
|
||||
foreach($parent->{$filter['name']} as $subComponent) {
|
||||
if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$filter['comp-filters'] && !$filter['prop-filters']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there are sub-filters, we need to find at least one component
|
||||
// for which the subfilters hold true.
|
||||
foreach($parent->{$filter['name']} as $subComponent) {
|
||||
|
||||
if (
|
||||
$this->validateCompFilters($subComponent, $filter['comp-filters']) &&
|
||||
$this->validatePropFilters($subComponent, $filter['prop-filters'])) {
|
||||
// We had a match, so this comp-filter succeeds
|
||||
continue 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If we got here it means there were sub-comp-filters or
|
||||
// sub-prop-filters and there was no match. This means this filter
|
||||
// needs to return false.
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// If we got here it means we got through all comp-filters alive so the
|
||||
// filters were all true.
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks the validity of prop-filters.
|
||||
*
|
||||
* A list of prop-filters needs to be specified. Also the parent of the
|
||||
* property we're checking should be specified, not the property to check
|
||||
* itself.
|
||||
*
|
||||
* @param VObject\Component $parent
|
||||
* @param array $filters
|
||||
* @return bool
|
||||
*/
|
||||
protected function validatePropFilters(VObject\Component $parent, array $filters) {
|
||||
|
||||
foreach($filters as $filter) {
|
||||
|
||||
$isDefined = isset($parent->{$filter['name']});
|
||||
|
||||
if ($filter['is-not-defined']) {
|
||||
|
||||
if ($isDefined) {
|
||||
return false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
if (!$isDefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($filter['time-range']) {
|
||||
foreach($parent->{$filter['name']} as $subComponent) {
|
||||
if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$filter['param-filters'] && !$filter['text-match']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there are sub-filters, we need to find at least one property
|
||||
// for which the subfilters hold true.
|
||||
foreach($parent->{$filter['name']} as $subComponent) {
|
||||
|
||||
if(
|
||||
$this->validateParamFilters($subComponent, $filter['param-filters']) &&
|
||||
(!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match']))
|
||||
) {
|
||||
// We had a match, so this prop-filter succeeds
|
||||
continue 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If we got here it means there were sub-param-filters or
|
||||
// text-match filters and there was no match. This means the
|
||||
// filter needs to return false.
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// If we got here it means we got through all prop-filters alive so the
|
||||
// filters were all true.
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks the validity of param-filters.
|
||||
*
|
||||
* A list of param-filters needs to be specified. Also the parent of the
|
||||
* parameter we're checking should be specified, not the parameter to check
|
||||
* itself.
|
||||
*
|
||||
* @param VObject\Property $parent
|
||||
* @param array $filters
|
||||
* @return bool
|
||||
*/
|
||||
protected function validateParamFilters(VObject\Property $parent, array $filters) {
|
||||
|
||||
foreach($filters as $filter) {
|
||||
|
||||
$isDefined = isset($parent[$filter['name']]);
|
||||
|
||||
if ($filter['is-not-defined']) {
|
||||
|
||||
if ($isDefined) {
|
||||
return false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
if (!$isDefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$filter['text-match']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (version_compare(VObject\Version::VERSION, '3.0.0beta1', '>=')) {
|
||||
|
||||
// If there are sub-filters, we need to find at least one parameter
|
||||
// for which the subfilters hold true.
|
||||
foreach($parent[$filter['name']]->getParts() as $subParam) {
|
||||
|
||||
if($this->validateTextMatch($subParam,$filter['text-match'])) {
|
||||
// We had a match, so this param-filter succeeds
|
||||
continue 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// If there are sub-filters, we need to find at least one parameter
|
||||
// for which the subfilters hold true.
|
||||
foreach($parent[$filter['name']] as $subParam) {
|
||||
|
||||
if($this->validateTextMatch($subParam,$filter['text-match'])) {
|
||||
// We had a match, so this param-filter succeeds
|
||||
continue 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If we got here it means there was a text-match filter and there
|
||||
// were no matches. This means the filter needs to return false.
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// If we got here it means we got through all param-filters alive so the
|
||||
// filters were all true.
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks the validity of a text-match.
|
||||
*
|
||||
* A single text-match should be specified as well as the specific property
|
||||
* or parameter we need to validate.
|
||||
*
|
||||
* @param VObject\Node|string $check Value to check against.
|
||||
* @param array $textMatch
|
||||
* @return bool
|
||||
*/
|
||||
protected function validateTextMatch($check, array $textMatch) {
|
||||
|
||||
if ($check instanceof VObject\Node) {
|
||||
$check = (string)$check;
|
||||
}
|
||||
|
||||
$isMatching = \Sabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']);
|
||||
|
||||
return ($textMatch['negate-condition'] xor $isMatching);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a component matches the given time range.
|
||||
*
|
||||
* This is all based on the rules specified in rfc4791, which are quite
|
||||
* complex.
|
||||
*
|
||||
* @param VObject\Node $component
|
||||
* @param DateTime $start
|
||||
* @param DateTime $end
|
||||
* @return bool
|
||||
*/
|
||||
protected function validateTimeRange(VObject\Node $component, $start, $end) {
|
||||
|
||||
if (is_null($start)) {
|
||||
$start = new DateTime('1900-01-01');
|
||||
}
|
||||
if (is_null($end)) {
|
||||
$end = new DateTime('3000-01-01');
|
||||
}
|
||||
|
||||
switch($component->name) {
|
||||
|
||||
case 'VEVENT' :
|
||||
case 'VTODO' :
|
||||
case 'VJOURNAL' :
|
||||
|
||||
return $component->isInTimeRange($start, $end);
|
||||
|
||||
case 'VALARM' :
|
||||
|
||||
// If the valarm is wrapped in a recurring event, we need to
|
||||
// expand the recursions, and validate each.
|
||||
//
|
||||
// Our datamodel doesn't easily allow us to do this straight
|
||||
// in the VALARM component code, so this is a hack, and an
|
||||
// expensive one too.
|
||||
if ($component->parent->name === 'VEVENT' && $component->parent->RRULE) {
|
||||
|
||||
// Fire up the iterator!
|
||||
$it = new VObject\RecurrenceIterator($component->parent->parent, (string)$component->parent->UID);
|
||||
while($it->valid()) {
|
||||
$expandedEvent = $it->getEventObject();
|
||||
|
||||
// We need to check from these expanded alarms, which
|
||||
// one is the first to trigger. Based on this, we can
|
||||
// determine if we can 'give up' expanding events.
|
||||
$firstAlarm = null;
|
||||
if ($expandedEvent->VALARM !== null) {
|
||||
foreach($expandedEvent->VALARM as $expandedAlarm) {
|
||||
|
||||
$effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
|
||||
if ($expandedAlarm->isInTimeRange($start, $end)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((string)$expandedAlarm->TRIGGER['VALUE'] === 'DATE-TIME') {
|
||||
// This is an alarm with a non-relative trigger
|
||||
// time, likely created by a buggy client. The
|
||||
// implication is that every alarm in this
|
||||
// recurring event trigger at the exact same
|
||||
// time. It doesn't make sense to traverse
|
||||
// further.
|
||||
} else {
|
||||
// We store the first alarm as a means to
|
||||
// figure out when we can stop traversing.
|
||||
if (!$firstAlarm || $effectiveTrigger < $firstAlarm) {
|
||||
$firstAlarm = $effectiveTrigger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_null($firstAlarm)) {
|
||||
// No alarm was found.
|
||||
//
|
||||
// Or technically: No alarm that will change for
|
||||
// every instance of the recurrence was found,
|
||||
// which means we can assume there was no match.
|
||||
return false;
|
||||
}
|
||||
if ($firstAlarm > $end) {
|
||||
return false;
|
||||
}
|
||||
$it->next();
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return $component->isInTimeRange($start, $end);
|
||||
}
|
||||
|
||||
case 'VFREEBUSY' :
|
||||
throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on ' . $component->name . ' components');
|
||||
|
||||
case 'COMPLETED' :
|
||||
case 'CREATED' :
|
||||
case 'DTEND' :
|
||||
case 'DTSTAMP' :
|
||||
case 'DTSTART' :
|
||||
case 'DUE' :
|
||||
case 'LAST-MODIFIED' :
|
||||
return ($start <= $component->getDateTime() && $end >= $component->getDateTime());
|
||||
|
||||
|
||||
|
||||
default :
|
||||
throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a ' . $component->name . ' component');
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
use Sabre\DAVACL\PrincipalBackend;
|
||||
|
||||
/**
|
||||
* Calendars collection
|
||||
*
|
||||
* This object is responsible for generating a list of calendar-homes for each
|
||||
* user.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class CalendarRootNode extends \Sabre\DAVACL\AbstractPrincipalCollection {
|
||||
|
||||
/**
|
||||
* CalDAV backend
|
||||
*
|
||||
* @var Sabre\CalDAV\Backend\BackendInterface
|
||||
*/
|
||||
protected $caldavBackend;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* This constructor needs both an authentication and a caldav backend.
|
||||
*
|
||||
* By default this class will show a list of calendar collections for
|
||||
* principals in the 'principals' collection. If your main principals are
|
||||
* actually located in a different path, use the $principalPrefix argument
|
||||
* to override this.
|
||||
*
|
||||
* @param PrincipalBackend\BackendInterface $principalBackend
|
||||
* @param Backend\BackendInterface $caldavBackend
|
||||
* @param string $principalPrefix
|
||||
*/
|
||||
public function __construct(PrincipalBackend\BackendInterface $principalBackend,Backend\BackendInterface $caldavBackend, $principalPrefix = 'principals') {
|
||||
|
||||
parent::__construct($principalBackend, $principalPrefix);
|
||||
$this->caldavBackend = $caldavBackend;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the nodename
|
||||
*
|
||||
* We're overriding this, because the default will be the 'principalPrefix',
|
||||
* and we want it to be Sabre\CalDAV\Plugin::CALENDAR_ROOT
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return Plugin::CALENDAR_ROOT;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a node for a principal.
|
||||
*
|
||||
* The passed array contains principal information, and is guaranteed to
|
||||
* at least contain a uri item. Other properties may or may not be
|
||||
* supplied by the authentication backend.
|
||||
*
|
||||
* @param array $principal
|
||||
* @return \Sabre\DAV\INode
|
||||
*/
|
||||
public function getChildForPrincipal(array $principal) {
|
||||
|
||||
return new UserCalendars($this->caldavBackend, $principal);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Exception;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* InvalidComponentType
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class InvalidComponentType extends DAV\Exception\Forbidden {
|
||||
|
||||
/**
|
||||
* Adds in extra information in the xml response.
|
||||
*
|
||||
* This method adds the {CALDAV:}supported-calendar-component as defined in rfc4791
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $errorNode
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server, \DOMElement $errorNode) {
|
||||
|
||||
$doc = $errorNode->ownerDocument;
|
||||
|
||||
$np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV,'cal:supported-calendar-component');
|
||||
$errorNode->appendChild($np);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\VObject;
|
||||
|
||||
/**
|
||||
* ICS Exporter
|
||||
*
|
||||
* This plugin adds the ability to export entire calendars as .ics files.
|
||||
* This is useful for clients that don't support CalDAV yet. They often do
|
||||
* support ics files.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ICSExportPlugin extends DAV\ServerPlugin {
|
||||
|
||||
/**
|
||||
* Reference to Server class
|
||||
*
|
||||
* @var \Sabre\DAV\Server
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
/**
|
||||
* Initializes the plugin and registers event handlers
|
||||
*
|
||||
* @param \Sabre\DAV\Server $server
|
||||
* @return void
|
||||
*/
|
||||
public function initialize(DAV\Server $server) {
|
||||
|
||||
$this->server = $server;
|
||||
$this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'), 90);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 'beforeMethod' event handles. This event handles intercepts GET requests ending
|
||||
* with ?export
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $uri
|
||||
* @return bool
|
||||
*/
|
||||
public function beforeMethod($method, $uri) {
|
||||
|
||||
if ($method!='GET') return;
|
||||
if ($this->server->httpRequest->getQueryString()!='export') return;
|
||||
|
||||
// splitting uri
|
||||
list($uri) = explode('?',$uri,2);
|
||||
|
||||
$node = $this->server->tree->getNodeForPath($uri);
|
||||
|
||||
if (!($node instanceof Calendar)) return;
|
||||
|
||||
// Checking ACL, if available.
|
||||
if ($aclPlugin = $this->server->getPlugin('acl')) {
|
||||
$aclPlugin->checkPrivileges($uri, '{DAV:}read');
|
||||
}
|
||||
|
||||
$this->server->httpResponse->setHeader('Content-Type','text/calendar');
|
||||
$this->server->httpResponse->sendStatus(200);
|
||||
|
||||
$nodes = $this->server->getPropertiesForPath($uri, array(
|
||||
'{' . Plugin::NS_CALDAV . '}calendar-data',
|
||||
),1);
|
||||
|
||||
$this->server->httpResponse->sendBody($this->generateICS($nodes));
|
||||
|
||||
// Returning false to break the event chain
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges all calendar objects, and builds one big ics export
|
||||
*
|
||||
* @param array $nodes
|
||||
* @return string
|
||||
*/
|
||||
public function generateICS(array $nodes) {
|
||||
|
||||
$calendar = new VObject\Component\VCalendar();
|
||||
$calendar->version = '2.0';
|
||||
if (DAV\Server::$exposeVersion) {
|
||||
$calendar->prodid = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
|
||||
} else {
|
||||
$calendar->prodid = '-//SabreDAV//SabreDAV//EN';
|
||||
}
|
||||
$calendar->calscale = 'GREGORIAN';
|
||||
|
||||
$collectedTimezones = array();
|
||||
|
||||
$timezones = array();
|
||||
$objects = array();
|
||||
|
||||
foreach($nodes as $node) {
|
||||
|
||||
if (!isset($node[200]['{' . Plugin::NS_CALDAV . '}calendar-data'])) {
|
||||
continue;
|
||||
}
|
||||
$nodeData = $node[200]['{' . Plugin::NS_CALDAV . '}calendar-data'];
|
||||
|
||||
$nodeComp = VObject\Reader::read($nodeData);
|
||||
|
||||
foreach($nodeComp->children() as $child) {
|
||||
|
||||
switch($child->name) {
|
||||
case 'VEVENT' :
|
||||
case 'VTODO' :
|
||||
case 'VJOURNAL' :
|
||||
$objects[] = $child;
|
||||
break;
|
||||
|
||||
// VTIMEZONE is special, because we need to filter out the duplicates
|
||||
case 'VTIMEZONE' :
|
||||
// Naively just checking tzid.
|
||||
if (in_array((string)$child->TZID, $collectedTimezones)) continue;
|
||||
|
||||
$timezones[] = $child;
|
||||
$collectedTimezones[] = $child->TZID;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach($timezones as $tz) $calendar->add($tz);
|
||||
foreach($objects as $obj) $calendar->add($obj);
|
||||
|
||||
return $calendar->serialize();
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* Calendar interface
|
||||
*
|
||||
* Implement this interface to allow a node to be recognized as an calendar.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface ICalendar extends DAV\ICollection {
|
||||
|
||||
/**
|
||||
* Performs a calendar-query on the contents of this calendar.
|
||||
*
|
||||
* The calendar-query is defined in RFC4791 : CalDAV. Using the
|
||||
* calendar-query it is possible for a client to request a specific set of
|
||||
* object, based on contents of iCalendar properties, date-ranges and
|
||||
* iCalendar component types (VTODO, VEVENT).
|
||||
*
|
||||
* This method should just return a list of (relative) urls that match this
|
||||
* query.
|
||||
*
|
||||
* The list of filters are specified as an array. The exact array is
|
||||
* documented by \Sabre\CalDAV\CalendarQueryParser.
|
||||
*
|
||||
* @param array $filters
|
||||
* @return array
|
||||
*/
|
||||
public function calendarQuery(array $filters);
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* CalendarObject interface
|
||||
*
|
||||
* Extend the ICalendarObject interface to allow your custom nodes to be picked up as
|
||||
* CalendarObjects.
|
||||
*
|
||||
* Calendar objects are resources such as Events, Todo's or Journals.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface ICalendarObject extends DAV\IFile {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* This interface represents a Calendar that can be shared with other users.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface IShareableCalendar extends ICalendar {
|
||||
|
||||
/**
|
||||
* Updates the list of shares.
|
||||
*
|
||||
* The first array is a list of people that are to be added to the
|
||||
* calendar.
|
||||
*
|
||||
* Every element in the add array has the following properties:
|
||||
* * href - A url. Usually a mailto: address
|
||||
* * commonName - Usually a first and last name, or false
|
||||
* * summary - A description of the share, can also be false
|
||||
* * readOnly - A boolean value
|
||||
*
|
||||
* Every element in the remove array is just the address string.
|
||||
*
|
||||
* @param array $add
|
||||
* @param array $remove
|
||||
* @return void
|
||||
*/
|
||||
function updateShares(array $add, array $remove);
|
||||
|
||||
/**
|
||||
* Returns the list of people whom this calendar is shared with.
|
||||
*
|
||||
* Every element in this array should have the following properties:
|
||||
* * href - Often a mailto: address
|
||||
* * commonName - Optional, for example a first + last name
|
||||
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
|
||||
* * readOnly - boolean
|
||||
* * summary - Optional, a description for the share
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function getShares();
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* This interface represents a Calendar that is shared by a different user.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface ISharedCalendar extends ICalendar {
|
||||
|
||||
/**
|
||||
* This method should return the url of the owners' copy of the shared
|
||||
* calendar.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getSharedUrl();
|
||||
|
||||
/**
|
||||
* Returns the list of people whom this calendar is shared with.
|
||||
*
|
||||
* Every element in this array should have the following properties:
|
||||
* * href - Often a mailto: address
|
||||
* * commonName - Optional, for example a first + last name
|
||||
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
|
||||
* * readOnly - boolean
|
||||
* * summary - Optional, a description for the share
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function getShares();
|
||||
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Notifications;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* This node represents a list of notifications.
|
||||
*
|
||||
* It provides no additional functionality, but you must implement this
|
||||
* interface to allow the Notifications plugin to mark the collection
|
||||
* as a notifications collection.
|
||||
*
|
||||
* This collection should only return Sabre\CalDAV\Notifications\INode nodes as
|
||||
* its children.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Collection extends DAV\Collection implements ICollection, DAVACL\IACL {
|
||||
|
||||
/**
|
||||
* The notification backend
|
||||
*
|
||||
* @var Sabre\CalDAV\Backend\NotificationSupport
|
||||
*/
|
||||
protected $caldavBackend;
|
||||
|
||||
/**
|
||||
* Principal uri
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $principalUri;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param CalDAV\Backend\NotificationSupport $caldavBackend
|
||||
* @param string $principalUri
|
||||
*/
|
||||
public function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri) {
|
||||
|
||||
$this->caldavBackend = $caldavBackend;
|
||||
$this->principalUri = $principalUri;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all notifications for a principal
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getChildren() {
|
||||
|
||||
$children = array();
|
||||
$notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri);
|
||||
|
||||
foreach($notifications as $notification) {
|
||||
|
||||
$children[] = new Node(
|
||||
$this->caldavBackend,
|
||||
$this->principalUri,
|
||||
$notification
|
||||
);
|
||||
}
|
||||
|
||||
return $children;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return 'notifications';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the owner principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOwner() {
|
||||
|
||||
return $this->principalUri;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a group principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGroup() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ACE's for this node.
|
||||
*
|
||||
* Each ACE has the following properties:
|
||||
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
|
||||
* currently the only supported privileges
|
||||
* * 'principal', a url to the principal who owns the node
|
||||
* * 'protected' (optional), indicating that this ACE is not allowed to
|
||||
* be updated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getACL() {
|
||||
|
||||
return array(
|
||||
array(
|
||||
'principal' => $this->getOwner(),
|
||||
'privilege' => '{DAV:}read',
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'principal' => $this->getOwner(),
|
||||
'privilege' => '{DAV:}write',
|
||||
'protected' => true,
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ACL
|
||||
*
|
||||
* This method will receive a list of new ACE's as an array argument.
|
||||
*
|
||||
* @param array $acl
|
||||
* @return void
|
||||
*/
|
||||
public function setACL(array $acl) {
|
||||
|
||||
throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of supported privileges for this node.
|
||||
*
|
||||
* The returned data structure is a list of nested privileges.
|
||||
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
|
||||
* standard structure.
|
||||
*
|
||||
* If null is returned from this method, the default privilege set is used,
|
||||
* which is fine for most common usecases.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getSupportedPrivilegeSet() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Notifications;
|
||||
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* This node represents a list of notifications.
|
||||
*
|
||||
* It provides no additional functionality, but you must implement this
|
||||
* interface to allow the Notifications plugin to mark the collection
|
||||
* as a notifications collection.
|
||||
*
|
||||
* This collection should only return Sabre\CalDAV\Notifications\INode nodes as
|
||||
* its children.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface ICollection extends DAV\ICollection {
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Notifications;
|
||||
|
||||
/**
|
||||
* This node represents a single notification.
|
||||
*
|
||||
* The signature is mostly identical to that of Sabre\DAV\IFile, but the get() method
|
||||
* MUST return an xml document that matches the requirements of the
|
||||
* 'caldav-notifications.txt' spec.
|
||||
*
|
||||
* For a complete example, check out the Notification class, which contains
|
||||
* some helper functions.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface INode {
|
||||
|
||||
/**
|
||||
* This method must return an xml element, using the
|
||||
* Sabre\CalDAV\Notifications\INotificationType classes.
|
||||
*
|
||||
* @return INotificationType
|
||||
*/
|
||||
function getNotificationType();
|
||||
|
||||
/**
|
||||
* Returns the etag for the notification.
|
||||
*
|
||||
* The etag must be surrounded by litteral double-quotes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getETag();
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Notifications;
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* This interface reflects a single notification type.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface INotificationType extends DAV\PropertyInterface {
|
||||
|
||||
/**
|
||||
* This method serializes the entire notification, as it is used in the
|
||||
* response body.
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
function serializeBody(DAV\Server $server, \DOMElement $node);
|
||||
|
||||
/**
|
||||
* Returns a unique id for this notification
|
||||
*
|
||||
* This is just the base url. This should generally be some kind of unique
|
||||
* id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getId();
|
||||
|
||||
/**
|
||||
* Returns the ETag for this notification.
|
||||
*
|
||||
* The ETag must be surrounded by literal double-quotes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getETag();
|
||||
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Notifications;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* This node represents a single notification.
|
||||
*
|
||||
* The signature is mostly identical to that of Sabre\DAV\IFile, but the get() method
|
||||
* MUST return an xml document that matches the requirements of the
|
||||
* 'caldav-notifications.txt' spec.
|
||||
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Node extends DAV\File implements INode, DAVACL\IACL {
|
||||
|
||||
/**
|
||||
* The notification backend
|
||||
*
|
||||
* @var Sabre\CalDAV\Backend\NotificationSupport
|
||||
*/
|
||||
protected $caldavBackend;
|
||||
|
||||
/**
|
||||
* The actual notification
|
||||
*
|
||||
* @var Sabre\CalDAV\Notifications\INotificationType
|
||||
*/
|
||||
protected $notification;
|
||||
|
||||
/**
|
||||
* Owner principal of the notification
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $principalUri;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param CalDAV\Backend\NotificationSupport $caldavBackend
|
||||
* @param string $principalUri
|
||||
* @param CalDAV\Notifications\INotificationType $notification
|
||||
*/
|
||||
public function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri, INotificationType $notification) {
|
||||
|
||||
$this->caldavBackend = $caldavBackend;
|
||||
$this->principalUri = $principalUri;
|
||||
$this->notification = $notification;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path name for this notification
|
||||
*
|
||||
* @return id
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return $this->notification->getId() . '.xml';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the etag for the notification.
|
||||
*
|
||||
* The etag must be surrounded by litteral double-quotes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getETag() {
|
||||
|
||||
return $this->notification->getETag();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method must return an xml element, using the
|
||||
* Sabre\CalDAV\Notifications\INotificationType classes.
|
||||
*
|
||||
* @return INotificationType
|
||||
*/
|
||||
public function getNotificationType() {
|
||||
|
||||
return $this->notification;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this notification
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete() {
|
||||
|
||||
$this->caldavBackend->deleteNotification($this->getOwner(), $this->notification);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the owner principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOwner() {
|
||||
|
||||
return $this->principalUri;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a group principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGroup() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ACE's for this node.
|
||||
*
|
||||
* Each ACE has the following properties:
|
||||
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
|
||||
* currently the only supported privileges
|
||||
* * 'principal', a url to the principal who owns the node
|
||||
* * 'protected' (optional), indicating that this ACE is not allowed to
|
||||
* be updated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getACL() {
|
||||
|
||||
return array(
|
||||
array(
|
||||
'principal' => $this->getOwner(),
|
||||
'privilege' => '{DAV:}read',
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'principal' => $this->getOwner(),
|
||||
'privilege' => '{DAV:}write',
|
||||
'protected' => true,
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ACL
|
||||
*
|
||||
* This method will receive a list of new ACE's as an array argument.
|
||||
*
|
||||
* @param array $acl
|
||||
* @return void
|
||||
*/
|
||||
public function setACL(array $acl) {
|
||||
|
||||
throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of supported privileges for this node.
|
||||
*
|
||||
* The returned data structure is a list of nested privileges.
|
||||
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
|
||||
* standard structure.
|
||||
*
|
||||
* If null is returned from this method, the default privilege set is used,
|
||||
* which is fine for most common usecases.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getSupportedPrivilegeSet() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,324 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Notifications\Notification;
|
||||
|
||||
use Sabre\CalDAV\SharingPlugin as SharingPlugin;
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* This class represents the cs:invite-notification notification element.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Invite extends DAV\Property implements CalDAV\Notifications\INotificationType {
|
||||
|
||||
/**
|
||||
* A unique id for the message
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* Timestamp of the notification
|
||||
*
|
||||
* @var DateTime
|
||||
*/
|
||||
protected $dtStamp;
|
||||
|
||||
/**
|
||||
* A url to the recipient of the notification. This can be an email
|
||||
* address (mailto:), or a principal url.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $href;
|
||||
|
||||
/**
|
||||
* The type of message, see the SharingPlugin::STATUS_* constants.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* True if access to a calendar is read-only.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $readOnly;
|
||||
|
||||
/**
|
||||
* A url to the shared calendar.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $hostUrl;
|
||||
|
||||
/**
|
||||
* Url to the sharer of the calendar
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $organizer;
|
||||
|
||||
/**
|
||||
* The name of the sharer.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $commonName;
|
||||
|
||||
/**
|
||||
* The name of the sharer.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $firstName;
|
||||
|
||||
/**
|
||||
* The name of the sharer.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $lastName;
|
||||
|
||||
/**
|
||||
* A description of the share request
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $summary;
|
||||
|
||||
/**
|
||||
* The Etag for the notification
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $etag;
|
||||
|
||||
/**
|
||||
* The list of supported components
|
||||
*
|
||||
* @var Sabre\CalDAV\Property\SupportedCalendarComponentSet
|
||||
*/
|
||||
protected $supportedComponents;
|
||||
|
||||
/**
|
||||
* Creates the Invite notification.
|
||||
*
|
||||
* This constructor receives an array with the following elements:
|
||||
*
|
||||
* * id - A unique id
|
||||
* * etag - The etag
|
||||
* * dtStamp - A DateTime object with a timestamp for the notification.
|
||||
* * type - The type of notification, see SharingPlugin::STATUS_*
|
||||
* constants for details.
|
||||
* * readOnly - This must be set to true, if this is an invite for
|
||||
* read-only access to a calendar.
|
||||
* * hostUrl - A url to the shared calendar.
|
||||
* * organizer - Url to the sharer principal.
|
||||
* * commonName - The real name of the sharer (optional).
|
||||
* * firstName - The first name of the sharer (optional).
|
||||
* * lastName - The last name of the sharer (optional).
|
||||
* * summary - Description of the share, can be the same as the
|
||||
* calendar, but may also be modified (optional).
|
||||
* * supportedComponents - An instance of
|
||||
* Sabre\CalDAV\Property\SupportedCalendarComponentSet.
|
||||
* This allows the client to determine which components
|
||||
* will be supported in the shared calendar. This is
|
||||
* also optional.
|
||||
*
|
||||
* @param array $values All the options
|
||||
*/
|
||||
public function __construct(array $values) {
|
||||
|
||||
$required = array(
|
||||
'id',
|
||||
'etag',
|
||||
'href',
|
||||
'dtStamp',
|
||||
'type',
|
||||
'readOnly',
|
||||
'hostUrl',
|
||||
'organizer',
|
||||
);
|
||||
foreach($required as $item) {
|
||||
if (!isset($values[$item])) {
|
||||
throw new \InvalidArgumentException($item . ' is a required constructor option');
|
||||
}
|
||||
}
|
||||
|
||||
foreach($values as $key=>$value) {
|
||||
if (!property_exists($this, $key)) {
|
||||
throw new \InvalidArgumentException('Unknown option: ' . $key);
|
||||
}
|
||||
$this->$key = $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the notification as a single property.
|
||||
*
|
||||
* You should usually just encode the single top-level element of the
|
||||
* notification.
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server, \DOMElement $node) {
|
||||
|
||||
$prop = $node->ownerDocument->createElement('cs:invite-notification');
|
||||
$node->appendChild($prop);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method serializes the entire notification, as it is used in the
|
||||
* response body.
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serializeBody(DAV\Server $server, \DOMElement $node) {
|
||||
|
||||
$doc = $node->ownerDocument;
|
||||
|
||||
$dt = $doc->createElement('cs:dtstamp');
|
||||
$this->dtStamp->setTimezone(new \DateTimezone('GMT'));
|
||||
$dt->appendChild($doc->createTextNode($this->dtStamp->format('Ymd\\THis\\Z')));
|
||||
$node->appendChild($dt);
|
||||
|
||||
$prop = $doc->createElement('cs:invite-notification');
|
||||
$node->appendChild($prop);
|
||||
|
||||
$uid = $doc->createElement('cs:uid');
|
||||
$uid->appendChild( $doc->createTextNode($this->id) );
|
||||
$prop->appendChild($uid);
|
||||
|
||||
$href = $doc->createElement('d:href');
|
||||
$href->appendChild( $doc->createTextNode( $this->href ) );
|
||||
$prop->appendChild($href);
|
||||
|
||||
$nodeName = null;
|
||||
switch($this->type) {
|
||||
|
||||
case SharingPlugin::STATUS_ACCEPTED :
|
||||
$nodeName = 'cs:invite-accepted';
|
||||
break;
|
||||
case SharingPlugin::STATUS_DECLINED :
|
||||
$nodeName = 'cs:invite-declined';
|
||||
break;
|
||||
case SharingPlugin::STATUS_DELETED :
|
||||
$nodeName = 'cs:invite-deleted';
|
||||
break;
|
||||
case SharingPlugin::STATUS_NORESPONSE :
|
||||
$nodeName = 'cs:invite-noresponse';
|
||||
break;
|
||||
|
||||
}
|
||||
$prop->appendChild(
|
||||
$doc->createElement($nodeName)
|
||||
);
|
||||
$hostHref = $doc->createElement('d:href', $server->getBaseUri() . $this->hostUrl);
|
||||
$hostUrl = $doc->createElement('cs:hosturl');
|
||||
$hostUrl->appendChild($hostHref);
|
||||
$prop->appendChild($hostUrl);
|
||||
|
||||
$access = $doc->createElement('cs:access');
|
||||
if ($this->readOnly) {
|
||||
$access->appendChild($doc->createElement('cs:read'));
|
||||
} else {
|
||||
$access->appendChild($doc->createElement('cs:read-write'));
|
||||
}
|
||||
$prop->appendChild($access);
|
||||
|
||||
$organizerUrl = $doc->createElement('cs:organizer');
|
||||
// If the organizer contains a 'mailto:' part, it means it should be
|
||||
// treated as absolute.
|
||||
if (strtolower(substr($this->organizer,0,7))==='mailto:') {
|
||||
$organizerHref = new DAV\Property\Href($this->organizer, false);
|
||||
} else {
|
||||
$organizerHref = new DAV\Property\Href($this->organizer, true);
|
||||
}
|
||||
$organizerHref->serialize($server, $organizerUrl);
|
||||
|
||||
if ($this->commonName) {
|
||||
$commonName = $doc->createElement('cs:common-name');
|
||||
$commonName->appendChild($doc->createTextNode($this->commonName));
|
||||
$organizerUrl->appendChild($commonName);
|
||||
|
||||
$commonNameOld = $doc->createElement('cs:organizer-cn');
|
||||
$commonNameOld->appendChild($doc->createTextNode($this->commonName));
|
||||
$prop->appendChild($commonNameOld);
|
||||
|
||||
}
|
||||
if ($this->firstName) {
|
||||
$firstName = $doc->createElement('cs:first-name');
|
||||
$firstName->appendChild($doc->createTextNode($this->firstName));
|
||||
$organizerUrl->appendChild($firstName);
|
||||
|
||||
$firstNameOld = $doc->createElement('cs:organizer-first');
|
||||
$firstNameOld->appendChild($doc->createTextNode($this->firstName));
|
||||
$prop->appendChild($firstNameOld);
|
||||
}
|
||||
if ($this->lastName) {
|
||||
$lastName = $doc->createElement('cs:last-name');
|
||||
$lastName->appendChild($doc->createTextNode($this->lastName));
|
||||
$organizerUrl->appendChild($lastName);
|
||||
|
||||
$lastNameOld = $doc->createElement('cs:organizer-last');
|
||||
$lastNameOld->appendChild($doc->createTextNode($this->lastName));
|
||||
$prop->appendChild($lastNameOld);
|
||||
}
|
||||
$prop->appendChild($organizerUrl);
|
||||
|
||||
if ($this->summary) {
|
||||
$summary = $doc->createElement('cs:summary');
|
||||
$summary->appendChild($doc->createTextNode($this->summary));
|
||||
$prop->appendChild($summary);
|
||||
}
|
||||
if ($this->supportedComponents) {
|
||||
|
||||
$xcomp = $doc->createElement('cal:supported-calendar-component-set');
|
||||
$this->supportedComponents->serialize($server, $xcomp);
|
||||
$prop->appendChild($xcomp);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unique id for this notification
|
||||
*
|
||||
* This is just the base url. This should generally be some kind of unique
|
||||
* id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId() {
|
||||
|
||||
return $this->id;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ETag for this notification.
|
||||
*
|
||||
* The ETag must be surrounded by literal double-quotes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getETag() {
|
||||
|
||||
return $this->etag;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Notifications\Notification;
|
||||
|
||||
use Sabre\CalDAV\SharingPlugin as SharingPlugin;
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* This class represents the cs:invite-reply notification element.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class InviteReply extends DAV\Property implements CalDAV\Notifications\INotificationType {
|
||||
|
||||
/**
|
||||
* A unique id for the message
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* Timestamp of the notification
|
||||
*
|
||||
* @var DateTime
|
||||
*/
|
||||
protected $dtStamp;
|
||||
|
||||
/**
|
||||
* The unique id of the notification this was a reply to.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $inReplyTo;
|
||||
|
||||
/**
|
||||
* A url to the recipient of the original (!) notification.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $href;
|
||||
|
||||
/**
|
||||
* The type of message, see the SharingPlugin::STATUS_ constants.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* A url to the shared calendar.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $hostUrl;
|
||||
|
||||
/**
|
||||
* A description of the share request
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $summary;
|
||||
|
||||
/**
|
||||
* Notification Etag
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $etag;
|
||||
|
||||
/**
|
||||
* Creates the Invite Reply Notification.
|
||||
*
|
||||
* This constructor receives an array with the following elements:
|
||||
*
|
||||
* * id - A unique id
|
||||
* * etag - The etag
|
||||
* * dtStamp - A DateTime object with a timestamp for the notification.
|
||||
* * inReplyTo - This should refer to the 'id' of the notification
|
||||
* this is a reply to.
|
||||
* * type - The type of notification, see SharingPlugin::STATUS_*
|
||||
* constants for details.
|
||||
* * hostUrl - A url to the shared calendar.
|
||||
* * summary - Description of the share, can be the same as the
|
||||
* calendar, but may also be modified (optional).
|
||||
*/
|
||||
public function __construct(array $values) {
|
||||
|
||||
$required = array(
|
||||
'id',
|
||||
'etag',
|
||||
'href',
|
||||
'dtStamp',
|
||||
'inReplyTo',
|
||||
'type',
|
||||
'hostUrl',
|
||||
);
|
||||
foreach($required as $item) {
|
||||
if (!isset($values[$item])) {
|
||||
throw new \InvalidArgumentException($item . ' is a required constructor option');
|
||||
}
|
||||
}
|
||||
|
||||
foreach($values as $key=>$value) {
|
||||
if (!property_exists($this, $key)) {
|
||||
throw new \InvalidArgumentException('Unknown option: ' . $key);
|
||||
}
|
||||
$this->$key = $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the notification as a single property.
|
||||
*
|
||||
* You should usually just encode the single top-level element of the
|
||||
* notification.
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server, \DOMElement $node) {
|
||||
|
||||
$prop = $node->ownerDocument->createElement('cs:invite-reply');
|
||||
$node->appendChild($prop);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method serializes the entire notification, as it is used in the
|
||||
* response body.
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serializeBody(DAV\Server $server, \DOMElement $node) {
|
||||
|
||||
$doc = $node->ownerDocument;
|
||||
|
||||
$dt = $doc->createElement('cs:dtstamp');
|
||||
$this->dtStamp->setTimezone(new \DateTimezone('GMT'));
|
||||
$dt->appendChild($doc->createTextNode($this->dtStamp->format('Ymd\\THis\\Z')));
|
||||
$node->appendChild($dt);
|
||||
|
||||
$prop = $doc->createElement('cs:invite-reply');
|
||||
$node->appendChild($prop);
|
||||
|
||||
$uid = $doc->createElement('cs:uid');
|
||||
$uid->appendChild($doc->createTextNode($this->id));
|
||||
$prop->appendChild($uid);
|
||||
|
||||
$inReplyTo = $doc->createElement('cs:in-reply-to');
|
||||
$inReplyTo->appendChild( $doc->createTextNode($this->inReplyTo) );
|
||||
$prop->appendChild($inReplyTo);
|
||||
|
||||
$href = $doc->createElement('d:href');
|
||||
$href->appendChild( $doc->createTextNode($this->href) );
|
||||
$prop->appendChild($href);
|
||||
|
||||
$nodeName = null;
|
||||
switch($this->type) {
|
||||
|
||||
case SharingPlugin::STATUS_ACCEPTED :
|
||||
$nodeName = 'cs:invite-accepted';
|
||||
break;
|
||||
case SharingPlugin::STATUS_DECLINED :
|
||||
$nodeName = 'cs:invite-declined';
|
||||
break;
|
||||
|
||||
}
|
||||
$prop->appendChild(
|
||||
$doc->createElement($nodeName)
|
||||
);
|
||||
$hostHref = $doc->createElement('d:href', $server->getBaseUri() . $this->hostUrl);
|
||||
$hostUrl = $doc->createElement('cs:hosturl');
|
||||
$hostUrl->appendChild($hostHref);
|
||||
$prop->appendChild($hostUrl);
|
||||
|
||||
if ($this->summary) {
|
||||
$summary = $doc->createElement('cs:summary');
|
||||
$summary->appendChild($doc->createTextNode($this->summary));
|
||||
$prop->appendChild($summary);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unique id for this notification
|
||||
*
|
||||
* This is just the base url. This should generally be some kind of unique
|
||||
* id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId() {
|
||||
|
||||
return $this->id;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ETag for this notification.
|
||||
*
|
||||
* The ETag must be surrounded by literal double-quotes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getETag() {
|
||||
|
||||
return $this->etag;
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Notifications\Notification;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* SystemStatus notification
|
||||
*
|
||||
* This notification can be used to indicate to the user that the system is
|
||||
* down.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class SystemStatus extends DAV\Property implements CalDAV\Notifications\INotificationType {
|
||||
|
||||
const TYPE_LOW = 1;
|
||||
const TYPE_MEDIUM = 2;
|
||||
const TYPE_HIGH = 3;
|
||||
|
||||
/**
|
||||
* A unique id
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The type of alert. This should be one of the TYPE_ constants.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* A human-readable description of the problem.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description;
|
||||
|
||||
/**
|
||||
* A url to a website with more information for the user.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $href;
|
||||
|
||||
/**
|
||||
* Notification Etag
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $etag;
|
||||
|
||||
/**
|
||||
* Creates the notification.
|
||||
*
|
||||
* Some kind of unique id should be provided. This is used to generate a
|
||||
* url.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $etag
|
||||
* @param int $type
|
||||
* @param string $description
|
||||
* @param string $href
|
||||
*/
|
||||
public function __construct($id, $etag, $type = self::TYPE_HIGH, $description = null, $href = null) {
|
||||
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
$this->description = $description;
|
||||
$this->href = $href;
|
||||
$this->etag = $etag;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the notification as a single property.
|
||||
*
|
||||
* You should usually just encode the single top-level element of the
|
||||
* notification.
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server, \DOMElement $node) {
|
||||
|
||||
switch($this->type) {
|
||||
case self::TYPE_LOW :
|
||||
$type = 'low';
|
||||
break;
|
||||
case self::TYPE_MEDIUM :
|
||||
$type = 'medium';
|
||||
break;
|
||||
default :
|
||||
case self::TYPE_HIGH :
|
||||
$type = 'high';
|
||||
break;
|
||||
}
|
||||
|
||||
$prop = $node->ownerDocument->createElement('cs:systemstatus');
|
||||
$prop->setAttribute('type', $type);
|
||||
|
||||
$node->appendChild($prop);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method serializes the entire notification, as it is used in the
|
||||
* response body.
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serializeBody(DAV\Server $server, \DOMElement $node) {
|
||||
|
||||
switch($this->type) {
|
||||
case self::TYPE_LOW :
|
||||
$type = 'low';
|
||||
break;
|
||||
case self::TYPE_MEDIUM :
|
||||
$type = 'medium';
|
||||
break;
|
||||
default :
|
||||
case self::TYPE_HIGH :
|
||||
$type = 'high';
|
||||
break;
|
||||
}
|
||||
|
||||
$prop = $node->ownerDocument->createElement('cs:systemstatus');
|
||||
$prop->setAttribute('type', $type);
|
||||
|
||||
if ($this->description) {
|
||||
$text = $node->ownerDocument->createTextNode($this->description);
|
||||
$desc = $node->ownerDocument->createElement('cs:description');
|
||||
$desc->appendChild($text);
|
||||
$prop->appendChild($desc);
|
||||
}
|
||||
if ($this->href) {
|
||||
$text = $node->ownerDocument->createTextNode($this->href);
|
||||
$href = $node->ownerDocument->createElement('d:href');
|
||||
$href->appendChild($text);
|
||||
$prop->appendChild($href);
|
||||
}
|
||||
|
||||
$node->appendChild($prop);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unique id for this notification
|
||||
*
|
||||
* This is just the base url. This should generally be some kind of unique
|
||||
* id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId() {
|
||||
|
||||
return $this->id;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the ETag for this notification.
|
||||
*
|
||||
* The ETag must be surrounded by literal double-quotes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getETag() {
|
||||
|
||||
return $this->etag;
|
||||
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Principal;
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* Principal collection
|
||||
*
|
||||
* This is an alternative collection to the standard ACL principal collection.
|
||||
* This collection adds support for the calendar-proxy-read and
|
||||
* calendar-proxy-write sub-principals, as defined by the caldav-proxy
|
||||
* specification.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Collection extends DAVACL\AbstractPrincipalCollection {
|
||||
|
||||
/**
|
||||
* Returns a child object based on principal information
|
||||
*
|
||||
* @param array $principalInfo
|
||||
* @return User
|
||||
*/
|
||||
public function getChildForPrincipal(array $principalInfo) {
|
||||
|
||||
return new User($this->principalBackend, $principalInfo);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Principal;
|
||||
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* ProxyRead principal interface
|
||||
*
|
||||
* Any principal node implementing this interface will be picked up as a 'proxy
|
||||
* principal group'.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface IProxyRead extends DAVACL\IPrincipal {
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Principal;
|
||||
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* ProxyWrite principal interface
|
||||
*
|
||||
* Any principal node implementing this interface will be picked up as a 'proxy
|
||||
* principal group'.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface IProxyWrite extends DAVACL\IPrincipal {
|
||||
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Principal;
|
||||
use Sabre\DAVACL;
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* ProxyRead principal
|
||||
*
|
||||
* This class represents a principal group, hosted under the main principal.
|
||||
* This is needed to implement 'Calendar delegation' support. This class is
|
||||
* instantiated by User.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ProxyRead implements IProxyRead {
|
||||
|
||||
/**
|
||||
* Principal information from the parent principal.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $principalInfo;
|
||||
|
||||
/**
|
||||
* Principal backend
|
||||
*
|
||||
* @var DAVACL\PrincipalBackend\BackendInterface
|
||||
*/
|
||||
protected $principalBackend;
|
||||
|
||||
/**
|
||||
* Creates the object.
|
||||
*
|
||||
* Note that you MUST supply the parent principal information.
|
||||
*
|
||||
* @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
|
||||
* @param array $principalInfo
|
||||
*/
|
||||
public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
|
||||
|
||||
$this->principalInfo = $principalInfo;
|
||||
$this->principalBackend = $principalBackend;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this principals name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return 'calendar-proxy-read';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modification time
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function getLastModified() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the current node
|
||||
*
|
||||
* @throws DAV\Exception\Forbidden
|
||||
* @return void
|
||||
*/
|
||||
public function delete() {
|
||||
|
||||
throw new DAV\Exception\Forbidden('Permission denied to delete node');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the node
|
||||
*
|
||||
* @throws DAV\Exception\Forbidden
|
||||
* @param string $name The new name
|
||||
* @return void
|
||||
*/
|
||||
public function setName($name) {
|
||||
|
||||
throw new DAV\Exception\Forbidden('Permission denied to rename file');
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of alternative urls for a principal
|
||||
*
|
||||
* This can for example be an email address, or ldap url.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAlternateUriSet() {
|
||||
|
||||
return array();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full principal url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrincipalUrl() {
|
||||
|
||||
return $this->principalInfo['uri'] . '/' . $this->getName();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of group members
|
||||
*
|
||||
* If this principal is a group, this function should return
|
||||
* all member principal uri's for the group.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGroupMemberSet() {
|
||||
|
||||
return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of groups this principal is member of
|
||||
*
|
||||
* If this principal is a member of a (list of) groups, this function
|
||||
* should return a list of principal uri's for it's members.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGroupMembership() {
|
||||
|
||||
return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a list of group members
|
||||
*
|
||||
* If this principal is a group, this method sets all the group members.
|
||||
* The list of members is always overwritten, never appended to.
|
||||
*
|
||||
* This method should throw an exception if the members could not be set.
|
||||
*
|
||||
* @param array $principals
|
||||
* @return void
|
||||
*/
|
||||
public function setGroupMemberSet(array $principals) {
|
||||
|
||||
$this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the displayname
|
||||
*
|
||||
* This should be a human readable name for the principal.
|
||||
* If none is available, return the nodename.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDisplayName() {
|
||||
|
||||
return $this->getName();
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Principal;
|
||||
use Sabre\DAVACL;
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* ProxyWrite principal
|
||||
*
|
||||
* This class represents a principal group, hosted under the main principal.
|
||||
* This is needed to implement 'Calendar delegation' support. This class is
|
||||
* instantiated by User.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ProxyWrite implements IProxyWrite {
|
||||
|
||||
/**
|
||||
* Parent principal information
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $principalInfo;
|
||||
|
||||
/**
|
||||
* Principal Backend
|
||||
*
|
||||
* @var DAVACL\PrincipalBackend\BackendInterface
|
||||
*/
|
||||
protected $principalBackend;
|
||||
|
||||
/**
|
||||
* Creates the object
|
||||
*
|
||||
* Note that you MUST supply the parent principal information.
|
||||
*
|
||||
* @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
|
||||
* @param array $principalInfo
|
||||
*/
|
||||
public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
|
||||
|
||||
$this->principalInfo = $principalInfo;
|
||||
$this->principalBackend = $principalBackend;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this principals name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return 'calendar-proxy-write';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modification time
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function getLastModified() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the current node
|
||||
*
|
||||
* @throws DAV\Exception\Forbidden
|
||||
* @return void
|
||||
*/
|
||||
public function delete() {
|
||||
|
||||
throw new DAV\Exception\Forbidden('Permission denied to delete node');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the node
|
||||
*
|
||||
* @throws DAV\Exception\Forbidden
|
||||
* @param string $name The new name
|
||||
* @return void
|
||||
*/
|
||||
public function setName($name) {
|
||||
|
||||
throw new DAV\Exception\Forbidden('Permission denied to rename file');
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of alternative urls for a principal
|
||||
*
|
||||
* This can for example be an email address, or ldap url.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAlternateUriSet() {
|
||||
|
||||
return array();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full principal url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrincipalUrl() {
|
||||
|
||||
return $this->principalInfo['uri'] . '/' . $this->getName();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of group members
|
||||
*
|
||||
* If this principal is a group, this function should return
|
||||
* all member principal uri's for the group.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGroupMemberSet() {
|
||||
|
||||
return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of groups this principal is member of
|
||||
*
|
||||
* If this principal is a member of a (list of) groups, this function
|
||||
* should return a list of principal uri's for it's members.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGroupMembership() {
|
||||
|
||||
return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a list of group members
|
||||
*
|
||||
* If this principal is a group, this method sets all the group members.
|
||||
* The list of members is always overwritten, never appended to.
|
||||
*
|
||||
* This method should throw an exception if the members could not be set.
|
||||
*
|
||||
* @param array $principals
|
||||
* @return void
|
||||
*/
|
||||
public function setGroupMemberSet(array $principals) {
|
||||
|
||||
$this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the displayname
|
||||
*
|
||||
* This should be a human readable name for the principal.
|
||||
* If none is available, return the nodename.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDisplayName() {
|
||||
|
||||
return $this->getName();
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Principal;
|
||||
use Sabre\DAV;
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* CalDAV principal
|
||||
*
|
||||
* This is a standard user-principal for CalDAV. This principal is also a
|
||||
* collection and returns the caldav-proxy-read and caldav-proxy-write child
|
||||
* principals.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class User extends DAVACL\Principal implements DAV\ICollection {
|
||||
|
||||
/**
|
||||
* Creates a new file in the directory
|
||||
*
|
||||
* @param string $name Name of the file
|
||||
* @param resource $data Initial payload, passed as a readable stream resource.
|
||||
* @throws DAV\Exception\Forbidden
|
||||
* @return void
|
||||
*/
|
||||
public function createFile($name, $data = null) {
|
||||
|
||||
throw new DAV\Exception\Forbidden('Permission denied to create file (filename ' . $name . ')');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new subdirectory
|
||||
*
|
||||
* @param string $name
|
||||
* @throws DAV\Exception\Forbidden
|
||||
* @return void
|
||||
*/
|
||||
public function createDirectory($name) {
|
||||
|
||||
throw new DAV\Exception\Forbidden('Permission denied to create directory');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific child node, referenced by its name
|
||||
*
|
||||
* @param string $name
|
||||
* @return DAV\INode
|
||||
*/
|
||||
public function getChild($name) {
|
||||
|
||||
$principal = $this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/' . $name);
|
||||
if (!$principal) {
|
||||
throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
|
||||
}
|
||||
if ($name === 'calendar-proxy-read')
|
||||
return new ProxyRead($this->principalBackend, $this->principalProperties);
|
||||
|
||||
if ($name === 'calendar-proxy-write')
|
||||
return new ProxyWrite($this->principalBackend, $this->principalProperties);
|
||||
|
||||
throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all the child nodes
|
||||
*
|
||||
* @return DAV\INode[]
|
||||
*/
|
||||
public function getChildren() {
|
||||
|
||||
$r = array();
|
||||
if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-read')) {
|
||||
$r[] = new ProxyRead($this->principalBackend, $this->principalProperties);
|
||||
}
|
||||
if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-write')) {
|
||||
$r[] = new ProxyWrite($this->principalBackend, $this->principalProperties);
|
||||
}
|
||||
|
||||
return $r;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the child node exists
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function childExists($name) {
|
||||
|
||||
try {
|
||||
$this->getChild($name);
|
||||
return true;
|
||||
} catch (DAV\Exception\NotFound $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ACE's for this node.
|
||||
*
|
||||
* Each ACE has the following properties:
|
||||
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
|
||||
* currently the only supported privileges
|
||||
* * 'principal', a url to the principal who owns the node
|
||||
* * 'protected' (optional), indicating that this ACE is not allowed to
|
||||
* be updated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getACL() {
|
||||
|
||||
$acl = parent::getACL();
|
||||
$acl[] = array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->principalProperties['uri'] . '/calendar-proxy-read',
|
||||
'protected' => true,
|
||||
);
|
||||
$acl[] = array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->principalProperties['uri'] . '/calendar-proxy-write',
|
||||
'protected' => true,
|
||||
);
|
||||
return $acl;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Property;
|
||||
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* AllowedSharingModes
|
||||
*
|
||||
* This property encodes the 'allowed-sharing-modes' property, as defined by
|
||||
* the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/
|
||||
* namespace.
|
||||
*
|
||||
* This property is a representation of the supported-calendar_component-set
|
||||
* property in the CalDAV namespace. It simply requires an array of components,
|
||||
* such as VEVENT, VTODO
|
||||
*
|
||||
* @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class AllowedSharingModes extends DAV\Property {
|
||||
|
||||
/**
|
||||
* Whether or not a calendar can be shared with another user
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $canBeShared;
|
||||
|
||||
/**
|
||||
* Whether or not the calendar can be placed on a public url.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $canBePublished;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param bool $canBeShared
|
||||
* @param bool $canBePublished
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($canBeShared, $canBePublished) {
|
||||
|
||||
$this->canBeShared = $canBeShared;
|
||||
$this->canBePublished = $canBePublished;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the property in a DOMDocument
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server, \DOMElement $node) {
|
||||
|
||||
$doc = $node->ownerDocument;
|
||||
if ($this->canBeShared) {
|
||||
$xcomp = $doc->createElement('cs:can-be-shared');
|
||||
$node->appendChild($xcomp);
|
||||
}
|
||||
if ($this->canBePublished) {
|
||||
$xcomp = $doc->createElement('cs:can-be-published');
|
||||
$node->appendChild($xcomp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Property;
|
||||
|
||||
use Sabre\CalDAV\SharingPlugin as SharingPlugin;
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* Invite property
|
||||
*
|
||||
* This property encodes the 'invite' property, as defined by
|
||||
* the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/
|
||||
* namespace.
|
||||
*
|
||||
* @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Invite extends DAV\Property {
|
||||
|
||||
/**
|
||||
* The list of users a calendar has been shared to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* The organizer contains information about the person who shared the
|
||||
* object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $organizer;
|
||||
|
||||
/**
|
||||
* Creates the property.
|
||||
*
|
||||
* Users is an array. Each element of the array has the following
|
||||
* properties:
|
||||
*
|
||||
* * href - Often a mailto: address
|
||||
* * commonName - Optional, for example a first and lastname for a user.
|
||||
* * status - One of the SharingPlugin::STATUS_* constants.
|
||||
* * readOnly - true or false
|
||||
* * summary - Optional, description of the share
|
||||
*
|
||||
* The organizer key is optional to specify. It's only useful when a
|
||||
* 'sharee' requests the sharing information.
|
||||
*
|
||||
* The organizer may have the following properties:
|
||||
* * href - Often a mailto: address.
|
||||
* * commonName - Optional human-readable name.
|
||||
* * firstName - Optional first name.
|
||||
* * lastName - Optional last name.
|
||||
*
|
||||
* If you wonder why these two structures are so different, I guess a
|
||||
* valid answer is that the current spec is still a draft.
|
||||
*
|
||||
* @param array $users
|
||||
*/
|
||||
public function __construct(array $users, array $organizer = null) {
|
||||
|
||||
$this->users = $users;
|
||||
$this->organizer = $organizer;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of users, as it was passed to the constructor.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getValue() {
|
||||
|
||||
return $this->users;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the property in a DOMDocument
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server,\DOMElement $node) {
|
||||
|
||||
$doc = $node->ownerDocument;
|
||||
|
||||
if (!is_null($this->organizer)) {
|
||||
|
||||
$xorganizer = $doc->createElement('cs:organizer');
|
||||
|
||||
$href = $doc->createElement('d:href');
|
||||
$href->appendChild($doc->createTextNode($this->organizer['href']));
|
||||
$xorganizer->appendChild($href);
|
||||
|
||||
if (isset($this->organizer['commonName']) && $this->organizer['commonName']) {
|
||||
$commonName = $doc->createElement('cs:common-name');
|
||||
$commonName->appendChild($doc->createTextNode($this->organizer['commonName']));
|
||||
$xorganizer->appendChild($commonName);
|
||||
}
|
||||
if (isset($this->organizer['firstName']) && $this->organizer['firstName']) {
|
||||
$firstName = $doc->createElement('cs:first-name');
|
||||
$firstName->appendChild($doc->createTextNode($this->organizer['firstName']));
|
||||
$xorganizer->appendChild($firstName);
|
||||
}
|
||||
if (isset($this->organizer['lastName']) && $this->organizer['lastName']) {
|
||||
$lastName = $doc->createElement('cs:last-name');
|
||||
$lastName->appendChild($doc->createTextNode($this->organizer['lastName']));
|
||||
$xorganizer->appendChild($lastName);
|
||||
}
|
||||
|
||||
$node->appendChild($xorganizer);
|
||||
|
||||
|
||||
}
|
||||
|
||||
foreach($this->users as $user) {
|
||||
|
||||
$xuser = $doc->createElement('cs:user');
|
||||
|
||||
$href = $doc->createElement('d:href');
|
||||
$href->appendChild($doc->createTextNode($user['href']));
|
||||
$xuser->appendChild($href);
|
||||
|
||||
if (isset($user['commonName']) && $user['commonName']) {
|
||||
$commonName = $doc->createElement('cs:common-name');
|
||||
$commonName->appendChild($doc->createTextNode($user['commonName']));
|
||||
$xuser->appendChild($commonName);
|
||||
}
|
||||
|
||||
switch($user['status']) {
|
||||
|
||||
case SharingPlugin::STATUS_ACCEPTED :
|
||||
$status = $doc->createElement('cs:invite-accepted');
|
||||
$xuser->appendChild($status);
|
||||
break;
|
||||
case SharingPlugin::STATUS_DECLINED :
|
||||
$status = $doc->createElement('cs:invite-declined');
|
||||
$xuser->appendChild($status);
|
||||
break;
|
||||
case SharingPlugin::STATUS_NORESPONSE :
|
||||
$status = $doc->createElement('cs:invite-noresponse');
|
||||
$xuser->appendChild($status);
|
||||
break;
|
||||
case SharingPlugin::STATUS_INVALID :
|
||||
$status = $doc->createElement('cs:invite-invalid');
|
||||
$xuser->appendChild($status);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
$xaccess = $doc->createElement('cs:access');
|
||||
|
||||
if ($user['readOnly']) {
|
||||
$xaccess->appendChild(
|
||||
$doc->createElement('cs:read')
|
||||
);
|
||||
} else {
|
||||
$xaccess->appendChild(
|
||||
$doc->createElement('cs:read-write')
|
||||
);
|
||||
}
|
||||
$xuser->appendChild($xaccess);
|
||||
|
||||
if (isset($user['summary']) && $user['summary']) {
|
||||
$summary = $doc->createElement('cs:summary');
|
||||
$summary->appendChild($doc->createTextNode($user['summary']));
|
||||
$xuser->appendChild($summary);
|
||||
}
|
||||
|
||||
$node->appendChild($xuser);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserializes the property.
|
||||
*
|
||||
* This static method should return a an instance of this object.
|
||||
*
|
||||
* @param \DOMElement $prop
|
||||
* @return DAV\IProperty
|
||||
*/
|
||||
static function unserialize(\DOMElement $prop) {
|
||||
|
||||
$xpath = new \DOMXPath($prop->ownerDocument);
|
||||
$xpath->registerNamespace('cs', CalDAV\Plugin::NS_CALENDARSERVER);
|
||||
$xpath->registerNamespace('d', 'urn:DAV');
|
||||
|
||||
$users = array();
|
||||
|
||||
foreach($xpath->query('cs:user', $prop) as $user) {
|
||||
|
||||
$status = null;
|
||||
if ($xpath->evaluate('boolean(cs:invite-accepted)', $user)) {
|
||||
$status = SharingPlugin::STATUS_ACCEPTED;
|
||||
} elseif ($xpath->evaluate('boolean(cs:invite-declined)', $user)) {
|
||||
$status = SharingPlugin::STATUS_DECLINED;
|
||||
} elseif ($xpath->evaluate('boolean(cs:invite-noresponse)', $user)) {
|
||||
$status = SharingPlugin::STATUS_NORESPONSE;
|
||||
} elseif ($xpath->evaluate('boolean(cs:invite-invalid)', $user)) {
|
||||
$status = SharingPlugin::STATUS_INVALID;
|
||||
} else {
|
||||
throw new DAV\Exception('Every cs:user property must have one of cs:invite-accepted, cs:invite-declined, cs:invite-noresponse or cs:invite-invalid');
|
||||
}
|
||||
$users[] = array(
|
||||
'href' => $xpath->evaluate('string(d:href)', $user),
|
||||
'commonName' => $xpath->evaluate('string(cs:common-name)', $user),
|
||||
'readOnly' => $xpath->evaluate('boolean(cs:access/cs:read)', $user),
|
||||
'summary' => $xpath->evaluate('string(cs:summary)', $user),
|
||||
'status' => $status,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return new self($users);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Property;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* schedule-calendar-transp property.
|
||||
*
|
||||
* This property is a representation of the schedule-calendar-transp property.
|
||||
* This property is defined in RFC6638 (caldav scheduling).
|
||||
*
|
||||
* Its values are either 'transparent' or 'opaque'. If it's transparent, it
|
||||
* means that this calendar will not be taken into consideration when a
|
||||
* different user queries for free-busy information. If it's 'opaque', it will.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ScheduleCalendarTransp extends DAV\Property {
|
||||
|
||||
const TRANSPARENT = 'transparent';
|
||||
const OPAQUE = 'opaque';
|
||||
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* Creates the property
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public function __construct($value) {
|
||||
|
||||
if ($value !== self::TRANSPARENT && $value !== self::OPAQUE) {
|
||||
throw new \InvalidArgumentException('The value must either be specified as "transparent" or "opaque"');
|
||||
}
|
||||
$this->value = $value;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getValue() {
|
||||
|
||||
return $this->value;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the property in a DOMDocument
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server,\DOMElement $node) {
|
||||
|
||||
$doc = $node->ownerDocument;
|
||||
switch($this->value) {
|
||||
case self::TRANSPARENT :
|
||||
$xval = $doc->createElement('cal:transparent');
|
||||
break;
|
||||
case self::OPAQUE :
|
||||
$xval = $doc->createElement('cal:opaque');
|
||||
break;
|
||||
}
|
||||
|
||||
$node->appendChild($xval);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserializes the DOMElement back into a Property class.
|
||||
*
|
||||
* @param \DOMElement $node
|
||||
* @return ScheduleCalendarTransp
|
||||
*/
|
||||
static function unserialize(\DOMElement $node) {
|
||||
|
||||
$value = null;
|
||||
foreach($node->childNodes as $childNode) {
|
||||
switch(DAV\XMLUtil::toClarkNotation($childNode)) {
|
||||
case '{' . CalDAV\Plugin::NS_CALDAV . '}opaque' :
|
||||
$value = self::OPAQUE;
|
||||
break;
|
||||
case '{' . CalDAV\Plugin::NS_CALDAV . '}transparent' :
|
||||
$value = self::TRANSPARENT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_null($value))
|
||||
return null;
|
||||
|
||||
return new self($value);
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Property;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* Supported component set property
|
||||
*
|
||||
* This property is a representation of the supported-calendar_component-set
|
||||
* property in the CalDAV namespace. It simply requires an array of components,
|
||||
* such as VEVENT, VTODO
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class SupportedCalendarComponentSet extends DAV\Property {
|
||||
|
||||
/**
|
||||
* List of supported components, such as "VEVENT, VTODO"
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $components;
|
||||
|
||||
/**
|
||||
* Creates the property
|
||||
*
|
||||
* @param array $components
|
||||
*/
|
||||
public function __construct(array $components) {
|
||||
|
||||
$this->components = $components;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of supported components
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getValue() {
|
||||
|
||||
return $this->components;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the property in a DOMDocument
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server,\DOMElement $node) {
|
||||
|
||||
$doc = $node->ownerDocument;
|
||||
foreach($this->components as $component) {
|
||||
|
||||
$xcomp = $doc->createElement('cal:comp');
|
||||
$xcomp->setAttribute('name',$component);
|
||||
$node->appendChild($xcomp);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserializes the DOMElement back into a Property class.
|
||||
*
|
||||
* @param \DOMElement $node
|
||||
* @return Property_SupportedCalendarComponentSet
|
||||
*/
|
||||
static function unserialize(\DOMElement $node) {
|
||||
|
||||
$components = array();
|
||||
foreach($node->childNodes as $childNode) {
|
||||
if (DAV\XMLUtil::toClarkNotation($childNode)==='{' . CalDAV\Plugin::NS_CALDAV . '}comp') {
|
||||
$components[] = $childNode->getAttribute('name');
|
||||
}
|
||||
}
|
||||
return new self($components);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Property;
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV\Plugin;
|
||||
|
||||
/**
|
||||
* Supported-calendar-data property
|
||||
*
|
||||
* This property is a representation of the supported-calendar-data property
|
||||
* in the CalDAV namespace. SabreDAV only has support for text/calendar;2.0
|
||||
* so the value is currently hardcoded.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class SupportedCalendarData extends DAV\Property {
|
||||
|
||||
/**
|
||||
* Serializes the property in a DOMDocument
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server,\DOMElement $node) {
|
||||
|
||||
$doc = $node->ownerDocument;
|
||||
|
||||
$prefix = isset($server->xmlNamespaces[Plugin::NS_CALDAV])?$server->xmlNamespaces[Plugin::NS_CALDAV]:'cal';
|
||||
|
||||
$caldata = $doc->createElement($prefix . ':calendar-data');
|
||||
$caldata->setAttribute('content-type','text/calendar');
|
||||
$caldata->setAttribute('version','2.0');
|
||||
|
||||
$node->appendChild($caldata);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Property;
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* supported-collation-set property
|
||||
*
|
||||
* This property is a representation of the supported-collation-set property
|
||||
* in the CalDAV namespace.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class SupportedCollationSet extends DAV\Property {
|
||||
|
||||
/**
|
||||
* Serializes the property in a DOM document
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @param \DOMElement $node
|
||||
* @return void
|
||||
*/
|
||||
public function serialize(DAV\Server $server,\DOMElement $node) {
|
||||
|
||||
$doc = $node->ownerDocument;
|
||||
|
||||
$prefix = $node->lookupPrefix('urn:ietf:params:xml:ns:caldav');
|
||||
if (!$prefix) $prefix = 'cal';
|
||||
|
||||
$node->appendChild(
|
||||
$doc->createElement($prefix . ':supported-collation','i;ascii-casemap')
|
||||
);
|
||||
$node->appendChild(
|
||||
$doc->createElement($prefix . ':supported-collation','i;octet')
|
||||
);
|
||||
$node->appendChild(
|
||||
$doc->createElement($prefix . ':supported-collation','i;unicode-casemap')
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Schedule;
|
||||
|
||||
use Sabre\VObject;
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* iMIP handler.
|
||||
*
|
||||
* This class is responsible for sending out iMIP messages. iMIP is the
|
||||
* email-based transport for iTIP. iTIP deals with scheduling operations for
|
||||
* iCalendar objects.
|
||||
*
|
||||
* If you want to customize the email that gets sent out, you can do so by
|
||||
* extending this class and overriding the sendMessage method.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class IMip {
|
||||
|
||||
/**
|
||||
* Email address used in From: header.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $senderEmail;
|
||||
|
||||
/**
|
||||
* Creates the email handler.
|
||||
*
|
||||
* @param string $senderEmail. The 'senderEmail' is the email that shows up
|
||||
* in the 'From:' address. This should
|
||||
* generally be some kind of no-reply email
|
||||
* address you own.
|
||||
*/
|
||||
public function __construct($senderEmail) {
|
||||
|
||||
$this->senderEmail = $senderEmail;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends one or more iTip messages through email.
|
||||
*
|
||||
* @param string $originator Originator Email
|
||||
* @param array $recipients Array of email addresses
|
||||
* @param VObject\Component $vObject
|
||||
* @param string $principal Principal Url of the originator
|
||||
* @return void
|
||||
*/
|
||||
public function sendMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
|
||||
|
||||
foreach($recipients as $recipient) {
|
||||
|
||||
$to = $recipient;
|
||||
$replyTo = $originator;
|
||||
$subject = 'SabreDAV iTIP message';
|
||||
|
||||
switch(strtoupper($vObject->METHOD)) {
|
||||
case 'REPLY' :
|
||||
$subject = 'Response for: ' . $vObject->VEVENT->SUMMARY;
|
||||
break;
|
||||
case 'REQUEST' :
|
||||
$subject = 'Invitation for: ' .$vObject->VEVENT->SUMMARY;
|
||||
break;
|
||||
case 'CANCEL' :
|
||||
$subject = 'Cancelled event: ' . $vObject->VEVENT->SUMMARY;
|
||||
break;
|
||||
}
|
||||
|
||||
$headers = array();
|
||||
$headers[] = 'Reply-To: ' . $replyTo;
|
||||
$headers[] = 'From: ' . $this->senderEmail;
|
||||
$headers[] = 'Content-Type: text/calendar; method=' . (string)$vObject->method . '; charset=utf-8';
|
||||
if (DAV\Server::$exposeVersion) {
|
||||
$headers[] = 'X-Sabre-Version: ' . DAV\Version::VERSION . '-' . DAV\Version::STABILITY;
|
||||
}
|
||||
|
||||
$vcalBody = $vObject->serialize();
|
||||
|
||||
$this->mail($to, $subject, $vcalBody, $headers);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
// This is deemed untestable in a reasonable manner
|
||||
|
||||
/**
|
||||
* This function is reponsible for sending the actual email.
|
||||
*
|
||||
* @param string $to Recipient email address
|
||||
* @param string $subject Subject of the email
|
||||
* @param string $body iCalendar body
|
||||
* @param array $headers List of headers
|
||||
* @return void
|
||||
*/
|
||||
protected function mail($to, $subject, $body, array $headers) {
|
||||
|
||||
|
||||
mail($to, $subject, $body, implode("\r\n", $headers));
|
||||
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Schedule;
|
||||
|
||||
/**
|
||||
* Implement this interface to have a node be recognized as a CalDAV scheduling
|
||||
* outbox.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface IOutbox extends \Sabre\DAV\ICollection, \Sabre\DAVACL\IACL {
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV\Schedule;
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* The CalDAV scheduling outbox
|
||||
*
|
||||
* The outbox is mainly used as an endpoint in the tree for a client to do
|
||||
* free-busy requests. This functionality is completely handled by the
|
||||
* Scheduling plugin, so this object is actually mostly static.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Outbox extends DAV\Collection implements IOutbox {
|
||||
|
||||
/**
|
||||
* The principal Uri
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $principalUri;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $principalUri
|
||||
*/
|
||||
public function __construct($principalUri) {
|
||||
|
||||
$this->principalUri = $principalUri;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the node.
|
||||
*
|
||||
* This is used to generate the url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return 'outbox';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all the child nodes
|
||||
*
|
||||
* @return \Sabre\DAV\INode[]
|
||||
*/
|
||||
public function getChildren() {
|
||||
|
||||
return array();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the owner principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOwner() {
|
||||
|
||||
return $this->principalUri;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a group principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGroup() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ACE's for this node.
|
||||
*
|
||||
* Each ACE has the following properties:
|
||||
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
|
||||
* currently the only supported privileges
|
||||
* * 'principal', a url to the principal who owns the node
|
||||
* * 'protected' (optional), indicating that this ACE is not allowed to
|
||||
* be updated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getACL() {
|
||||
|
||||
return array(
|
||||
array(
|
||||
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
|
||||
'principal' => $this->getOwner(),
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
|
||||
'principal' => $this->getOwner(),
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->getOwner(),
|
||||
'protected' => true,
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ACL
|
||||
*
|
||||
* This method will receive a list of new ACE's.
|
||||
*
|
||||
* @param array $acl
|
||||
* @return void
|
||||
*/
|
||||
public function setACL(array $acl) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of supported privileges for this node.
|
||||
*
|
||||
* The returned data structure is a list of nested privileges.
|
||||
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
|
||||
* standard structure.
|
||||
*
|
||||
* If null is returned from this method, the default privilege set is used,
|
||||
* which is fine for most common usecases.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getSupportedPrivilegeSet() {
|
||||
|
||||
$default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
|
||||
$default['aggregates'][] = array(
|
||||
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
|
||||
);
|
||||
$default['aggregates'][] = array(
|
||||
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
|
||||
);
|
||||
|
||||
return $default;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* This object represents a CalDAV calendar that can be shared with other
|
||||
* users.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ShareableCalendar extends Calendar implements IShareableCalendar {
|
||||
|
||||
/**
|
||||
* Updates the list of shares.
|
||||
*
|
||||
* The first array is a list of people that are to be added to the
|
||||
* calendar.
|
||||
*
|
||||
* Every element in the add array has the following properties:
|
||||
* * href - A url. Usually a mailto: address
|
||||
* * commonName - Usually a first and last name, or false
|
||||
* * summary - A description of the share, can also be false
|
||||
* * readOnly - A boolean value
|
||||
*
|
||||
* Every element in the remove array is just the address string.
|
||||
*
|
||||
* @param array $add
|
||||
* @param array $remove
|
||||
* @return void
|
||||
*/
|
||||
public function updateShares(array $add, array $remove) {
|
||||
|
||||
$this->caldavBackend->updateShares($this->calendarInfo['id'], $add, $remove);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of people whom this calendar is shared with.
|
||||
*
|
||||
* Every element in this array should have the following properties:
|
||||
* * href - Often a mailto: address
|
||||
* * commonName - Optional, for example a first + last name
|
||||
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
|
||||
* * readOnly - boolean
|
||||
* * summary - Optional, a description for the share
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getShares() {
|
||||
|
||||
return $this->caldavBackend->getShares($this->calendarInfo['id']);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this calendar as published.
|
||||
*
|
||||
* Publishing a calendar should automatically create a read-only, public,
|
||||
* subscribable calendar.
|
||||
*
|
||||
* @param bool $value
|
||||
* @return void
|
||||
*/
|
||||
public function setPublishStatus($value) {
|
||||
|
||||
$this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* This object represents a CalDAV calendar that is shared by a different user.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class SharedCalendar extends Calendar implements ISharedCalendar {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Backend\BackendInterface $caldavBackend
|
||||
* @param array $calendarInfo
|
||||
*/
|
||||
public function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) {
|
||||
|
||||
$required = array(
|
||||
'{http://calendarserver.org/ns/}shared-url',
|
||||
'{http://sabredav.org/ns}owner-principal',
|
||||
'{http://sabredav.org/ns}read-only',
|
||||
);
|
||||
foreach($required as $r) {
|
||||
if (!isset($calendarInfo[$r])) {
|
||||
throw new \InvalidArgumentException('The ' . $r . ' property must be specified for SharedCalendar(s)');
|
||||
}
|
||||
}
|
||||
|
||||
parent::__construct($caldavBackend, $calendarInfo);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should return the url of the owners' copy of the shared
|
||||
* calendar.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSharedUrl() {
|
||||
|
||||
return $this->calendarInfo['{http://calendarserver.org/ns/}shared-url'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the owner principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOwner() {
|
||||
|
||||
return $this->calendarInfo['{http://sabredav.org/ns}owner-principal'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ACE's for this node.
|
||||
*
|
||||
* Each ACE has the following properties:
|
||||
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
|
||||
* currently the only supported privileges
|
||||
* * 'principal', a url to the principal who owns the node
|
||||
* * 'protected' (optional), indicating that this ACE is not allowed to
|
||||
* be updated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getACL() {
|
||||
|
||||
// The top-level ACL only contains access information for the true
|
||||
// owner of the calendar, so we need to add the information for the
|
||||
// sharee.
|
||||
$acl = parent::getACL();
|
||||
$acl[] = array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->calendarInfo['principaluri'],
|
||||
'protected' => true,
|
||||
);
|
||||
if (!$this->calendarInfo['{http://sabredav.org/ns}read-only']) {
|
||||
$acl[] = array(
|
||||
'privilege' => '{DAV:}write',
|
||||
'principal' => $this->calendarInfo['principaluri'],
|
||||
'protected' => true,
|
||||
);
|
||||
}
|
||||
return $acl;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of people whom this calendar is shared with.
|
||||
*
|
||||
* Every element in this array should have the following properties:
|
||||
* * href - Often a mailto: address
|
||||
* * commonName - Optional, for example a first + last name
|
||||
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
|
||||
* * readOnly - boolean
|
||||
* * summary - Optional, a description for the share
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getShares() {
|
||||
|
||||
return $this->caldavBackend->getShares($this->calendarInfo['id']);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,526 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* This plugin implements support for caldav sharing.
|
||||
*
|
||||
* This spec is defined at:
|
||||
* http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
|
||||
*
|
||||
* See:
|
||||
* Sabre\CalDAV\Backend\SharingSupport for all the documentation.
|
||||
*
|
||||
* Note: This feature is experimental, and may change in between different
|
||||
* SabreDAV versions.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class SharingPlugin extends DAV\ServerPlugin {
|
||||
|
||||
/**
|
||||
* These are the various status constants used by sharing-messages.
|
||||
*/
|
||||
const STATUS_ACCEPTED = 1;
|
||||
const STATUS_DECLINED = 2;
|
||||
const STATUS_DELETED = 3;
|
||||
const STATUS_NORESPONSE = 4;
|
||||
const STATUS_INVALID = 5;
|
||||
|
||||
/**
|
||||
* Reference to SabreDAV server object.
|
||||
*
|
||||
* @var Sabre\DAV\Server
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
/**
|
||||
* This method should return a list of server-features.
|
||||
*
|
||||
* This is for example 'versioning' and is added to the DAV: header
|
||||
* in an OPTIONS response.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFeatures() {
|
||||
|
||||
return array('calendarserver-sharing');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a plugin name.
|
||||
*
|
||||
* Using this name other plugins will be able to access other plugins
|
||||
* using Sabre\DAV\Server::getPlugin
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginName() {
|
||||
|
||||
return 'caldav-sharing';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This initializes the plugin.
|
||||
*
|
||||
* This function is called by Sabre\DAV\Server, after
|
||||
* addPlugin is called.
|
||||
*
|
||||
* This method should set up the required event subscriptions.
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @return void
|
||||
*/
|
||||
public function initialize(DAV\Server $server) {
|
||||
|
||||
$this->server = $server;
|
||||
$server->resourceTypeMapping['Sabre\\CalDAV\\ISharedCalendar'] = '{' . Plugin::NS_CALENDARSERVER . '}shared';
|
||||
|
||||
array_push(
|
||||
$this->server->protectedProperties,
|
||||
'{' . Plugin::NS_CALENDARSERVER . '}invite',
|
||||
'{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes',
|
||||
'{' . Plugin::NS_CALENDARSERVER . '}shared-url'
|
||||
);
|
||||
|
||||
$this->server->subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties'));
|
||||
$this->server->subscribeEvent('afterGetProperties', array($this, 'afterGetProperties'));
|
||||
$this->server->subscribeEvent('updateProperties', array($this, 'updateProperties'));
|
||||
$this->server->subscribeEvent('unknownMethod', array($this,'unknownMethod'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This event is triggered when properties are requested for a certain
|
||||
* node.
|
||||
*
|
||||
* This allows us to inject any properties early.
|
||||
*
|
||||
* @param string $path
|
||||
* @param DAV\INode $node
|
||||
* @param array $requestedProperties
|
||||
* @param array $returnedProperties
|
||||
* @return void
|
||||
*/
|
||||
public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
|
||||
|
||||
if ($node instanceof IShareableCalendar) {
|
||||
if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}invite', $requestedProperties))!==false) {
|
||||
|
||||
unset($requestedProperties[$index]);
|
||||
$returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}invite'] =
|
||||
new Property\Invite(
|
||||
$node->getShares()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($node instanceof ISharedCalendar) {
|
||||
|
||||
if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}shared-url', $requestedProperties))!==false) {
|
||||
|
||||
unset($requestedProperties[$index]);
|
||||
$returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}shared-url'] =
|
||||
new DAV\Property\Href(
|
||||
$node->getSharedUrl()
|
||||
);
|
||||
|
||||
}
|
||||
// The 'invite' property is slightly different for the 'shared'
|
||||
// instance of the calendar, as it also contains the owner
|
||||
// information.
|
||||
if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}invite', $requestedProperties))!==false) {
|
||||
|
||||
unset($requestedProperties[$index]);
|
||||
|
||||
// Fetching owner information
|
||||
$props = $this->server->getPropertiesForPath($node->getOwner(), array(
|
||||
'{http://sabredav.org/ns}email-address',
|
||||
'{DAV:}displayname',
|
||||
), 1);
|
||||
|
||||
$ownerInfo = array(
|
||||
'href' => $node->getOwner(),
|
||||
);
|
||||
|
||||
if (isset($props[0][200])) {
|
||||
|
||||
// We're mapping the internal webdav properties to the
|
||||
// elements caldav-sharing expects.
|
||||
if (isset($props[0][200]['{http://sabredav.org/ns}email-address'])) {
|
||||
$ownerInfo['href'] = 'mailto:' . $props[0][200]['{http://sabredav.org/ns}email-address'];
|
||||
}
|
||||
if (isset($props[0][200]['{DAV:}displayname'])) {
|
||||
$ownerInfo['commonName'] = $props[0][200]['{DAV:}displayname'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}invite'] =
|
||||
new Property\Invite(
|
||||
$node->getShares(),
|
||||
$ownerInfo
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is triggered *after* all properties have been retrieved.
|
||||
* This allows us to inject the correct resourcetype for calendars that
|
||||
* have been shared.
|
||||
*
|
||||
* @param string $path
|
||||
* @param array $properties
|
||||
* @param DAV\INode $node
|
||||
* @return void
|
||||
*/
|
||||
public function afterGetProperties($path, &$properties, DAV\INode $node) {
|
||||
|
||||
if ($node instanceof IShareableCalendar) {
|
||||
if (isset($properties[200]['{DAV:}resourcetype'])) {
|
||||
if (count($node->getShares())>0) {
|
||||
$properties[200]['{DAV:}resourcetype']->add(
|
||||
'{' . Plugin::NS_CALENDARSERVER . '}shared-owner'
|
||||
);
|
||||
}
|
||||
}
|
||||
$propName = '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes';
|
||||
if (array_key_exists($propName, $properties[404])) {
|
||||
unset($properties[404][$propName]);
|
||||
$properties[200][$propName] = new Property\AllowedSharingModes(true,false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is trigged when a user attempts to update a node's
|
||||
* properties.
|
||||
*
|
||||
* A previous draft of the sharing spec stated that it was possible to use
|
||||
* PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing
|
||||
* the calendar.
|
||||
*
|
||||
* Even though this is no longer in the current spec, we keep this around
|
||||
* because OS X 10.7 may still make use of this feature.
|
||||
*
|
||||
* @param array $mutations
|
||||
* @param array $result
|
||||
* @param DAV\INode $node
|
||||
* @return void
|
||||
*/
|
||||
public function updateProperties(array &$mutations, array &$result, DAV\INode $node) {
|
||||
|
||||
if (!$node instanceof IShareableCalendar)
|
||||
return;
|
||||
|
||||
if (!isset($mutations['{DAV:}resourcetype'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only doing something if shared-owner is indeed not in the list.
|
||||
if($mutations['{DAV:}resourcetype']->is('{' . Plugin::NS_CALENDARSERVER . '}shared-owner')) return;
|
||||
|
||||
$shares = $node->getShares();
|
||||
$remove = array();
|
||||
foreach($shares as $share) {
|
||||
$remove[] = $share['href'];
|
||||
}
|
||||
$node->updateShares(array(), $remove);
|
||||
|
||||
// We're marking this update as 200 OK
|
||||
$result[200]['{DAV:}resourcetype'] = null;
|
||||
|
||||
// Removing it from the mutations list
|
||||
unset($mutations['{DAV:}resourcetype']);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This event is triggered when the server didn't know how to handle a
|
||||
* certain request.
|
||||
*
|
||||
* We intercept this to handle POST requests on calendars.
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $uri
|
||||
* @return null|bool
|
||||
*/
|
||||
public function unknownMethod($method, $uri) {
|
||||
|
||||
if ($method!=='POST') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handling xml
|
||||
$contentType = $this->server->httpRequest->getHeader('Content-Type');
|
||||
if (strpos($contentType,'application/xml')===false && strpos($contentType,'text/xml')===false)
|
||||
return;
|
||||
|
||||
// Making sure the node exists
|
||||
try {
|
||||
$node = $this->server->tree->getNodeForPath($uri);
|
||||
} catch (DAV\Exception\NotFound $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requestBody = $this->server->httpRequest->getBody(true);
|
||||
|
||||
// If this request handler could not deal with this POST request, it
|
||||
// will return 'null' and other plugins get a chance to handle the
|
||||
// request.
|
||||
//
|
||||
// However, we already requested the full body. This is a problem,
|
||||
// because a body can only be read once. This is why we preemptively
|
||||
// re-populated the request body with the existing data.
|
||||
$this->server->httpRequest->setBody($requestBody);
|
||||
|
||||
$dom = DAV\XMLUtil::loadDOMDocument($requestBody);
|
||||
|
||||
$documentType = DAV\XMLUtil::toClarkNotation($dom->firstChild);
|
||||
|
||||
switch($documentType) {
|
||||
|
||||
// Dealing with the 'share' document, which modified invitees on a
|
||||
// calendar.
|
||||
case '{' . Plugin::NS_CALENDARSERVER . '}share' :
|
||||
|
||||
// We can only deal with IShareableCalendar objects
|
||||
if (!$node instanceof IShareableCalendar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting ACL info
|
||||
$acl = $this->server->getPlugin('acl');
|
||||
|
||||
// If there's no ACL support, we allow everything
|
||||
if ($acl) {
|
||||
$acl->checkPrivileges($uri, '{DAV:}write');
|
||||
}
|
||||
|
||||
$mutations = $this->parseShareRequest($dom);
|
||||
|
||||
$node->updateShares($mutations[0], $mutations[1]);
|
||||
|
||||
$this->server->httpResponse->sendStatus(200);
|
||||
// Adding this because sending a response body may cause issues,
|
||||
// and I wanted some type of indicator the response was handled.
|
||||
$this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
|
||||
|
||||
// Breaking the event chain
|
||||
return false;
|
||||
|
||||
// The invite-reply document is sent when the user replies to an
|
||||
// invitation of a calendar share.
|
||||
case '{'. Plugin::NS_CALENDARSERVER.'}invite-reply' :
|
||||
|
||||
// This only works on the calendar-home-root node.
|
||||
if (!$node instanceof UserCalendars) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting ACL info
|
||||
$acl = $this->server->getPlugin('acl');
|
||||
|
||||
// If there's no ACL support, we allow everything
|
||||
if ($acl) {
|
||||
$acl->checkPrivileges($uri, '{DAV:}write');
|
||||
}
|
||||
|
||||
$message = $this->parseInviteReplyRequest($dom);
|
||||
|
||||
$url = $node->shareReply(
|
||||
$message['href'],
|
||||
$message['status'],
|
||||
$message['calendarUri'],
|
||||
$message['inReplyTo'],
|
||||
$message['summary']
|
||||
);
|
||||
|
||||
$this->server->httpResponse->sendStatus(200);
|
||||
// Adding this because sending a response body may cause issues,
|
||||
// and I wanted some type of indicator the response was handled.
|
||||
$this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
|
||||
|
||||
if ($url) {
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$root = $dom->createElement('cs:shared-as');
|
||||
foreach($this->server->xmlNamespaces as $namespace => $prefix) {
|
||||
$root->setAttribute('xmlns:' . $prefix, $namespace);
|
||||
}
|
||||
|
||||
$dom->appendChild($root);
|
||||
$href = new DAV\Property\Href($url);
|
||||
|
||||
$href->serialize($this->server, $root);
|
||||
$this->server->httpResponse->setHeader('Content-Type','application/xml');
|
||||
$this->server->httpResponse->sendBody($dom->saveXML());
|
||||
|
||||
}
|
||||
|
||||
// Breaking the event chain
|
||||
return false;
|
||||
|
||||
case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar' :
|
||||
|
||||
// We can only deal with IShareableCalendar objects
|
||||
if (!$node instanceof IShareableCalendar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting ACL info
|
||||
$acl = $this->server->getPlugin('acl');
|
||||
|
||||
// If there's no ACL support, we allow everything
|
||||
if ($acl) {
|
||||
$acl->checkPrivileges($uri, '{DAV:}write');
|
||||
}
|
||||
|
||||
$node->setPublishStatus(true);
|
||||
|
||||
// iCloud sends back the 202, so we will too.
|
||||
$this->server->httpResponse->sendStatus(202);
|
||||
|
||||
// Adding this because sending a response body may cause issues,
|
||||
// and I wanted some type of indicator the response was handled.
|
||||
$this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
|
||||
|
||||
// Breaking the event chain
|
||||
return false;
|
||||
|
||||
case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar' :
|
||||
|
||||
// We can only deal with IShareableCalendar objects
|
||||
if (!$node instanceof IShareableCalendar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting ACL info
|
||||
$acl = $this->server->getPlugin('acl');
|
||||
|
||||
// If there's no ACL support, we allow everything
|
||||
if ($acl) {
|
||||
$acl->checkPrivileges($uri, '{DAV:}write');
|
||||
}
|
||||
|
||||
$node->setPublishStatus(false);
|
||||
|
||||
$this->server->httpResponse->sendStatus(200);
|
||||
|
||||
// Adding this because sending a response body may cause issues,
|
||||
// and I wanted some type of indicator the response was handled.
|
||||
$this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
|
||||
|
||||
// Breaking the event chain
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the 'share' POST request.
|
||||
*
|
||||
* This method returns an array, containing two arrays.
|
||||
* The first array is a list of new sharees. Every element is a struct
|
||||
* containing a:
|
||||
* * href element. (usually a mailto: address)
|
||||
* * commonName element (often a first and lastname, but can also be
|
||||
* false)
|
||||
* * readOnly (true or false)
|
||||
* * summary (A description of the share, can also be false)
|
||||
*
|
||||
* The second array is a list of sharees that are to be removed. This is
|
||||
* just a simple array with 'hrefs'.
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
* @return array
|
||||
*/
|
||||
protected function parseShareRequest(\DOMDocument $dom) {
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$xpath->registerNamespace('cs', Plugin::NS_CALENDARSERVER);
|
||||
$xpath->registerNamespace('d', 'urn:DAV');
|
||||
|
||||
$set = array();
|
||||
$elems = $xpath->query('cs:set');
|
||||
|
||||
for($i=0; $i < $elems->length; $i++) {
|
||||
|
||||
$xset = $elems->item($i);
|
||||
$set[] = array(
|
||||
'href' => $xpath->evaluate('string(d:href)', $xset),
|
||||
'commonName' => $xpath->evaluate('string(cs:common-name)', $xset),
|
||||
'summary' => $xpath->evaluate('string(cs:summary)', $xset),
|
||||
'readOnly' => $xpath->evaluate('boolean(cs:read)', $xset)!==false
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
$remove = array();
|
||||
$elems = $xpath->query('cs:remove');
|
||||
|
||||
for($i=0; $i < $elems->length; $i++) {
|
||||
|
||||
$xremove = $elems->item($i);
|
||||
$remove[] = $xpath->evaluate('string(d:href)', $xremove);
|
||||
|
||||
}
|
||||
|
||||
return array($set, $remove);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the 'invite-reply' POST request.
|
||||
*
|
||||
* This method returns an array, containing the following properties:
|
||||
* * href - The sharee who is replying
|
||||
* * status - One of the self::STATUS_* constants
|
||||
* * calendarUri - The url of the shared calendar
|
||||
* * inReplyTo - The unique id of the share invitation.
|
||||
* * summary - Optional description of the reply.
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
* @return array
|
||||
*/
|
||||
protected function parseInviteReplyRequest(\DOMDocument $dom) {
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$xpath->registerNamespace('cs', Plugin::NS_CALENDARSERVER);
|
||||
$xpath->registerNamespace('d', 'urn:DAV');
|
||||
|
||||
$hostHref = $xpath->evaluate('string(cs:hosturl/d:href)');
|
||||
if (!$hostHref) {
|
||||
throw new DAV\Exception\BadRequest('The {' . Plugin::NS_CALENDARSERVER . '}hosturl/{DAV:}href element is required');
|
||||
}
|
||||
|
||||
return array(
|
||||
'href' => $xpath->evaluate('string(d:href)'),
|
||||
'calendarUri' => $this->server->calculateUri($hostHref),
|
||||
'inReplyTo' => $xpath->evaluate('string(cs:in-reply-to)'),
|
||||
'summary' => $xpath->evaluate('string(cs:summary)'),
|
||||
'status' => $xpath->evaluate('boolean(cs:invite-accepted)')?self::STATUS_ACCEPTED:self::STATUS_DECLINED
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,342 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* The UserCalenders class contains all calendars associated to one user
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class UserCalendars implements DAV\IExtendedCollection, DAVACL\IACL {
|
||||
|
||||
/**
|
||||
* CalDAV backend
|
||||
*
|
||||
* @var Sabre\CalDAV\Backend\BackendInterface
|
||||
*/
|
||||
protected $caldavBackend;
|
||||
|
||||
/**
|
||||
* Principal information
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $principalInfo;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Backend\BackendInterface $caldavBackend
|
||||
* @param mixed $userUri
|
||||
*/
|
||||
public function __construct(Backend\BackendInterface $caldavBackend, $principalInfo) {
|
||||
|
||||
$this->caldavBackend = $caldavBackend;
|
||||
$this->principalInfo = $principalInfo;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
list(,$name) = DAV\URLUtil::splitPath($this->principalInfo['uri']);
|
||||
return $name;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the name of this object
|
||||
*
|
||||
* @param string $name
|
||||
* @return void
|
||||
*/
|
||||
public function setName($name) {
|
||||
|
||||
throw new DAV\Exception\Forbidden();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete() {
|
||||
|
||||
throw new DAV\Exception\Forbidden();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modification date
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLastModified() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file under this object.
|
||||
*
|
||||
* This is currently not allowed
|
||||
*
|
||||
* @param string $filename
|
||||
* @param resource $data
|
||||
* @return void
|
||||
*/
|
||||
public function createFile($filename, $data=null) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new directory under this object.
|
||||
*
|
||||
* This is currently not allowed.
|
||||
*
|
||||
* @param string $filename
|
||||
* @return void
|
||||
*/
|
||||
public function createDirectory($filename) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single calendar, by name
|
||||
*
|
||||
* @param string $name
|
||||
* @todo needs optimizing
|
||||
* @return Calendar
|
||||
*/
|
||||
public function getChild($name) {
|
||||
|
||||
foreach($this->getChildren() as $child) {
|
||||
if ($name==$child->getName())
|
||||
return $child;
|
||||
|
||||
}
|
||||
throw new DAV\Exception\NotFound('Calendar with name \'' . $name . '\' could not be found');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a calendar exists.
|
||||
*
|
||||
* @param string $name
|
||||
* @todo needs optimizing
|
||||
* @return bool
|
||||
*/
|
||||
public function childExists($name) {
|
||||
|
||||
foreach($this->getChildren() as $child) {
|
||||
if ($name==$child->getName())
|
||||
return true;
|
||||
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of calendars
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getChildren() {
|
||||
|
||||
$calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
|
||||
$objs = array();
|
||||
foreach($calendars as $calendar) {
|
||||
if ($this->caldavBackend instanceof Backend\SharingSupport) {
|
||||
if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) {
|
||||
$objs[] = new SharedCalendar($this->caldavBackend, $calendar);
|
||||
} else {
|
||||
$objs[] = new ShareableCalendar($this->caldavBackend, $calendar);
|
||||
}
|
||||
} else {
|
||||
$objs[] = new Calendar($this->caldavBackend, $calendar);
|
||||
}
|
||||
}
|
||||
$objs[] = new Schedule\Outbox($this->principalInfo['uri']);
|
||||
|
||||
// We're adding a notifications node, if it's supported by the backend.
|
||||
if ($this->caldavBackend instanceof Backend\NotificationSupport) {
|
||||
$objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
|
||||
}
|
||||
return $objs;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new calendar
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $resourceType
|
||||
* @param array $properties
|
||||
* @return void
|
||||
*/
|
||||
public function createExtendedCollection($name, array $resourceType, array $properties) {
|
||||
|
||||
$isCalendar = false;
|
||||
foreach($resourceType as $rt) {
|
||||
switch ($rt) {
|
||||
case '{DAV:}collection' :
|
||||
case '{http://calendarserver.org/ns/}shared-owner' :
|
||||
// ignore
|
||||
break;
|
||||
case '{urn:ietf:params:xml:ns:caldav}calendar' :
|
||||
$isCalendar = true;
|
||||
break;
|
||||
default :
|
||||
throw new DAV\Exception\InvalidResourceType('Unknown resourceType: ' . $rt);
|
||||
}
|
||||
}
|
||||
if (!$isCalendar) {
|
||||
throw new DAV\Exception\InvalidResourceType('You can only create calendars in this collection');
|
||||
}
|
||||
$this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the owner principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOwner() {
|
||||
|
||||
return $this->principalInfo['uri'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a group principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGroup() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ACE's for this node.
|
||||
*
|
||||
* Each ACE has the following properties:
|
||||
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
|
||||
* currently the only supported privileges
|
||||
* * 'principal', a url to the principal who owns the node
|
||||
* * 'protected' (optional), indicating that this ACE is not allowed to
|
||||
* be updated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getACL() {
|
||||
|
||||
return array(
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->principalInfo['uri'],
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}write',
|
||||
'principal' => $this->principalInfo['uri'],
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}write',
|
||||
'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->principalInfo['uri'] . '/calendar-proxy-read',
|
||||
'protected' => true,
|
||||
),
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ACL
|
||||
*
|
||||
* This method will receive a list of new ACE's.
|
||||
*
|
||||
* @param array $acl
|
||||
* @return void
|
||||
*/
|
||||
public function setACL(array $acl) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of supported privileges for this node.
|
||||
*
|
||||
* The returned data structure is a list of nested privileges.
|
||||
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
|
||||
* standard structure.
|
||||
*
|
||||
* If null is returned from this method, the default privilege set is used,
|
||||
* which is fine for most common usecases.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getSupportedPrivilegeSet() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when a user replied to a request to share.
|
||||
*
|
||||
* This method should return the url of the newly created calendar if the
|
||||
* share was accepted.
|
||||
*
|
||||
* @param string href The sharee who is replying (often a mailto: address)
|
||||
* @param int status One of the SharingPlugin::STATUS_* constants
|
||||
* @param string $calendarUri The url to the calendar thats being shared
|
||||
* @param string $inReplyTo The unique id this message is a response to
|
||||
* @param string $summary A description of the reply
|
||||
* @return null|string
|
||||
*/
|
||||
public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) {
|
||||
|
||||
if (!$this->caldavBackend instanceof Backend\SharingSupport) {
|
||||
throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.');
|
||||
}
|
||||
|
||||
return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CalDAV;
|
||||
|
||||
/**
|
||||
* This class contains the Sabre\CalDAV version constants.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Version {
|
||||
|
||||
/**
|
||||
* Full version number
|
||||
*/
|
||||
const VERSION = '1.8.7';
|
||||
|
||||
/**
|
||||
* Stability : alpha, beta, stable
|
||||
*/
|
||||
const STABILITY = 'stable';
|
||||
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CardDAV;
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* The AddressBook class represents a CardDAV addressbook, owned by a specific user
|
||||
*
|
||||
* The AddressBook can contain multiple vcards
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class AddressBook extends DAV\Collection implements IAddressBook, DAV\IProperties, DAVACL\IACL {
|
||||
|
||||
/**
|
||||
* This is an array with addressbook information
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $addressBookInfo;
|
||||
|
||||
/**
|
||||
* CardDAV backend
|
||||
*
|
||||
* @var Backend\BackendInterface
|
||||
*/
|
||||
protected $carddavBackend;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Backend\BackendInterface $carddavBackend
|
||||
* @param array $addressBookInfo
|
||||
*/
|
||||
public function __construct(Backend\BackendInterface $carddavBackend, array $addressBookInfo) {
|
||||
|
||||
$this->carddavBackend = $carddavBackend;
|
||||
$this->addressBookInfo = $addressBookInfo;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the addressbook
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return $this->addressBookInfo['uri'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a card
|
||||
*
|
||||
* @param string $name
|
||||
* @return \ICard
|
||||
*/
|
||||
public function getChild($name) {
|
||||
|
||||
$obj = $this->carddavBackend->getCard($this->addressBookInfo['id'],$name);
|
||||
if (!$obj) throw new DAV\Exception\NotFound('Card not found');
|
||||
return new Card($this->carddavBackend,$this->addressBookInfo,$obj);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full list of cards
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getChildren() {
|
||||
|
||||
$objs = $this->carddavBackend->getCards($this->addressBookInfo['id']);
|
||||
$children = array();
|
||||
foreach($objs as $obj) {
|
||||
$children[] = new Card($this->carddavBackend,$this->addressBookInfo,$obj);
|
||||
}
|
||||
return $children;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new directory
|
||||
*
|
||||
* We actually block this, as subdirectories are not allowed in addressbooks.
|
||||
*
|
||||
* @param string $name
|
||||
* @return void
|
||||
*/
|
||||
public function createDirectory($name) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('Creating collections in addressbooks is not allowed');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file
|
||||
*
|
||||
* The contents of the new file must be a valid VCARD.
|
||||
*
|
||||
* This method may return an ETag.
|
||||
*
|
||||
* @param string $name
|
||||
* @param resource $vcardData
|
||||
* @return string|null
|
||||
*/
|
||||
public function createFile($name,$vcardData = null) {
|
||||
|
||||
if (is_resource($vcardData)) {
|
||||
$vcardData = stream_get_contents($vcardData);
|
||||
}
|
||||
// Converting to UTF-8, if needed
|
||||
$vcardData = DAV\StringUtil::ensureUTF8($vcardData);
|
||||
|
||||
return $this->carddavBackend->createCard($this->addressBookInfo['id'],$name,$vcardData);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entire addressbook.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete() {
|
||||
|
||||
$this->carddavBackend->deleteAddressBook($this->addressBookInfo['id']);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the addressbook
|
||||
*
|
||||
* @param string $newName
|
||||
* @return void
|
||||
*/
|
||||
public function setName($newName) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('Renaming addressbooks is not yet supported');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modification date as a unix timestamp.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getLastModified() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates properties on this node,
|
||||
*
|
||||
* The properties array uses the propertyName in clark-notation as key,
|
||||
* and the array value for the property value. In the case a property
|
||||
* should be deleted, the property value will be null.
|
||||
*
|
||||
* This method must be atomic. If one property cannot be changed, the
|
||||
* entire operation must fail.
|
||||
*
|
||||
* If the operation was successful, true can be returned.
|
||||
* If the operation failed, false can be returned.
|
||||
*
|
||||
* Deletion of a non-existent property is always successful.
|
||||
*
|
||||
* Lastly, it is optional to return detailed information about any
|
||||
* failures. In this case an array should be returned with the following
|
||||
* structure:
|
||||
*
|
||||
* array(
|
||||
* 403 => array(
|
||||
* '{DAV:}displayname' => null,
|
||||
* ),
|
||||
* 424 => array(
|
||||
* '{DAV:}owner' => null,
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* In this example it was forbidden to update {DAV:}displayname.
|
||||
* (403 Forbidden), which in turn also caused {DAV:}owner to fail
|
||||
* (424 Failed Dependency) because the request needs to be atomic.
|
||||
*
|
||||
* @param array $mutations
|
||||
* @return bool|array
|
||||
*/
|
||||
public function updateProperties($mutations) {
|
||||
|
||||
return $this->carddavBackend->updateAddressBook($this->addressBookInfo['id'], $mutations);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of properties for this nodes.
|
||||
*
|
||||
* The properties list is a list of propertynames the client requested,
|
||||
* encoded in clark-notation {xmlnamespace}tagname
|
||||
*
|
||||
* If the array is empty, it means 'all properties' were requested.
|
||||
*
|
||||
* @param array $properties
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties($properties) {
|
||||
|
||||
$response = array();
|
||||
foreach($properties as $propertyName) {
|
||||
|
||||
if (isset($this->addressBookInfo[$propertyName])) {
|
||||
|
||||
$response[$propertyName] = $this->addressBookInfo[$propertyName];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the owner principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOwner() {
|
||||
|
||||
return $this->addressBookInfo['principaluri'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a group principal
|
||||
*
|
||||
* This must be a url to a principal, or null if there's no owner
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGroup() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ACE's for this node.
|
||||
*
|
||||
* Each ACE has the following properties:
|
||||
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
|
||||
* currently the only supported privileges
|
||||
* * 'principal', a url to the principal who owns the node
|
||||
* * 'protected' (optional), indicating that this ACE is not allowed to
|
||||
* be updated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getACL() {
|
||||
|
||||
return array(
|
||||
array(
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->addressBookInfo['principaluri'],
|
||||
'protected' => true,
|
||||
),
|
||||
array(
|
||||
'privilege' => '{DAV:}write',
|
||||
'principal' => $this->addressBookInfo['principaluri'],
|
||||
'protected' => true,
|
||||
),
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ACL
|
||||
*
|
||||
* This method will receive a list of new ACE's.
|
||||
*
|
||||
* @param array $acl
|
||||
* @return void
|
||||
*/
|
||||
public function setACL(array $acl) {
|
||||
|
||||
throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of supported privileges for this node.
|
||||
*
|
||||
* The returned data structure is a list of nested privileges.
|
||||
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
|
||||
* standard structure.
|
||||
*
|
||||
* If null is returned from this method, the default privilege set is used,
|
||||
* which is fine for most common usecases.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getSupportedPrivilegeSet() {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CardDAV;
|
||||
|
||||
use Sabre\DAV;
|
||||
|
||||
/**
|
||||
* Parses the addressbook-query report request body.
|
||||
*
|
||||
* Whoever designed this format, and the CalDAV equivalent even more so,
|
||||
* has no feel for design.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class AddressBookQueryParser {
|
||||
|
||||
const TEST_ANYOF = 'anyof';
|
||||
const TEST_ALLOF = 'allof';
|
||||
|
||||
/**
|
||||
* List of requested properties the client wanted
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $requestedProperties;
|
||||
|
||||
/**
|
||||
* The number of results the client wants
|
||||
*
|
||||
* null means it wasn't specified, which in most cases means 'all results'.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $limit;
|
||||
|
||||
/**
|
||||
* List of property filters.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $filters;
|
||||
|
||||
/**
|
||||
* Either TEST_ANYOF or TEST_ALLOF
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $test;
|
||||
|
||||
/**
|
||||
* DOM Document
|
||||
*
|
||||
* @var DOMDocument
|
||||
*/
|
||||
protected $dom;
|
||||
|
||||
/**
|
||||
* DOM XPath object
|
||||
*
|
||||
* @var DOMXPath
|
||||
*/
|
||||
protected $xpath;
|
||||
|
||||
/**
|
||||
* Creates the parser
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
*/
|
||||
public function __construct(\DOMDocument $dom) {
|
||||
|
||||
$this->dom = $dom;
|
||||
|
||||
$this->xpath = new \DOMXPath($dom);
|
||||
$this->xpath->registerNameSpace('card',Plugin::NS_CARDDAV);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function parse() {
|
||||
|
||||
$filterNode = null;
|
||||
|
||||
$limit = $this->xpath->evaluate('number(/card:addressbook-query/card:limit/card:nresults)');
|
||||
if (is_nan($limit)) $limit = null;
|
||||
|
||||
$filter = $this->xpath->query('/card:addressbook-query/card:filter');
|
||||
|
||||
// According to the CardDAV spec there needs to be exactly 1 filter
|
||||
// element. However, KDE 4.8.2 contains a bug that will encode 0 filter
|
||||
// elements, so this is a workaround for that.
|
||||
//
|
||||
// See: https://bugs.kde.org/show_bug.cgi?id=300047
|
||||
if ($filter->length === 0) {
|
||||
$test = null;
|
||||
$filter = null;
|
||||
} elseif ($filter->length === 1) {
|
||||
$filter = $filter->item(0);
|
||||
$test = $this->xpath->evaluate('string(@test)', $filter);
|
||||
} else {
|
||||
throw new DAV\Exception\BadRequest('Only one filter element is allowed');
|
||||
}
|
||||
|
||||
if (!$test) $test = self::TEST_ANYOF;
|
||||
if ($test !== self::TEST_ANYOF && $test !== self::TEST_ALLOF) {
|
||||
throw new DAV\Exception\BadRequest('The test attribute must either hold "anyof" or "allof"');
|
||||
}
|
||||
|
||||
$propFilters = array();
|
||||
|
||||
$propFilterNodes = $this->xpath->query('card:prop-filter', $filter);
|
||||
for($ii=0; $ii < $propFilterNodes->length; $ii++) {
|
||||
|
||||
$propFilters[] = $this->parsePropFilterNode($propFilterNodes->item($ii));
|
||||
|
||||
|
||||
}
|
||||
|
||||
$this->filters = $propFilters;
|
||||
$this->limit = $limit;
|
||||
$this->requestedProperties = array_keys(DAV\XMLUtil::parseProperties($this->dom->firstChild));
|
||||
$this->test = $test;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the prop-filter xml element
|
||||
*
|
||||
* @param \DOMElement $propFilterNode
|
||||
* @return array
|
||||
*/
|
||||
protected function parsePropFilterNode(\DOMElement $propFilterNode) {
|
||||
|
||||
$propFilter = array();
|
||||
$propFilter['name'] = $propFilterNode->getAttribute('name');
|
||||
$propFilter['test'] = $propFilterNode->getAttribute('test');
|
||||
if (!$propFilter['test']) $propFilter['test'] = 'anyof';
|
||||
|
||||
$propFilter['is-not-defined'] = $this->xpath->query('card:is-not-defined', $propFilterNode)->length>0;
|
||||
|
||||
$paramFilterNodes = $this->xpath->query('card:param-filter', $propFilterNode);
|
||||
|
||||
$propFilter['param-filters'] = array();
|
||||
|
||||
|
||||
for($ii=0;$ii<$paramFilterNodes->length;$ii++) {
|
||||
|
||||
$propFilter['param-filters'][] = $this->parseParamFilterNode($paramFilterNodes->item($ii));
|
||||
|
||||
}
|
||||
$propFilter['text-matches'] = array();
|
||||
$textMatchNodes = $this->xpath->query('card:text-match', $propFilterNode);
|
||||
|
||||
for($ii=0;$ii<$textMatchNodes->length;$ii++) {
|
||||
|
||||
$propFilter['text-matches'][] = $this->parseTextMatchNode($textMatchNodes->item($ii));
|
||||
|
||||
}
|
||||
|
||||
return $propFilter;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the param-filter element
|
||||
*
|
||||
* @param \DOMElement $paramFilterNode
|
||||
* @return array
|
||||
*/
|
||||
public function parseParamFilterNode(\DOMElement $paramFilterNode) {
|
||||
|
||||
$paramFilter = array();
|
||||
$paramFilter['name'] = $paramFilterNode->getAttribute('name');
|
||||
$paramFilter['is-not-defined'] = $this->xpath->query('card:is-not-defined', $paramFilterNode)->length>0;
|
||||
$paramFilter['text-match'] = null;
|
||||
|
||||
$textMatch = $this->xpath->query('card:text-match', $paramFilterNode);
|
||||
if ($textMatch->length>0) {
|
||||
$paramFilter['text-match'] = $this->parseTextMatchNode($textMatch->item(0));
|
||||
}
|
||||
|
||||
return $paramFilter;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Text match
|
||||
*
|
||||
* @param \DOMElement $textMatchNode
|
||||
* @return array
|
||||
*/
|
||||
public function parseTextMatchNode(\DOMElement $textMatchNode) {
|
||||
|
||||
$matchType = $textMatchNode->getAttribute('match-type');
|
||||
if (!$matchType) $matchType = 'contains';
|
||||
|
||||
if (!in_array($matchType, array('contains', 'equals', 'starts-with', 'ends-with'))) {
|
||||
throw new DAV\Exception\BadRequest('Unknown match-type: ' . $matchType);
|
||||
}
|
||||
|
||||
$negateCondition = $textMatchNode->getAttribute('negate-condition');
|
||||
$negateCondition = $negateCondition==='yes';
|
||||
$collation = $textMatchNode->getAttribute('collation');
|
||||
if (!$collation) $collation = 'i;unicode-casemap';
|
||||
|
||||
return array(
|
||||
'negate-condition' => $negateCondition,
|
||||
'collation' => $collation,
|
||||
'match-type' => $matchType,
|
||||
'value' => $textMatchNode->nodeValue
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CardDAV;
|
||||
|
||||
use Sabre\DAVACL;
|
||||
|
||||
/**
|
||||
* AddressBook rootnode
|
||||
*
|
||||
* This object lists a collection of users, which can contain addressbooks.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class AddressBookRoot extends DAVACL\AbstractPrincipalCollection {
|
||||
|
||||
/**
|
||||
* Principal Backend
|
||||
*
|
||||
* @var Sabre\DAVACL\PrincipalBackend\BackendInteface
|
||||
*/
|
||||
protected $principalBackend;
|
||||
|
||||
/**
|
||||
* CardDAV backend
|
||||
*
|
||||
* @var Backend\BackendInterface
|
||||
*/
|
||||
protected $carddavBackend;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* This constructor needs both a principal and a carddav backend.
|
||||
*
|
||||
* By default this class will show a list of addressbook collections for
|
||||
* principals in the 'principals' collection. If your main principals are
|
||||
* actually located in a different path, use the $principalPrefix argument
|
||||
* to override this.
|
||||
*
|
||||
* @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
|
||||
* @param Backend\BackendInterface $carddavBackend
|
||||
* @param string $principalPrefix
|
||||
*/
|
||||
public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend,Backend\BackendInterface $carddavBackend, $principalPrefix = 'principals') {
|
||||
|
||||
$this->carddavBackend = $carddavBackend;
|
||||
parent::__construct($principalBackend, $principalPrefix);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the node
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
|
||||
return Plugin::ADDRESSBOOK_ROOT;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a node for a principal.
|
||||
*
|
||||
* The passed array contains principal information, and is guaranteed to
|
||||
* at least contain a uri item. Other properties may or may not be
|
||||
* supplied by the authentication backend.
|
||||
*
|
||||
* @param array $principal
|
||||
* @return \Sabre\DAV\INode
|
||||
*/
|
||||
public function getChildForPrincipal(array $principal) {
|
||||
|
||||
return new UserAddressBooks($this->carddavBackend, $principal['uri']);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CardDAV\Backend;
|
||||
|
||||
/**
|
||||
* CardDAV abstract Backend
|
||||
*
|
||||
* This class serves as a base-class for addressbook backends
|
||||
*
|
||||
* This class doesn't do much, but it was added for consistency.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
abstract class AbstractBackend implements BackendInterface {
|
||||
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
namespace Sabre\CardDAV\Backend;
|
||||
|
||||
/**
|
||||
* CardDAV Backend Interface
|
||||
*
|
||||
* Any CardDAV backend must implement this interface.
|
||||
*
|
||||
* Note that there are references to 'addressBookId' scattered throughout the
|
||||
* class. The value of the addressBookId is completely up to you, it can be any
|
||||
* arbitrary value you can use as an unique identifier.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface BackendInterface {
|
||||
|
||||
/**
|
||||
* Returns the list of addressbooks for a specific user.
|
||||
*
|
||||
* Every addressbook should have the following properties:
|
||||
* id - an arbitrary unique id
|
||||
* uri - the 'basename' part of the url
|
||||
* principaluri - Same as the passed parameter
|
||||
*
|
||||
* Any additional clark-notation property may be passed besides this. Some
|
||||
* common ones are :
|
||||
* {DAV:}displayname
|
||||
* {urn:ietf:params:xml:ns:carddav}addressbook-description
|
||||
* {http://calendarserver.org/ns/}getctag
|
||||
*
|
||||
* @param string $principalUri
|
||||
* @return array
|
||||
*/
|
||||
public function getAddressBooksForUser($principalUri);
|
||||
|
||||
/**
|
||||
* Updates an addressbook's properties
|
||||
*
|
||||
* See Sabre\DAV\IProperties for a description of the mutations array, as
|
||||
* well as the return value.
|
||||
*
|
||||
* @param mixed $addressBookId
|
||||
* @param array $mutations
|
||||
* @see Sabre\DAV\IProperties::updateProperties
|
||||
* @return bool|array
|
||||
*/
|
||||
public function updateAddressBook($addressBookId, array $mutations);
|
||||
|
||||
/**
|
||||
* Creates a new address book
|
||||
*
|
||||
* @param string $principalUri
|
||||
* @param string $url Just the 'basename' of the url.
|
||||
* @param array $properties
|
||||
* @return void
|
||||
*/
|
||||
public function createAddressBook($principalUri, $url, array $properties);
|
||||
|
||||
/**
|
||||
* Deletes an entire addressbook and all its contents
|
||||
*
|
||||
* @param mixed $addressBookId
|
||||
* @return void
|
||||
*/
|
||||
public function deleteAddressBook($addressBookId);
|
||||
|
||||
/**
|
||||
* Returns all cards for a specific addressbook id.
|
||||
*
|
||||
* This method should return the following properties for each card:
|
||||
* * carddata - raw vcard data
|
||||
* * uri - Some unique url
|
||||
* * lastmodified - A unix timestamp
|
||||
*
|
||||
* It's recommended to also return the following properties:
|
||||
* * etag - A unique etag. This must change every time the card changes.
|
||||
* * size - The size of the card in bytes.
|
||||
*
|
||||
* If these last two properties are provided, less time will be spent
|
||||
* calculating them. If they are specified, you can also ommit carddata.
|
||||
* This may speed up certain requests, especially with large cards.
|
||||
*
|
||||
* @param mixed $addressbookId
|
||||
* @return array
|
||||
*/
|
||||
public function getCards($addressbookId);
|
||||
|
||||
/**
|
||||
* Returns a specfic card.
|
||||
*
|
||||
* The same set of properties must be returned as with getCards. The only
|
||||
* exception is that 'carddata' is absolutely required.
|
||||
*
|
||||
* @param mixed $addressBookId
|
||||
* @param string $cardUri
|
||||
* @return array
|
||||
*/
|
||||
public function getCard($addressBookId, $cardUri);
|
||||
|
||||
/**
|
||||
* Creates a new card.
|
||||
*
|
||||
* The addressbook id will be passed as the first argument. This is the
|
||||
* same id as it is returned from the getAddressbooksForUser method.
|
||||
*
|
||||
* The cardUri is a base uri, and doesn't include the full path. The
|
||||
* cardData argument is the vcard body, and is passed as a string.
|
||||
*
|
||||
* It is possible to return an ETag from this method. This ETag is for the
|
||||
* newly created resource, and must be enclosed with double quotes (that
|
||||
* is, the string itself must contain the double quotes).
|
||||
*
|
||||
* You should only return the ETag if you store the carddata as-is. If a
|
||||
* subsequent GET request on the same card does not have the same body,
|
||||
* byte-by-byte and you did return an ETag here, clients tend to get
|
||||
* confused.
|
||||
*
|
||||
* If you don't return an ETag, you can just return null.
|
||||
*
|
||||
* @param mixed $addressBookId
|
||||
* @param string $cardUri
|
||||
* @param string $cardData
|
||||
* @return string|null
|
||||
*/
|
||||
public function createCard($addressBookId, $cardUri, $cardData);
|
||||
|
||||
/**
|
||||
* Updates a card.
|
||||
*
|
||||
* The addressbook id will be passed as the first argument. This is the
|
||||
* same id as it is returned from the getAddressbooksForUser method.
|
||||
*
|
||||
* The cardUri is a base uri, and doesn't include the full path. The
|
||||
* cardData argument is the vcard body, and is passed as a string.
|
||||
*
|
||||
* It is possible to return an ETag from this method. This ETag should
|
||||
* match that of the updated resource, and must be enclosed with double
|
||||
* quotes (that is: the string itself must contain the actual quotes).
|
||||
*
|
||||
* You should only return the ETag if you store the carddata as-is. If a
|
||||
* subsequent GET request on the same card does not have the same body,
|
||||
* byte-by-byte and you did return an ETag here, clients tend to get
|
||||
* confused.
|
||||
*
|
||||
* If you don't return an ETag, you can just return null.
|
||||
*
|
||||
* @param mixed $addressBookId
|
||||
* @param string $cardUri
|
||||
* @param string $cardData
|
||||
* @return string|null
|
||||
*/
|
||||
public function updateCard($addressBookId, $cardUri, $cardData);
|
||||
|
||||
/**
|
||||
* Deletes a card
|
||||
*
|
||||
* @param mixed $addressBookId
|
||||
* @param string $cardUri
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteCard($addressBookId, $cardUri);
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue