snipe-it/app/Importer/Importer.php
snipe 87464e6ec0
Merge branch 'develop' into integrations/2020-04-15-v5-merge
# Conflicts:
#	README.md
#	app/Http/Controllers/AccessoriesController.php
#	app/Http/Controllers/Api/AssetsController.php
#	app/Http/Controllers/Api/LicensesController.php
#	app/Http/Controllers/Api/LocationsController.php
#	app/Http/Controllers/Api/SettingsController.php
#	app/Http/Controllers/Api/UsersController.php
#	app/Http/Controllers/AssetModelsController.php
#	app/Http/Controllers/Assets/AssetsController.php
#	app/Http/Controllers/Auth/ForgotPasswordController.php
#	app/Http/Controllers/CategoriesController.php
#	app/Http/Controllers/CompaniesController.php
#	app/Http/Controllers/ComponentsController.php
#	app/Http/Controllers/ConsumablesController.php
#	app/Http/Controllers/CustomFieldsetsController.php
#	app/Http/Controllers/DepartmentsController.php
#	app/Http/Controllers/LicensesController.php
#	app/Http/Controllers/LocationsController.php
#	app/Http/Controllers/ManufacturersController.php
#	app/Http/Controllers/SettingsController.php
#	app/Http/Controllers/SuppliersController.php
#	app/Http/Controllers/UsersController.php
#	app/Http/Requests/AssetRequest.php
#	app/Http/Requests/ImageUploadRequest.php
#	app/Models/LicenseSeat.php
#	app/Models/Location.php
#	app/Models/Setting.php
#	composer.json
#	composer.lock
#	config/database.php
#	config/version.php
#	npm-shrinkwrap.json
#	package.json
#	public/css/AdminLTE.css
#	public/css/AdminLTE.css.map
#	public/css/overrides.css
#	public/css/overrides.css.map
#	public/css/skins/skin-blue-light.css
#	public/css/skins/skin-blue.css
#	public/css/skins/skin-green-dark.min.css
#	public/js/app.js
#	public/js/bootstrap-table.js
#	public/js/bootstrap/js/bootstrap.js
#	public/js/bootstrap/js/bootstrap.min.js
#	public/js/build/all.js
#	public/js/build/vue.js
#	public/js/build/vue.js.map
#	public/js/demo.js
#	public/js/ekko-lightbox.js
#	public/js/ekko-lightbox.min.js
#	public/js/extensions/export/bootstrap-table-export.js
#	public/js/extensions/multiple-sort/bootstrap-table-multiple-sort.js
#	public/js/extensions/multiple-sort/bootstrap-table-multiple-sort.min.js
#	public/js/extensions/toolbar/bootstrap-table-toolbar.min.js
#	public/js/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.js
#	public/js/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js
#	public/js/plugins/timepicker/bootstrap-timepicker.js
#	public/js/plugins/timepicker/bootstrap-timepicker.min.js
#	public/js/vue.js
#	public/mix-manifest.json
#	resources/assets/js/bootstrap-js.js
#	resources/assets/js/bootstrap.min.js
#	resources/assets/js/ekko-lightbox.js
#	resources/assets/js/ekko-lightbox.min.js
#	resources/assets/js/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.js
#	resources/assets/js/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js
#	resources/assets/js/plugins/chartjs/Chart.js
#	resources/assets/js/plugins/timepicker/bootstrap-timepicker.js
#	resources/assets/js/plugins/timepicker/bootstrap-timepicker.min.js
#	resources/assets/less/AdminLTE.less
#	resources/assets/less/overrides.less
#	resources/assets/less/skins/_all-skins.less
#	resources/assets/less/skins/skin-black.less
#	resources/assets/less/skins/skin-blue.less
#	resources/assets/less/skins/skin-green.less
#	resources/assets/less/skins/skin-purple.less
#	resources/assets/less/skins/skin-red.less
#	resources/assets/less/skins/skin-yellow.less
#	resources/assets/less/variables.less
#	resources/js/components/importer/importer-file.vue
#	resources/lang/en/auth/message.php
#	resources/lang/en/passwords.php
#	resources/lang/es-CO/general.php
#	resources/lang/es-ES/general.php
#	resources/lang/es-VE/general.php
#	resources/less/skins/skin-black-dark.less
#	resources/less/skins/skin-blue-dark.less
#	resources/less/skins/skin-contrast.less
#	resources/less/skins/skin-green-dark.less
#	resources/less/skins/skin-orange-dark.less
#	resources/less/skins/skin-orange.less
#	resources/less/skins/skin-purple-dark.less
#	resources/less/skins/skin-red-dark.less
#	resources/less/skins/skin-yellow-dark.less
#	resources/views/accessories/checkin.blade.php
#	resources/views/accessories/checkout.blade.php
#	resources/views/accessories/edit.blade.php
#	resources/views/account/profile.blade.php
#	resources/views/account/view-assets.blade.php
#	resources/views/asset_maintenances/edit.blade.php
#	resources/views/auth/passwords/email.blade.php
#	resources/views/auth/passwords/reset.blade.php
#	resources/views/categories/edit.blade.php
#	resources/views/companies/edit.blade.php
#	resources/views/components/checkin.blade.php
#	resources/views/components/checkout.blade.php
#	resources/views/components/edit.blade.php
#	resources/views/consumables/checkout.blade.php
#	resources/views/consumables/edit.blade.php
#	resources/views/custom_fields/fields/edit.blade.php
#	resources/views/custom_fields/fieldsets/edit.blade.php
#	resources/views/dashboard.blade.php
#	resources/views/departments/edit.blade.php
#	resources/views/groups/edit.blade.php
#	resources/views/hardware/audit.blade.php
#	resources/views/hardware/bulk-checkout.blade.php
#	resources/views/hardware/bulk.blade.php
#	resources/views/hardware/checkin.blade.php
#	resources/views/hardware/checkout.blade.php
#	resources/views/hardware/edit.blade.php
#	resources/views/hardware/index.blade.php
#	resources/views/hardware/quickscan.blade.php
#	resources/views/hardware/view.blade.php
#	resources/views/importer/import.blade.php
#	resources/views/layouts/basic.blade.php
#	resources/views/layouts/default.blade.php
#	resources/views/layouts/edit-form.blade.php
#	resources/views/licenses/checkin.blade.php
#	resources/views/licenses/checkout.blade.php
#	resources/views/licenses/edit.blade.php
#	resources/views/locations/edit.blade.php
#	resources/views/manufacturers/edit.blade.php
#	resources/views/modals/upload-file.blade.php
#	resources/views/models/bulk-edit.blade.php
#	resources/views/models/custom_fields_form.blade.php
#	resources/views/models/edit.blade.php
#	resources/views/partials/bootstrap-table.blade.php
#	resources/views/partials/forms/edit/address.blade.php
#	resources/views/partials/forms/edit/asset-select.blade.php
#	resources/views/partials/forms/edit/category-select.blade.php
#	resources/views/partials/forms/edit/category.blade.php
#	resources/views/partials/forms/edit/company-select.blade.php
#	resources/views/partials/forms/edit/company.blade.php
#	resources/views/partials/forms/edit/department-select.blade.php
#	resources/views/partials/forms/edit/depreciation.blade.php
#	resources/views/partials/forms/edit/email.blade.php
#	resources/views/partials/forms/edit/image-upload.blade.php
#	resources/views/partials/forms/edit/item_number.blade.php
#	resources/views/partials/forms/edit/location-profile-select.blade.php
#	resources/views/partials/forms/edit/location-select.blade.php
#	resources/views/partials/forms/edit/location.blade.php
#	resources/views/partials/forms/edit/maintenance_type.blade.php
#	resources/views/partials/forms/edit/manufacturer-select.blade.php
#	resources/views/partials/forms/edit/manufacturer.blade.php
#	resources/views/partials/forms/edit/minimum_quantity.blade.php
#	resources/views/partials/forms/edit/model-select.blade.php
#	resources/views/partials/forms/edit/model_number.blade.php
#	resources/views/partials/forms/edit/name.blade.php
#	resources/views/partials/forms/edit/notes.blade.php
#	resources/views/partials/forms/edit/order_number.blade.php
#	resources/views/partials/forms/edit/phone.blade.php
#	resources/views/partials/forms/edit/purchase_cost.blade.php
#	resources/views/partials/forms/edit/purchase_date.blade.php
#	resources/views/partials/forms/edit/quantity.blade.php
#	resources/views/partials/forms/edit/serial.blade.php
#	resources/views/partials/forms/edit/status.blade.php
#	resources/views/partials/forms/edit/submit.blade.php
#	resources/views/partials/forms/edit/supplier-select.blade.php
#	resources/views/partials/forms/edit/supplier.blade.php
#	resources/views/partials/forms/edit/user-select.blade.php
#	resources/views/reports/custom.blade.php
#	resources/views/settings/alerts.blade.php
#	resources/views/settings/asset_tags.blade.php
#	resources/views/settings/barcodes.blade.php
#	resources/views/settings/branding.blade.php
#	resources/views/settings/general.blade.php
#	resources/views/settings/labels.blade.php
#	resources/views/settings/ldap.blade.php
#	resources/views/settings/localization.blade.php
#	resources/views/settings/security.blade.php
#	resources/views/setup/user.blade.php
#	resources/views/suppliers/edit.blade.php
#	resources/views/users/bulk-edit.blade.php
#	resources/views/users/edit.blade.php
#	resources/views/users/ldap.blade.php
#	resources/views/users/print.blade.php
#	resources/views/users/view.blade.php
#	routes/api.php
#	routes/web/hardware.php
#	webpack.mix.js
2020-04-20 23:20:34 -07:00

511 lines
16 KiB
PHP

<?php
namespace App\Importer;
use App\Models\CustomField;
use App\Models\Department;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use ForceUTF8\Encoding;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use League\Csv\Reader;
abstract class Importer
{
protected $csv;
/**
* Id of User performing import
* @var
*/
protected $user_id;
/**
* Are we updating items in the import
* @var bool
*/
protected $updating;
/**
* Default Map of item fields->csv names
* @var array
*/
private $defaultFieldMap = [
'asset_tag' => 'asset tag',
'activated' => 'activated',
'category' => 'category',
'checkout_class' => 'checkout type', // Supports Location or User for assets. Using checkout_class instead of checkout_type because type exists on asset already.
'checkout_location' => 'checkout location',
'company' => 'company',
'item_name' => 'item name',
'item_number' => "item number",
'image' => 'image',
'expiration_date' => 'expiration date',
'location' => 'location',
'notes' => 'notes',
'license_email' => 'licensed to email',
'license_name' => "licensed to name",
'maintained' => 'maintained',
'manufacturer' => 'manufacturer',
'asset_model' => "model name",
'model_number' => 'model number',
'order_number' => 'order number',
'purchase_cost' => 'purchase cost',
'purchase_date' => 'purchase date',
'purchase_order' => 'purchase order',
'qty' => 'quantity',
'reassignable' => 'reassignable',
'requestable' => 'requestable',
'seats' => 'seats',
'serial_number' => 'serial number',
'status' => 'status',
'supplier' => 'supplier',
'termination_date' => 'termination date',
'warranty_months' => 'warranty',
'full_name' => 'full name',
'email' => 'email',
'username' => 'username',
'jobtitle' => 'job title',
'employee_num' => 'employee number',
'phone_number' => 'phone number',
'first_name' => 'first name',
'last_name' => 'last name',
'department' => 'department',
'manager_first_name' => 'manager first name',
'manager_last_name' => 'manager last name',
];
/**
* Map of item fields->csv names
* @var array
*/
protected $fieldMap = [];
/**
* @var callable
*/
protected $logCallback;
protected $tempPassword;
/**
* @var callable
*/
protected $progressCallback;
/**
* @var null
*/
protected $usernameFormat;
/**
* @var callable
*/
protected $errorCallback;
/**
* ObjectImporter constructor.
* @param string $file
*/
public function __construct($file)
{
$this->fieldMap = $this->defaultFieldMap;
if (! ini_get("auto_detect_line_endings")) {
ini_set("auto_detect_line_endings", '1');
}
// By default the importer passes a url to the file.
// However, for testing we also support passing a string directly
if (is_file($file)) {
$this->csv = Reader::createFromPath($file);
} else {
$this->csv = Reader::createFromString($file);
}
$this->tempPassword = substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, 20);
}
// Cached Values for import lookups
protected $customFields;
/**
* Sets up the database transaction and logging for the importer
*
* @return void
* @author Daniel Meltzer
* @since 5.0
*/
public function import()
{
$headerRow = $this->csv->fetchOne();
$this->csv->setHeaderOffset(0); //explicitly sets the CSV document header record
$results = $this->normalizeInputArray($this->csv->getRecords($headerRow));
$this->populateCustomFields($headerRow);
DB::transaction(function () use (&$results) {
Model::unguard();
$resultsCount = sizeof($results);
foreach ($results as $row) {
$this->handle($row);
if ($this->progressCallback) {
call_user_func($this->progressCallback, $resultsCount);
}
$this->log('------------- Action Summary ----------------');
}
});
}
abstract protected function handle($row);
/**
* Fetch custom fields from database and translate/parse them into a format
* appropriate for use in the importer.
* @return void
* @author Daniel Meltzer
* @since 5.0
*/
protected function populateCustomFields($headerRow)
{
// Stolen From https://adamwathan.me/2016/07/14/customizing-keys-when-mapping-collections/
// This 'inverts' the fields such that we have a collection of fields indexed by name.
$this->customFields = CustomField::All()->reduce(function ($nameLookup, $field) {
$nameLookup[$field['name']] = $field;
return $nameLookup;
});
// Remove any custom fields that do not exist in the header row. This prevents nulling out values that shouldn't exist.
// In detail, we compare the lower case name of custom fields (indexed by name) to the keys in the header row. This
// results in an array with only custom fields that are in the file.
if ($this->customFields) {
$this->customFields = array_intersect_key(
array_change_key_case($this->customFields),
array_change_key_case(array_flip($headerRow))
);
}
}
/**
* Check to see if the given key exists in the array, and trim excess white space before returning it
*
* @author Daniel Melzter
* @since 3.0
* @param $array array
* @param $key string
* @param $default string
* @return string
*/
public function findCsvMatch(array $array, $key, $default = null)
{
$val = $default;
$key = $this->lookupCustomKey($key);
// $this->log("Custom Key: ${key}");
if (array_key_exists($key, $array)) {
$val = Encoding::toUTF8(trim($array[ $key ]));
}
// $this->log("${key}: ${val}");
return $val;
}
/**
* Looks up A custom key in the custom field map
*
* @author Daniel Melzter
* @since 4.0
* @param $key string
* @return string|null
*/
public function lookupCustomKey($key)
{
if (array_key_exists($key, $this->fieldMap)) {
return $this->fieldMap[$key];
}
// Otherwise no custom key, return original.
return $key;
}
/**
* Used to lowercase header values to ensure we're comparing values properly.
*
* @param $results
* @return array
*/
public function normalizeInputArray($results)
{
$newArray = [];
foreach ($results as $index => $arrayToNormalize) {
$newArray[$index] = array_change_key_case($arrayToNormalize);
}
return $newArray;
}
/**
* Figure out the fieldname of the custom field
*
* @author A. Gianotto <snipe@snipe.net>
* @since 3.0
* @param $array array
* @return string
*/
public function array_smart_custom_field_fetch(array $array, $key)
{
$index_name = strtolower($key->name);
return array_key_exists($index_name, $array) ? trim($array[$index_name]) : false;
}
protected function log($string)
{
if ($this->logCallback) {
call_user_func($this->logCallback, $string);
}
}
protected function logError($item, $field)
{
if ($this->errorCallback) {
call_user_func($this->errorCallback, $item, $field, $item->getErrors());
}
}
/**
* Finds the user matching given data, or creates a new one if there is no match.
* This is NOT used by the User Import, only for Asset/Accessory/etc where
* there are users listed and we have to create them and associate them at
* the same time. [ALG]
*
* @author Daniel Melzter
* @since 3.0
* @param $row array
* @return User Model w/ matching name
* @internal param array $user_array User details parsed from csv
*/
protected function createOrFetchUser($row)
{
$user_array = [
'full_name' => $this->findCsvMatch($row, "full_name"),
'email' => $this->findCsvMatch($row, "email"),
'manager_id'=> '',
'department_id' => '',
'username' => $this->findCsvMatch($row, "username"),
'activated' => $this->fetchHumanBoolean($this->findCsvMatch($row, 'activated')),
];
// Maybe we're lucky and the user already exists.
if($user = User::where('username', $user_array['username'])->first()) {
$this->log('User '.$user_array['username'].' already exists');
return $user;
}
// If the full name is empty, bail out--we need this to extract first name (at the very least)
if(empty($user_array['full_name'])) {
$this->log('Insufficient user data provided (Full name is required)- skipping user creation, just adding asset');
return false;
}
// Is the user actually an ID?
if($user = $this->findUserByNumber($user_array['full_name'])) {
return $user;
}
$this->log('User does not appear to be an id with number: '.$user_array['full_name'].'. Continuing through our processes');
// Populate email if it does not exist.
if(empty($user_array['email'])) {
$user_array['email'] = User::generateEmailFromFullName($user_array['full_name']);
}
$user_formatted_array = User::generateFormattedNameFromFullName($user_array['full_name'], Setting::getSettings()->username_format);
$user_array['first_name'] = $user_formatted_array['first_name'];
$user_array['last_name'] = $user_formatted_array['last_name'];
if (empty($user_array['username'])) {
$user_array['username'] = $user_formatted_array['username'];
if ($this->usernameFormat =='email') {
$user_array['username'] = $user_array['email'];
}
}
// Does this ever actually fire??
// Check for a matching user after trying to guess username.
if ($user = User::where('username', $user_array['username'])->first()) {
$this->log('User '.$user_array['username'].' already exists');
return $user;
}
// If at this point we have not found a username or first name, bail out in shame.
if(empty($user_array['username']) || empty($user_array['first_name'])) {
return false;
}
// No Luck, let's create one.
$user = new User;
$user->first_name = $user_array['first_name'];
$user->last_name = $user_array['last_name'];
$user->username = $user_array['username'];
$user->email = $user_array['email'];
$user->manager_id = $user_array['manager_id'] ?? null;
$user->department_id = $user_array['department_id'] ?? null;
$user->activated = 1;
$user->password = $this->tempPassword;
\Log::debug('Creating a user with the following attributes: '.print_r($user_array, true));
if ($user->save()) {
$this->log('User '.$user_array['username'].' created');
return $user;
}
$this->logError($user, 'User "' . $user_array['username'] . '" was not able to be created.');
return false;
}
/**
* Matches a user by user_id if user_name provided is a number
* @param string $user_name users full name from csv
* @return User User Matching ID
*/
protected function findUserByNumber($user_name)
{
// A number was given instead of a name
if (is_numeric($user_name)) {
$this->log('User '.$user_name.' is a number - lets see if it matches a user id');
return User::find($user_name);
}
}
/**
* Sets the Id of User performing import.
*
* @param mixed $user_id the user id
*
* @return self
*/
public function setUserId($user_id)
{
$this->user_id = $user_id;
return $this;
}
/**
* Sets the Are we updating items in the import.
*
* @param bool $updating the updating
*
* @return self
*/
public function setUpdating($updating)
{
$this->updating = $updating;
return $this;
}
/**
* Sets whether or not we should notify the user with a welcome email
*
* @param bool $send_welcome the send-welcome flag
*
* @return self
*/
public function setShouldNotify($send_welcome)
{
$this->send_welcome = $send_welcome;
return $this;
}
/**
* Defines mappings of csv fields
*
* @param bool $updating the updating
*
* @return self
*/
public function setFieldMappings($fields)
{
// Some initial sanitization.
$fields = array_map('strtolower', $fields);
$this->fieldMap = array_merge($this->defaultFieldMap, $fields);
// $this->log($this->fieldMap);
return $this;
}
/**
* Sets the callbacks for the import
*
* @param callable $logCallback Function to call when we have data to log
* @param callable $progressCallback Function to call to display progress
* @param callable $errorCallback Function to call when we have errors
*
* @return self
*/
public function setCallbacks(callable $logCallback, callable $progressCallback, callable $errorCallback)
{
$this->logCallback = $logCallback;
$this->progressCallback = $progressCallback;
$this->errorCallback = $errorCallback;
return $this;
}
/**
* Sets the value of usernameFormat.
*
* @param string $usernameFormat the username format
*
* @return self
*/
public function setUsernameFormat($usernameFormat)
{
$this->usernameFormat = $usernameFormat;
return $this;
}
public function fetchHumanBoolean($value)
{
return (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
/**
* Fetch an existing department, or create new if it doesn't exist
*
* @author A. Gianotto
* @since 4.6.5
* @param $user_department string
* @return int id of company created/found
*/
public function createOrFetchDepartment($user_department_name)
{
if ($user_department_name!='') {
$department = Department::where('name', '=', $user_department_name)->first();
if ($department) {
$this->log('A matching Department ' . $user_department_name . ' already exists');
return $department->id;
}
$department = new Department();
$department->name = $user_department_name;
if ($department->save()) {
$this->log('Department ' . $user_department_name . ' was created');
return $department->id;
}
$this->logError($department, 'Department');
}
return null;
}
/**
* Fetch an existing manager
*
* @author A. Gianotto
* @since 4.6.5
* @param $user_manager string
* @return int id of company created/found
*/
public function fetchManager($user_manager_first_name, $user_manager_last_name)
{
$manager = User::where('first_name', '=', $user_manager_first_name)
->where('last_name', '=', $user_manager_last_name)->first();
if ($manager) {
$this->log('A matching Manager ' . $user_manager_first_name . ' '. $user_manager_last_name . ' already exists');
return $manager->id;
}
$this->log('No matching Manager ' . $user_manager_first_name . ' '. $user_manager_last_name . ' found. If their user account is being created through this import, you should re-process this file again. ');
return null;
}
}