commit
ba104bd674
9 changed files with 754 additions and 0 deletions
@ -0,0 +1,42 @@
|
||||
# laravel-bconnect |
||||
|
||||
The Bluesquare Connect package allows you to use its OAuth server and sync its resources. |
||||
|
||||
## Installation |
||||
|
||||
First in your composer.json, add: |
||||
|
||||
``` |
||||
"require": { |
||||
"bluesquare/laravel-connect": "dev-master" |
||||
} |
||||
``` |
||||
|
||||
|
||||
``` |
||||
"repositories": [ |
||||
{ |
||||
"type": "vcs", |
||||
"url": "https://git.bluesquare.io/bluesquare/laravel-connect" |
||||
} |
||||
] |
||||
``` |
||||
|
||||
Next, update your package: |
||||
|
||||
```bash |
||||
composer update bluesquare/laravel-connect |
||||
``` |
||||
|
||||
Eventually, if you want to customize the config system: |
||||
|
||||
```bash |
||||
php artisan vendor:publish |
||||
``` |
||||
|
||||
Finally, add in your `.env`: |
||||
|
||||
```bash |
||||
BCONNECT_CLIENT_ID=your_client_id |
||||
BCONNECT_CLIENT_SECRET=your_client_secret |
||||
``` |
@ -0,0 +1,39 @@
|
||||
{ |
||||
"name": "bluesquare/laravel-connect", |
||||
"description": "Consume Bluesquare Connect resources and use its OAuth server.", |
||||
"keywords": [ |
||||
"package", |
||||
"bluesquare", |
||||
"api", |
||||
"connect", |
||||
"oauth" |
||||
], |
||||
"homepage": "https://git.bluesquare.io/bluesquare/laravel-connect", |
||||
"license": "proprietary", |
||||
"authors": [ |
||||
{ |
||||
"name": "Bluesquare", |
||||
"email": "contact@bluesquare.io", |
||||
"homepage": "https://bluesquare.io/", |
||||
"role": "Developers" |
||||
} |
||||
], |
||||
"minimum-stability": "dev", |
||||
"autoload": { |
||||
"psr-4": { |
||||
"Bluesquare\\Connect\\": "src/" |
||||
} |
||||
}, |
||||
"extra": { |
||||
"laravel": { |
||||
"providers": [ |
||||
"Bluesquare\\Connect\\ConnectServiceProvider" |
||||
] |
||||
} |
||||
}, |
||||
"require": { |
||||
"guzzlehttp/guzzle": "^6.5", |
||||
"php": "^7.2" |
||||
}, |
||||
"prefer-stable": true |
||||
} |
@ -0,0 +1,297 @@
|
||||
{ |
||||
"_readme": [ |
||||
"This file locks the dependencies of your project to a known state", |
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", |
||||
"This file is @generated automatically" |
||||
], |
||||
"content-hash": "98b09ff09c6392aadb0f56bb9dc51a96", |
||||
"packages": [ |
||||
{ |
||||
"name": "guzzlehttp/guzzle", |
||||
"version": "6.5.2", |
||||
"source": { |
||||
"type": "git", |
||||
"url": "https://github.com/guzzle/guzzle.git", |
||||
"reference": "43ece0e75098b7ecd8d13918293029e555a50f82" |
||||
}, |
||||
"dist": { |
||||
"type": "zip", |
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82", |
||||
"reference": "43ece0e75098b7ecd8d13918293029e555a50f82", |
||||
"shasum": "" |
||||
}, |
||||
"require": { |
||||
"ext-json": "*", |
||||
"guzzlehttp/promises": "^1.0", |
||||
"guzzlehttp/psr7": "^1.6.1", |
||||
"php": ">=5.5" |
||||
}, |
||||
"require-dev": { |
||||
"ext-curl": "*", |
||||
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", |
||||
"psr/log": "^1.1" |
||||
}, |
||||
"suggest": { |
||||
"ext-intl": "Required for Internationalized Domain Name (IDN) support", |
||||
"psr/log": "Required for using the Log middleware" |
||||
}, |
||||
"type": "library", |
||||
"extra": { |
||||
"branch-alias": { |
||||
"dev-master": "6.5-dev" |
||||
} |
||||
}, |
||||
"autoload": { |
||||
"psr-4": { |
||||
"GuzzleHttp\\": "src/" |
||||
}, |
||||
"files": [ |
||||
"src/functions_include.php" |
||||
] |
||||
}, |
||||
"notification-url": "https://packagist.org/downloads/", |
||||
"license": [ |
||||
"MIT" |
||||
], |
||||
"authors": [ |
||||
{ |
||||
"name": "Michael Dowling", |
||||
"email": "mtdowling@gmail.com", |
||||
"homepage": "https://github.com/mtdowling" |
||||
} |
||||
], |
||||
"description": "Guzzle is a PHP HTTP client library", |
||||
"homepage": "http://guzzlephp.org/", |
||||
"keywords": [ |
||||
"client", |
||||
"curl", |
||||
"framework", |
||||
"http", |
||||
"http client", |
||||
"rest", |
||||
"web service" |
||||
], |
||||
"time": "2019-12-23T11:57:10+00:00" |
||||
}, |
||||
{ |
||||
"name": "guzzlehttp/promises", |
||||
"version": "v1.3.1", |
||||
"source": { |
||||
"type": "git", |
||||
"url": "https://github.com/guzzle/promises.git", |
||||
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" |
||||
}, |
||||
"dist": { |
||||
"type": "zip", |
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", |
||||
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", |
||||
"shasum": "" |
||||
}, |
||||
"require": { |
||||
"php": ">=5.5.0" |
||||
}, |
||||
"require-dev": { |
||||
"phpunit/phpunit": "^4.0" |
||||
}, |
||||
"type": "library", |
||||
"extra": { |
||||
"branch-alias": { |
||||
"dev-master": "1.4-dev" |
||||
} |
||||
}, |
||||
"autoload": { |
||||
"psr-4": { |
||||
"GuzzleHttp\\Promise\\": "src/" |
||||
}, |
||||
"files": [ |
||||
"src/functions_include.php" |
||||
] |
||||
}, |
||||
"notification-url": "https://packagist.org/downloads/", |
||||
"license": [ |
||||
"MIT" |
||||
], |
||||
"authors": [ |
||||
{ |
||||
"name": "Michael Dowling", |
||||
"email": "mtdowling@gmail.com", |
||||
"homepage": "https://github.com/mtdowling" |
||||
} |
||||
], |
||||
"description": "Guzzle promises library", |
||||
"keywords": [ |
||||
"promise" |
||||
], |
||||
"time": "2016-12-20T10:07:11+00:00" |
||||
}, |
||||
{ |
||||
"name": "guzzlehttp/psr7", |
||||
"version": "1.6.1", |
||||
"source": { |
||||
"type": "git", |
||||
"url": "https://github.com/guzzle/psr7.git", |
||||
"reference": "239400de7a173fe9901b9ac7c06497751f00727a" |
||||
}, |
||||
"dist": { |
||||
"type": "zip", |
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", |
||||
"reference": "239400de7a173fe9901b9ac7c06497751f00727a", |
||||
"shasum": "" |
||||
}, |
||||
"require": { |
||||
"php": ">=5.4.0", |
||||
"psr/http-message": "~1.0", |
||||
"ralouphie/getallheaders": "^2.0.5 || ^3.0.0" |
||||
}, |
||||
"provide": { |
||||
"psr/http-message-implementation": "1.0" |
||||
}, |
||||
"require-dev": { |
||||
"ext-zlib": "*", |
||||
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" |
||||
}, |
||||
"suggest": { |
||||
"zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" |
||||
}, |
||||
"type": "library", |
||||
"extra": { |
||||
"branch-alias": { |
||||
"dev-master": "1.6-dev" |
||||
} |
||||
}, |
||||
"autoload": { |
||||
"psr-4": { |
||||
"GuzzleHttp\\Psr7\\": "src/" |
||||
}, |
||||
"files": [ |
||||
"src/functions_include.php" |
||||
] |
||||
}, |
||||
"notification-url": "https://packagist.org/downloads/", |
||||
"license": [ |
||||
"MIT" |
||||
], |
||||
"authors": [ |
||||
{ |
||||
"name": "Michael Dowling", |
||||
"email": "mtdowling@gmail.com", |
||||
"homepage": "https://github.com/mtdowling" |
||||
}, |
||||
{ |
||||
"name": "Tobias Schultze", |
||||
"homepage": "https://github.com/Tobion" |
||||
} |
||||
], |
||||
"description": "PSR-7 message implementation that also provides common utility methods", |
||||
"keywords": [ |
||||
"http", |
||||
"message", |
||||
"psr-7", |
||||
"request", |
||||
"response", |
||||
"stream", |
||||
"uri", |
||||
"url" |
||||
], |
||||
"time": "2019-07-01T23:21:34+00:00" |
||||
}, |
||||
{ |
||||
"name": "psr/http-message", |
||||
"version": "1.0.1", |
||||
"source": { |
||||
"type": "git", |
||||
"url": "https://github.com/php-fig/http-message.git", |
||||
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" |
||||
}, |
||||
"dist": { |
||||
"type": "zip", |
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", |
||||
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", |
||||
"shasum": "" |
||||
}, |
||||
"require": { |
||||
"php": ">=5.3.0" |
||||
}, |
||||
"type": "library", |
||||
"extra": { |
||||
"branch-alias": { |
||||
"dev-master": "1.0.x-dev" |
||||
} |
||||
}, |
||||
"autoload": { |
||||
"psr-4": { |
||||
"Psr\\Http\\Message\\": "src/" |
||||
} |
||||
}, |
||||
"notification-url": "https://packagist.org/downloads/", |
||||
"license": [ |
||||
"MIT" |
||||
], |
||||
"authors": [ |
||||
{ |
||||
"name": "PHP-FIG", |
||||
"homepage": "http://www.php-fig.org/" |
||||
} |
||||
], |
||||
"description": "Common interface for HTTP messages", |
||||
"homepage": "https://github.com/php-fig/http-message", |
||||
"keywords": [ |
||||
"http", |
||||
"http-message", |
||||
"psr", |
||||
"psr-7", |
||||
"request", |
||||
"response" |
||||
], |
||||
"time": "2016-08-06T14:39:51+00:00" |
||||
}, |
||||
{ |
||||
"name": "ralouphie/getallheaders", |
||||
"version": "3.0.3", |
||||
"source": { |
||||
"type": "git", |
||||
"url": "https://github.com/ralouphie/getallheaders.git", |
||||
"reference": "120b605dfeb996808c31b6477290a714d356e822" |
||||
}, |
||||
"dist": { |
||||
"type": "zip", |
||||
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", |
||||
"reference": "120b605dfeb996808c31b6477290a714d356e822", |
||||
"shasum": "" |
||||
}, |
||||
"require": { |
||||
"php": ">=5.6" |
||||
}, |
||||
"require-dev": { |
||||
"php-coveralls/php-coveralls": "^2.1", |
||||
"phpunit/phpunit": "^5 || ^6.5" |
||||
}, |
||||
"type": "library", |
||||
"autoload": { |
||||
"files": [ |
||||
"src/getallheaders.php" |
||||
] |
||||
}, |
||||
"notification-url": "https://packagist.org/downloads/", |
||||
"license": [ |
||||
"MIT" |
||||
], |
||||
"authors": [ |
||||
{ |
||||
"name": "Ralph Khattar", |
||||
"email": "ralph.khattar@gmail.com" |
||||
} |
||||
], |
||||
"description": "A polyfill for getallheaders.", |
||||
"time": "2019-03-08T08:55:37+00:00" |
||||
} |
||||
], |
||||
"packages-dev": [], |
||||
"aliases": [], |
||||
"minimum-stability": "stable", |
||||
"stability-flags": [], |
||||
"prefer-stable": false, |
||||
"prefer-lowest": false, |
||||
"platform": [], |
||||
"platform-dev": [] |
||||
} |
@ -0,0 +1,34 @@
|
||||
<?php |
||||
|
||||
return [ |
||||
/** |
||||
* OAuth model |
||||
*/ |
||||
'model' => \App\User::class, |
||||
|
||||
/** |
||||
* OAuth redirect URI |
||||
*/ |
||||
'redirect' => env('BCONNECT_REDIRECT', url('/oauth/callback')), |
||||
|
||||
/** |
||||
* OAuth client id |
||||
*/ |
||||
'client_id' => env('BCONNECT_CLIENT_ID', null), |
||||
|
||||
/** |
||||
* OAuth client secret |
||||
*/ |
||||
'client_secret' => env('BCONNECT_CLIENT_SECRET', null), |
||||
|
||||
/** |
||||
* OAuth scopes (separated with commas) |
||||
*/ |
||||
'user_scopes' => '*', |
||||
'client_scopes' => '*', |
||||
|
||||
/** |
||||
* Bluesquare Connect URL |
||||
*/ |
||||
'url' => env('BCONNECT_URL', null), |
||||
]; |
@ -0,0 +1,264 @@
|
||||
<?php |
||||
|
||||
namespace Bluesquare\Connect; |
||||
|
||||
use Bluesquare\Connect\Traits\HasConnectSync; |
||||
use GuzzleHttp\Client; |
||||
use Illuminate\Http\Request; |
||||
use Illuminate\Support\Str; |
||||
use Psr\Http\Message\StreamInterface; |
||||
|
||||
class Connect |
||||
{ |
||||
protected $app; |
||||
protected $synchronized = []; |
||||
|
||||
public function __construct(array $app) |
||||
{ |
||||
$this->app = $app; |
||||
} |
||||
|
||||
// User config |
||||
|
||||
public function setSynchronized($models) |
||||
{ |
||||
$this->synchronized = []; |
||||
|
||||
foreach ($models as $model) |
||||
{ |
||||
if (!in_array(HasConnectSync::class, class_uses($model))) |
||||
throw new ConnectException("$model does not implement HasConnectSync trait."); |
||||
|
||||
$class = explode('\\', $model); |
||||
$resource = $model::$connectResource ?? end($class); |
||||
|
||||
$this->synchronized[$resource] = $model; |
||||
} |
||||
} |
||||
|
||||
// API |
||||
|
||||
/** |
||||
* @param $method |
||||
* @param $uri |
||||
* @param null $data |
||||
* @return \Psr\Http\Message\StreamInterface |
||||
* @throws ConnectException |
||||
*/ |
||||
public function request($method, $uri, $data = null, $auth = true): StreamInterface |
||||
{ |
||||
$url = config('bconnect.url') ?? 'https://connect.bluesquare.io'; |
||||
$url = $url . '/' . trim($uri, '/'); |
||||
|
||||
$client = new Client(); |
||||
|
||||
$config = [ |
||||
'headers' => [ |
||||
'Accept' => 'application/json' |
||||
] |
||||
]; |
||||
|
||||
if ($auth === true) { |
||||
$config['Authorization'] = 'Bearer ' . $this->getAccessToken(); |
||||
} |
||||
elseif ($auth !== false) { |
||||
$config['Authorization'] = 'Bearer ' . $auth; |
||||
} |
||||
|
||||
if (!is_null($data)) { |
||||
$config['form_params'] = $data; |
||||
} |
||||
|
||||
try { |
||||
return json_decode( |
||||
$client->request($method, $url, $config)->getBody(), |
||||
true |
||||
); |
||||
|
||||
} catch(\Exception $e) { |
||||
$this->deleteAccessToken(); |
||||
throw new ConnectException($e->getMessage()); |
||||
} |
||||
} |
||||
|
||||
// OAuth (user) |
||||
|
||||
public function redirect($state = null) |
||||
{ |
||||
if (is_null($state)) |
||||
$state = Str::random(); |
||||
|
||||
$states = session()->get('connect_states'); |
||||
if (!is_array($states)) |
||||
$states = []; |
||||
$states[] = $state; |
||||
session()->put('connect_states', $states); |
||||
|
||||
$query = http_build_query([ |
||||
'client_id' => config('bconnect.client_id'), |
||||
'scope' => config('bconnect.scopes'), |
||||
'redirect_uri' => config('bconnect.redirect'), |
||||
'response_type' => 'code', |
||||
'state' => $state |
||||
]); |
||||
|
||||
$url = config('bconnect.url') . '/oauth/authorize?' . $query; |
||||
return redirect()->to($url); |
||||
} |
||||
|
||||
public function loginFromCallback(Request $request) |
||||
{ |
||||
// State check |
||||
|
||||
if (!session()->has('connect_states')) |
||||
abort(403, "Session expired"); |
||||
|
||||
$states = session()->get('connect_states'); |
||||
|
||||
if (!is_array($states)) |
||||
abort(403, "Session expired"); |
||||
|
||||
if (!$request->has('state') || !in_array($request->state, $states)) |
||||
abort(403, "Invalid state"); |
||||
|
||||
unset($states[array_search($request->state, $states)]); |
||||
|
||||
session()->put('connect_states', $states); |
||||
|
||||
// Code check |
||||
|
||||
if (!$request->has('code')) |
||||
abort(403, "Missing authorization code"); |
||||
|
||||
$code = $request->code; |
||||
|
||||
// Access token |
||||
|
||||
$data = $this->request('post', 'oauth/token', [ |
||||
'grant_type' => 'authorization_code', |
||||
'client_id' => config('bconnect.client_id'), |
||||
'client_secret' => config('bconnect.client_secret'), |
||||
'scope' => config('bconnect.user_scopes'), |
||||
'authorization_code' => $code |
||||
], false); |
||||
|
||||
return $data; |
||||
} |
||||
|
||||
// OAuth (client) |
||||
|
||||
public function getAccessToken() |
||||
{ |
||||
$access_token = cache()->get('bconnect.access_token'); |
||||
$access_token_expiration = cache()->get('bconnect.access_token_expiration'); |
||||
|
||||
if ($access_token && $access_token_expiration > time() + 60) { |
||||
return $access_token; |
||||
} |
||||
|
||||
$data = $this->request('post', '/oauth/token', [ |
||||
'grant_type' => 'client_credentials', |
||||
'client_id' => config('bconnect.client_id'), |
||||
'client_secret' => config('bconnect.client_secret'), |
||||
'scope' => config('bconnect.client_scopes') |
||||
], false); |
||||
|
||||
cache()->set('bconnect.access_token', $data['access_token']); |
||||
cache()->set('bconnect.access_token_expiration', time() + $data['expires_in']); |
||||
|
||||
return $data['access_token']; |
||||
} |
||||
|
||||
public function deleteAccessToken() |
||||
{ |
||||
cache()->delete('bconnect.access_token'); |
||||
cache()->delete('bconnect.access_token_expiration'); |
||||
} |
||||
|
||||
// Webhooks handler |
||||
|
||||
/** |
||||
* @param Request $request |
||||
* @return bool |
||||
*/ |
||||
public function handleWebhook(Request $request) |
||||
{ |
||||
$data = $request->validate([ |
||||
'connectEventType' => 'required|in:created,updated,deleted', |
||||
'connectResourceType' => 'required', |
||||
'connectResourceTable' => 'required', |
||||
'connectResourceData' => 'required|array', |
||||
'connectResourceData.id' => 'required' |
||||
]); |
||||
|
||||
if (!array_key_exists($data['connectResourceType'], $this->synchronized)) |
||||
return false; |
||||
|
||||
$model = $this->synchronized[$data['connectResourceType']]; |
||||
$method = $this->getEventMethod($data['connectEventType']); |
||||
$data = $data['connectResourceData']; |
||||
$model::$method($data['id'], $data); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
// Resources endpoints |
||||
|
||||
public function getAll($resourceType) |
||||
{ |
||||
return $this->request('get', "api/resources/$resourceType"); |
||||
} |
||||
|
||||
public function get($resourceType, $resourceId) |
||||
{ |
||||
return $this->request('get', "api/resources/$resourceType/$resourceId"); |
||||
} |
||||
|
||||
// Resources sync |
||||
|
||||
public function syncAll($resourceTypes = null) |
||||
{ |
||||
$resourceTypes = $resourceTypes ?? $this->synchronized; |
||||
|
||||
foreach ($resourceTypes as $resourceType) |
||||
{ |
||||
if (!array_key_exists($resourceType, $this->synchronized)) |
||||
throw new ConnectException("Resource $resourceType not declared as synchronized."); |
||||
|
||||
$resources = $this->getAll($resourceType); |
||||
$model = $this->synchronized[$resourceType]; |
||||
$identifiers = []; |
||||
|
||||
foreach ($resources as $data) |
||||
{ |
||||
$identifiers[] = intval($data['id']); |
||||
$this->sync($resourceType, $data['id'], $data); |
||||
} |
||||
|
||||
foreach ($model::all() as $item) |
||||
{ |
||||
if (!in_array(intval($item->id), $identifiers)) |
||||
$item->delete(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public function sync($resourceType, $resourceId, $resourceData = null) |
||||
{ |
||||
if (is_null($resourceData)) { |
||||
$resourceData = $this->get($resourceType, $resourceId); |
||||
} |
||||
|
||||
$model = $this->synchronized[$resourceType]; |
||||
$item = $model::find($resourceId); |
||||
$method = $this->getEventMethod($item ? 'updated' : 'created'); |
||||
$model::$method($resourceId, $resourceData); |
||||
} |
||||
|
||||
// |
||||
|
||||
protected function getEventMethod($event) |
||||
{ |
||||
return 'onConnectResource' . ucfirst($event); |
||||
} |
||||
} |
@ -0,0 +1,5 @@
|
||||
<?php |
||||
|
||||
namespace Bluesquare\Connect; |
||||
|
||||
class ConnectException extends \Exception {} |
@ -0,0 +1,37 @@
|
||||
<?php |
||||
|
||||
namespace Bluesquare\Connect; |
||||
|
||||
use Illuminate\Support\ServiceProvider; |
||||
|
||||
class ConnectServiceProvider extends ServiceProvider |
||||
{ |
||||
/** |
||||
* Register any application services. |
||||
* |
||||
* @return void |
||||
*/ |
||||
public function register() |
||||
{ |
||||
$this->mergeConfigFrom( |
||||
__DIR__ . '/../config/bconnect.php', |
||||
'bmail' |
||||
); |
||||
|
||||
$this->app->singleton(Connect::class, function ($app) { |
||||
return new Connect($app); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Bootstrap any application services. |
||||
* |
||||
* @return void |
||||
*/ |
||||
public function boot() |
||||
{ |
||||
$this->publishes([ |
||||
__DIR__ . '/../config/bconnect.php' => config_path('bconnect.php') |
||||
], 'config'); |
||||
} |
||||
} |
@ -0,0 +1,34 @@
|
||||
<?php |
||||
|
||||
namespace Bluesquare\Connect\Traits; |
||||
|
||||
trait HasConnectSync |
||||
{ |
||||
abstract function fill($data); |
||||
abstract function save(); |
||||
abstract function delete(); |
||||
|
||||
public static $connectResource; |
||||
|
||||
public static function onConnectResourceCreated($id, $data) |
||||
{ |
||||
$record = new self; |
||||
$record->fill($data); |
||||
$record->save(); |
||||
return true; |
||||
} |
||||
|
||||
public static function onConnectResourceUpdated($id, $data) |
||||
{ |
||||
$record = self::find($id) ?? new self; |
||||
$record->fill($data); |
||||
$record->save(); |
||||
return true; |
||||
} |
||||
|
||||
public static function onConnectResourceDeleted($id, $data) |
||||
{ |
||||
$record = self::find($id); |
||||
return $record ? $record->delete() : false; |
||||
} |
||||
} |
Loading…
Reference in new issue