mirror of https://github.com/itflow-org/itflow
commit
b11730303e
21
CHANGELOG.md
21
CHANGELOG.md
|
|
@ -2,6 +2,27 @@
|
|||
|
||||
This file documents all notable changes made to ITFlow.
|
||||
|
||||
## [25.09.2]
|
||||
|
||||
### Fixes
|
||||
- Fix Payment Method Select box in Revenue.
|
||||
- Remove Extra Feeback Wording When Invoice Sends.
|
||||
- Updated all CSV exports to use escape parameters.
|
||||
- Fix Missing First row on Asset interface export.
|
||||
- Fix Edit User not working due to incorrect modal footer path.
|
||||
- Fix Add Certificate breaking due spelling on function.
|
||||
- Update all CSV Exports to include company name or client name depending on when its being exported from.
|
||||
- Introduced new function sanitize_filename and implmented it in all exports.
|
||||
- Spruced up UI/UX Saved Paymented section in Client Portal.
|
||||
- Fix add Payment Link in client portal recurring invoice section.
|
||||
- Better Logic handling for default page redirect.
|
||||
|
||||
### Features
|
||||
- Introduced new Beta mail parser cron using webklex imap library instead of php-imap as this is deprecated --Not Enabled on existing installs, only new installs.
|
||||
- Introduced Beta support for OAUTH2 Authentication for Microsoft 365 and Google Workspaces for both incoming ticket parsing and outgoing email but must use new mail parser and mail queue for this to work, and requires changing the cron jobs: scripts/cron_mail_queue.php to cron/mail_queue.php and scripts/cron_ticket_email_parser.php to cron/ticket_email_parser.php.
|
||||
|
||||
---
|
||||
|
||||
## [25.09.1]
|
||||
|
||||
### Fixes
|
||||
|
|
|
|||
|
|
@ -3968,11 +3968,35 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
|
|||
|
||||
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.2'");
|
||||
}
|
||||
|
||||
if (CURRENT_DATABASE_VERSION == '2.3.2') {
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE settings
|
||||
ADD `config_imap_provider` ENUM('standard_imap','google_oauth','microsoft_oauth') NULL DEFAULT NULL AFTER `config_mail_from_name`,
|
||||
ADD `config_mail_oauth_client_id` VARCHAR(255) NULL AFTER `config_imap_provider`,
|
||||
ADD `config_mail_oauth_client_secret` VARCHAR(255) NULL AFTER `config_mail_oauth_client_id`,
|
||||
ADD `config_mail_oauth_tenant_id` VARCHAR(255) NULL AFTER `config_mail_oauth_client_secret`,
|
||||
ADD `config_mail_oauth_refresh_token` TEXT NULL AFTER `config_mail_oauth_tenant_id`,
|
||||
ADD `config_mail_oauth_access_token` TEXT NULL AFTER `config_mail_oauth_refresh_token`,
|
||||
ADD `config_mail_oauth_access_token_expires_at` DATETIME NULL AFTER `config_mail_oauth_access_token`
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.3'");
|
||||
}
|
||||
|
||||
if (CURRENT_DATABASE_VERSION == '2.3.3') {
|
||||
|
||||
mysqli_query($mysqli, "ALTER TABLE settings
|
||||
ADD `config_smtp_provider` ENUM('standard_smtp','google_oauth','microsoft_oauth') NULL DEFAULT NULL AFTER `config_start_page`
|
||||
");
|
||||
|
||||
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.4'");
|
||||
}
|
||||
|
||||
// if (CURRENT_DATABASE_VERSION == '2.3.2') {
|
||||
// // Insert queries here required to update to DB version 2.3.3
|
||||
// if (CURRENT_DATABASE_VERSION == '2.3.4') {
|
||||
// // Insert queries here required to update to DB version 2.3.4
|
||||
// // Then, update the database to the next sequential version
|
||||
// mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.3'");
|
||||
// mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.5'");
|
||||
// }
|
||||
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- Main Sidebar Container -->
|
||||
<aside class="main-sidebar sidebar-dark-<?php echo nullable_htmlentities($config_theme); ?> d-print-none">
|
||||
<a class="brand-link pb-1 mt-1" href="../user/dashboard.php">
|
||||
<a class="brand-link pb-1 mt-1" href="../user/<?php echo $config_start_page ?>">
|
||||
<p class="h6">
|
||||
<i class="nav-icon fas fa-arrow-left ml-3 mr-2"></i>
|
||||
<span class="brand-text">
|
||||
|
|
|
|||
|
|
@ -1,204 +0,0 @@
|
|||
<?php
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$user_id = intval($_GET['id']);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT * FROM users
|
||||
LEFT JOIN user_settings ON users.user_id = user_settings.user_id
|
||||
WHERE users.user_id = $user_id LIMIT 1"
|
||||
);
|
||||
|
||||
$row = mysqli_fetch_array($sql);
|
||||
$user_name = nullable_htmlentities($row['user_name']);
|
||||
$user_email = nullable_htmlentities($row['user_email']);
|
||||
$user_avatar = nullable_htmlentities($row['user_avatar']);
|
||||
$user_token = nullable_htmlentities($row['user_token']);
|
||||
$user_config_force_mfa = intval($row['user_config_force_mfa']);
|
||||
$user_role_id = intval($row['user_role_id']);
|
||||
$user_initials = nullable_htmlentities(initials($user_name));
|
||||
|
||||
// Get User Client Access Permissions
|
||||
$user_client_access_sql = mysqli_query($mysqli,"SELECT client_id FROM user_client_permissions WHERE user_id = $user_id");
|
||||
$client_access_array = [];
|
||||
while ($row = mysqli_fetch_assoc($user_client_access_sql)) {
|
||||
$client_access_array[] = intval($row['client_id']);
|
||||
}
|
||||
|
||||
// Generate the HTML form content using output buffering.
|
||||
ob_start();
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-user-edit mr-2"></i>Editing user:
|
||||
<strong><?php echo $user_name; ?></strong></h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="user_id" value="<?php echo $user_id; ?>">
|
||||
<div class="modal-body">
|
||||
|
||||
<ul class="nav nav-pills nav-justified mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="pill" href="#pills-user-details<?php echo $user_id; ?>">Details</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="pill" href="#pills-user-access<?php echo $user_id; ?>">Restrict Access</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
<div class="tab-pane fade show active" id="pills-user-details<?php echo $user_id; ?>">
|
||||
|
||||
<center class="mb-3">
|
||||
<?php if (!empty($user_avatar)) { ?>
|
||||
<img class="img-fluid" src="<?php echo "uploads/users/$user_id/$user_avatar"; ?>">
|
||||
<?php } else { ?>
|
||||
<span class="fa-stack fa-4x">
|
||||
<i class="fa fa-circle fa-stack-2x text-secondary"></i>
|
||||
<span class="fa fa-stack-1x text-white"><?php echo $user_initials; ?></span>
|
||||
</span>
|
||||
<?php } ?>
|
||||
</center>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="name" placeholder="Full Name" maxlength="200"
|
||||
value="<?php echo $user_name; ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Email <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-envelope"></i></span>
|
||||
</div>
|
||||
<input type="email" class="form-control" name="email" placeholder="Email Address" maxlength="200"
|
||||
value="<?php echo $user_email; ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>New Password</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
|
||||
</div>
|
||||
<input type="password" class="form-control" data-toggle="password" name="new_password"
|
||||
placeholder="Leave Blank For No Password Change" autocomplete="new-password">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Role <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user-shield"></i></span>
|
||||
</div>
|
||||
<select class="form-control select2" name="role" required>
|
||||
<?php
|
||||
$sql_user_roles = mysqli_query($mysqli, "SELECT * FROM user_roles WHERE role_archived_at IS NULL");
|
||||
while ($row = mysqli_fetch_array($sql_user_roles)) {
|
||||
$role_id = intval($row['role_id']);
|
||||
$role_name = nullable_htmlentities($row['role_name']);
|
||||
|
||||
?>
|
||||
<option <?php if ($role_id == $user_role_id) {echo "selected";} ?> value="<?php echo $role_id; ?>"><?php echo $role_name; ?></option>
|
||||
<?php } ?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Avatar</label>
|
||||
<input type="file" class="form-control-file" accept="image/*" name="file">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input class="custom-control-input" type="checkbox" id="forceMFACheckBox<?php echo $user_id; ?>" name="force_mfa" value="1" <?php if($user_config_force_mfa == 1){ echo "checked"; } ?>>
|
||||
<label for="forceMFACheckBox<?php echo $user_id; ?>" class="custom-control-label">
|
||||
Force MFA
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($user_token)) { ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label>2FA</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-id-card"></i></span>
|
||||
</div>
|
||||
<select class="form-control" name="2fa">
|
||||
<option value="">Keep enabled</option>
|
||||
<option value="disable">Disable</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-user-access<?php echo $user_id; ?>">
|
||||
|
||||
<div class="alert alert-info">
|
||||
Check boxes to authorize user client access. No boxes grant full client access. Admin users are unaffected.
|
||||
</div>
|
||||
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item bg-dark">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" onclick="this.closest('.tab-pane').querySelectorAll('.client-checkbox').forEach(checkbox => checkbox.checked = this.checked);">
|
||||
<label class="form-check-label ml-3"><strong>Restrict Access to Clients</strong></label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<?php
|
||||
|
||||
$sql_client_select = mysqli_query($mysqli, "SELECT * FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_array($sql_client_select)) {
|
||||
$client_id_select = intval($row['client_id']);
|
||||
$client_name_select = nullable_htmlentities($row['client_name']);
|
||||
|
||||
?>
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input client-checkbox" name="clients[]" value="<?php echo $client_id_select; ?>" <?php if (in_array($client_id_select, $client_access_array)) { echo "checked"; } ?>>
|
||||
<label class="form-check-label ml-2"><?php echo $client_name_select; ?></label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_user" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fas fa-times mr-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../includes/modal_footer.php';
|
||||
|
|
@ -201,4 +201,4 @@ ob_start();
|
|||
</form>
|
||||
|
||||
<?php
|
||||
require_once "../../../includes/modal_footer_new.php";
|
||||
require_once "../../../includes/modal_footer.php";
|
||||
|
|
|
|||
|
|
@ -3,41 +3,85 @@
|
|||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
if (isset($_POST['edit_mail_smtp_settings'])) {
|
||||
|
||||
|
||||
validateCSRFToken($_POST['csrf_token']);
|
||||
|
||||
$config_smtp_host = sanitizeInput($_POST['config_smtp_host']);
|
||||
$config_smtp_port = intval($_POST['config_smtp_port']);
|
||||
$config_smtp_encryption = sanitizeInput($_POST['config_smtp_encryption']);
|
||||
$config_smtp_username = sanitizeInput($_POST['config_smtp_username']);
|
||||
$config_smtp_password = sanitizeInput($_POST['config_smtp_password']);
|
||||
$config_smtp_provider = sanitizeInput($_POST['config_smtp_provider'] ?? 'standard_smtp');
|
||||
$config_smtp_host = sanitizeInput($_POST['config_smtp_host']);
|
||||
$config_smtp_port = intval($_POST['config_smtp_port'] ?? 0);
|
||||
$config_smtp_encryption = sanitizeInput($_POST['config_smtp_encryption']);
|
||||
$config_smtp_username = sanitizeInput($_POST['config_smtp_username']);
|
||||
$config_smtp_password = sanitizeInput($_POST['config_smtp_password']);
|
||||
|
||||
mysqli_query($mysqli,"UPDATE settings SET config_smtp_host = '$config_smtp_host', config_smtp_port = $config_smtp_port, config_smtp_encryption = '$config_smtp_encryption', config_smtp_username = '$config_smtp_username', config_smtp_password = '$config_smtp_password' WHERE company_id = 1");
|
||||
// Shared OAuth fields
|
||||
$config_mail_oauth_client_id = sanitizeInput($_POST['config_mail_oauth_client_id']);
|
||||
$config_mail_oauth_client_secret = sanitizeInput($_POST['config_mail_oauth_client_secret']);
|
||||
$config_mail_oauth_tenant_id = sanitizeInput($_POST['config_mail_oauth_tenant_id']);
|
||||
$config_mail_oauth_refresh_token = sanitizeInput($_POST['config_mail_oauth_refresh_token']);
|
||||
$config_mail_oauth_access_token = sanitizeInput($_POST['config_mail_oauth_access_token']);
|
||||
|
||||
logAction("Settings", "Edit", "$session_name edited SMTP mail settings");
|
||||
mysqli_query($mysqli, "
|
||||
UPDATE settings SET
|
||||
config_smtp_provider = " . ($config_smtp_provider === 'none' ? "NULL" : "'$config_smtp_provider'") . ",
|
||||
config_smtp_host = '$config_smtp_host',
|
||||
config_smtp_port = $config_smtp_port,
|
||||
config_smtp_encryption = '$config_smtp_encryption',
|
||||
config_smtp_username = '$config_smtp_username',
|
||||
config_smtp_password = '$config_smtp_password',
|
||||
config_mail_oauth_client_id = '$config_mail_oauth_client_id',
|
||||
config_mail_oauth_client_secret = '$config_mail_oauth_client_secret',
|
||||
config_mail_oauth_tenant_id = '$config_mail_oauth_tenant_id',
|
||||
config_mail_oauth_refresh_token = '$config_mail_oauth_refresh_token',
|
||||
config_mail_oauth_access_token = '$config_mail_oauth_access_token'
|
||||
WHERE company_id = 1
|
||||
");
|
||||
|
||||
logAction("Settings", "Edit", "$session_name edited SMTP settings");
|
||||
|
||||
flash_alert("SMTP Mail Settings updated");
|
||||
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_mail_imap_settings'])) {
|
||||
|
||||
|
||||
validateCSRFToken($_POST['csrf_token']);
|
||||
|
||||
$config_imap_host = sanitizeInput($_POST['config_imap_host']);
|
||||
$config_imap_username = sanitizeInput($_POST['config_imap_username']);
|
||||
$config_imap_password = sanitizeInput($_POST['config_imap_password']);
|
||||
$config_imap_port = intval($_POST['config_imap_port']);
|
||||
$config_imap_encryption = sanitizeInput($_POST['config_imap_encryption']);
|
||||
$config_imap_provider = sanitizeInput($_POST['config_imap_provider'] ?? 'standard_imap');
|
||||
$config_imap_host = sanitizeInput($_POST['config_imap_host']);
|
||||
$config_imap_port = intval($_POST['config_imap_port'] ?? 0);
|
||||
$config_imap_encryption = sanitizeInput($_POST['config_imap_encryption']);
|
||||
$config_imap_username = sanitizeInput($_POST['config_imap_username']);
|
||||
$config_imap_password = sanitizeInput($_POST['config_imap_password']);
|
||||
|
||||
mysqli_query($mysqli,"UPDATE settings SET config_imap_host = '$config_imap_host', config_imap_port = $config_imap_port, config_imap_encryption = '$config_imap_encryption', config_imap_username = '$config_imap_username', config_imap_password = '$config_imap_password' WHERE company_id = 1");
|
||||
// Shared OAuth fields
|
||||
$config_mail_oauth_client_id = sanitizeInput($_POST['config_mail_oauth_client_id']);
|
||||
$config_mail_oauth_client_secret = sanitizeInput($_POST['config_mail_oauth_client_secret']);
|
||||
$config_mail_oauth_tenant_id = sanitizeInput($_POST['config_mail_oauth_tenant_id']);
|
||||
$config_mail_oauth_refresh_token = sanitizeInput($_POST['config_mail_oauth_refresh_token']);
|
||||
$config_mail_oauth_access_token = sanitizeInput($_POST['config_mail_oauth_access_token']);
|
||||
|
||||
logAction("Settings", "Edit", "$session_name edited IMAP mail settings");
|
||||
mysqli_query($mysqli, "
|
||||
UPDATE settings SET
|
||||
config_imap_provider = " . ($config_imap_provider === 'none' ? "NULL" : "'$config_imap_provider'") . ",
|
||||
config_imap_host = '$config_imap_host',
|
||||
config_imap_port = $config_imap_port,
|
||||
config_imap_encryption = '$config_imap_encryption',
|
||||
config_imap_username = '$config_imap_username',
|
||||
config_imap_password = '$config_imap_password',
|
||||
config_mail_oauth_client_id = '$config_mail_oauth_client_id',
|
||||
config_mail_oauth_client_secret = '$config_mail_oauth_client_secret',
|
||||
config_mail_oauth_tenant_id = '$config_mail_oauth_tenant_id',
|
||||
config_mail_oauth_refresh_token = '$config_mail_oauth_refresh_token',
|
||||
config_mail_oauth_access_token = '$config_mail_oauth_access_token'
|
||||
WHERE company_id = 1
|
||||
");
|
||||
|
||||
logAction("Settings", "Edit", "$session_name edited IMAP settings");
|
||||
|
||||
flash_alert("IMAP Mail Settings updated");
|
||||
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,6 +266,8 @@ if (isset($_POST['export_users_csv'])) {
|
|||
|
||||
if ($count > 0) {
|
||||
$delimiter = ",";
|
||||
$enclosure = '"';
|
||||
$escape = '\\'; // backslash
|
||||
$filename = "Users-" . date('Y-m-d') . ".csv";
|
||||
|
||||
//create a file pointer
|
||||
|
|
@ -273,7 +275,7 @@ if (isset($_POST['export_users_csv'])) {
|
|||
|
||||
//set column headers
|
||||
$fields = array('Name', 'Email', 'Role', 'Status', 'Creation Date');
|
||||
fputcsv($f, $fields, $delimiter);
|
||||
fputcsv($f, $fields, $delimiter, $enclosure, $escape);
|
||||
|
||||
//output each row of the data, format line as csv and write to file pointer
|
||||
while($row = $sql->fetch_assoc()) {
|
||||
|
|
@ -288,7 +290,7 @@ if (isset($_POST['export_users_csv'])) {
|
|||
}
|
||||
|
||||
$lineData = array($row['user_name'], $row['user_email'], $row['role_name'], $user_status_display, $row['user_created_at']);
|
||||
fputcsv($f, $lineData, $delimiter);
|
||||
fputcsv($f, $lineData, $delimiter, $enclosure, $escape);
|
||||
}
|
||||
|
||||
//move back to beginning of file
|
||||
|
|
|
|||
|
|
@ -10,61 +10,87 @@ require_once "includes/inc_all_admin.php";
|
|||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<!-- SMTP Provider -->
|
||||
<div class="form-group">
|
||||
<label>SMTP Host</label>
|
||||
<label>SMTP Provider</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-server"></i></span>
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cloud"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="config_smtp_host" placeholder="Mail Server Address" value="<?php echo nullable_htmlentities($config_smtp_host); ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>SMTP Port</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
|
||||
</div>
|
||||
<input type="number" min="0" class="form-control" name="config_smtp_port" placeholder="Mail Server Port Number" value="<?php echo intval($config_smtp_port); ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Encryption</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
|
||||
</div>
|
||||
<select class="form-control" name="config_smtp_encryption">
|
||||
<option value=''>None</option>
|
||||
<option <?php if ($config_smtp_encryption == 'tls') { echo "selected"; } ?> value="tls">TLS</option>
|
||||
<option <?php if ($config_smtp_encryption == 'ssl') { echo "selected"; } ?> value="ssl">SSL</option>
|
||||
<select class="form-control" name="config_smtp_provider" id="config_smtp_provider">
|
||||
<option value="none" <?php if(($config_smtp_provider ?? '')==='none' || ($config_smtp_provider ?? '')==='') echo 'selected'; ?>>None (Disabled)</option>
|
||||
<option value="standard_smtp" <?php if(($config_smtp_provider ?? 'standard_smtp')==='standard_smtp') echo 'selected'; ?>>Standard SMTP (Username/Password)</option>
|
||||
<option value="google_oauth" <?php if(($config_smtp_provider ?? '')==='google_oauth') echo 'selected'; ?>>Google Workspace (OAuth)</option>
|
||||
<option value="microsoft_oauth" <?php if(($config_smtp_provider ?? '')==='microsoft_oauth') echo 'selected'; ?>>Microsoft 365 (OAuth)</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-secondary d-block mt-1" id="smtp_provider_hint">
|
||||
Choose your SMTP provider. OAuth options ignore the SMTP password here.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>SMTP Username</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="config_smtp_username" placeholder="Username (Leave blank if no auth is required)" value="<?php echo nullable_htmlentities($config_smtp_username); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>SMTP Password</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||
</div>
|
||||
<input type="password" class="form-control" data-toggle="password" name="config_smtp_password" placeholder="Password (Leave blank if no auth is required)" value="<?php echo nullable_htmlentities($config_smtp_password); ?>" autocomplete="new-password">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
|
||||
<!-- Standard SMTP fields (show only for standard_smtp) -->
|
||||
<div id="smtp_standard_fields">
|
||||
<div class="form-group">
|
||||
<label>SMTP Host</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-server"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="config_smtp_host" placeholder="Mail Server Address" value="<?php echo nullable_htmlentities($config_smtp_host); ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>SMTP Port</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
|
||||
</div>
|
||||
<input type="number" min="0" class="form-control" name="config_smtp_port" placeholder="Mail Server Port Number" value="<?php echo intval($config_smtp_port); ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Encryption</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
|
||||
</div>
|
||||
<select class="form-control" name="config_smtp_encryption">
|
||||
<option value=''>None</option>
|
||||
<option <?php if ($config_smtp_encryption == 'tls') { echo "selected"; } ?> value="tls">TLS</option>
|
||||
<option <?php if ($config_smtp_encryption == 'ssl') { echo "selected"; } ?> value="ssl">SSL</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>SMTP Username</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="config_smtp_username" placeholder="Username (Leave blank if no auth is required)" value="<?php echo nullable_htmlentities($config_smtp_username); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="smtp_password_group">
|
||||
<div class="form-group">
|
||||
<label>SMTP Password</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||
</div>
|
||||
<input type="password" class="form-control" data-toggle="password" name="config_smtp_password" placeholder="Password (Leave blank if no auth is required)" value="<?php echo nullable_htmlentities($config_smtp_password); ?>" autocomplete="new-password">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
|
@ -84,37 +110,56 @@ require_once "includes/inc_all_admin.php";
|
|||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label>IMAP Host</label>
|
||||
<label>IMAP Provider</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-server"></i></span>
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cloud"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="config_imap_host" placeholder="Incoming Mail Server Address (for email to ticket parsing)" value="<?php echo nullable_htmlentities($config_imap_host); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>IMAP Port</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
|
||||
</div>
|
||||
<input type="number" min="0" class="form-control" name="config_imap_port" placeholder="Incoming Mail Server Port Number (993)" value="<?php echo intval($config_imap_port); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>IMAP Encryption</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
|
||||
</div>
|
||||
<select class="form-control" name="config_imap_encryption">
|
||||
<option value=''>None</option>
|
||||
<option <?php if ($config_imap_encryption == 'tls') { echo "selected"; } ?> value="tls">TLS</option>
|
||||
<option <?php if ($config_imap_encryption == 'ssl') { echo "selected"; } ?> value="ssl">SSL</option>
|
||||
<select class="form-control" name="config_imap_provider" id="config_imap_provider">
|
||||
<option value="none" <?php if($config_imap_provider ==='') echo 'selected'; ?>>None (Disabled)</option>
|
||||
<option value="standard_imap" <?php if(($config_imap_provider ?? 'standard_imap')==='standard_imap') echo 'selected'; ?>>Standard IMAP (Username/Password)</option>
|
||||
<option value="google_oauth" <?php if(($config_imap_provider ?? '')==='google_oauth') echo 'selected'; ?>>Google Workspace (OAuth)</option>
|
||||
<option value="microsoft_oauth" <?php if(($config_imap_provider ?? '')==='microsoft_oauth') echo 'selected'; ?>>Microsoft 365 (OAuth)</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-secondary d-block mt-1" id="imap_provider_hint">
|
||||
Select your mailbox provider. OAuth options ignore the IMAP password here.
|
||||
</small>
|
||||
</div>
|
||||
<div id="standard_fields" style="display:none;">
|
||||
<div class="form-group">
|
||||
<label>IMAP Host</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-server"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" name="config_imap_host" placeholder="Incoming Mail Server Address (for email to ticket parsing)" value="<?php echo nullable_htmlentities($config_imap_host); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>IMAP Port</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
|
||||
</div>
|
||||
<input type="number" min="0" class="form-control" name="config_imap_port" placeholder="Incoming Mail Server Port Number (993)" value="<?php echo intval($config_imap_port); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>IMAP Encryption</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
|
||||
</div>
|
||||
<select class="form-control" name="config_imap_encryption">
|
||||
<option value=''>None</option>
|
||||
<option <?php if ($config_imap_encryption == 'tls') { echo "selected"; } ?> value="tls">TLS</option>
|
||||
<option <?php if ($config_imap_encryption == 'ssl') { echo "selected"; } ?> value="ssl">SSL</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
|
|
@ -123,25 +168,81 @@ require_once "includes/inc_all_admin.php";
|
|||
<div class='input-group-prepend'>
|
||||
<span class='input-group-text'><i class='fa fa-fw fa-user'></i></span>
|
||||
</div>
|
||||
<input type='text' class='form-control' name='config_imap_username' placeholder='Username' value="<?php
|
||||
echo nullable_htmlentities($config_imap_username); ?>" required>
|
||||
<input type='text' class='form-control' name='config_imap_username' placeholder='Username (email address)' value="<?php echo nullable_htmlentities($config_imap_username); ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<div class='form-group' id="imap_password_group">
|
||||
<label>IMAP Password</label>
|
||||
<div class='input-group'>
|
||||
<div class='input-group-prepend'>
|
||||
<span class='input-group-text'><i class='fa fa-fw fa-key'></i></span>
|
||||
</div>
|
||||
<input type='password' class='form-control' data-toggle='password' name='config_imap_password' placeholder='Password' value="<?php
|
||||
echo nullable_htmlentities($config_imap_password); ?>" autocomplete='new-password' required>
|
||||
<input type='password' class='form-control' data-toggle='password' name='config_imap_password' placeholder='Password (not used for OAuth)' value="<?php echo nullable_htmlentities($config_imap_password); ?>" autocomplete='new-password'>
|
||||
<div class='input-group-append'>
|
||||
<span class='input-group-text'><i class='fa fa-fw fa-eye'></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OAuth shared fields (show for google_oauth / microsoft_oauth) -->
|
||||
<div id="smtp_oauth_fields" style="display:none;">
|
||||
<hr>
|
||||
<h5 class="mb-2">OAuth Settings (shared for IMAP & SMTP)</h5>
|
||||
<p class="text-secondary" id="oauth_hint">
|
||||
Configure OAuth credentials for the selected provider.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>OAuth Client ID</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text"><i class="fa fa-fw fa-id-badge"></i></span></div>
|
||||
<input type="text" class="form-control" name="config_mail_oauth_client_id"
|
||||
value="<?php echo nullable_htmlentities($config_mail_oauth_client_id ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>OAuth Client Secret</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text"><i class="fa fa-fw fa-key"></i></span></div>
|
||||
<input type="password" class="form-control" data-toggle="password" name="config_mail_oauth_client_secret"
|
||||
value="<?php echo nullable_htmlentities($config_mail_oauth_client_secret ?? ''); ?>" autocomplete="new-password">
|
||||
<div class="input-group-append"><span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="tenant_row" style="display:none;">
|
||||
<label>Tenant ID (Microsoft 365 only)</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text"><i class="fa fa-fw fa-building"></i></span></div>
|
||||
<input type="text" class="form-control" name="config_mail_oauth_tenant_id"
|
||||
value="<?php echo nullable_htmlentities($config_mail_oauth_tenant_id ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Refresh Token</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text"><i class="fa fa-fw fa-sync-alt"></i></span></div>
|
||||
<textarea class="form-control" name="config_mail_oauth_refresh_token" rows="2"
|
||||
placeholder="Paste refresh token"><?php echo nullable_htmlentities($config_mail_oauth_refresh_token ?? ''); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Access Token (optional – will refresh if expired)</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text"><i class="fa fa-fw fa-shield-alt"></i></span></div>
|
||||
<textarea class="form-control" name="config_mail_oauth_access_token" rows="2"
|
||||
placeholder="Can be left blank; system refreshes using the refresh token"><?php echo nullable_htmlentities($config_mail_oauth_access_token ?? ''); ?></textarea>
|
||||
</div>
|
||||
<small class="text-secondary">
|
||||
Expires at: <?php echo !empty($config_mail_oauth_access_token_expires_at) ? htmlspecialchars($config_mail_oauth_access_token_expires_at) : 'n/a'; ?>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<button type="submit" name="edit_mail_imap_settings" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save</button>
|
||||
|
|
@ -327,5 +428,68 @@ require_once "includes/inc_all_admin.php";
|
|||
|
||||
<?php } ?>
|
||||
|
||||
<?php require_once "../includes/footer.php";
|
||||
<script>
|
||||
(function(){
|
||||
function setDisabled(container, disabled){
|
||||
if(!container) return;
|
||||
container.querySelectorAll('input, select, textarea').forEach(el => el.disabled = !!disabled);
|
||||
}
|
||||
|
||||
function wireProvider(selectId, standardWrapId, passwordGroupId, oauthWrapId, tenantRowId, hintId, oauthHintId){
|
||||
const sel = document.getElementById(selectId);
|
||||
const std = document.getElementById(standardWrapId);
|
||||
const pwd = document.getElementById(passwordGroupId);
|
||||
const oauth = document.getElementById(oauthWrapId);
|
||||
const ten = document.getElementById(tenantRowId);
|
||||
const hint = document.getElementById(hintId);
|
||||
const ohint = document.getElementById(oauthHintId);
|
||||
|
||||
function toggle(){
|
||||
const v = (sel && sel.value) || '';
|
||||
const isNone = (v === 'none' || v === '');
|
||||
const isStd = v === 'standard_smtp' || v === 'standard_imap';
|
||||
const isG = v === 'google_oauth';
|
||||
const isM = v === 'microsoft_oauth';
|
||||
const isOAuth = isG || isM;
|
||||
|
||||
if (std) std.style.display = isStd ? '' : 'none';
|
||||
if (pwd) pwd.style.display = isStd ? '' : 'none';
|
||||
if (oauth) oauth.style.display = isOAuth ? '' : 'none';
|
||||
if (ten) ten.style.display = isM ? '' : 'none';
|
||||
|
||||
setDisabled(std, !isStd);
|
||||
setDisabled(pwd, !isStd);
|
||||
setDisabled(oauth, !isOAuth);
|
||||
|
||||
if (hint) {
|
||||
hint.textContent = isNone
|
||||
? 'Disabled.'
|
||||
: isStd
|
||||
? 'Standard: provide host, port, encryption, username & password.'
|
||||
: isG
|
||||
? 'Google OAuth: set Client ID/Secret; paste a refresh token; username should be the mailbox email.'
|
||||
: 'Microsoft 365 OAuth: set Client ID/Secret/Tenant; paste a refresh token; username should be the mailbox email.';
|
||||
}
|
||||
if (ohint) {
|
||||
ohint.textContent = isG
|
||||
? 'Google Workspace OAuth: Client ID/Secret from Google Cloud; Refresh token via consent.'
|
||||
: isM
|
||||
? 'Microsoft 365 OAuth: Client ID/Secret/Tenant from Entra ID; Refresh token via consent.'
|
||||
: 'Configure OAuth credentials for the selected provider.';
|
||||
}
|
||||
}
|
||||
|
||||
if (sel) { sel.addEventListener('change', toggle); toggle(); }
|
||||
}
|
||||
|
||||
// IMAP (you already have these IDs in your page)
|
||||
wireProvider('config_imap_provider', 'standard_fields', 'imap_password_group',
|
||||
'oauth_fields', 'tenant_row', 'imap_provider_hint', 'oauth_hint');
|
||||
|
||||
// SMTP (the IDs we just added)
|
||||
wireProvider('config_smtp_provider', 'smtp_standard_fields', 'smtp_password_group',
|
||||
'smtp_oauth_fields', 'smtp_tenant_row', 'smtp_provider_hint', 'smtp_oauth_hint');
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php require_once "../includes/footer.php";
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ $payment_provider_threshold = floatval($row['payment_provider_threshold']);
|
|||
</select>
|
||||
</form>
|
||||
<?php } else { ?>
|
||||
<a href="saved_payment_method.php">Add a Payment Method</a>
|
||||
<a href="saved_payment_methods.php">Add a Payment Method</a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<?php } ?>
|
||||
|
|
|
|||
|
|
@ -64,28 +64,17 @@ if (!$stripe_public_key || !$stripe_secret_key) {
|
|||
<div class="col-md-6">
|
||||
|
||||
<?php if (!$stripe_customer_id) { ?>
|
||||
<b>Save card details</b><br>
|
||||
In order to set up automatic payments, you must create a customer record in Stripe.<br>
|
||||
In order to set up automatic payments, you must create a customer record in Stripe.
|
||||
First, you must authorize Stripe to store your card details for the purpose of automatic payment.
|
||||
<br><br>
|
||||
|
||||
<div class="col-5">
|
||||
<form action="post.php" method="POST">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input class="custom-control-input" type="checkbox" id="consent" name="consent" value="1" required>
|
||||
<label for="consent" class="custom-control-label">
|
||||
I grant consent for automatic payments
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="form-control btn-success" name="create_stripe_customer">Create Stripe Customer Record</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form action="post.php" method="POST">
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-success" name="create_stripe_customer"><strong><i class="fas fa-check mr-2"></i>I grant consent for automatic payments</strong></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php } else { ?>
|
||||
|
||||
<b>Manage saved payment methods</b><br><br>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,339 @@
|
|||
<?php
|
||||
// Set working directory to the directory this cron script lives at.
|
||||
chdir(dirname(__FILE__));
|
||||
|
||||
// Ensure we're running from command line
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
die("This script must be run from the command line.\n");
|
||||
}
|
||||
|
||||
require_once "../config.php";
|
||||
require_once "../includes/inc_set_timezone.php";
|
||||
require_once "../functions.php";
|
||||
require_once "../plugins/vendor/autoload.php";
|
||||
|
||||
// PHP Mailer Libs
|
||||
require_once "../plugins/PHPMailer/src/Exception.php";
|
||||
require_once "../plugins/PHPMailer/src/PHPMailer.php";
|
||||
require_once "../plugins/PHPMailer/src/SMTP.php";
|
||||
require_once "../plugins/PHPMailer/src/OAuthTokenProvider.php";
|
||||
require_once "../plugins/PHPMailer/src/OAuth.php";
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
use PHPMailer\PHPMailer\OAuthTokenProvider;
|
||||
|
||||
/** =======================================================================
|
||||
* XOAUTH2 Token Provider for PHPMailer (simple “static” provider)
|
||||
* ======================================================================= */
|
||||
class StaticTokenProvider implements OAuthTokenProvider {
|
||||
private string $email;
|
||||
private string $accessToken;
|
||||
public function __construct(string $email, string $accessToken) {
|
||||
$this->email = $email;
|
||||
$this->accessToken = $accessToken;
|
||||
}
|
||||
public function getOauth64(): string {
|
||||
$auth = "user={$this->email}\x01auth=Bearer {$this->accessToken}\x01\x01";
|
||||
return base64_encode($auth);
|
||||
}
|
||||
}
|
||||
|
||||
/** =======================================================================
|
||||
* Load settings
|
||||
* ======================================================================= */
|
||||
$sql_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1");
|
||||
$row = mysqli_fetch_array($sql_settings);
|
||||
|
||||
$config_enable_cron = intval($row['config_enable_cron']);
|
||||
|
||||
// SMTP baseline
|
||||
$config_smtp_host = $row['config_smtp_host'];
|
||||
$config_smtp_username = $row['config_smtp_username'];
|
||||
$config_smtp_password = $row['config_smtp_password'];
|
||||
$config_smtp_port = intval($row['config_smtp_port']);
|
||||
$config_smtp_encryption = $row['config_smtp_encryption'];
|
||||
|
||||
// SMTP provider + shared OAuth fields
|
||||
$config_smtp_provider = $row['config_smtp_provider'] ?? 'standard_smtp'; // 'standard_smtp' | 'google_oauth' | 'microsoft_oauth'
|
||||
$config_mail_oauth_client_id = $row['config_mail_oauth_client_id'] ?? '';
|
||||
$config_mail_oauth_client_secret = $row['config_mail_oauth_client_secret'] ?? '';
|
||||
$config_mail_oauth_tenant_id = $row['config_mail_oauth_tenant_id'] ?? '';
|
||||
$config_mail_oauth_refresh_token = $row['config_mail_oauth_refresh_token'] ?? '';
|
||||
$config_mail_oauth_access_token = $row['config_mail_oauth_access_token'] ?? '';
|
||||
$config_mail_oauth_access_token_expires_at = $row['config_mail_oauth_access_token_expires_at'] ?? '';
|
||||
|
||||
if ($config_enable_cron == 0) {
|
||||
logApp("Cron-Mail-Queue", "error", "Cron Mail Queue unable to run - cron not enabled in admin settings.");
|
||||
exit("Cron: is not enabled -- Quitting..");
|
||||
}
|
||||
|
||||
/** =======================================================================
|
||||
* Lock file
|
||||
* ======================================================================= */
|
||||
$temp_dir = sys_get_temp_dir();
|
||||
$lock_file_path = "{$temp_dir}/itflow_mail_queue_{$installation_id}.lock";
|
||||
|
||||
if (file_exists($lock_file_path)) {
|
||||
$file_age = time() - filemtime($lock_file_path);
|
||||
if ($file_age > 600) {
|
||||
unlink($lock_file_path);
|
||||
logApp("Cron-Mail-Queue", "warning", "Cron Mail Queue detected a lock file was present but was over 10 minutes old so it removed it.");
|
||||
} else {
|
||||
logApp("Cron-Mail-Queue", "info", "Cron Mail Queue attempted to execute but was already executing so instead it terminated.");
|
||||
exit("Script is already running. Exiting.");
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($lock_file_path, "Locked");
|
||||
|
||||
/** =======================================================================
|
||||
* Mail sender function (defined inside this cron)
|
||||
* - Handles standard SMTP and XOAUTH2 for Google/Microsoft
|
||||
* - Reuses shared OAuth settings
|
||||
* ======================================================================= */
|
||||
function sendQueueEmail(
|
||||
string $provider,
|
||||
string $host,
|
||||
int $port,
|
||||
string $encryption,
|
||||
string $username,
|
||||
string $password,
|
||||
string $from_email,
|
||||
string $from_name,
|
||||
string $to_email,
|
||||
string $to_name,
|
||||
string $subject,
|
||||
string $html_body,
|
||||
string $ics_str,
|
||||
string $oauth_client_id,
|
||||
string $oauth_client_secret,
|
||||
string $oauth_tenant_id,
|
||||
string $oauth_refresh_token,
|
||||
string $oauth_access_token,
|
||||
string $oauth_access_token_expires_at
|
||||
) {
|
||||
// Sensible defaults for OAuth providers if fields were left blank
|
||||
if ($provider === 'google_oauth') {
|
||||
if (!$host) $host = 'smtp.gmail.com';
|
||||
if (!$port) $port = 587;
|
||||
if (!$encryption) $encryption = 'tls';
|
||||
if (!$username) $username = $from_email;
|
||||
} elseif ($provider === 'microsoft_oauth') {
|
||||
if (!$host) $host = 'smtp.office365.com';
|
||||
if (!$port) $port = 587;
|
||||
if (!$encryption) $encryption = 'tls';
|
||||
if (!$username) $username = $from_email;
|
||||
}
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->CharSet = "UTF-8";
|
||||
$mail->SMTPDebug = 0;
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $host;
|
||||
$mail->Port = $port;
|
||||
|
||||
$enc = strtolower($encryption);
|
||||
if ($enc === '' || $enc === 'none') {
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->SMTPSecure = false;
|
||||
$mail->SMTPOptions = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]];
|
||||
} else {
|
||||
$mail->SMTPSecure = $enc; // 'tls' | 'ssl'
|
||||
}
|
||||
|
||||
if ($provider === 'google_oauth' || $provider === 'microsoft_oauth') {
|
||||
// XOAUTH2
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->AuthType = 'XOAUTH2';
|
||||
$mail->Username = $username;
|
||||
|
||||
// Pick/refresh access token
|
||||
$accessToken = trim($oauth_access_token);
|
||||
$needsRefresh = empty($accessToken);
|
||||
if (!$needsRefresh && !empty($oauth_access_token_expires_at)) {
|
||||
$expTs = strtotime($oauth_access_token_expires_at);
|
||||
if ($expTs && $expTs <= time() + 60) $needsRefresh = true;
|
||||
}
|
||||
|
||||
if ($needsRefresh) {
|
||||
if ($provider === 'google_oauth' && function_exists('getGoogleAccessToken')) {
|
||||
$accessToken = getGoogleAccessToken($username);
|
||||
} elseif ($provider === 'microsoft_oauth' && function_exists('getMicrosoftAccessToken')) {
|
||||
$accessToken = getMicrosoftAccessToken($username);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($accessToken)) {
|
||||
throw new Exception("Missing OAuth access token for XOAUTH2 SMTP.");
|
||||
}
|
||||
|
||||
$mail->setOAuth(new StaticTokenProvider($username, $accessToken));
|
||||
} else {
|
||||
// Standard SMTP (with or without auth)
|
||||
$mail->SMTPAuth = !empty($username);
|
||||
$mail->Username = $username ?: '';
|
||||
$mail->Password = $password ?: '';
|
||||
}
|
||||
|
||||
// Recipients & content
|
||||
$mail->setFrom($from_email, $from_name);
|
||||
$mail->addAddress($to_email, $to_name);
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = "<html><head><style>
|
||||
body { font-family: Arial, sans-serif; color: #333; line-height: 1.6; }
|
||||
.email-container { max-width: 600px; margin: auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
|
||||
</style></head><body><div class='email-container'>{$html_body}</div></body></html>";
|
||||
|
||||
if (!empty($ics_str)) {
|
||||
$mail->addStringAttachment($ics_str, 'Scheduled_ticket.ics', 'base64', 'text/calendar');
|
||||
}
|
||||
|
||||
$mail->send();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** =======================================================================
|
||||
* SEND: status = 0 (Queued)
|
||||
* ======================================================================= */
|
||||
$sql_queue = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_status = 0 AND email_queued_at <= NOW()");
|
||||
|
||||
if (mysqli_num_rows($sql_queue) > 0) {
|
||||
while ($rowq = mysqli_fetch_array($sql_queue)) {
|
||||
$email_id = (int)$rowq['email_id'];
|
||||
$email_from = $rowq['email_from'];
|
||||
$email_from_name = $rowq['email_from_name'];
|
||||
$email_recipient = $rowq['email_recipient'];
|
||||
$email_recipient_name = $rowq['email_recipient_name'];
|
||||
$email_subject = $rowq['email_subject'];
|
||||
$email_content = $rowq['email_content'];
|
||||
$email_ics_str = $rowq['email_cal_str'];
|
||||
|
||||
if (!filter_var($email_from, FILTER_VALIDATE_EMAIL)) {
|
||||
$email_from_logging = sanitizeInput($rowq['email_from']);
|
||||
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = 99 WHERE email_id = $email_id");
|
||||
logApp("Cron-Mail-Queue", "Error", "Failed to send email #$email_id due to invalid sender address: $email_from_logging - check configuration in settings.");
|
||||
appNotify("Mail", "Failed to send email #$email_id due to invalid sender address");
|
||||
continue;
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 1 WHERE email_id = $email_id");
|
||||
|
||||
if (!filter_var($email_recipient, FILTER_VALIDATE_EMAIL)) {
|
||||
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = 99 WHERE email_id = $email_id");
|
||||
$email_subject_logging = sanitizeInput($rowq['email_subject']);
|
||||
logApp("Cron-Mail-Queue", "Error", "Failed to send email: $email_id due to invalid recipient address. Email subject was: $email_subject_logging");
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
sendQueueEmail(
|
||||
($config_smtp_provider ?: 'standard_smtp'),
|
||||
$config_smtp_host,
|
||||
(int)$config_smtp_port,
|
||||
(string)$config_smtp_encryption,
|
||||
(string)$config_smtp_username,
|
||||
(string)$config_smtp_password,
|
||||
(string)$email_from,
|
||||
(string)$email_from_name,
|
||||
(string)$email_recipient,
|
||||
(string)$email_recipient_name,
|
||||
(string)$email_subject,
|
||||
(string)$email_content,
|
||||
(string)$email_ics_str,
|
||||
(string)$config_mail_oauth_client_id,
|
||||
(string)$config_mail_oauth_client_secret,
|
||||
(string)$config_mail_oauth_tenant_id,
|
||||
(string)$config_mail_oauth_refresh_token,
|
||||
(string)$config_mail_oauth_access_token,
|
||||
(string)$config_mail_oauth_access_token_expires_at
|
||||
);
|
||||
|
||||
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 3, email_sent_at = NOW(), email_attempts = 1 WHERE email_id = $email_id");
|
||||
|
||||
} catch (Exception $e) {
|
||||
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_failed_at = NOW(), email_attempts = 1 WHERE email_id = $email_id");
|
||||
|
||||
$email_recipient_logging = sanitizeInput($rowq['email_recipient']);
|
||||
$email_subject_logging = sanitizeInput($rowq['email_subject']);
|
||||
$err = substr("Mailer Error: " . $e->getMessage(), 0, 100) . "...";
|
||||
|
||||
appNotify("Cron-Mail-Queue", "Failed to send email #$email_id to $email_recipient_logging");
|
||||
logApp("Cron-Mail-Queue", "Error", "Failed to send email: $email_id to $email_recipient_logging regarding $email_subject_logging. $err");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** =======================================================================
|
||||
* RETRIES: status = 2 (Failed), attempts < 4, wait 30 min
|
||||
* NOTE: Backoff is `email_failed_at <= NOW() - INTERVAL 30 MINUTE`
|
||||
* ======================================================================= */
|
||||
$sql_failed_queue = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT * FROM email_queue
|
||||
WHERE email_status = 2
|
||||
AND email_attempts < 4
|
||||
AND email_failed_at <= NOW() - INTERVAL 30 MINUTE"
|
||||
);
|
||||
|
||||
if (mysqli_num_rows($sql_failed_queue) > 0) {
|
||||
while ($rowf = mysqli_fetch_array($sql_failed_queue)) {
|
||||
$email_id = (int)$rowf['email_id'];
|
||||
$email_from = $rowf['email_from'];
|
||||
$email_from_name = $rowf['email_from_name'];
|
||||
$email_recipient = $rowf['email_recipient'];
|
||||
$email_recipient_name = $rowf['email_recipient_name'];
|
||||
$email_subject = $rowf['email_subject'];
|
||||
$email_content = $rowf['email_content'];
|
||||
$email_ics_str = $rowf['email_cal_str'];
|
||||
$email_attempts = (int)$rowf['email_attempts'] + 1;
|
||||
|
||||
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 1 WHERE email_id = $email_id");
|
||||
|
||||
if (!filter_var($email_recipient, FILTER_VALIDATE_EMAIL)) {
|
||||
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = $email_attempts WHERE email_id = $email_id");
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
sendQueueEmail(
|
||||
($config_smtp_provider ?: 'standard_smtp'),
|
||||
$config_smtp_host,
|
||||
(int)$config_smtp_port,
|
||||
(string)$config_smtp_encryption,
|
||||
(string)$config_smtp_username,
|
||||
(string)$config_smtp_password,
|
||||
(string)$email_from,
|
||||
(string)$email_from_name,
|
||||
(string)$email_recipient,
|
||||
(string)$email_recipient_name,
|
||||
(string)$email_subject,
|
||||
(string)$email_content,
|
||||
(string)$email_ics_str,
|
||||
(string)$config_mail_oauth_client_id,
|
||||
(string)$config_mail_oauth_client_secret,
|
||||
(string)$config_mail_oauth_tenant_id,
|
||||
(string)$config_mail_oauth_refresh_token,
|
||||
(string)$config_mail_oauth_access_token,
|
||||
(string)$config_mail_oauth_access_token_expires_at
|
||||
);
|
||||
|
||||
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 3, email_sent_at = NOW(), email_attempts = $email_attempts WHERE email_id = $email_id");
|
||||
|
||||
} catch (Exception $e) {
|
||||
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_failed_at = NOW(), email_attempts = $email_attempts WHERE email_id = $email_id");
|
||||
|
||||
$email_recipient_logging = sanitizeInput($rowf['email_recipient']);
|
||||
$email_subject_logging = sanitizeInput($rowf['email_subject']);
|
||||
$err = substr("Mailer Error: " . $e->getMessage(), 0, 100) . "...";
|
||||
|
||||
logApp("Cron-Mail-Queue", "Error", "Failed to re-send email #$email_id to $email_recipient_logging regarding $email_subject_logging. $err");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** =======================================================================
|
||||
* Unlock
|
||||
* ======================================================================= */
|
||||
unlink($lock_file_path);
|
||||
|
|
@ -0,0 +1,724 @@
|
|||
<?php
|
||||
/*
|
||||
* CRON - Email Parser (Webklex PHP-IMAP)
|
||||
* Process emails and create/update tickets using Webklex\PHPIMAP instead of native IMAP
|
||||
*/
|
||||
|
||||
// Start the timer
|
||||
$script_start_time = microtime(true);
|
||||
|
||||
// Set working directory to the directory this cron script lives at.
|
||||
chdir(dirname(__FILE__));
|
||||
|
||||
// Ensure we're running from command line
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
die("This script must be run from the command line.\n");
|
||||
}
|
||||
|
||||
// Autoload (Webklex & any composer deps)
|
||||
require_once "../plugins/vendor/autoload.php";
|
||||
|
||||
// Get ITFlow config & helper functions
|
||||
require_once "../config.php";
|
||||
|
||||
// Set Timezone
|
||||
require_once "../includes/inc_set_timezone.php";
|
||||
require_once "../functions.php";
|
||||
|
||||
// Get settings for the "default" company
|
||||
require_once "../includes/load_global_settings.php";
|
||||
|
||||
$config_ticket_prefix = sanitizeInput($config_ticket_prefix);
|
||||
$config_ticket_from_name = sanitizeInput($config_ticket_from_name);
|
||||
$config_ticket_email_parse_unknown_senders = intval($row['config_ticket_email_parse_unknown_senders']);
|
||||
|
||||
// Get company name & phone & timezone
|
||||
$sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1");
|
||||
$row = mysqli_fetch_array($sql);
|
||||
$company_name = sanitizeInput($row['company_name']);
|
||||
$company_phone = sanitizeInput(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
||||
|
||||
// Check setting enabled
|
||||
if ($config_ticket_email_parse == 0) {
|
||||
logApp("Cron-Email-Parser", "error", "Cron Email Parser unable to run - not enabled in admin settings.");
|
||||
exit("Email Parser: Feature is not enabled - check Settings > Ticketing > Email-to-ticket parsing. See https://docs.itflow.org/ticket_email_parse -- Quitting..");
|
||||
}
|
||||
|
||||
// System temp directory & lock
|
||||
$temp_dir = sys_get_temp_dir();
|
||||
$lock_file_path = "{$temp_dir}/itflow_email_parser_{$installation_id}.lock";
|
||||
|
||||
if (file_exists($lock_file_path)) {
|
||||
$file_age = time() - filemtime($lock_file_path);
|
||||
if ($file_age > 300) {
|
||||
unlink($lock_file_path);
|
||||
logApp("Cron-Email-Parser", "warning", "Cron Email Parser detected a lock file was present but was over 5 minutes old so it removed it.");
|
||||
} else {
|
||||
logApp("Cron-Email-Parser", "warning", "Lock file present. Cron Email Parser attempted to execute but was already executing, so instead it terminated.");
|
||||
exit("Script is already running. Exiting.");
|
||||
}
|
||||
}
|
||||
file_put_contents($lock_file_path, "Locked");
|
||||
|
||||
// Ensure lock gets removed even on fatal error
|
||||
register_shutdown_function(function() use ($lock_file_path) {
|
||||
if (file_exists($lock_file_path)) {
|
||||
@unlink($lock_file_path);
|
||||
}
|
||||
});
|
||||
|
||||
// Allowed attachment extensions
|
||||
$allowed_extensions = array('jpg', 'jpeg', 'gif', 'png', 'webp', 'pdf', 'txt', 'md', 'doc', 'docx', 'csv', 'xls', 'xlsx', 'xlsm', 'zip', 'tar', 'gz');
|
||||
|
||||
/** ------------------------------------------------------------------
|
||||
* Ticket / Reply helpers (unchanged)
|
||||
* ------------------------------------------------------------------ */
|
||||
function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file) {
|
||||
global $mysqli, $config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_ticket_new_ticket_notification_email, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_ticket_default_billable, $allowed_extensions;
|
||||
|
||||
$ticket_number_sql = mysqli_fetch_array(mysqli_query($mysqli, "SELECT config_ticket_next_number FROM settings WHERE company_id = 1"));
|
||||
$ticket_number = intval($ticket_number_sql['config_ticket_next_number']);
|
||||
$new_config_ticket_next_number = $ticket_number + 1;
|
||||
mysqli_query($mysqli, "UPDATE settings SET config_ticket_next_number = $new_config_ticket_next_number WHERE company_id = 1");
|
||||
|
||||
// Clean up the message
|
||||
$message = trim($message);
|
||||
$message = preg_replace('/\s+/', ' ', $message);
|
||||
$message = nl2br($message);
|
||||
$message = "<i>Email from: <b>$contact_name</b> <$contact_email> at $date:-</i> <br><br><div style='line-height:1.5;'>$message</div>";
|
||||
|
||||
$ticket_prefix_esc = mysqli_real_escape_string($mysqli, $config_ticket_prefix);
|
||||
$message_esc = mysqli_real_escape_string($mysqli, $message);
|
||||
$contact_email_esc = mysqli_real_escape_string($mysqli, $contact_email);
|
||||
$client_id = intval($client_id);
|
||||
|
||||
$url_key = randomString(156);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$ticket_prefix_esc', ticket_number = $ticket_number, ticket_source = 'Email', ticket_subject = '$subject', ticket_details = '$message_esc', ticket_priority = 'Low', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = 0, ticket_contact_id = $contact_id, ticket_url_key = '$url_key', ticket_client_id = $client_id");
|
||||
$id = mysqli_insert_id($mysqli);
|
||||
|
||||
// Logging
|
||||
logAction("Ticket", "Create", "Email parser: Client contact $contact_email_esc created ticket $ticket_prefix_esc$ticket_number ($subject) ($id)", $client_id, $id);
|
||||
|
||||
mkdirMissing('../uploads/tickets/');
|
||||
$att_dir = "../uploads/tickets/" . $id . "/";
|
||||
mkdirMissing($att_dir);
|
||||
|
||||
// Move original .eml into the ticket folder
|
||||
rename("../uploads/tmp/{$original_message_file}", "{$att_dir}/{$original_message_file}");
|
||||
$original_message_file_esc = mysqli_real_escape_string($mysqli, $original_message_file);
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = 'Original-parsed-email.eml', ticket_attachment_reference_name = '$original_message_file_esc', ticket_attachment_ticket_id = $id");
|
||||
|
||||
// Save non-inline attachments
|
||||
foreach ($attachments as $attachment) {
|
||||
$att_name = $attachment['name'];
|
||||
$att_extension = strtolower(pathinfo($att_name, PATHINFO_EXTENSION));
|
||||
|
||||
if (in_array($att_extension, $allowed_extensions)) {
|
||||
$att_saved_filename = md5(uniqid(rand(), true)) . '.' . $att_extension;
|
||||
$att_saved_path = $att_dir . $att_saved_filename;
|
||||
file_put_contents($att_saved_path, $attachment['content']);
|
||||
|
||||
$ticket_attachment_name = sanitizeInput($att_name);
|
||||
$ticket_attachment_reference_name = sanitizeInput($att_saved_filename);
|
||||
|
||||
$ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_name);
|
||||
$ticket_attachment_reference_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_reference_name);
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name_esc', ticket_attachment_reference_name = '$ticket_attachment_reference_name_esc', ticket_attachment_ticket_id = $id");
|
||||
} else {
|
||||
$ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $att_name);
|
||||
logAction("Ticket", "Edit", "Email parser: Blocked attachment $ticket_attachment_name_esc from Client contact $contact_email_esc for ticket $ticket_prefix_esc$ticket_number", $client_id, $id);
|
||||
}
|
||||
}
|
||||
|
||||
// Guest ticket watchers
|
||||
if ($client_id == 0) {
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_watchers SET watcher_email = '$contact_email_esc', watcher_ticket_id = $id");
|
||||
}
|
||||
|
||||
$data = [];
|
||||
if ($config_ticket_client_general_notifications == 1) {
|
||||
$subject_email = "Ticket created - [$config_ticket_prefix$ticket_number] - $subject";
|
||||
$body = "<i style='color: #808080'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you.<br><br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $subject<br>Status: New<br>Portal: <a href='https://$config_base_url/guest/guest_view_ticket.php?ticket_id=$id&url_key=$url_key'>View ticket</a><br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
||||
$data[] = [
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $contact_email,
|
||||
'recipient_name' => $contact_name,
|
||||
'subject' => $subject_email,
|
||||
'body' => mysqli_real_escape_string($mysqli, $body)
|
||||
];
|
||||
}
|
||||
|
||||
if ($config_ticket_new_ticket_notification_email) {
|
||||
if ($client_id == 0) {
|
||||
$client_name = "Guest";
|
||||
} else {
|
||||
$client_sql = mysqli_query($mysqli, "SELECT client_name FROM clients WHERE client_id = $client_id");
|
||||
$client_row = mysqli_fetch_array($client_sql);
|
||||
$client_name = sanitizeInput($client_row['client_name']);
|
||||
}
|
||||
$email_subject = "$config_app_name - New Ticket - $client_name: $subject";
|
||||
$email_body = "Hello, <br><br>This is a notification that a new ticket has been raised in ITFlow. <br>Client: $client_name<br>Priority: Low (email parsed)<br>Link: https://$config_base_url/user/ticket.php?ticket_id=$id <br><br>--------------------------------<br><br><b>$subject</b><br>$message";
|
||||
|
||||
$data[] = [
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $config_ticket_new_ticket_notification_email,
|
||||
'recipient_name' => $config_ticket_from_name,
|
||||
'subject' => $email_subject,
|
||||
'body' => mysqli_real_escape_string($mysqli, $email_body)
|
||||
];
|
||||
}
|
||||
|
||||
addToMailQueue($data);
|
||||
customAction('ticket_create', $id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function addReply($from_email, $date, $subject, $ticket_number, $message, $attachments) {
|
||||
global $mysqli, $config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $allowed_extensions;
|
||||
|
||||
$ticket_reply_type = 'Client';
|
||||
$message_parts = explode("##- Please type your reply above this line -##", $message);
|
||||
$message_body = $message_parts[0];
|
||||
$message_body = trim($message_body);
|
||||
$message_body = preg_replace('/\r\n|\r|\n/', ' ', $message_body);
|
||||
$message_body = nl2br($message_body);
|
||||
|
||||
$message = "<i>Email from: $from_email at $date:-</i> <br><br><div style='line-height:1.5;'>$message_body</div>";
|
||||
|
||||
$ticket_number_esc = intval($ticket_number);
|
||||
$message_esc = mysqli_real_escape_string($mysqli, $message);
|
||||
$from_email_esc = mysqli_real_escape_string($mysqli, $from_email);
|
||||
|
||||
$row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT ticket_id, ticket_subject, ticket_status, ticket_contact_id, ticket_client_id, contact_email, client_name
|
||||
FROM tickets
|
||||
LEFT JOIN contacts on tickets.ticket_contact_id = contacts.contact_id
|
||||
LEFT JOIN clients on tickets.ticket_client_id = clients.client_id
|
||||
WHERE ticket_number = $ticket_number_esc LIMIT 1"));
|
||||
|
||||
if ($row) {
|
||||
$ticket_id = intval($row['ticket_id']);
|
||||
$ticket_subject = sanitizeInput($row['ticket_subject']);
|
||||
$ticket_status = sanitizeInput($row['ticket_status']);
|
||||
$ticket_reply_contact = intval($row['ticket_contact_id']);
|
||||
$ticket_contact_email = sanitizeInput($row['contact_email']);
|
||||
$client_id = intval($row['ticket_client_id']);
|
||||
$client_name = sanitizeInput($row['client_name']);
|
||||
|
||||
if ($ticket_status == 5) {
|
||||
$config_ticket_prefix_esc = mysqli_real_escape_string($mysqli, $config_ticket_prefix);
|
||||
$ticket_number_esc2 = mysqli_real_escape_string($mysqli, $ticket_number);
|
||||
|
||||
appNotify("Ticket", "Email parser: $from_email attempted to re-open ticket $config_ticket_prefix_esc$ticket_number_esc2 (ID $ticket_id) - check inbox manually to see email", "ticket.php?ticket_id=$ticket_id", $client_id);
|
||||
|
||||
$email_subject = "Action required: This ticket is already closed";
|
||||
$email_body = "Hi there, <br><br>You've tried to reply to a ticket that is closed - we won't see your response. <br><br>Please raise a new ticket by sending a new e-mail to our support address below. <br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
|
||||
|
||||
$data = [
|
||||
[
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $from_email,
|
||||
'recipient_name' => $from_email,
|
||||
'subject' => $email_subject,
|
||||
'body' => mysqli_real_escape_string($mysqli, $email_body)
|
||||
]
|
||||
];
|
||||
|
||||
addToMailQueue($data);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (empty($ticket_contact_email) || $ticket_contact_email !== $from_email) {
|
||||
$from_email_esc2 = mysqli_real_escape_string($mysqli, $from_email);
|
||||
$row2 = mysqli_fetch_array(mysqli_query($mysqli, "SELECT contact_id FROM contacts WHERE contact_email = '$from_email_esc2' AND contact_client_id = $client_id LIMIT 1"));
|
||||
if ($row2) {
|
||||
$ticket_reply_contact = intval($row2['contact_id']);
|
||||
} else {
|
||||
$ticket_reply_type = 'Internal';
|
||||
$ticket_reply_contact = '0';
|
||||
$message = "<b>WARNING: Contact email mismatch</b><br>$message";
|
||||
$message_esc = mysqli_real_escape_string($mysqli, $message);
|
||||
}
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$message_esc', ticket_reply_type = '$ticket_reply_type', ticket_reply_time_worked = '00:00:00', ticket_reply_by = $ticket_reply_contact, ticket_reply_ticket_id = $ticket_id");
|
||||
$reply_id = mysqli_insert_id($mysqli);
|
||||
|
||||
$ticket_dir = "../uploads/tickets/" . $ticket_id . "/";
|
||||
mkdirMissing($ticket_dir);
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
$att_name = $attachment['name'];
|
||||
$att_extension = strtolower(pathinfo($att_name, PATHINFO_EXTENSION));
|
||||
|
||||
if (in_array($att_extension, $allowed_extensions)) {
|
||||
$att_saved_filename = md5(uniqid(rand(), true)) . '.' . $att_extension;
|
||||
$att_saved_path = $ticket_dir . $att_saved_filename;
|
||||
file_put_contents($att_saved_path, $attachment['content']);
|
||||
|
||||
$ticket_attachment_name = sanitizeInput($att_name);
|
||||
$ticket_attachment_reference_name = sanitizeInput($att_saved_filename);
|
||||
|
||||
$ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_name);
|
||||
$ticket_attachment_reference_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_reference_name);
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name_esc', ticket_attachment_reference_name = '$ticket_attachment_reference_name_esc', ticket_attachment_reply_id = $reply_id, ticket_attachment_ticket_id = $ticket_id");
|
||||
} else {
|
||||
$ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $att_name);
|
||||
logAction("Ticket", "Edit", "Email parser: Blocked attachment $ticket_attachment_name_esc from Client contact $from_email_esc for ticket $config_ticket_prefix$ticket_number_esc", $client_id, $ticket_id);
|
||||
}
|
||||
}
|
||||
|
||||
$ticket_assigned_to_sql = mysqli_query($mysqli, "SELECT ticket_assigned_to FROM tickets WHERE ticket_id = $ticket_id LIMIT 1");
|
||||
if ($ticket_assigned_to_sql) {
|
||||
$row3 = mysqli_fetch_array($ticket_assigned_to_sql);
|
||||
$ticket_assigned_to = intval($row3['ticket_assigned_to']);
|
||||
|
||||
if ($ticket_assigned_to) {
|
||||
$tech_sql = mysqli_query($mysqli, "SELECT user_email, user_name FROM users WHERE user_id = $ticket_assigned_to LIMIT 1");
|
||||
$tech_row = mysqli_fetch_array($tech_sql);
|
||||
$tech_email = sanitizeInput($tech_row['user_email']);
|
||||
$tech_name = sanitizeInput($tech_row['user_name']);
|
||||
|
||||
$email_subject = "$config_app_name - Ticket updated - [$config_ticket_prefix$ticket_number] $ticket_subject";
|
||||
$email_body = "Hello $tech_name,<br><br>A new reply has been added to the below ticket, check ITFlow for full details.<br><br>Client: $client_name<br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $ticket_subject<br><br>https://$config_base_url/user/ticket.php?ticket_id=$ticket_id";
|
||||
|
||||
$data = [
|
||||
[
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $tech_email,
|
||||
'recipient_name' => $tech_name,
|
||||
'subject' => mysqli_real_escape_string($mysqli, $email_subject),
|
||||
'body' => mysqli_real_escape_string($mysqli, $email_body)
|
||||
]
|
||||
];
|
||||
addToMailQueue($data);
|
||||
}
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id AND ticket_client_id = $client_id LIMIT 1");
|
||||
|
||||
logAction("Ticket", "Edit", "Email parser: Client contact $from_email_esc updated ticket $config_ticket_prefix$ticket_number_esc ($subject)", $client_id, $ticket_id);
|
||||
customAction('ticket_reply_client', $ticket_id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------
|
||||
* OAuth helpers + provider guard
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
// returns true if expires_at ('Y-m-d H:i:s') is in the past (or missing)
|
||||
function tokenExpired(?string $expires_at): bool {
|
||||
if (empty($expires_at)) return true;
|
||||
$ts = strtotime($expires_at);
|
||||
if ($ts === false) return true;
|
||||
// refresh a little early (60s) to avoid race
|
||||
return ($ts - 60) <= time();
|
||||
}
|
||||
|
||||
// very small form-encoded POST helper using curl
|
||||
function httpFormPost(string $url, array $fields): array {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields, '', '&'));
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
$raw = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
return ['ok' => ($raw !== false && $code >= 200 && $code < 300), 'body' => $raw, 'code' => $code, 'err' => $err];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a valid access token for Google Workspace IMAP via refresh token if needed.
|
||||
* Uses settings: config_mail_oauth_client_id / _client_secret / _refresh_token / _access_token / _access_token_expires_at
|
||||
* Updates globals if refreshed (so later logging can reflect it if you want to persist).
|
||||
*/
|
||||
function getGoogleAccessToken(string $username): ?string {
|
||||
// pull from global settings variables you already load
|
||||
global $mysqli,
|
||||
$config_mail_oauth_client_id,
|
||||
$config_mail_oauth_client_secret,
|
||||
$config_mail_oauth_refresh_token,
|
||||
$config_mail_oauth_access_token,
|
||||
$config_mail_oauth_access_token_expires_at;
|
||||
|
||||
// If we have a not-expired token, use it
|
||||
if (!empty($config_mail_oauth_access_token) && !tokenExpired($config_mail_oauth_access_token_expires_at)) {
|
||||
return $config_mail_oauth_access_token;
|
||||
}
|
||||
|
||||
// Need to refresh?
|
||||
if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token)) {
|
||||
// Nothing we can do
|
||||
return null;
|
||||
}
|
||||
|
||||
$resp = httpFormPost(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
[
|
||||
'client_id' => $config_mail_oauth_client_id,
|
||||
'client_secret' => $config_mail_oauth_client_secret,
|
||||
'refresh_token' => $config_mail_oauth_refresh_token,
|
||||
'grant_type' => 'refresh_token',
|
||||
]
|
||||
);
|
||||
|
||||
if (!$resp['ok']) return null;
|
||||
|
||||
$json = json_decode($resp['body'], true);
|
||||
if (!is_array($json) || empty($json['access_token'])) return null;
|
||||
|
||||
// Calculate new expiry
|
||||
$expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
|
||||
|
||||
// Update in-memory globals (and persist to DB)
|
||||
$config_mail_oauth_access_token = $json['access_token'];
|
||||
$config_mail_oauth_access_token_expires_at = $expires_at;
|
||||
|
||||
$at_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token);
|
||||
$exp_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token_expires_at);
|
||||
mysqli_query($mysqli, "UPDATE settings SET
|
||||
config_mail_oauth_access_token = '{$at_esc}',
|
||||
config_mail_oauth_access_token_expires_at = '{$exp_esc}'
|
||||
WHERE company_id = 1
|
||||
");
|
||||
|
||||
return $config_mail_oauth_access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a valid access token for Microsoft 365 IMAP via refresh token if needed.
|
||||
* Uses settings: config_mail_oauth_client_id / _client_secret / _tenant_id / _refresh_token / _access_token / _access_token_expires_at
|
||||
*/
|
||||
function getMicrosoftAccessToken(string $username): ?string {
|
||||
global $mysqli,
|
||||
$config_mail_oauth_client_id,
|
||||
$config_mail_oauth_client_secret,
|
||||
$config_mail_oauth_tenant_id,
|
||||
$config_mail_oauth_refresh_token,
|
||||
$config_mail_oauth_access_token,
|
||||
$config_mail_oauth_access_token_expires_at;
|
||||
|
||||
if (!empty($config_mail_oauth_access_token) && !tokenExpired($config_mail_oauth_access_token_expires_at)) {
|
||||
return $config_mail_oauth_access_token;
|
||||
}
|
||||
|
||||
if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token) || empty($config_mail_oauth_tenant_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = "https://login.microsoftonline.com/".rawurlencode($config_mail_oauth_tenant_id)."/oauth2/v2.0/token";
|
||||
|
||||
$resp = httpFormPost($url, [
|
||||
'client_id' => $config_mail_oauth_client_id,
|
||||
'client_secret' => $config_mail_oauth_client_secret,
|
||||
'refresh_token' => $config_mail_oauth_refresh_token,
|
||||
'grant_type' => 'refresh_token',
|
||||
// IMAP/SMTP scopes typically included at initial consent; not needed for refresh
|
||||
]);
|
||||
|
||||
if (!$resp['ok']) return null;
|
||||
|
||||
$json = json_decode($resp['body'], true);
|
||||
if (!is_array($json) || empty($json['access_token'])) return null;
|
||||
|
||||
$expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
|
||||
|
||||
$config_mail_oauth_access_token = $json['access_token'];
|
||||
$config_mail_oauth_access_token_expires_at = $expires_at;
|
||||
|
||||
$at_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token);
|
||||
$exp_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token_expires_at);
|
||||
mysqli_query($mysqli, "UPDATE settings SET
|
||||
config_mail_oauth_access_token = '{$at_esc}',
|
||||
config_mail_oauth_access_token_expires_at = '{$exp_esc}'
|
||||
WHERE company_id = 1
|
||||
");
|
||||
|
||||
return $config_mail_oauth_access_token;
|
||||
}
|
||||
|
||||
// Provider from settings (may be NULL/empty to disable IMAP polling)
|
||||
$imap_provider = $config_imap_provider ?? '';
|
||||
if ($imap_provider === null) $imap_provider = '';
|
||||
|
||||
if ($imap_provider === '') {
|
||||
// IMAP disabled by admin: exit cleanly
|
||||
logApp("Cron-Email-Parser", "info", "IMAP polling skipped: provider not configured.");
|
||||
@unlink($lock_file_path);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------
|
||||
* Webklex IMAP setup (supports Standard / Google OAuth / Microsoft OAuth)
|
||||
* ------------------------------------------------------------------ */
|
||||
use Webklex\PHPIMAP\ClientManager;
|
||||
|
||||
$validate_cert = true;
|
||||
|
||||
// Defaults from settings (standard IMAP)
|
||||
$host = $config_imap_host;
|
||||
$port = (int)$config_imap_port;
|
||||
$encr = !empty($config_imap_encryption) ? $config_imap_encryption : null; // 'ssl'|'tls'|null
|
||||
$user = $config_imap_username;
|
||||
$pass = $config_imap_password;
|
||||
$auth = null; // 'oauth' for OAuth providers
|
||||
|
||||
if ($imap_provider === 'google_oauth') {
|
||||
$host = 'imap.gmail.com';
|
||||
$port = 993;
|
||||
$encr = 'ssl';
|
||||
$auth = 'oauth';
|
||||
$pass = getGoogleAccessToken($user);
|
||||
if (empty($pass)) {
|
||||
logApp("Cron-Email-Parser", "error", "Google OAuth: no usable access token (check refresh token/client credentials).");
|
||||
@unlink($lock_file_path);
|
||||
exit(1);
|
||||
}
|
||||
} elseif ($imap_provider === 'microsoft_oauth') {
|
||||
$host = 'outlook.office365.com';
|
||||
$port = 993;
|
||||
$encr = 'ssl';
|
||||
$auth = 'oauth';
|
||||
$pass = getMicrosoftAccessToken($user);
|
||||
if (empty($pass)) {
|
||||
logApp("Cron-Email-Parser", "error", "Microsoft OAuth: no usable access token (check refresh token/client credentials/tenant).");
|
||||
@unlink($lock_file_path);
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
// standard_imap (username/password)
|
||||
if (empty($host) || empty($port) || empty($user)) {
|
||||
logApp("Cron-Email-Parser", "error", "Standard IMAP: missing host/port/username.");
|
||||
@unlink($lock_file_path);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$cm = new ClientManager();
|
||||
|
||||
$client = $cm->make(array_filter([
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'encryption' => $encr, // 'ssl' | 'tls' | null
|
||||
'validate_cert' => (bool)$validate_cert,
|
||||
'username' => $user, // full mailbox address (OAuth uses user as principal)
|
||||
'password' => $pass, // access token when $auth === 'oauth'
|
||||
'authentication' => $auth, // 'oauth' or null
|
||||
'protocol' => 'imap',
|
||||
]));
|
||||
|
||||
try {
|
||||
$client->connect();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error connecting to IMAP server: " . $e->getMessage();
|
||||
@unlink($lock_file_path);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$inbox = $client->getFolderByPath('INBOX');
|
||||
|
||||
$targetFolderPath = 'ITFlow';
|
||||
try {
|
||||
$targetFolder = $client->getFolderByPath($targetFolderPath);
|
||||
} catch (\Throwable $e) {
|
||||
$client->createFolder($targetFolderPath);
|
||||
$targetFolder = $client->getFolderByPath($targetFolderPath);
|
||||
}
|
||||
|
||||
// Fetch unseen messages
|
||||
$messages = $inbox->messages()->leaveUnread()->unseen()->get();
|
||||
|
||||
// Counters
|
||||
$processed_count = 0;
|
||||
$unprocessed_count = 0;
|
||||
|
||||
// Process messages
|
||||
foreach ($messages as $message) {
|
||||
$email_processed = false;
|
||||
|
||||
// Save original RFC822 message as .eml
|
||||
mkdirMissing('../uploads/tmp/');
|
||||
$original_message_file = "processed-eml-" . randomString(200) . ".eml";
|
||||
|
||||
try {
|
||||
// getRawMessage() available in v3; for v2 use getHeader()->raw or getStructure()? We'll try getRawMessage()
|
||||
$raw_message = $message->getRawMessage();
|
||||
} catch (\Throwable $e) {
|
||||
// Fallback to rebuilding from headers + body if raw not available
|
||||
$raw_message = (string)$message->getHeader()->raw . "\r\n\r\n" . ($message->getRawBody() ?? $message->getHTMLBody() ?? $message->getTextBody());
|
||||
}
|
||||
file_put_contents("../uploads/tmp/{$original_message_file}", $raw_message);
|
||||
|
||||
// From
|
||||
$fromCol = $message->getFrom();
|
||||
$fromFirst = ($fromCol && $fromCol->count()) ? $fromCol->first() : null;
|
||||
$from_email = sanitizeInput($fromFirst->mail ?? 'itflow-guest@example.com');
|
||||
$from_name = sanitizeInput($fromFirst->personal ?? 'Unknown');
|
||||
|
||||
$from_domain = explode("@", $from_email);
|
||||
$from_domain = sanitizeInput(end($from_domain));
|
||||
|
||||
// Subject
|
||||
$subject = sanitizeInput((string)$message->getSubject() ?: 'No Subject');
|
||||
|
||||
// Date (string)
|
||||
$dateAttr = $message->getDate(); // Attribute
|
||||
$dateRaw = $dateAttr ? (string)$dateAttr : ''; // e.g. "Tue, 10 Sep 2025 13:22:05 +0000"
|
||||
$ts = $dateRaw ? strtotime($dateRaw) : false;
|
||||
$date = sanitizeInput($ts !== false ? date('Y-m-d H:i:s', $ts) : date('Y-m-d H:i:s'));
|
||||
|
||||
// Body (prefer HTML)
|
||||
$message_body_html = $message->getHTMLBody();
|
||||
$message_body_text = $message->getTextBody();
|
||||
$message_body = $message_body_html ?: nl2br(htmlspecialchars((string)$message_body_text));
|
||||
|
||||
// Handle attachments (inline vs regular)
|
||||
$attachments = [];
|
||||
foreach ($message->getAttachments() as $att) {
|
||||
$attrs = $att->getAttributes(); // v6.2: canonical source
|
||||
$dispo = strtolower((string)($attrs['disposition'] ?? ''));
|
||||
$cid = $attrs['id'] ?? null; // Content-ID
|
||||
$content = $attrs['content'] ?? null; // binary
|
||||
$mime = $att->getMimeType();
|
||||
$name = $att->getName() ?: 'attachment';
|
||||
|
||||
$is_inline = false;
|
||||
if ($dispo === 'inline' && $cid && $content !== null) {
|
||||
$cid_trim = trim($cid, '<>');
|
||||
$dataUri = "data:$mime;base64,".base64_encode($content);
|
||||
$message_body = str_replace(["cid:$cid_trim", "cid:$cid"], $dataUri, $message_body);
|
||||
$is_inline = true;
|
||||
}
|
||||
|
||||
if (!$is_inline && $content !== null) {
|
||||
$attachments[] = ['name' => $name, 'content' => $content];
|
||||
}
|
||||
}
|
||||
|
||||
// Decide whether it's a reply to an existing ticket or a new ticket
|
||||
if (preg_match("/\[$config_ticket_prefix(\d+)\]/", $subject, $ticket_number_matches)) {
|
||||
$ticket_number = intval($ticket_number_matches[1]);
|
||||
if (addReply($from_email, $date, $subject, $ticket_number, $message_body, $attachments)) {
|
||||
$email_processed = true;
|
||||
}
|
||||
} else {
|
||||
// Known contact?
|
||||
$from_email_esc = mysqli_real_escape_string($mysqli, $from_email);
|
||||
$any_contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from_email_esc' LIMIT 1");
|
||||
$rowc = mysqli_fetch_array($any_contact_sql);
|
||||
|
||||
if ($rowc) {
|
||||
$contact_name = sanitizeInput($rowc['contact_name']);
|
||||
$contact_id = intval($rowc['contact_id']);
|
||||
$contact_email = sanitizeInput($rowc['contact_email']);
|
||||
$client_id = intval($rowc['contact_client_id']);
|
||||
|
||||
if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message_body, $attachments, $original_message_file)) {
|
||||
$email_processed = true;
|
||||
}
|
||||
} else {
|
||||
// Known domain?
|
||||
$from_domain_esc = mysqli_real_escape_string($mysqli, $from_domain);
|
||||
$domain_sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$from_domain_esc' LIMIT 1");
|
||||
$rowd = mysqli_fetch_assoc($domain_sql);
|
||||
|
||||
if ($rowd && $from_domain == $rowd['domain_name']) {
|
||||
$client_id = intval($rowd['domain_client_id']);
|
||||
|
||||
// Create a new contact
|
||||
$contact_name = $from_name;
|
||||
$contact_email = $from_email;
|
||||
mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '".mysqli_real_escape_string($mysqli, $contact_name)."', contact_email = '".mysqli_real_escape_string($mysqli, $contact_email)."', contact_notes = 'Added automatically via email parsing.', contact_client_id = $client_id");
|
||||
$contact_id = mysqli_insert_id($mysqli);
|
||||
|
||||
// Logging
|
||||
logAction("Contact", "Create", "Email parser: created contact " . mysqli_real_escape_string($mysqli, $contact_name) . "", $client_id, $contact_id);
|
||||
customAction('contact_create', $contact_id);
|
||||
|
||||
if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message_body, $attachments, $original_message_file)) {
|
||||
$email_processed = true;
|
||||
}
|
||||
} elseif ($config_ticket_email_parse_unknown_senders) {
|
||||
// Unknown sender allowed?
|
||||
$bad_from_pattern = "/daemon|postmaster/i";
|
||||
if (!(preg_match($bad_from_pattern, $from_email))) {
|
||||
if (addTicket(0, $from_name, $from_email, 0, $date, $subject, $message_body, $attachments, $original_message_file)) {
|
||||
$email_processed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flag/move based on processing result
|
||||
if ($email_processed) {
|
||||
$processed_count++; // increment first so a move failure doesn't hide the success
|
||||
try {
|
||||
$message->setFlag('Seen');
|
||||
// Move using the Folder object (top-level "ITFlow")
|
||||
$message->move($targetFolderPath);
|
||||
// optional: logApp("Cron-Email-Parser", "info", "Moved message to ITFlow");
|
||||
} catch (\Throwable $e) {
|
||||
// >>> Put the extra logging RIGHT HERE
|
||||
$subj = (string)$message->getSubject();
|
||||
$uid = method_exists($message, 'getUid') ? $message->getUid() : 'n/a';
|
||||
$path = property_exists($targetFolder, 'path') ? $targetFolder->path : 'ITFlow';
|
||||
logApp(
|
||||
"Cron-Email-Parser",
|
||||
"warning",
|
||||
"Move failed (subject=\"$subj\", uid=$uid) to [$path]: ".$e->getMessage()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$unprocessed_count++;
|
||||
try {
|
||||
$message->setFlag('Flagged');
|
||||
$message->unsetFlag('Seen');
|
||||
} catch (\Throwable $e) {
|
||||
logApp("Cron-Email-Parser", "warning", "Flag update failed: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup temp .eml if still present (e.g., reply path)
|
||||
if (isset($original_message_file)) {
|
||||
$tmp_path = "../uploads/tmp/{$original_message_file}";
|
||||
if (file_exists($tmp_path)) { @unlink($tmp_path); }
|
||||
}
|
||||
}
|
||||
|
||||
// Expunge & disconnect
|
||||
try {
|
||||
$client->expunge();
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
$client->disconnect();
|
||||
|
||||
// Execution timing (optional)
|
||||
$script_end_time = microtime(true);
|
||||
$execution_time = $script_end_time - $script_start_time;
|
||||
$execution_time_formatted = number_format($execution_time, 2);
|
||||
|
||||
$processed_info = "Processed: $processed_count email(s), Unprocessed: $unprocessed_count email(s)";
|
||||
// logAction("Cron-Email-Parser", "Execution", "Cron Email Parser executed in $execution_time_formatted seconds. $processed_info");
|
||||
|
||||
// Remove the lock file
|
||||
unlink($lock_file_path);
|
||||
|
||||
// DEBUG
|
||||
echo "\nLock File Path: $lock_file_path\n";
|
||||
if (file_exists($lock_file_path)) {
|
||||
echo "\nLock is present\n\n";
|
||||
}
|
||||
echo "Processed Emails into tickets: $processed_count\n";
|
||||
echo "Unprocessed Emails: $unprocessed_count\n";
|
||||
14
db.sql
14
db.sql
|
|
@ -1,9 +1,9 @@
|
|||
/*M!999999\- enable the sandbox mode */
|
||||
-- MariaDB dump 10.19 Distrib 10.11.11-MariaDB, for debian-linux-gnu (x86_64)
|
||||
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: itflow_dev
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.11.11-MariaDB-0+deb12u1
|
||||
-- Server version 10.11.14-MariaDB-0+deb12u2
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
|
|
@ -1981,6 +1981,7 @@ CREATE TABLE `settings` (
|
|||
`company_id` int(11) NOT NULL,
|
||||
`config_current_database_version` varchar(10) NOT NULL,
|
||||
`config_start_page` varchar(200) DEFAULT 'clients.php',
|
||||
`config_smtp_provider` enum('standard_smtp','google_oauth','microsoft_oauth') DEFAULT NULL,
|
||||
`config_smtp_host` varchar(200) DEFAULT NULL,
|
||||
`config_smtp_port` int(5) DEFAULT NULL,
|
||||
`config_smtp_encryption` varchar(200) DEFAULT NULL,
|
||||
|
|
@ -1988,6 +1989,13 @@ CREATE TABLE `settings` (
|
|||
`config_smtp_password` varchar(200) DEFAULT NULL,
|
||||
`config_mail_from_email` varchar(200) DEFAULT NULL,
|
||||
`config_mail_from_name` varchar(200) DEFAULT NULL,
|
||||
`config_imap_provider` enum('standard_imap','google_oauth','microsoft_oauth') DEFAULT NULL,
|
||||
`config_mail_oauth_client_id` varchar(255) DEFAULT NULL,
|
||||
`config_mail_oauth_client_secret` varchar(255) DEFAULT NULL,
|
||||
`config_mail_oauth_tenant_id` varchar(255) DEFAULT NULL,
|
||||
`config_mail_oauth_refresh_token` text DEFAULT NULL,
|
||||
`config_mail_oauth_access_token` text DEFAULT NULL,
|
||||
`config_mail_oauth_access_token_expires_at` datetime DEFAULT NULL,
|
||||
`config_imap_host` varchar(200) DEFAULT NULL,
|
||||
`config_imap_port` int(5) DEFAULT NULL,
|
||||
`config_imap_encryption` varchar(200) DEFAULT NULL,
|
||||
|
|
@ -2758,4 +2766,4 @@ CREATE TABLE `vendors` (
|
|||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2025-08-28 11:43:32
|
||||
-- Dump completed on 2025-09-15 17:22:23
|
||||
|
|
|
|||
|
|
@ -1694,4 +1694,34 @@ function redirect($url = null, $permanent = false) {
|
|||
function flash_alert(string $message, string $type = 'success'): void {
|
||||
$_SESSION['alert_type'] = $type;
|
||||
$_SESSION['alert_message'] = $message;
|
||||
}
|
||||
|
||||
// Sanitize File Names
|
||||
function sanitize_filename($filename, $strict = false) {
|
||||
// Remove path information and dots around the filename
|
||||
$filename = basename($filename);
|
||||
|
||||
// Replace spaces and underscores with dashes
|
||||
$filename = str_replace([' ', '_'], '-', $filename);
|
||||
|
||||
// Remove anything which isn't a word, number, dot, or dash
|
||||
$filename = preg_replace('/[^A-Za-z0-9\.\-]/', '', $filename);
|
||||
|
||||
// Optionally make filename strict alphanumeric (keep dot and dash)
|
||||
if ($strict) {
|
||||
$filename = preg_replace('/[^A-Za-z0-9\.\-]/', '', $filename);
|
||||
}
|
||||
|
||||
// Avoid multiple consecutive dashes
|
||||
$filename = preg_replace('/-+/', '-', $filename);
|
||||
|
||||
// Remove leading/trailing dots and dashes
|
||||
$filename = trim($filename, '.-');
|
||||
|
||||
// Ensure it’s not empty
|
||||
if (empty($filename)) {
|
||||
$filename = 'file';
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
|
@ -5,4 +5,4 @@
|
|||
* Update this file each time we merge develop into master. Format is YY.MM (add a .v if there is more than one release a month.
|
||||
*/
|
||||
|
||||
DEFINE("APP_VERSION", "25.09.1");
|
||||
DEFINE("APP_VERSION", "25.09.2");
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@
|
|||
* It is used in conjunction with database_updates.php
|
||||
*/
|
||||
|
||||
DEFINE("LATEST_DATABASE_VERSION", "2.3.2");
|
||||
DEFINE("LATEST_DATABASE_VERSION", "2.3.4");
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ $config_azure_client_id = $row['config_azure_client_id'];
|
|||
$config_azure_client_secret = $row['config_azure_client_secret'];
|
||||
|
||||
// Mail - SMTP
|
||||
$config_smtp_provider = $row['config_smtp_provider'];
|
||||
$config_smtp_host = $row['config_smtp_host'];
|
||||
$config_smtp_port = intval($row['config_smtp_port']);
|
||||
$config_smtp_encryption = $row['config_smtp_encryption'];
|
||||
|
|
@ -21,14 +22,23 @@ $config_mail_from_email = $row['config_mail_from_email'];
|
|||
$config_mail_from_name = $row['config_mail_from_name'];
|
||||
|
||||
// Mail - IMAP
|
||||
$config_imap_provider = $row['config_imap_provider'];
|
||||
$config_imap_host = $row['config_imap_host'];
|
||||
$config_imap_port = intval($row['config_imap_port']);
|
||||
$config_imap_encryption = $row['config_imap_encryption'];
|
||||
$config_imap_username = $row['config_imap_username'];
|
||||
$config_imap_password = $row['config_imap_password'];
|
||||
|
||||
// Mail OAUTH2
|
||||
$config_mail_oauth_client_id = $row['config_mail_oauth_client_id'];
|
||||
$config_mail_oauth_client_secret = $row['config_mail_oauth_client_secret'];
|
||||
$config_mail_oauth_tenant_id = $row['config_mail_oauth_tenant_id'];
|
||||
$config_mail_oauth_refresh_token = $row['config_mail_oauth_refresh_token'];
|
||||
$config_mail_oauth_access_token = $row['config_mail_oauth_access_token'];
|
||||
$config_mail_oauth_access_token_expires_at = $row['config_mail_oauth_access_token_expires_at'];
|
||||
|
||||
// Defaults
|
||||
$config_start_page = $row['config_start_page'];
|
||||
$config_start_page = $row['config_start_page'] ?? 'clients.php';
|
||||
$config_default_transfer_from_account = intval($row['config_default_transfer_from_account']);
|
||||
$config_default_transfer_to_account = intval($row['config_default_transfer_to_account']);
|
||||
$config_default_payment_account = intval($row['config_default_payment_account']);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<ul class="navbar-nav ml-auto">
|
||||
|
||||
<!-- SEARCH FORM -->
|
||||
<form class="form-inline" action="global_search.php">
|
||||
<form class="form-inline" action="../user/global_search.php">
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control form-control-navbar" type="search" placeholder="Search everywhere" name="query"
|
||||
value="<?php if (isset($_GET['query'])) { echo nullable_htmlentities($_GET['query']); } ?>">
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@
|
|||
|
||||
if (file_exists("config.php")) {
|
||||
//require_once "includes/check_login.php";
|
||||
|
||||
if (isset($config_start_page)) {
|
||||
header("Location: /user/$config_start_page");
|
||||
} else {
|
||||
header("Location: /user");
|
||||
}
|
||||
|
||||
header("Location: /user/$config_start_page");
|
||||
|
||||
} else {
|
||||
header("Location: /setup");
|
||||
exit();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"require": {
|
||||
"webklex/php-imap": "^6.2"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit9b9826e5b5cc7806cd328c4112cca75e::getLoader();
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../nesbot/carbon/bin/carbon)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) {
|
||||
include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Carbon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# carbonphp/carbon-doctrine-types
|
||||
|
||||
Types to use Carbon in Doctrine
|
||||
|
||||
## Documentation
|
||||
|
||||
[Check how to use in the official Carbon documentation](https://carbon.nesbot.com/symfony/)
|
||||
|
||||
This package is an externalization of [src/Carbon/Doctrine](https://github.com/briannesbitt/Carbon/tree/2.71.0/src/Carbon/Doctrine)
|
||||
from `nestbot/carbon` package.
|
||||
|
||||
Externalization allows to better deal with different versions of dbal. With
|
||||
version 4.0 of dbal, it no longer sustainable to be compatible with all version
|
||||
using a single code.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
"description": "Types to use Carbon in Doctrine",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"date",
|
||||
"time",
|
||||
"DateTime",
|
||||
"Carbon",
|
||||
"Doctrine"
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^4.0.0",
|
||||
"nesbot/carbon": "^2.71.0 || ^3.0.0",
|
||||
"phpunit/phpunit": "^10.3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<4.0.0 || >=5.0.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "KyleKatarn",
|
||||
"email": "kylekatarnls@gmail.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
16
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonDoctrineType.php
vendored
Normal file
16
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonDoctrineType.php
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
interface CarbonDoctrineType
|
||||
{
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform);
|
||||
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform);
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform);
|
||||
}
|
||||
9
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonImmutableType.php
vendored
Normal file
9
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonImmutableType.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class CarbonImmutableType extends DateTimeImmutableType implements CarbonDoctrineType
|
||||
{
|
||||
}
|
||||
9
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonType.php
vendored
Normal file
9
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonType.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class CarbonType extends DateTimeType implements CarbonDoctrineType
|
||||
{
|
||||
}
|
||||
131
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonTypeConverter.php
vendored
Normal file
131
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonTypeConverter.php
vendored
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Platforms\DB2Platform;
|
||||
use Doctrine\DBAL\Platforms\OraclePlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
||||
use Doctrine\DBAL\Types\Exception\InvalidType;
|
||||
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @template T of CarbonInterface
|
||||
*/
|
||||
trait CarbonTypeConverter
|
||||
{
|
||||
/**
|
||||
* This property differentiates types installed by carbonphp/carbon-doctrine-types
|
||||
* from the ones embedded previously in nesbot/carbon source directly.
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
public bool $external = true;
|
||||
|
||||
/**
|
||||
* @return class-string<T>
|
||||
*/
|
||||
protected function getCarbonClassName(): string
|
||||
{
|
||||
return Carbon::class;
|
||||
}
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
|
||||
{
|
||||
$precision = min(
|
||||
$fieldDeclaration['precision'] ?? DateTimeDefaultPrecision::get(),
|
||||
$this->getMaximumPrecision($platform),
|
||||
);
|
||||
|
||||
$type = parent::getSQLDeclaration($fieldDeclaration, $platform);
|
||||
|
||||
if (!$precision) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if (str_contains($type, '(')) {
|
||||
return preg_replace('/\(\d+\)/', "($precision)", $type);
|
||||
}
|
||||
|
||||
[$before, $after] = explode(' ', "$type ");
|
||||
|
||||
return trim("$before($precision) $after");
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value->format('Y-m-d H:i:s.u');
|
||||
}
|
||||
|
||||
throw InvalidType::new(
|
||||
$value,
|
||||
static::class,
|
||||
['null', 'DateTime', 'Carbon']
|
||||
);
|
||||
}
|
||||
|
||||
private function doConvertToPHPValue(mixed $value)
|
||||
{
|
||||
$class = $this->getCarbonClassName();
|
||||
|
||||
if ($value === null || is_a($value, $class)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $class::instance($value);
|
||||
}
|
||||
|
||||
$date = null;
|
||||
$error = null;
|
||||
|
||||
try {
|
||||
$date = $class::parse($value);
|
||||
} catch (Exception $exception) {
|
||||
$error = $exception;
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
throw ValueNotConvertible::new(
|
||||
$value,
|
||||
static::class,
|
||||
'Y-m-d H:i:s.u or any format supported by '.$class.'::parse()',
|
||||
$error
|
||||
);
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
private function getMaximumPrecision(AbstractPlatform $platform): int
|
||||
{
|
||||
if ($platform instanceof DB2Platform) {
|
||||
return 12;
|
||||
}
|
||||
|
||||
if ($platform instanceof OraclePlatform) {
|
||||
return 9;
|
||||
}
|
||||
|
||||
if ($platform instanceof SQLServerPlatform || $platform instanceof SQLitePlatform) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
30
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeDefaultPrecision.php
vendored
Normal file
30
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeDefaultPrecision.php
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class DateTimeDefaultPrecision
|
||||
{
|
||||
private static $precision = 6;
|
||||
|
||||
/**
|
||||
* Change the default Doctrine datetime and datetime_immutable precision.
|
||||
*
|
||||
* @param int $precision
|
||||
*/
|
||||
public static function set(int $precision): void
|
||||
{
|
||||
self::$precision = $precision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default Doctrine datetime and datetime_immutable precision.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get(): int
|
||||
{
|
||||
return self::$precision;
|
||||
}
|
||||
}
|
||||
32
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeImmutableType.php
vendored
Normal file
32
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeImmutableType.php
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\VarDateTimeImmutableType;
|
||||
|
||||
class DateTimeImmutableType extends VarDateTimeImmutableType implements CarbonDoctrineType
|
||||
{
|
||||
/** @use CarbonTypeConverter<CarbonImmutable> */
|
||||
use CarbonTypeConverter;
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?CarbonImmutable
|
||||
{
|
||||
return $this->doConvertToPHPValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<CarbonImmutable>
|
||||
*/
|
||||
protected function getCarbonClassName(): string
|
||||
{
|
||||
return CarbonImmutable::class;
|
||||
}
|
||||
}
|
||||
24
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeType.php
vendored
Normal file
24
plugins/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeType.php
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\VarDateTimeType;
|
||||
|
||||
class DateTimeType extends VarDateTimeType implements CarbonDoctrineType
|
||||
{
|
||||
/** @use CarbonTypeConverter<Carbon> */
|
||||
use CarbonTypeConverter;
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Carbon
|
||||
{
|
||||
return $this->doConvertToPHPValue($value);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,572 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var ?string */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array<int, string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array<string, string[]>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* @psalm-var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var bool[]
|
||||
* @psalm-var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var ?string */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var self[]
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param ?string $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return array<string, array<int, string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return array<string, string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return array<string, string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] Array of classname => path
|
||||
* @psalm-return array<string, string>
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $classMap Class to filename map
|
||||
* @psalm-param array<string, string> $classMap
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param string[]|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
(array) $paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
(array) $paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param string[]|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
(array) $paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
(array) $paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param string[]|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param string[]|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders indexed by their corresponding vendor directories.
|
||||
*
|
||||
* @return self[]
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
* @private
|
||||
*/
|
||||
function includeFile($file)
|
||||
{
|
||||
include $file;
|
||||
}
|
||||
|
|
@ -0,0 +1,350 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints($constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
|
||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $installed[count($installed) - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = require __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
$installed[] = self::$installed;
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
||||
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
||||
'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
|
||||
'DateInvalidTimeZoneException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php',
|
||||
'DateMalformedIntervalStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php',
|
||||
'DateMalformedPeriodStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php',
|
||||
'DateMalformedStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php',
|
||||
'DateObjectError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php',
|
||||
'DateRangeError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php',
|
||||
'Deprecated' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php',
|
||||
'NoDiscard' => $vendorDir . '/symfony/polyfill-php85/Resources/stubs/NoDiscard.php',
|
||||
'Override' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/Override.php',
|
||||
'ReflectionConstant' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
|
||||
'SQLite3Exception' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
|
||||
);
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
// autoload_files.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
|
||||
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
|
||||
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
|
||||
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
|
||||
'23f09fe3194f8c2f70923f90d6702129' => $vendorDir . '/illuminate/collections/functions.php',
|
||||
'60799491728b879e74601d83e38b2cad' => $vendorDir . '/illuminate/collections/helpers.php',
|
||||
'f625ee536139dfb962a398b200bdb2bd' => $vendorDir . '/illuminate/support/functions.php',
|
||||
'72579e7bd17821bb1321b87411366eae' => $vendorDir . '/illuminate/support/helpers.php',
|
||||
);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
|
||||
'Webklex\\PHPIMAP\\' => array($vendorDir . '/webklex/php-imap/src'),
|
||||
'Symfony\\Polyfill\\Php85\\' => array($vendorDir . '/symfony/polyfill-php85'),
|
||||
'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'),
|
||||
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
|
||||
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
|
||||
'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'),
|
||||
'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
|
||||
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
|
||||
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
|
||||
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
|
||||
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
|
||||
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
|
||||
'Illuminate\\Support\\' => array($vendorDir . '/illuminate/macroable', $vendorDir . '/illuminate/conditionable', $vendorDir . '/illuminate/collections', $vendorDir . '/illuminate/support'),
|
||||
'Illuminate\\Pagination\\' => array($vendorDir . '/illuminate/pagination'),
|
||||
'Illuminate\\Contracts\\' => array($vendorDir . '/illuminate/contracts'),
|
||||
'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/src'),
|
||||
'Carbon\\Doctrine\\' => array($vendorDir . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine'),
|
||||
'Carbon\\' => array($vendorDir . '/nesbot/carbon/src/Carbon'),
|
||||
);
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit9b9826e5b5cc7806cd328c4112cca75e', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit9b9826e5b5cc7806cd328c4112cca75e', 'loadClassLoader'));
|
||||
|
||||
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
|
||||
if ($useStaticLoader) {
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e::getInitializer($loader));
|
||||
} else {
|
||||
$map = require __DIR__ . '/autoload_namespaces.php';
|
||||
foreach ($map as $namespace => $path) {
|
||||
$loader->set($namespace, $path);
|
||||
}
|
||||
|
||||
$map = require __DIR__ . '/autoload_psr4.php';
|
||||
foreach ($map as $namespace => $path) {
|
||||
$loader->setPsr4($namespace, $path);
|
||||
}
|
||||
|
||||
$classMap = require __DIR__ . '/autoload_classmap.php';
|
||||
if ($classMap) {
|
||||
$loader->addClassMap($classMap);
|
||||
}
|
||||
}
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
if ($useStaticLoader) {
|
||||
$includeFiles = Composer\Autoload\ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e::$files;
|
||||
} else {
|
||||
$includeFiles = require __DIR__ . '/autoload_files.php';
|
||||
}
|
||||
foreach ($includeFiles as $fileIdentifier => $file) {
|
||||
composerRequire9b9826e5b5cc7806cd328c4112cca75e($fileIdentifier, $file);
|
||||
}
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileIdentifier
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
function composerRequire9b9826e5b5cc7806cd328c4112cca75e($fileIdentifier, $file)
|
||||
{
|
||||
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||
|
||||
require $file;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
{
|
||||
public static $files = array (
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
|
||||
'606a39d89246991a373564698c2d8383' => __DIR__ . '/..' . '/symfony/polyfill-php85/bootstrap.php',
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
|
||||
'2203a247e6fda86070a5e4e07aed533a' => __DIR__ . '/..' . '/symfony/clock/Resources/now.php',
|
||||
'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
|
||||
'9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php',
|
||||
'23f09fe3194f8c2f70923f90d6702129' => __DIR__ . '/..' . '/illuminate/collections/functions.php',
|
||||
'60799491728b879e74601d83e38b2cad' => __DIR__ . '/..' . '/illuminate/collections/helpers.php',
|
||||
'f625ee536139dfb962a398b200bdb2bd' => __DIR__ . '/..' . '/illuminate/support/functions.php',
|
||||
'72579e7bd17821bb1321b87411366eae' => __DIR__ . '/..' . '/illuminate/support/helpers.php',
|
||||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'v' =>
|
||||
array (
|
||||
'voku\\' => 5,
|
||||
),
|
||||
'W' =>
|
||||
array (
|
||||
'Webklex\\PHPIMAP\\' => 16,
|
||||
),
|
||||
'S' =>
|
||||
array (
|
||||
'Symfony\\Polyfill\\Php85\\' => 23,
|
||||
'Symfony\\Polyfill\\Php84\\' => 23,
|
||||
'Symfony\\Polyfill\\Php83\\' => 23,
|
||||
'Symfony\\Polyfill\\Mbstring\\' => 26,
|
||||
'Symfony\\Contracts\\Translation\\' => 30,
|
||||
'Symfony\\Component\\Translation\\' => 30,
|
||||
'Symfony\\Component\\HttpFoundation\\' => 33,
|
||||
'Symfony\\Component\\Clock\\' => 24,
|
||||
),
|
||||
'P' =>
|
||||
array (
|
||||
'Psr\\SimpleCache\\' => 16,
|
||||
'Psr\\Container\\' => 14,
|
||||
'Psr\\Clock\\' => 10,
|
||||
),
|
||||
'I' =>
|
||||
array (
|
||||
'Illuminate\\Support\\' => 19,
|
||||
'Illuminate\\Pagination\\' => 22,
|
||||
'Illuminate\\Contracts\\' => 21,
|
||||
),
|
||||
'D' =>
|
||||
array (
|
||||
'Doctrine\\Inflector\\' => 19,
|
||||
),
|
||||
'C' =>
|
||||
array (
|
||||
'Carbon\\Doctrine\\' => 16,
|
||||
'Carbon\\' => 7,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'voku\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/voku/portable-ascii/src/voku',
|
||||
),
|
||||
'Webklex\\PHPIMAP\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/webklex/php-imap/src',
|
||||
),
|
||||
'Symfony\\Polyfill\\Php85\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php85',
|
||||
),
|
||||
'Symfony\\Polyfill\\Php84\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php84',
|
||||
),
|
||||
'Symfony\\Polyfill\\Php83\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php83',
|
||||
),
|
||||
'Symfony\\Polyfill\\Mbstring\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
|
||||
),
|
||||
'Symfony\\Contracts\\Translation\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/translation-contracts',
|
||||
),
|
||||
'Symfony\\Component\\Translation\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/translation',
|
||||
),
|
||||
'Symfony\\Component\\HttpFoundation\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/http-foundation',
|
||||
),
|
||||
'Symfony\\Component\\Clock\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/clock',
|
||||
),
|
||||
'Psr\\SimpleCache\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/simple-cache/src',
|
||||
),
|
||||
'Psr\\Container\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/container/src',
|
||||
),
|
||||
'Psr\\Clock\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/clock/src',
|
||||
),
|
||||
'Illuminate\\Support\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/illuminate/macroable',
|
||||
1 => __DIR__ . '/..' . '/illuminate/conditionable',
|
||||
2 => __DIR__ . '/..' . '/illuminate/collections',
|
||||
3 => __DIR__ . '/..' . '/illuminate/support',
|
||||
),
|
||||
'Illuminate\\Pagination\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/illuminate/pagination',
|
||||
),
|
||||
'Illuminate\\Contracts\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/illuminate/contracts',
|
||||
),
|
||||
'Doctrine\\Inflector\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/doctrine/inflector/src',
|
||||
),
|
||||
'Carbon\\Doctrine\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine',
|
||||
),
|
||||
'Carbon\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/nesbot/carbon/src/Carbon',
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
||||
'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
||||
'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
|
||||
'DateInvalidTimeZoneException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php',
|
||||
'DateMalformedIntervalStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php',
|
||||
'DateMalformedPeriodStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php',
|
||||
'DateMalformedStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php',
|
||||
'DateObjectError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php',
|
||||
'DateRangeError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php',
|
||||
'Deprecated' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php',
|
||||
'NoDiscard' => __DIR__ . '/..' . '/symfony/polyfill-php85/Resources/stubs/NoDiscard.php',
|
||||
'Override' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/Override.php',
|
||||
'ReflectionConstant' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
|
||||
'SQLite3Exception' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e::$prefixDirsPsr4;
|
||||
$loader->classMap = ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,248 @@
|
|||
<?php return array(
|
||||
'root' => array(
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'reference' => '981fb9585d0c76e8b9c31812d58dfdd5b56d6454',
|
||||
'name' => '__root__',
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'reference' => '981fb9585d0c76e8b9c31812d58dfdd5b56d6454',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'carbonphp/carbon-doctrine-types' => array(
|
||||
'pretty_version' => '3.2.0',
|
||||
'version' => '3.2.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../carbonphp/carbon-doctrine-types',
|
||||
'aliases' => array(),
|
||||
'reference' => '18ba5ddfec8976260ead6e866180bd5d2f71aa1d',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'doctrine/inflector' => array(
|
||||
'pretty_version' => '2.1.0',
|
||||
'version' => '2.1.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../doctrine/inflector',
|
||||
'aliases' => array(),
|
||||
'reference' => '6d6c96277ea252fc1304627204c3d5e6e15faa3b',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'illuminate/collections' => array(
|
||||
'pretty_version' => 'v12.28.1',
|
||||
'version' => '12.28.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../illuminate/collections',
|
||||
'aliases' => array(),
|
||||
'reference' => '2737a0477a3f4855a71bf8f3d0b8d32596ded628',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'illuminate/conditionable' => array(
|
||||
'pretty_version' => 'v12.28.1',
|
||||
'version' => '12.28.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../illuminate/conditionable',
|
||||
'aliases' => array(),
|
||||
'reference' => 'ec677967c1f2faf90b8428919124d2184a4c9b49',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'illuminate/contracts' => array(
|
||||
'pretty_version' => 'v12.28.1',
|
||||
'version' => '12.28.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../illuminate/contracts',
|
||||
'aliases' => array(),
|
||||
'reference' => 'f1c4cf02c9ab81a9ce47940cf261fa2386ed6c5d',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'illuminate/macroable' => array(
|
||||
'pretty_version' => 'v12.28.1',
|
||||
'version' => '12.28.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../illuminate/macroable',
|
||||
'aliases' => array(),
|
||||
'reference' => 'e862e5648ee34004fa56046b746f490dfa86c613',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'illuminate/pagination' => array(
|
||||
'pretty_version' => 'v12.28.1',
|
||||
'version' => '12.28.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../illuminate/pagination',
|
||||
'aliases' => array(),
|
||||
'reference' => '1d95e70671177108b202e6ceb61bc7bc9924bdbf',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'illuminate/support' => array(
|
||||
'pretty_version' => 'v12.28.1',
|
||||
'version' => '12.28.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../illuminate/support',
|
||||
'aliases' => array(),
|
||||
'reference' => '487bbe527806615b818e87c364d93ba91f27db9b',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'nesbot/carbon' => array(
|
||||
'pretty_version' => '3.10.3',
|
||||
'version' => '3.10.3.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../nesbot/carbon',
|
||||
'aliases' => array(),
|
||||
'reference' => '8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/clock' => array(
|
||||
'pretty_version' => '1.0.0',
|
||||
'version' => '1.0.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/clock',
|
||||
'aliases' => array(),
|
||||
'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/clock-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '1.0',
|
||||
),
|
||||
),
|
||||
'psr/container' => array(
|
||||
'pretty_version' => '2.0.2',
|
||||
'version' => '2.0.2.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/container',
|
||||
'aliases' => array(),
|
||||
'reference' => 'c71ecc56dfe541dbd90c5360474fbc405f8d5963',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/simple-cache' => array(
|
||||
'pretty_version' => '3.0.0',
|
||||
'version' => '3.0.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/simple-cache',
|
||||
'aliases' => array(),
|
||||
'reference' => '764e0b3939f5ca87cb904f570ef9be2d78a07865',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'spatie/once' => array(
|
||||
'dev_requirement' => false,
|
||||
'replaced' => array(
|
||||
0 => '*',
|
||||
),
|
||||
),
|
||||
'symfony/clock' => array(
|
||||
'pretty_version' => 'v7.3.0',
|
||||
'version' => '7.3.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/clock',
|
||||
'aliases' => array(),
|
||||
'reference' => 'b81435fbd6648ea425d1ee96a2d8e68f4ceacd24',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/deprecation-contracts' => array(
|
||||
'pretty_version' => 'v3.6.0',
|
||||
'version' => '3.6.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
|
||||
'aliases' => array(),
|
||||
'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/http-foundation' => array(
|
||||
'pretty_version' => 'v7.3.3',
|
||||
'version' => '7.3.3.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/http-foundation',
|
||||
'aliases' => array(),
|
||||
'reference' => '7475561ec27020196c49bb7c4f178d33d7d3dc00',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-mbstring' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
|
||||
'aliases' => array(),
|
||||
'reference' => '6d857f4d76bd4b343eac26d6b539585d2bc56493',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-php83' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-php83',
|
||||
'aliases' => array(),
|
||||
'reference' => '17f6f9a6b1735c0f163024d959f700cfbc5155e5',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-php84' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-php84',
|
||||
'aliases' => array(),
|
||||
'reference' => 'd8ced4d875142b6a7426000426b8abc631d6b191',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-php85' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-php85',
|
||||
'aliases' => array(),
|
||||
'reference' => 'd4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/translation' => array(
|
||||
'pretty_version' => 'v7.3.3',
|
||||
'version' => '7.3.3.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/translation',
|
||||
'aliases' => array(),
|
||||
'reference' => 'e0837b4cbcef63c754d89a4806575cada743a38d',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/translation-contracts' => array(
|
||||
'pretty_version' => 'v3.6.0',
|
||||
'version' => '3.6.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/translation-contracts',
|
||||
'aliases' => array(),
|
||||
'reference' => 'df210c7a2573f1913b2d17cc95f90f53a73d8f7d',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/translation-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '2.3|3.0',
|
||||
),
|
||||
),
|
||||
'voku/portable-ascii' => array(
|
||||
'pretty_version' => '2.0.3',
|
||||
'version' => '2.0.3.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../voku/portable-ascii',
|
||||
'aliases' => array(),
|
||||
'reference' => 'b1d923f88091c6bf09699efcd7c8a1b1bfd7351d',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'webklex/php-imap' => array(
|
||||
'pretty_version' => '6.2.0',
|
||||
'version' => '6.2.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../webklex/php-imap',
|
||||
'aliases' => array(),
|
||||
'reference' => '6b8ef85d621bbbaf52741b00cca8e9237e2b2e05',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 80200)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2006-2015 Doctrine Project
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Doctrine Inflector
|
||||
|
||||
Doctrine Inflector is a small library that can perform string manipulations
|
||||
with regard to uppercase/lowercase and singular/plural forms of words.
|
||||
|
||||
[](https://github.com/doctrine/inflector/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.0.x)
|
||||
[](https://codecov.io/gh/doctrine/inflector/branch/2.0.x)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"name": "doctrine/inflector",
|
||||
"description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.",
|
||||
"license": "MIT",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"php",
|
||||
"strings",
|
||||
"words",
|
||||
"manipulation",
|
||||
"inflector",
|
||||
"inflection",
|
||||
"uppercase",
|
||||
"lowercase",
|
||||
"singular",
|
||||
"plural"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Guilherme Blanco",
|
||||
"email": "guilhermeblanco@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Roman Borschel",
|
||||
"email": "roman@code-factory.org"
|
||||
},
|
||||
{
|
||||
"name": "Benjamin Eberlei",
|
||||
"email": "kontakt@beberlei.de"
|
||||
},
|
||||
{
|
||||
"name": "Jonathan Wage",
|
||||
"email": "jonwage@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Johannes Schmitt",
|
||||
"email": "schmittjoh@gmail.com"
|
||||
}
|
||||
],
|
||||
"homepage": "https://www.doctrine-project.org/projects/inflector.html",
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^12.0 || ^13.0",
|
||||
"phpstan/phpstan": "^1.12 || ^2.0",
|
||||
"phpstan/phpstan-phpunit": "^1.4 || ^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
|
||||
"phpunit/phpunit": "^8.5 || ^12.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Inflector\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Tests\\Inflector\\": "tests"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
},
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
Introduction
|
||||
============
|
||||
|
||||
The Doctrine Inflector has methods for inflecting text. The features include pluralization,
|
||||
singularization, converting between camelCase and under_score and capitalizing
|
||||
words.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
You can install the Inflector with composer:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ composer require doctrine/inflector
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Using the inflector is easy, you can create a new ``Doctrine\Inflector\Inflector`` instance by using
|
||||
the ``Doctrine\Inflector\InflectorFactory`` class:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Doctrine\Inflector\InflectorFactory;
|
||||
|
||||
$inflector = InflectorFactory::create()->build();
|
||||
|
||||
By default it will create an English inflector. If you want to use another language, just pass the language
|
||||
you want to create an inflector for to the ``createForLanguage()`` method:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Doctrine\Inflector\InflectorFactory;
|
||||
use Doctrine\Inflector\Language;
|
||||
|
||||
$inflector = InflectorFactory::createForLanguage(Language::SPANISH)->build();
|
||||
|
||||
The supported languages are as follows:
|
||||
|
||||
- ``Language::ENGLISH``
|
||||
- ``Language::ESPERANTO``
|
||||
- ``Language::FRENCH``
|
||||
- ``Language::NORWEGIAN_BOKMAL``
|
||||
- ``Language::PORTUGUESE``
|
||||
- ``Language::SPANISH``
|
||||
- ``Language::TURKISH``
|
||||
|
||||
If you want to manually construct the inflector instead of using a factory, you can do so like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Doctrine\Inflector\CachedWordInflector;
|
||||
use Doctrine\Inflector\RulesetInflector;
|
||||
use Doctrine\Inflector\Rules\English;
|
||||
|
||||
$inflector = new Inflector(
|
||||
new CachedWordInflector(new RulesetInflector(
|
||||
English\Rules::getSingularRuleset()
|
||||
)),
|
||||
new CachedWordInflector(new RulesetInflector(
|
||||
English\Rules::getPluralRuleset()
|
||||
))
|
||||
);
|
||||
|
||||
Adding Languages
|
||||
----------------
|
||||
|
||||
If you are interested in adding support for your language, take a look at the other languages defined in the
|
||||
``Doctrine\Inflector\Rules`` namespace and the tests located in ``Doctrine\Tests\Inflector\Rules``. You can copy
|
||||
one of the languages and update the rules for your language.
|
||||
|
||||
Once you have done this, send a pull request to the ``doctrine/inflector`` repository with the additions.
|
||||
|
||||
Custom Setup
|
||||
============
|
||||
|
||||
If you want to setup custom singular and plural rules, you can configure these in the factory:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Doctrine\Inflector\InflectorFactory;
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
use Doctrine\Inflector\Rules\Patterns;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
use Doctrine\Inflector\Rules\Substitution;
|
||||
use Doctrine\Inflector\Rules\Substitutions;
|
||||
use Doctrine\Inflector\Rules\Transformation;
|
||||
use Doctrine\Inflector\Rules\Transformations;
|
||||
use Doctrine\Inflector\Rules\Word;
|
||||
|
||||
$inflector = InflectorFactory::create()
|
||||
->withSingularRules(
|
||||
new Ruleset(
|
||||
new Transformations(
|
||||
new Transformation(new Pattern('/^(bil)er$/i'), '\1'),
|
||||
new Transformation(new Pattern('/^(inflec|contribu)tors$/i'), '\1ta')
|
||||
),
|
||||
new Patterns(new Pattern('singulars')),
|
||||
new Substitutions(new Substitution(new Word('spins'), new Word('spinor')))
|
||||
)
|
||||
)
|
||||
->withPluralRules(
|
||||
new Ruleset(
|
||||
new Transformations(
|
||||
new Transformation(new Pattern('^(bil)er$'), '\1'),
|
||||
new Transformation(new Pattern('^(inflec|contribu)tors$'), '\1ta')
|
||||
),
|
||||
new Patterns(new Pattern('noflect'), new Pattern('abtuse')),
|
||||
new Substitutions(
|
||||
new Substitution(new Word('amaze'), new Word('amazable')),
|
||||
new Substitution(new Word('phone'), new Word('phonezes'))
|
||||
)
|
||||
)
|
||||
)
|
||||
->build();
|
||||
|
||||
No operation inflector
|
||||
----------------------
|
||||
|
||||
The ``Doctrine\Inflector\NoopWordInflector`` may be used to configure an inflector that doesn't perform any operation for
|
||||
pluralization and/or singularization. If will simply return the input as output.
|
||||
|
||||
This is an implementation of the `Null Object design pattern <https://sourcemaking.com/design_patterns/null_object>`_.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
use Doctrine\Inflector\Inflector;
|
||||
use Doctrine\Inflector\NoopWordInflector;
|
||||
|
||||
$inflector = new Inflector(new NoopWordInflector(), new NoopWordInflector());
|
||||
|
||||
Tableize
|
||||
========
|
||||
|
||||
Converts ``ModelName`` to ``model_name``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
echo $inflector->tableize('ModelName'); // model_name
|
||||
|
||||
Classify
|
||||
========
|
||||
|
||||
Converts ``model_name`` to ``ModelName``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
echo $inflector->classify('model_name'); // ModelName
|
||||
|
||||
Camelize
|
||||
========
|
||||
|
||||
This method uses `Classify`_ and then converts the first character to lowercase:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
echo $inflector->camelize('model_name'); // modelName
|
||||
|
||||
Capitalize
|
||||
==========
|
||||
|
||||
Takes a string and capitalizes all of the words, like PHP's built-in
|
||||
``ucwords`` function. This extends that behavior, however, by allowing the
|
||||
word delimiters to be configured, rather than only separating on
|
||||
whitespace.
|
||||
|
||||
Here is an example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$string = 'top-o-the-morning to all_of_you!';
|
||||
|
||||
echo $inflector->capitalize($string); // Top-O-The-Morning To All_of_you!
|
||||
|
||||
echo $inflector->capitalize($string, '-_ '); // Top-O-The-Morning To All_Of_You!
|
||||
|
||||
Pluralize
|
||||
=========
|
||||
|
||||
Returns a word in plural form.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
echo $inflector->pluralize('browser'); // browsers
|
||||
|
||||
Singularize
|
||||
===========
|
||||
|
||||
Returns a word in singular form.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
echo $inflector->singularize('browsers'); // browser
|
||||
|
||||
Urlize
|
||||
======
|
||||
|
||||
Generate a URL friendly string from a string of text:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
echo $inflector->urlize('My first blog post'); // my-first-blog-post
|
||||
|
||||
Unaccent
|
||||
========
|
||||
|
||||
You can unaccent a string of text using the ``unaccent()`` method:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
echo $inflector->unaccent('año'); // ano
|
||||
|
||||
Legacy API
|
||||
==========
|
||||
|
||||
The API present in Inflector 1.x is still available, but will be deprecated in a future release and dropped for 3.0.
|
||||
Support for languages other than English is available in the 2.0 API only.
|
||||
|
||||
Acknowledgements
|
||||
================
|
||||
|
||||
The language rules in this library have been adapted from several different sources, including but not limited to:
|
||||
|
||||
- `Ruby On Rails Inflector <http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>`_
|
||||
- `ICanBoogie Inflector <https://github.com/ICanBoogie/Inflector>`_
|
||||
- `CakePHP Inflector <https://book.cakephp.org/3.0/en/core-libraries/inflector.html>`_
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector;
|
||||
|
||||
class CachedWordInflector implements WordInflector
|
||||
{
|
||||
/** @var WordInflector */
|
||||
private $wordInflector;
|
||||
|
||||
/** @var string[] */
|
||||
private $cache = [];
|
||||
|
||||
public function __construct(WordInflector $wordInflector)
|
||||
{
|
||||
$this->wordInflector = $wordInflector;
|
||||
}
|
||||
|
||||
public function inflect(string $word): string
|
||||
{
|
||||
return $this->cache[$word] ?? $this->cache[$word] = $this->wordInflector->inflect($word);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector;
|
||||
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
use function array_unshift;
|
||||
|
||||
abstract class GenericLanguageInflectorFactory implements LanguageInflectorFactory
|
||||
{
|
||||
/** @var Ruleset[] */
|
||||
private $singularRulesets = [];
|
||||
|
||||
/** @var Ruleset[] */
|
||||
private $pluralRulesets = [];
|
||||
|
||||
final public function __construct()
|
||||
{
|
||||
$this->singularRulesets[] = $this->getSingularRuleset();
|
||||
$this->pluralRulesets[] = $this->getPluralRuleset();
|
||||
}
|
||||
|
||||
final public function build(): Inflector
|
||||
{
|
||||
return new Inflector(
|
||||
new CachedWordInflector(new RulesetInflector(
|
||||
...$this->singularRulesets
|
||||
)),
|
||||
new CachedWordInflector(new RulesetInflector(
|
||||
...$this->pluralRulesets
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
final public function withSingularRules(?Ruleset $singularRules, bool $reset = false): LanguageInflectorFactory
|
||||
{
|
||||
if ($reset) {
|
||||
$this->singularRulesets = [];
|
||||
}
|
||||
|
||||
if ($singularRules instanceof Ruleset) {
|
||||
array_unshift($this->singularRulesets, $singularRules);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function withPluralRules(?Ruleset $pluralRules, bool $reset = false): LanguageInflectorFactory
|
||||
{
|
||||
if ($reset) {
|
||||
$this->pluralRulesets = [];
|
||||
}
|
||||
|
||||
if ($pluralRules instanceof Ruleset) {
|
||||
array_unshift($this->pluralRulesets, $pluralRules);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
abstract protected function getSingularRuleset(): Ruleset;
|
||||
|
||||
abstract protected function getPluralRuleset(): Ruleset;
|
||||
}
|
||||
|
|
@ -0,0 +1,507 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
use function chr;
|
||||
use function function_exists;
|
||||
use function lcfirst;
|
||||
use function mb_strtolower;
|
||||
use function ord;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use function strtolower;
|
||||
use function strtr;
|
||||
use function trim;
|
||||
use function ucwords;
|
||||
|
||||
class Inflector
|
||||
{
|
||||
private const ACCENTED_CHARACTERS = [
|
||||
'À' => 'A',
|
||||
'Á' => 'A',
|
||||
'Â' => 'A',
|
||||
'Ã' => 'A',
|
||||
'Ä' => 'Ae',
|
||||
'Æ' => 'Ae',
|
||||
'Å' => 'Aa',
|
||||
'æ' => 'a',
|
||||
'Ç' => 'C',
|
||||
'È' => 'E',
|
||||
'É' => 'E',
|
||||
'Ê' => 'E',
|
||||
'Ë' => 'E',
|
||||
'Ì' => 'I',
|
||||
'Í' => 'I',
|
||||
'Î' => 'I',
|
||||
'Ï' => 'I',
|
||||
'Ñ' => 'N',
|
||||
'Ò' => 'O',
|
||||
'Ó' => 'O',
|
||||
'Ô' => 'O',
|
||||
'Õ' => 'O',
|
||||
'Ö' => 'Oe',
|
||||
'Ù' => 'U',
|
||||
'Ú' => 'U',
|
||||
'Û' => 'U',
|
||||
'Ü' => 'Ue',
|
||||
'Ý' => 'Y',
|
||||
'ß' => 'ss',
|
||||
'à' => 'a',
|
||||
'á' => 'a',
|
||||
'â' => 'a',
|
||||
'ã' => 'a',
|
||||
'ä' => 'ae',
|
||||
'å' => 'aa',
|
||||
'ç' => 'c',
|
||||
'è' => 'e',
|
||||
'é' => 'e',
|
||||
'ê' => 'e',
|
||||
'ë' => 'e',
|
||||
'ì' => 'i',
|
||||
'í' => 'i',
|
||||
'î' => 'i',
|
||||
'ï' => 'i',
|
||||
'ñ' => 'n',
|
||||
'ò' => 'o',
|
||||
'ó' => 'o',
|
||||
'ô' => 'o',
|
||||
'õ' => 'o',
|
||||
'ö' => 'oe',
|
||||
'ù' => 'u',
|
||||
'ú' => 'u',
|
||||
'û' => 'u',
|
||||
'ü' => 'ue',
|
||||
'ý' => 'y',
|
||||
'ÿ' => 'y',
|
||||
'Ā' => 'A',
|
||||
'ā' => 'a',
|
||||
'Ă' => 'A',
|
||||
'ă' => 'a',
|
||||
'Ą' => 'A',
|
||||
'ą' => 'a',
|
||||
'Ć' => 'C',
|
||||
'ć' => 'c',
|
||||
'Ĉ' => 'C',
|
||||
'ĉ' => 'c',
|
||||
'Ċ' => 'C',
|
||||
'ċ' => 'c',
|
||||
'Č' => 'C',
|
||||
'č' => 'c',
|
||||
'Ď' => 'D',
|
||||
'ď' => 'd',
|
||||
'Đ' => 'D',
|
||||
'đ' => 'd',
|
||||
'Ē' => 'E',
|
||||
'ē' => 'e',
|
||||
'Ĕ' => 'E',
|
||||
'ĕ' => 'e',
|
||||
'Ė' => 'E',
|
||||
'ė' => 'e',
|
||||
'Ę' => 'E',
|
||||
'ę' => 'e',
|
||||
'Ě' => 'E',
|
||||
'ě' => 'e',
|
||||
'Ĝ' => 'G',
|
||||
'ĝ' => 'g',
|
||||
'Ğ' => 'G',
|
||||
'ğ' => 'g',
|
||||
'Ġ' => 'G',
|
||||
'ġ' => 'g',
|
||||
'Ģ' => 'G',
|
||||
'ģ' => 'g',
|
||||
'Ĥ' => 'H',
|
||||
'ĥ' => 'h',
|
||||
'Ħ' => 'H',
|
||||
'ħ' => 'h',
|
||||
'Ĩ' => 'I',
|
||||
'ĩ' => 'i',
|
||||
'Ī' => 'I',
|
||||
'ī' => 'i',
|
||||
'Ĭ' => 'I',
|
||||
'ĭ' => 'i',
|
||||
'Į' => 'I',
|
||||
'į' => 'i',
|
||||
'İ' => 'I',
|
||||
'ı' => 'i',
|
||||
'IJ' => 'IJ',
|
||||
'ij' => 'ij',
|
||||
'Ĵ' => 'J',
|
||||
'ĵ' => 'j',
|
||||
'Ķ' => 'K',
|
||||
'ķ' => 'k',
|
||||
'ĸ' => 'k',
|
||||
'Ĺ' => 'L',
|
||||
'ĺ' => 'l',
|
||||
'Ļ' => 'L',
|
||||
'ļ' => 'l',
|
||||
'Ľ' => 'L',
|
||||
'ľ' => 'l',
|
||||
'Ŀ' => 'L',
|
||||
'ŀ' => 'l',
|
||||
'Ł' => 'L',
|
||||
'ł' => 'l',
|
||||
'Ń' => 'N',
|
||||
'ń' => 'n',
|
||||
'Ņ' => 'N',
|
||||
'ņ' => 'n',
|
||||
'Ň' => 'N',
|
||||
'ň' => 'n',
|
||||
'ʼn' => 'N',
|
||||
'Ŋ' => 'n',
|
||||
'ŋ' => 'N',
|
||||
'Ō' => 'O',
|
||||
'ō' => 'o',
|
||||
'Ŏ' => 'O',
|
||||
'ŏ' => 'o',
|
||||
'Ő' => 'O',
|
||||
'ő' => 'o',
|
||||
'Œ' => 'OE',
|
||||
'œ' => 'oe',
|
||||
'Ø' => 'O',
|
||||
'ø' => 'o',
|
||||
'Ŕ' => 'R',
|
||||
'ŕ' => 'r',
|
||||
'Ŗ' => 'R',
|
||||
'ŗ' => 'r',
|
||||
'Ř' => 'R',
|
||||
'ř' => 'r',
|
||||
'Ś' => 'S',
|
||||
'ś' => 's',
|
||||
'Ŝ' => 'S',
|
||||
'ŝ' => 's',
|
||||
'Ş' => 'S',
|
||||
'ş' => 's',
|
||||
'Š' => 'S',
|
||||
'š' => 's',
|
||||
'Ţ' => 'T',
|
||||
'ţ' => 't',
|
||||
'Ť' => 'T',
|
||||
'ť' => 't',
|
||||
'Ŧ' => 'T',
|
||||
'ŧ' => 't',
|
||||
'Ũ' => 'U',
|
||||
'ũ' => 'u',
|
||||
'Ū' => 'U',
|
||||
'ū' => 'u',
|
||||
'Ŭ' => 'U',
|
||||
'ŭ' => 'u',
|
||||
'Ů' => 'U',
|
||||
'ů' => 'u',
|
||||
'Ű' => 'U',
|
||||
'ű' => 'u',
|
||||
'Ų' => 'U',
|
||||
'ų' => 'u',
|
||||
'Ŵ' => 'W',
|
||||
'ŵ' => 'w',
|
||||
'Ŷ' => 'Y',
|
||||
'ŷ' => 'y',
|
||||
'Ÿ' => 'Y',
|
||||
'Ź' => 'Z',
|
||||
'ź' => 'z',
|
||||
'Ż' => 'Z',
|
||||
'ż' => 'z',
|
||||
'Ž' => 'Z',
|
||||
'ž' => 'z',
|
||||
'ſ' => 's',
|
||||
'€' => 'E',
|
||||
'£' => '',
|
||||
];
|
||||
|
||||
/** @var WordInflector */
|
||||
private $singularizer;
|
||||
|
||||
/** @var WordInflector */
|
||||
private $pluralizer;
|
||||
|
||||
public function __construct(WordInflector $singularizer, WordInflector $pluralizer)
|
||||
{
|
||||
$this->singularizer = $singularizer;
|
||||
$this->pluralizer = $pluralizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a word into the format for a Doctrine table name. Converts 'ModelName' to 'model_name'.
|
||||
*/
|
||||
public function tableize(string $word): string
|
||||
{
|
||||
$tableized = preg_replace('~(?<=\\w)([A-Z])~u', '_$1', $word);
|
||||
|
||||
if ($tableized === null) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'preg_replace returned null for value "%s"',
|
||||
$word
|
||||
));
|
||||
}
|
||||
|
||||
return mb_strtolower($tableized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a word into the format for a Doctrine class name. Converts 'table_name' to 'TableName'.
|
||||
*/
|
||||
public function classify(string $word): string
|
||||
{
|
||||
return str_replace([' ', '_', '-'], '', ucwords($word, ' _-'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Camelizes a word. This uses the classify() method and turns the first character to lowercase.
|
||||
*/
|
||||
public function camelize(string $word): string
|
||||
{
|
||||
return lcfirst($this->classify($word));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uppercases words with configurable delimiters between words.
|
||||
*
|
||||
* Takes a string and capitalizes all of the words, like PHP's built-in
|
||||
* ucwords function. This extends that behavior, however, by allowing the
|
||||
* word delimiters to be configured, rather than only separating on
|
||||
* whitespace.
|
||||
*
|
||||
* Here is an example:
|
||||
* <code>
|
||||
* <?php
|
||||
* $string = 'top-o-the-morning to all_of_you!';
|
||||
* echo $inflector->capitalize($string);
|
||||
* // Top-O-The-Morning To All_of_you!
|
||||
*
|
||||
* echo $inflector->capitalize($string, '-_ ');
|
||||
* // Top-O-The-Morning To All_Of_You!
|
||||
* ?>
|
||||
* </code>
|
||||
*
|
||||
* @param string $string The string to operate on.
|
||||
* @param string $delimiters A list of word separators.
|
||||
*
|
||||
* @return string The string with all delimiter-separated words capitalized.
|
||||
*/
|
||||
public function capitalize(string $string, string $delimiters = " \n\t\r\0\x0B-"): string
|
||||
{
|
||||
return ucwords($string, $delimiters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given string seems like it has utf8 characters in it.
|
||||
*
|
||||
* @param string $string The string to check for utf8 characters in.
|
||||
*/
|
||||
public function seemsUtf8(string $string): bool
|
||||
{
|
||||
for ($i = 0; $i < strlen($string); $i++) {
|
||||
if (ord($string[$i]) < 0x80) {
|
||||
continue; // 0bbbbbbb
|
||||
}
|
||||
|
||||
if ((ord($string[$i]) & 0xE0) === 0xC0) {
|
||||
$n = 1; // 110bbbbb
|
||||
} elseif ((ord($string[$i]) & 0xF0) === 0xE0) {
|
||||
$n = 2; // 1110bbbb
|
||||
} elseif ((ord($string[$i]) & 0xF8) === 0xF0) {
|
||||
$n = 3; // 11110bbb
|
||||
} elseif ((ord($string[$i]) & 0xFC) === 0xF8) {
|
||||
$n = 4; // 111110bb
|
||||
} elseif ((ord($string[$i]) & 0xFE) === 0xFC) {
|
||||
$n = 5; // 1111110b
|
||||
} else {
|
||||
return false; // Does not match any model
|
||||
}
|
||||
|
||||
for ($j = 0; $j < $n; $j++) { // n bytes matching 10bbbbbb follow ?
|
||||
if (++$i === strlen($string) || ((ord($string[$i]) & 0xC0) !== 0x80)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any illegal characters, accents, etc.
|
||||
*
|
||||
* @param string $string String to unaccent
|
||||
*
|
||||
* @return string Unaccented string
|
||||
*/
|
||||
public function unaccent(string $string): string
|
||||
{
|
||||
if (preg_match('/[\x80-\xff]/', $string) === false) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
if ($this->seemsUtf8($string)) {
|
||||
$string = strtr($string, self::ACCENTED_CHARACTERS);
|
||||
} else {
|
||||
$characters = [];
|
||||
|
||||
// Assume ISO-8859-1 if not UTF-8
|
||||
$characters['in'] =
|
||||
chr(128)
|
||||
. chr(131)
|
||||
. chr(138)
|
||||
. chr(142)
|
||||
. chr(154)
|
||||
. chr(158)
|
||||
. chr(159)
|
||||
. chr(162)
|
||||
. chr(165)
|
||||
. chr(181)
|
||||
. chr(192)
|
||||
. chr(193)
|
||||
. chr(194)
|
||||
. chr(195)
|
||||
. chr(196)
|
||||
. chr(197)
|
||||
. chr(199)
|
||||
. chr(200)
|
||||
. chr(201)
|
||||
. chr(202)
|
||||
. chr(203)
|
||||
. chr(204)
|
||||
. chr(205)
|
||||
. chr(206)
|
||||
. chr(207)
|
||||
. chr(209)
|
||||
. chr(210)
|
||||
. chr(211)
|
||||
. chr(212)
|
||||
. chr(213)
|
||||
. chr(214)
|
||||
. chr(216)
|
||||
. chr(217)
|
||||
. chr(218)
|
||||
. chr(219)
|
||||
. chr(220)
|
||||
. chr(221)
|
||||
. chr(224)
|
||||
. chr(225)
|
||||
. chr(226)
|
||||
. chr(227)
|
||||
. chr(228)
|
||||
. chr(229)
|
||||
. chr(231)
|
||||
. chr(232)
|
||||
. chr(233)
|
||||
. chr(234)
|
||||
. chr(235)
|
||||
. chr(236)
|
||||
. chr(237)
|
||||
. chr(238)
|
||||
. chr(239)
|
||||
. chr(241)
|
||||
. chr(242)
|
||||
. chr(243)
|
||||
. chr(244)
|
||||
. chr(245)
|
||||
. chr(246)
|
||||
. chr(248)
|
||||
. chr(249)
|
||||
. chr(250)
|
||||
. chr(251)
|
||||
. chr(252)
|
||||
. chr(253)
|
||||
. chr(255);
|
||||
|
||||
$characters['out'] = 'EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy';
|
||||
|
||||
$string = strtr($string, $characters['in'], $characters['out']);
|
||||
|
||||
$doubleChars = [];
|
||||
|
||||
$doubleChars['in'] = [
|
||||
chr(140),
|
||||
chr(156),
|
||||
chr(198),
|
||||
chr(208),
|
||||
chr(222),
|
||||
chr(223),
|
||||
chr(230),
|
||||
chr(240),
|
||||
chr(254),
|
||||
];
|
||||
|
||||
$doubleChars['out'] = ['OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th'];
|
||||
|
||||
$string = str_replace($doubleChars['in'], $doubleChars['out'], $string);
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any passed string to a url friendly string.
|
||||
* Converts 'My first blog post' to 'my-first-blog-post'
|
||||
*
|
||||
* @param string $string String to urlize.
|
||||
*
|
||||
* @return string Urlized string.
|
||||
*/
|
||||
public function urlize(string $string): string
|
||||
{
|
||||
// Remove all non url friendly characters with the unaccent function
|
||||
$unaccented = $this->unaccent($string);
|
||||
|
||||
if (function_exists('mb_strtolower')) {
|
||||
$lowered = mb_strtolower($unaccented);
|
||||
} else {
|
||||
$lowered = strtolower($unaccented);
|
||||
}
|
||||
|
||||
$replacements = [
|
||||
'/\W/' => ' ',
|
||||
'/([A-Z]+)([A-Z][a-z])/' => '\1_\2',
|
||||
'/([a-z\d])([A-Z])/' => '\1_\2',
|
||||
'/[^A-Z^a-z^0-9^\/]+/' => '-',
|
||||
];
|
||||
|
||||
$urlized = $lowered;
|
||||
|
||||
foreach ($replacements as $pattern => $replacement) {
|
||||
$replaced = preg_replace($pattern, $replacement, $urlized);
|
||||
|
||||
if ($replaced === null) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'preg_replace returned null for value "%s"',
|
||||
$urlized
|
||||
));
|
||||
}
|
||||
|
||||
$urlized = $replaced;
|
||||
}
|
||||
|
||||
return trim($urlized, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a word in singular form.
|
||||
*
|
||||
* @param string $word The word in plural form.
|
||||
*
|
||||
* @return string The word in singular form.
|
||||
*/
|
||||
public function singularize(string $word): string
|
||||
{
|
||||
return $this->singularizer->inflect($word);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a word in plural form.
|
||||
*
|
||||
* @param string $word The word in singular form.
|
||||
*
|
||||
* @return string The word in plural form.
|
||||
*/
|
||||
public function pluralize(string $word): string
|
||||
{
|
||||
return $this->pluralizer->inflect($word);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector;
|
||||
|
||||
use Doctrine\Inflector\Rules\English;
|
||||
use Doctrine\Inflector\Rules\Esperanto;
|
||||
use Doctrine\Inflector\Rules\French;
|
||||
use Doctrine\Inflector\Rules\Italian;
|
||||
use Doctrine\Inflector\Rules\NorwegianBokmal;
|
||||
use Doctrine\Inflector\Rules\Portuguese;
|
||||
use Doctrine\Inflector\Rules\Spanish;
|
||||
use Doctrine\Inflector\Rules\Turkish;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InflectorFactory
|
||||
{
|
||||
public static function create(): LanguageInflectorFactory
|
||||
{
|
||||
return self::createForLanguage(Language::ENGLISH);
|
||||
}
|
||||
|
||||
public static function createForLanguage(string $language): LanguageInflectorFactory
|
||||
{
|
||||
switch ($language) {
|
||||
case Language::ENGLISH:
|
||||
return new English\InflectorFactory();
|
||||
|
||||
case Language::ESPERANTO:
|
||||
return new Esperanto\InflectorFactory();
|
||||
|
||||
case Language::FRENCH:
|
||||
return new French\InflectorFactory();
|
||||
|
||||
case Language::ITALIAN:
|
||||
return new Italian\InflectorFactory();
|
||||
|
||||
case Language::NORWEGIAN_BOKMAL:
|
||||
return new NorwegianBokmal\InflectorFactory();
|
||||
|
||||
case Language::PORTUGUESE:
|
||||
return new Portuguese\InflectorFactory();
|
||||
|
||||
case Language::SPANISH:
|
||||
return new Spanish\InflectorFactory();
|
||||
|
||||
case Language::TURKISH:
|
||||
return new Turkish\InflectorFactory();
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Language "%s" is not supported.',
|
||||
$language
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector;
|
||||
|
||||
final class Language
|
||||
{
|
||||
public const ENGLISH = 'english';
|
||||
public const ESPERANTO = 'esperanto';
|
||||
public const FRENCH = 'french';
|
||||
public const ITALIAN = 'italian';
|
||||
public const NORWEGIAN_BOKMAL = 'norwegian-bokmal';
|
||||
public const PORTUGUESE = 'portuguese';
|
||||
public const SPANISH = 'spanish';
|
||||
public const TURKISH = 'turkish';
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector;
|
||||
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
interface LanguageInflectorFactory
|
||||
{
|
||||
/**
|
||||
* Applies custom rules for singularisation
|
||||
*
|
||||
* @param bool $reset If true, will unset default inflections for all new rules
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withSingularRules(?Ruleset $singularRules, bool $reset = false): self;
|
||||
|
||||
/**
|
||||
* Applies custom rules for pluralisation
|
||||
*
|
||||
* @param bool $reset If true, will unset default inflections for all new rules
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withPluralRules(?Ruleset $pluralRules, bool $reset = false): self;
|
||||
|
||||
/**
|
||||
* Builds the inflector instance with all applicable rules
|
||||
*/
|
||||
public function build(): Inflector;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector;
|
||||
|
||||
class NoopWordInflector implements WordInflector
|
||||
{
|
||||
public function inflect(string $word): string
|
||||
{
|
||||
return $word;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\English;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
use Doctrine\Inflector\Rules\Substitution;
|
||||
use Doctrine\Inflector\Rules\Transformation;
|
||||
use Doctrine\Inflector\Rules\Word;
|
||||
|
||||
class Inflectible
|
||||
{
|
||||
/** @return Transformation[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('(s)tatuses$'), '\1\2tatus');
|
||||
yield new Transformation(new Pattern('(s)tatus$'), '\1\2tatus');
|
||||
yield new Transformation(new Pattern('(c)ampus$'), '\1\2ampus');
|
||||
yield new Transformation(new Pattern('^(.*)(menu)s$'), '\1\2');
|
||||
yield new Transformation(new Pattern('(quiz)zes$'), '\\1');
|
||||
yield new Transformation(new Pattern('(matr)ices$'), '\1ix');
|
||||
yield new Transformation(new Pattern('(vert|ind)ices$'), '\1ex');
|
||||
yield new Transformation(new Pattern('^(ox)en'), '\1');
|
||||
yield new Transformation(new Pattern('(alias)(es)*$'), '\1');
|
||||
yield new Transformation(new Pattern('(buffal|her|potat|tomat|volcan)oes$'), '\1o');
|
||||
yield new Transformation(new Pattern('(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$'), '\1us');
|
||||
yield new Transformation(new Pattern('([ftw]ax)es'), '\1');
|
||||
yield new Transformation(new Pattern('(analys|ax|cris|test|thes)es$'), '\1is');
|
||||
yield new Transformation(new Pattern('(shoe|slave)s$'), '\1');
|
||||
yield new Transformation(new Pattern('(o)es$'), '\1');
|
||||
yield new Transformation(new Pattern('ouses$'), 'ouse');
|
||||
yield new Transformation(new Pattern('([^a])uses$'), '\1us');
|
||||
yield new Transformation(new Pattern('([m|l])ice$'), '\1ouse');
|
||||
yield new Transformation(new Pattern('(x|ch|ss|sh)es$'), '\1');
|
||||
yield new Transformation(new Pattern('(m)ovies$'), '\1\2ovie');
|
||||
yield new Transformation(new Pattern('(s)eries$'), '\1\2eries');
|
||||
yield new Transformation(new Pattern('([^aeiouy]|qu)ies$'), '\1y');
|
||||
yield new Transformation(new Pattern('([lr])ves$'), '\1f');
|
||||
yield new Transformation(new Pattern('(tive)s$'), '\1');
|
||||
yield new Transformation(new Pattern('(hive)s$'), '\1');
|
||||
yield new Transformation(new Pattern('(drive)s$'), '\1');
|
||||
yield new Transformation(new Pattern('(dive)s$'), '\1');
|
||||
yield new Transformation(new Pattern('(olive)s$'), '\1');
|
||||
yield new Transformation(new Pattern('([^fo])ves$'), '\1fe');
|
||||
yield new Transformation(new Pattern('(^analy)ses$'), '\1sis');
|
||||
yield new Transformation(new Pattern('(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$'), '\1\2sis');
|
||||
yield new Transformation(new Pattern('(tax)a$'), '\1on');
|
||||
yield new Transformation(new Pattern('(c)riteria$'), '\1riterion');
|
||||
yield new Transformation(new Pattern('([ti])a(?<!regatta)$'), '\1um');
|
||||
yield new Transformation(new Pattern('(p)eople$'), '\1\2erson');
|
||||
yield new Transformation(new Pattern('(m)en$'), '\1an');
|
||||
yield new Transformation(new Pattern('(c)hildren$'), '\1\2hild');
|
||||
yield new Transformation(new Pattern('(f)eet$'), '\1oot');
|
||||
yield new Transformation(new Pattern('(n)ews$'), '\1\2ews');
|
||||
yield new Transformation(new Pattern('eaus$'), 'eau');
|
||||
yield new Transformation(new Pattern('^tights$'), 'tights');
|
||||
yield new Transformation(new Pattern('^shorts$'), 'shorts');
|
||||
yield new Transformation(new Pattern('s$'), '');
|
||||
}
|
||||
|
||||
/** @return Transformation[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('(s)tatus$'), '\1\2tatuses');
|
||||
yield new Transformation(new Pattern('(quiz)$'), '\1zes');
|
||||
yield new Transformation(new Pattern('^(ox)$'), '\1\2en');
|
||||
yield new Transformation(new Pattern('([m|l])ouse$'), '\1ice');
|
||||
yield new Transformation(new Pattern('(matr|vert|ind)(ix|ex)$'), '\1ices');
|
||||
yield new Transformation(new Pattern('(x|ch|ss|sh)$'), '\1es');
|
||||
yield new Transformation(new Pattern('([^aeiouy]|qu)y$'), '\1ies');
|
||||
yield new Transformation(new Pattern('(hive|gulf)$'), '\1s');
|
||||
yield new Transformation(new Pattern('(?:([^f])fe|([lr])f)$'), '\1\2ves');
|
||||
yield new Transformation(new Pattern('sis$'), 'ses');
|
||||
yield new Transformation(new Pattern('([ti])um$'), '\1a');
|
||||
yield new Transformation(new Pattern('(tax)on$'), '\1a');
|
||||
yield new Transformation(new Pattern('(c)riterion$'), '\1riteria');
|
||||
yield new Transformation(new Pattern('(p)erson$'), '\1eople');
|
||||
yield new Transformation(new Pattern('(m)an$'), '\1en');
|
||||
yield new Transformation(new Pattern('(c)hild$'), '\1hildren');
|
||||
yield new Transformation(new Pattern('(f)oot$'), '\1eet');
|
||||
yield new Transformation(new Pattern('(buffal|her|potat|tomat|volcan)o$'), '\1\2oes');
|
||||
yield new Transformation(new Pattern('(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$'), '\1i');
|
||||
yield new Transformation(new Pattern('us$'), 'uses');
|
||||
yield new Transformation(new Pattern('(alias)$'), '\1es');
|
||||
yield new Transformation(new Pattern('(analys|ax|cris|test|thes)is$'), '\1es');
|
||||
yield new Transformation(new Pattern('s$'), 's');
|
||||
yield new Transformation(new Pattern('^$'), '');
|
||||
yield new Transformation(new Pattern('$'), 's');
|
||||
}
|
||||
|
||||
/** @return Substitution[] */
|
||||
public static function getIrregular(): iterable
|
||||
{
|
||||
yield new Substitution(new Word('abuse'), new Word('abuses'));
|
||||
yield new Substitution(new Word('alga'), new Word('algae'));
|
||||
yield new Substitution(new Word('atlas'), new Word('atlases'));
|
||||
yield new Substitution(new Word('avalanche'), new Word('avalanches'));
|
||||
yield new Substitution(new Word('axis'), new Word('axes'));
|
||||
yield new Substitution(new Word('axe'), new Word('axes'));
|
||||
yield new Substitution(new Word('beef'), new Word('beefs'));
|
||||
yield new Substitution(new Word('blouse'), new Word('blouses'));
|
||||
yield new Substitution(new Word('brother'), new Word('brothers'));
|
||||
yield new Substitution(new Word('brownie'), new Word('brownies'));
|
||||
yield new Substitution(new Word('cache'), new Word('caches'));
|
||||
yield new Substitution(new Word('cafe'), new Word('cafes'));
|
||||
yield new Substitution(new Word('canvas'), new Word('canvases'));
|
||||
yield new Substitution(new Word('cave'), new Word('caves'));
|
||||
yield new Substitution(new Word('chateau'), new Word('chateaux'));
|
||||
yield new Substitution(new Word('child'), new Word('children'));
|
||||
yield new Substitution(new Word('cookie'), new Word('cookies'));
|
||||
yield new Substitution(new Word('corpus'), new Word('corpuses'));
|
||||
yield new Substitution(new Word('cow'), new Word('cows'));
|
||||
yield new Substitution(new Word('criterion'), new Word('criteria'));
|
||||
yield new Substitution(new Word('curriculum'), new Word('curricula'));
|
||||
yield new Substitution(new Word('curve'), new Word('curves'));
|
||||
yield new Substitution(new Word('demo'), new Word('demos'));
|
||||
yield new Substitution(new Word('die'), new Word('dice'));
|
||||
yield new Substitution(new Word('domino'), new Word('dominoes'));
|
||||
yield new Substitution(new Word('echo'), new Word('echoes'));
|
||||
yield new Substitution(new Word('emphasis'), new Word('emphases'));
|
||||
yield new Substitution(new Word('epoch'), new Word('epochs'));
|
||||
yield new Substitution(new Word('foe'), new Word('foes'));
|
||||
yield new Substitution(new Word('foot'), new Word('feet'));
|
||||
yield new Substitution(new Word('fungus'), new Word('fungi'));
|
||||
yield new Substitution(new Word('ganglion'), new Word('ganglions'));
|
||||
yield new Substitution(new Word('gas'), new Word('gases'));
|
||||
yield new Substitution(new Word('genie'), new Word('genies'));
|
||||
yield new Substitution(new Word('genus'), new Word('genera'));
|
||||
yield new Substitution(new Word('goose'), new Word('geese'));
|
||||
yield new Substitution(new Word('graffito'), new Word('graffiti'));
|
||||
yield new Substitution(new Word('grave'), new Word('graves'));
|
||||
yield new Substitution(new Word('hippopotamus'), new Word('hippopotami'));
|
||||
yield new Substitution(new Word('hoax'), new Word('hoaxes'));
|
||||
yield new Substitution(new Word('hoof'), new Word('hoofs'));
|
||||
yield new Substitution(new Word('human'), new Word('humans'));
|
||||
yield new Substitution(new Word('iris'), new Word('irises'));
|
||||
yield new Substitution(new Word('larva'), new Word('larvae'));
|
||||
yield new Substitution(new Word('leaf'), new Word('leaves'));
|
||||
yield new Substitution(new Word('lens'), new Word('lenses'));
|
||||
yield new Substitution(new Word('loaf'), new Word('loaves'));
|
||||
yield new Substitution(new Word('man'), new Word('men'));
|
||||
yield new Substitution(new Word('medium'), new Word('media'));
|
||||
yield new Substitution(new Word('memorandum'), new Word('memoranda'));
|
||||
yield new Substitution(new Word('money'), new Word('monies'));
|
||||
yield new Substitution(new Word('mongoose'), new Word('mongooses'));
|
||||
yield new Substitution(new Word('motto'), new Word('mottoes'));
|
||||
yield new Substitution(new Word('move'), new Word('moves'));
|
||||
yield new Substitution(new Word('mythos'), new Word('mythoi'));
|
||||
yield new Substitution(new Word('neurosis'), new Word('neuroses'));
|
||||
yield new Substitution(new Word('niche'), new Word('niches'));
|
||||
yield new Substitution(new Word('niveau'), new Word('niveaux'));
|
||||
yield new Substitution(new Word('nucleus'), new Word('nuclei'));
|
||||
yield new Substitution(new Word('numen'), new Word('numina'));
|
||||
yield new Substitution(new Word('nursery'), new Word('nurseries'));
|
||||
yield new Substitution(new Word('oasis'), new Word('oases'));
|
||||
yield new Substitution(new Word('occiput'), new Word('occiputs'));
|
||||
yield new Substitution(new Word('octopus'), new Word('octopuses'));
|
||||
yield new Substitution(new Word('opus'), new Word('opuses'));
|
||||
yield new Substitution(new Word('ox'), new Word('oxen'));
|
||||
yield new Substitution(new Word('passerby'), new Word('passersby'));
|
||||
yield new Substitution(new Word('penis'), new Word('penises'));
|
||||
yield new Substitution(new Word('person'), new Word('people'));
|
||||
yield new Substitution(new Word('plateau'), new Word('plateaux'));
|
||||
yield new Substitution(new Word('runner-up'), new Word('runners-up'));
|
||||
yield new Substitution(new Word('safe'), new Word('safes'));
|
||||
yield new Substitution(new Word('save'), new Word('saves'));
|
||||
yield new Substitution(new Word('sex'), new Word('sexes'));
|
||||
yield new Substitution(new Word('sieve'), new Word('sieves'));
|
||||
yield new Substitution(new Word('soliloquy'), new Word('soliloquies'));
|
||||
yield new Substitution(new Word('son-in-law'), new Word('sons-in-law'));
|
||||
yield new Substitution(new Word('stadium'), new Word('stadiums'));
|
||||
yield new Substitution(new Word('syllabus'), new Word('syllabi'));
|
||||
yield new Substitution(new Word('testis'), new Word('testes'));
|
||||
yield new Substitution(new Word('thief'), new Word('thieves'));
|
||||
yield new Substitution(new Word('tooth'), new Word('teeth'));
|
||||
yield new Substitution(new Word('tornado'), new Word('tornadoes'));
|
||||
yield new Substitution(new Word('trilby'), new Word('trilbys'));
|
||||
yield new Substitution(new Word('turf'), new Word('turfs'));
|
||||
yield new Substitution(new Word('valve'), new Word('valves'));
|
||||
yield new Substitution(new Word('volcano'), new Word('volcanoes'));
|
||||
yield new Substitution(new Word('wave'), new Word('waves'));
|
||||
yield new Substitution(new Word('zombie'), new Word('zombies'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\English;
|
||||
|
||||
use Doctrine\Inflector\GenericLanguageInflectorFactory;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
final class InflectorFactory extends GenericLanguageInflectorFactory
|
||||
{
|
||||
protected function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getSingularRuleset();
|
||||
}
|
||||
|
||||
protected function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getPluralRuleset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\English;
|
||||
|
||||
use Doctrine\Inflector\Rules\Patterns;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
use Doctrine\Inflector\Rules\Substitutions;
|
||||
use Doctrine\Inflector\Rules\Transformations;
|
||||
|
||||
final class Rules
|
||||
{
|
||||
public static function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getSingular()),
|
||||
new Patterns(...Uninflected::getSingular()),
|
||||
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getPlural()),
|
||||
new Patterns(...Uninflected::getPlural()),
|
||||
new Substitutions(...Inflectible::getIrregular())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\English;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
|
||||
final class Uninflected
|
||||
{
|
||||
/** @return Pattern[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
|
||||
yield new Pattern('.*ss');
|
||||
yield new Pattern('clothes');
|
||||
yield new Pattern('data');
|
||||
yield new Pattern('fascia');
|
||||
yield new Pattern('fuchsia');
|
||||
yield new Pattern('galleria');
|
||||
yield new Pattern('mafia');
|
||||
yield new Pattern('militia');
|
||||
yield new Pattern('pants');
|
||||
yield new Pattern('petunia');
|
||||
yield new Pattern('sepia');
|
||||
yield new Pattern('trivia');
|
||||
yield new Pattern('utopia');
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
|
||||
yield new Pattern('people');
|
||||
yield new Pattern('trivia');
|
||||
yield new Pattern('\w+ware$');
|
||||
yield new Pattern('media');
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
private static function getDefault(): iterable
|
||||
{
|
||||
yield new Pattern('\w+media');
|
||||
yield new Pattern('advice');
|
||||
yield new Pattern('aircraft');
|
||||
yield new Pattern('amoyese');
|
||||
yield new Pattern('art');
|
||||
yield new Pattern('audio');
|
||||
yield new Pattern('baggage');
|
||||
yield new Pattern('bison');
|
||||
yield new Pattern('borghese');
|
||||
yield new Pattern('bream');
|
||||
yield new Pattern('breeches');
|
||||
yield new Pattern('britches');
|
||||
yield new Pattern('buffalo');
|
||||
yield new Pattern('butter');
|
||||
yield new Pattern('cantus');
|
||||
yield new Pattern('carp');
|
||||
yield new Pattern('cattle');
|
||||
yield new Pattern('chassis');
|
||||
yield new Pattern('clippers');
|
||||
yield new Pattern('clothing');
|
||||
yield new Pattern('coal');
|
||||
yield new Pattern('cod');
|
||||
yield new Pattern('coitus');
|
||||
yield new Pattern('compensation');
|
||||
yield new Pattern('congoese');
|
||||
yield new Pattern('contretemps');
|
||||
yield new Pattern('coreopsis');
|
||||
yield new Pattern('corps');
|
||||
yield new Pattern('cotton');
|
||||
yield new Pattern('data');
|
||||
yield new Pattern('debris');
|
||||
yield new Pattern('deer');
|
||||
yield new Pattern('diabetes');
|
||||
yield new Pattern('djinn');
|
||||
yield new Pattern('education');
|
||||
yield new Pattern('eland');
|
||||
yield new Pattern('elk');
|
||||
yield new Pattern('emoji');
|
||||
yield new Pattern('equipment');
|
||||
yield new Pattern('evidence');
|
||||
yield new Pattern('faroese');
|
||||
yield new Pattern('feedback');
|
||||
yield new Pattern('fish');
|
||||
yield new Pattern('flounder');
|
||||
yield new Pattern('flour');
|
||||
yield new Pattern('foochowese');
|
||||
yield new Pattern('food');
|
||||
yield new Pattern('furniture');
|
||||
yield new Pattern('gallows');
|
||||
yield new Pattern('genevese');
|
||||
yield new Pattern('genoese');
|
||||
yield new Pattern('gilbertese');
|
||||
yield new Pattern('gold');
|
||||
yield new Pattern('headquarters');
|
||||
yield new Pattern('herpes');
|
||||
yield new Pattern('hijinks');
|
||||
yield new Pattern('homework');
|
||||
yield new Pattern('hottentotese');
|
||||
yield new Pattern('impatience');
|
||||
yield new Pattern('information');
|
||||
yield new Pattern('innings');
|
||||
yield new Pattern('jackanapes');
|
||||
yield new Pattern('jeans');
|
||||
yield new Pattern('jedi');
|
||||
yield new Pattern('kin');
|
||||
yield new Pattern('kiplingese');
|
||||
yield new Pattern('knowledge');
|
||||
yield new Pattern('kongoese');
|
||||
yield new Pattern('leather');
|
||||
yield new Pattern('love');
|
||||
yield new Pattern('lucchese');
|
||||
yield new Pattern('luggage');
|
||||
yield new Pattern('mackerel');
|
||||
yield new Pattern('Maltese');
|
||||
yield new Pattern('management');
|
||||
yield new Pattern('metadata');
|
||||
yield new Pattern('mews');
|
||||
yield new Pattern('money');
|
||||
yield new Pattern('moose');
|
||||
yield new Pattern('mumps');
|
||||
yield new Pattern('music');
|
||||
yield new Pattern('nankingese');
|
||||
yield new Pattern('news');
|
||||
yield new Pattern('nexus');
|
||||
yield new Pattern('niasese');
|
||||
yield new Pattern('nutrition');
|
||||
yield new Pattern('offspring');
|
||||
yield new Pattern('oil');
|
||||
yield new Pattern('patience');
|
||||
yield new Pattern('pekingese');
|
||||
yield new Pattern('piedmontese');
|
||||
yield new Pattern('pincers');
|
||||
yield new Pattern('pistoiese');
|
||||
yield new Pattern('plankton');
|
||||
yield new Pattern('pliers');
|
||||
yield new Pattern('pokemon');
|
||||
yield new Pattern('police');
|
||||
yield new Pattern('polish');
|
||||
yield new Pattern('portuguese');
|
||||
yield new Pattern('proceedings');
|
||||
yield new Pattern('progress');
|
||||
yield new Pattern('rabies');
|
||||
yield new Pattern('rain');
|
||||
yield new Pattern('research');
|
||||
yield new Pattern('rhinoceros');
|
||||
yield new Pattern('rice');
|
||||
yield new Pattern('salmon');
|
||||
yield new Pattern('sand');
|
||||
yield new Pattern('sarawakese');
|
||||
yield new Pattern('scissors');
|
||||
yield new Pattern('sea[- ]bass');
|
||||
yield new Pattern('series');
|
||||
yield new Pattern('shavese');
|
||||
yield new Pattern('shears');
|
||||
yield new Pattern('sheep');
|
||||
yield new Pattern('siemens');
|
||||
yield new Pattern('silk');
|
||||
yield new Pattern('sms');
|
||||
yield new Pattern('soap');
|
||||
yield new Pattern('social media');
|
||||
yield new Pattern('spam');
|
||||
yield new Pattern('species');
|
||||
yield new Pattern('staff');
|
||||
yield new Pattern('sugar');
|
||||
yield new Pattern('swine');
|
||||
yield new Pattern('talent');
|
||||
yield new Pattern('toothpaste');
|
||||
yield new Pattern('traffic');
|
||||
yield new Pattern('travel');
|
||||
yield new Pattern('trousers');
|
||||
yield new Pattern('trout');
|
||||
yield new Pattern('tuna');
|
||||
yield new Pattern('us');
|
||||
yield new Pattern('vermontese');
|
||||
yield new Pattern('vinegar');
|
||||
yield new Pattern('weather');
|
||||
yield new Pattern('wenchowese');
|
||||
yield new Pattern('wheat');
|
||||
yield new Pattern('whiting');
|
||||
yield new Pattern('wildebeest');
|
||||
yield new Pattern('wood');
|
||||
yield new Pattern('wool');
|
||||
yield new Pattern('yengeese');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Esperanto;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
use Doctrine\Inflector\Rules\Substitution;
|
||||
use Doctrine\Inflector\Rules\Transformation;
|
||||
use Doctrine\Inflector\Rules\Word;
|
||||
|
||||
class Inflectible
|
||||
{
|
||||
/** @return Transformation[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('oj$'), 'o');
|
||||
}
|
||||
|
||||
/** @return Transformation[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('o$'), 'oj');
|
||||
}
|
||||
|
||||
/** @return Substitution[] */
|
||||
public static function getIrregular(): iterable
|
||||
{
|
||||
yield new Substitution(new Word(''), new Word(''));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Esperanto;
|
||||
|
||||
use Doctrine\Inflector\GenericLanguageInflectorFactory;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
final class InflectorFactory extends GenericLanguageInflectorFactory
|
||||
{
|
||||
protected function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getSingularRuleset();
|
||||
}
|
||||
|
||||
protected function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getPluralRuleset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Esperanto;
|
||||
|
||||
use Doctrine\Inflector\Rules\Patterns;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
use Doctrine\Inflector\Rules\Substitutions;
|
||||
use Doctrine\Inflector\Rules\Transformations;
|
||||
|
||||
final class Rules
|
||||
{
|
||||
public static function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getSingular()),
|
||||
new Patterns(...Uninflected::getSingular()),
|
||||
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getPlural()),
|
||||
new Patterns(...Uninflected::getPlural()),
|
||||
new Substitutions(...Inflectible::getIrregular())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Esperanto;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
|
||||
final class Uninflected
|
||||
{
|
||||
/** @return Pattern[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
private static function getDefault(): iterable
|
||||
{
|
||||
yield new Pattern('');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\French;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
use Doctrine\Inflector\Rules\Substitution;
|
||||
use Doctrine\Inflector\Rules\Transformation;
|
||||
use Doctrine\Inflector\Rules\Word;
|
||||
|
||||
class Inflectible
|
||||
{
|
||||
/** @return Transformation[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/(b|cor|ém|gemm|soupir|trav|vant|vitr)aux$/'), '\1ail');
|
||||
yield new Transformation(new Pattern('/ails$/'), 'ail');
|
||||
yield new Transformation(new Pattern('/(journ|chev|loc)aux$/'), '\1al');
|
||||
yield new Transformation(new Pattern('/(bijou|caillou|chou|genou|hibou|joujou|pou|au|eu|eau)x$/'), '\1');
|
||||
yield new Transformation(new Pattern('/s$/'), '');
|
||||
}
|
||||
|
||||
/** @return Transformation[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/(s|x|z)$/'), '\1');
|
||||
yield new Transformation(new Pattern('/(b|cor|ém|gemm|soupir|trav|vant|vitr)ail$/'), '\1aux');
|
||||
yield new Transformation(new Pattern('/ail$/'), 'ails');
|
||||
yield new Transformation(new Pattern('/(chacal|carnaval|festival|récital)$/'), '\1s');
|
||||
yield new Transformation(new Pattern('/al$/'), 'aux');
|
||||
yield new Transformation(new Pattern('/(bleu|émeu|landau|pneu|sarrau)$/'), '\1s');
|
||||
yield new Transformation(new Pattern('/(bijou|caillou|chou|genou|hibou|joujou|lieu|pou|au|eu|eau)$/'), '\1x');
|
||||
yield new Transformation(new Pattern('/$/'), 's');
|
||||
}
|
||||
|
||||
/** @return Substitution[] */
|
||||
public static function getIrregular(): iterable
|
||||
{
|
||||
yield new Substitution(new Word('monsieur'), new Word('messieurs'));
|
||||
yield new Substitution(new Word('madame'), new Word('mesdames'));
|
||||
yield new Substitution(new Word('mademoiselle'), new Word('mesdemoiselles'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\French;
|
||||
|
||||
use Doctrine\Inflector\GenericLanguageInflectorFactory;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
final class InflectorFactory extends GenericLanguageInflectorFactory
|
||||
{
|
||||
protected function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getSingularRuleset();
|
||||
}
|
||||
|
||||
protected function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getPluralRuleset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\French;
|
||||
|
||||
use Doctrine\Inflector\Rules\Patterns;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
use Doctrine\Inflector\Rules\Substitutions;
|
||||
use Doctrine\Inflector\Rules\Transformations;
|
||||
|
||||
final class Rules
|
||||
{
|
||||
public static function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getSingular()),
|
||||
new Patterns(...Uninflected::getSingular()),
|
||||
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getPlural()),
|
||||
new Patterns(...Uninflected::getPlural()),
|
||||
new Substitutions(...Inflectible::getIrregular())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\French;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
|
||||
final class Uninflected
|
||||
{
|
||||
/** @return Pattern[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
|
||||
yield new Pattern('bois');
|
||||
yield new Pattern('mas');
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
private static function getDefault(): iterable
|
||||
{
|
||||
yield new Pattern('');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Italian;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
use Doctrine\Inflector\Rules\Substitution;
|
||||
use Doctrine\Inflector\Rules\Transformation;
|
||||
use Doctrine\Inflector\Rules\Word;
|
||||
|
||||
class Inflectible
|
||||
{
|
||||
/** @return iterable<Transformation> */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
// Reverse of -sce → -scia (fasce → fascia)
|
||||
yield new Transformation(new Pattern('([aeiou])sce$'), '\\1scia');
|
||||
|
||||
// Reverse of -cie → -cia (farmacia → farmacie)
|
||||
yield new Transformation(new Pattern('cie$'), 'cia');
|
||||
|
||||
// Reverse of -gie → -gia (bugia → bugie)
|
||||
yield new Transformation(new Pattern('gie$'), 'gia');
|
||||
|
||||
// Reverse of -ce → -cia (arance → arancia)
|
||||
yield new Transformation(new Pattern('([^aeiou])ce$'), '\1cia');
|
||||
|
||||
// Reverse of -ge → -gia (valige → valigia)
|
||||
yield new Transformation(new Pattern('([^aeiou])ge$'), '\1gia');
|
||||
|
||||
// Reverse of -chi → -co (bachi → baco)
|
||||
yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])chi$'), '\1co');
|
||||
|
||||
// Reverse of -ghi → -go (laghi → lago)
|
||||
yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])ghi$'), '\1go');
|
||||
|
||||
// Reverse of -ci → -co (medici → medico)
|
||||
yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])ci$'), '\1co');
|
||||
|
||||
// Reverse of -gi → -go (psicologi → psicologo)
|
||||
yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])gi$'), '\1go');
|
||||
|
||||
// Reverse of -i → -io (zii → zio, negozi → negozio)
|
||||
// This is more complex due to Italian's stress patterns, but we'll handle the basic case
|
||||
yield new Transformation(new Pattern('([^aeiou])i$'), '\1io');
|
||||
|
||||
// Handle words that end with -i but should go to -co/-go (amici → amico, not amice)
|
||||
yield new Transformation(new Pattern('([^aeiou])ci$'), '\1co');
|
||||
yield new Transformation(new Pattern('([^aeiou])gi$'), '\1go');
|
||||
|
||||
// Reverse of -a → -e
|
||||
yield new Transformation(new Pattern('e$'), 'a');
|
||||
|
||||
// Reverse of -e → -i
|
||||
yield new Transformation(new Pattern('i$'), 'e');
|
||||
|
||||
// Reverse of -o → -i
|
||||
yield new Transformation(new Pattern('i$'), 'o');
|
||||
}
|
||||
|
||||
/** @return iterable<Transformation> */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
// Words ending in -scia without stress on 'i' become -sce (e.g. fascia → fasce)
|
||||
yield new Transformation(new Pattern('([aeiou])scia$'), '\\1sce');
|
||||
|
||||
// Words ending in -cia/gia with stress on 'i' keep the 'i' in plural
|
||||
yield new Transformation(new Pattern('cia$'), 'cie'); // e.g. farmacia → farmacie
|
||||
yield new Transformation(new Pattern('gia$'), 'gie'); // e.g. bugia → bugie
|
||||
|
||||
// Words ending in -cia/gia without stress on 'i' lose the 'i' in plural
|
||||
yield new Transformation(new Pattern('([^aeiou])cia$'), '\\1ce'); // e.g. arancia → arance
|
||||
yield new Transformation(new Pattern('([^aeiou])gia$'), '\\1ge'); // e.g. valigia → valige
|
||||
|
||||
// Words ending in -co/-go with stress on 'o' become -chi/-ghi
|
||||
yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])co$'), '\\1chi'); // e.g. baco → bachi
|
||||
yield new Transformation(new Pattern('([bcdfghjklmnpqrstvwxyz][aeiou])go$'), '\\1ghi'); // e.g. lago → laghi
|
||||
|
||||
// Words ending in -co/-go with stress on the penultimate syllable become -ci/-gi
|
||||
yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])co$'), '\\1ci'); // e.g. medico → medici
|
||||
yield new Transformation(new Pattern('([aeiou][bcdfghjklmnpqrstvwxyz])go$'), '\\1gi'); // e.g. psicologo → psicologi
|
||||
|
||||
// Words ending in -io with stress on 'i' keep the 'i' in plural
|
||||
yield new Transformation(new Pattern('([^aeiou])io$'), '\\1i'); // e.g. zio → zii
|
||||
|
||||
// Words ending in -io with stress on 'o' lose the 'i' in plural
|
||||
yield new Transformation(new Pattern('([aeiou])io$'), '\\1i'); // e.g. negozio → negozi
|
||||
|
||||
// Standard ending rules
|
||||
yield new Transformation(new Pattern('a$'), 'e'); // -a → -e
|
||||
yield new Transformation(new Pattern('e$'), 'i'); // -e → -i
|
||||
yield new Transformation(new Pattern('o$'), 'i'); // -o → -i
|
||||
}
|
||||
|
||||
/** @return iterable<Substitution> */
|
||||
public static function getIrregular(): iterable
|
||||
{
|
||||
// Irregular substitutions (singular => plural)
|
||||
$irregulars = [
|
||||
'ala' => 'ali',
|
||||
'albergo' => 'alberghi',
|
||||
'amica' => 'amiche',
|
||||
'amico' => 'amici',
|
||||
'ampio' => 'ampi',
|
||||
'arancia' => 'arance',
|
||||
'arma' => 'armi',
|
||||
'asparago' => 'asparagi',
|
||||
'banca' => 'banche',
|
||||
'belga' => 'belgi',
|
||||
'braccio' => 'braccia',
|
||||
'budello' => 'budella',
|
||||
'bue' => 'buoi',
|
||||
'caccia' => 'cacce',
|
||||
'calcagno' => 'calcagna',
|
||||
'camicia' => 'camicie',
|
||||
'cane' => 'cani',
|
||||
'capitale' => 'capitali',
|
||||
'carcere' => 'carceri',
|
||||
'casa' => 'case',
|
||||
'cavaliere' => 'cavalieri',
|
||||
'centinaio' => 'centinaia',
|
||||
'cerchio' => 'cerchia',
|
||||
'cervello' => 'cervella',
|
||||
'chiave' => 'chiavi',
|
||||
'chirurgo' => 'chirurgi',
|
||||
'ciglio' => 'ciglia',
|
||||
'città' => 'città',
|
||||
'corno' => 'corna',
|
||||
'corpo' => 'corpi',
|
||||
'crisi' => 'crisi',
|
||||
'dente' => 'denti',
|
||||
'dio' => 'dei',
|
||||
'dito' => 'dita',
|
||||
'dottore' => 'dottori',
|
||||
'fiore' => 'fiori',
|
||||
'fratello' => 'fratelli',
|
||||
'fuoco' => 'fuochi',
|
||||
'gamba' => 'gambe',
|
||||
'ginocchio' => 'ginocchia',
|
||||
'gioco' => 'giochi',
|
||||
'giornale' => 'giornali',
|
||||
'giraffa' => 'giraffe',
|
||||
'labbro' => 'labbra',
|
||||
'lenzuolo' => 'lenzuola',
|
||||
'libro' => 'libri',
|
||||
'madre' => 'madri',
|
||||
'maestro' => 'maestri',
|
||||
'magico' => 'magici',
|
||||
'mago' => 'maghi',
|
||||
'maniaco' => 'maniaci',
|
||||
'manico' => 'manici',
|
||||
'mano' => 'mani',
|
||||
'medico' => 'medici',
|
||||
'membro' => 'membri',
|
||||
'metropoli' => 'metropoli',
|
||||
'migliaio' => 'migliaia',
|
||||
'miglio' => 'miglia',
|
||||
'mille' => 'mila',
|
||||
'mio' => 'miei',
|
||||
'moglie' => 'mogli',
|
||||
'mosaico' => 'mosaici',
|
||||
'muro' => 'muri',
|
||||
'nemico' => 'nemici',
|
||||
'nome' => 'nomi',
|
||||
'occhio' => 'occhi',
|
||||
'orecchio' => 'orecchi',
|
||||
'osso' => 'ossa',
|
||||
'paio' => 'paia',
|
||||
'pane' => 'pani',
|
||||
'papa' => 'papi',
|
||||
'pasta' => 'paste',
|
||||
'penna' => 'penne',
|
||||
'pesce' => 'pesci',
|
||||
'piede' => 'piedi',
|
||||
'pittore' => 'pittori',
|
||||
'poeta' => 'poeti',
|
||||
'porco' => 'porci',
|
||||
'porto' => 'porti',
|
||||
'problema' => 'problemi',
|
||||
'ragazzo' => 'ragazzi',
|
||||
're' => 're',
|
||||
'rene' => 'reni',
|
||||
'riso' => 'risa',
|
||||
'rosa' => 'rosa',
|
||||
'sale' => 'sali',
|
||||
'sarto' => 'sarti',
|
||||
'scuola' => 'scuole',
|
||||
'serie' => 'serie',
|
||||
'serramento' => 'serramenta',
|
||||
'sorella' => 'sorelle',
|
||||
'specie' => 'specie',
|
||||
'staio' => 'staia',
|
||||
'stazione' => 'stazioni',
|
||||
'strido' => 'strida',
|
||||
'strillo' => 'strilla',
|
||||
'studio' => 'studi',
|
||||
'suo' => 'suoi',
|
||||
'superficie' => 'superfici',
|
||||
'tavolo' => 'tavoli',
|
||||
'tempio' => 'templi',
|
||||
'treno' => 'treni',
|
||||
'tuo' => 'tuoi',
|
||||
'uomo' => 'uomini',
|
||||
'uovo' => 'uova',
|
||||
'urlo' => 'urla',
|
||||
'valigia' => 'valigie',
|
||||
'vestigio' => 'vestigia',
|
||||
'vino' => 'vini',
|
||||
'viola' => 'viola',
|
||||
'zio' => 'zii',
|
||||
];
|
||||
|
||||
foreach ($irregulars as $singular => $plural) {
|
||||
yield new Substitution(new Word($singular), new Word($plural));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Italian;
|
||||
|
||||
use Doctrine\Inflector\GenericLanguageInflectorFactory;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
final class InflectorFactory extends GenericLanguageInflectorFactory
|
||||
{
|
||||
protected function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getSingularRuleset();
|
||||
}
|
||||
|
||||
protected function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getPluralRuleset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Italian;
|
||||
|
||||
use Doctrine\Inflector\Rules\Patterns;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
use Doctrine\Inflector\Rules\Substitutions;
|
||||
use Doctrine\Inflector\Rules\Transformations;
|
||||
|
||||
final class Rules
|
||||
{
|
||||
public static function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getSingular()),
|
||||
new Patterns(...Uninflected::getSingular()),
|
||||
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getPlural()),
|
||||
new Patterns(...Uninflected::getPlural()),
|
||||
new Substitutions(...Inflectible::getIrregular())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Italian;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
|
||||
final class Uninflected
|
||||
{
|
||||
/** @return iterable<Pattern> */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return iterable<Pattern> */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return iterable<Pattern> */
|
||||
private static function getDefault(): iterable
|
||||
{
|
||||
// Invariable words (same form in singular and plural)
|
||||
$invariables = [
|
||||
'alpaca',
|
||||
'auto',
|
||||
'bar',
|
||||
'blu',
|
||||
'boia',
|
||||
'boomerang',
|
||||
'brindisi',
|
||||
'campus',
|
||||
'computer',
|
||||
'crisi',
|
||||
'crocevia',
|
||||
'dopocena',
|
||||
'film',
|
||||
'foto',
|
||||
'fuchsia',
|
||||
'gnu',
|
||||
'gorilla',
|
||||
'gru',
|
||||
'iguana',
|
||||
'kamikaze',
|
||||
'karaoke',
|
||||
'koala',
|
||||
'lama',
|
||||
'menu',
|
||||
'metropoli',
|
||||
'moto',
|
||||
'opossum',
|
||||
'panda',
|
||||
'quiz',
|
||||
'radio',
|
||||
're',
|
||||
'scacciapensieri',
|
||||
'serie',
|
||||
'smartphone',
|
||||
'sosia',
|
||||
'sottoscala',
|
||||
'specie',
|
||||
'sport',
|
||||
'tablet',
|
||||
'taxi',
|
||||
'vaglia',
|
||||
'virtù',
|
||||
'virus',
|
||||
'yogurt',
|
||||
'foto',
|
||||
'fuchsia',
|
||||
];
|
||||
|
||||
foreach ($invariables as $word) {
|
||||
yield new Pattern($word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\NorwegianBokmal;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
use Doctrine\Inflector\Rules\Substitution;
|
||||
use Doctrine\Inflector\Rules\Transformation;
|
||||
use Doctrine\Inflector\Rules\Word;
|
||||
|
||||
class Inflectible
|
||||
{
|
||||
/** @return Transformation[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/re$/i'), 'r');
|
||||
yield new Transformation(new Pattern('/er$/i'), '');
|
||||
}
|
||||
|
||||
/** @return Transformation[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/e$/i'), 'er');
|
||||
yield new Transformation(new Pattern('/r$/i'), 're');
|
||||
yield new Transformation(new Pattern('/$/'), 'er');
|
||||
}
|
||||
|
||||
/** @return Substitution[] */
|
||||
public static function getIrregular(): iterable
|
||||
{
|
||||
yield new Substitution(new Word('konto'), new Word('konti'));
|
||||
}
|
||||
}
|
||||
21
plugins/vendor/doctrine/inflector/src/Rules/NorwegianBokmal/InflectorFactory.php
vendored
Normal file
21
plugins/vendor/doctrine/inflector/src/Rules/NorwegianBokmal/InflectorFactory.php
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\NorwegianBokmal;
|
||||
|
||||
use Doctrine\Inflector\GenericLanguageInflectorFactory;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
final class InflectorFactory extends GenericLanguageInflectorFactory
|
||||
{
|
||||
protected function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getSingularRuleset();
|
||||
}
|
||||
|
||||
protected function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getPluralRuleset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\NorwegianBokmal;
|
||||
|
||||
use Doctrine\Inflector\Rules\Patterns;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
use Doctrine\Inflector\Rules\Substitutions;
|
||||
use Doctrine\Inflector\Rules\Transformations;
|
||||
|
||||
final class Rules
|
||||
{
|
||||
public static function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getSingular()),
|
||||
new Patterns(...Uninflected::getSingular()),
|
||||
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getPlural()),
|
||||
new Patterns(...Uninflected::getPlural()),
|
||||
new Substitutions(...Inflectible::getIrregular())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\NorwegianBokmal;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
|
||||
final class Uninflected
|
||||
{
|
||||
/** @return Pattern[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
private static function getDefault(): iterable
|
||||
{
|
||||
yield new Pattern('barn');
|
||||
yield new Pattern('fjell');
|
||||
yield new Pattern('hus');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
final class Pattern
|
||||
{
|
||||
/** @var string */
|
||||
private $pattern;
|
||||
|
||||
/** @var string */
|
||||
private $regex;
|
||||
|
||||
public function __construct(string $pattern)
|
||||
{
|
||||
$this->pattern = $pattern;
|
||||
|
||||
if (isset($this->pattern[0]) && $this->pattern[0] === '/') {
|
||||
$this->regex = $this->pattern;
|
||||
} else {
|
||||
$this->regex = '/' . $this->pattern . '/i';
|
||||
}
|
||||
}
|
||||
|
||||
public function getPattern(): string
|
||||
{
|
||||
return $this->pattern;
|
||||
}
|
||||
|
||||
public function getRegex(): string
|
||||
{
|
||||
return $this->regex;
|
||||
}
|
||||
|
||||
public function matches(string $word): bool
|
||||
{
|
||||
return preg_match($this->getRegex(), $word) === 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules;
|
||||
|
||||
use function array_map;
|
||||
use function implode;
|
||||
use function preg_match;
|
||||
|
||||
class Patterns
|
||||
{
|
||||
/** @var string */
|
||||
private $regex;
|
||||
|
||||
public function __construct(Pattern ...$patterns)
|
||||
{
|
||||
$patterns = array_map(static function (Pattern $pattern): string {
|
||||
return $pattern->getPattern();
|
||||
}, $patterns);
|
||||
|
||||
$this->regex = '/^(?:' . implode('|', $patterns) . ')$/i';
|
||||
}
|
||||
|
||||
public function matches(string $word): bool
|
||||
{
|
||||
return preg_match($this->regex, $word, $regs) === 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Portuguese;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
use Doctrine\Inflector\Rules\Substitution;
|
||||
use Doctrine\Inflector\Rules\Transformation;
|
||||
use Doctrine\Inflector\Rules\Word;
|
||||
|
||||
class Inflectible
|
||||
{
|
||||
/** @return Transformation[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/^(g|)ases$/i'), '\1ás');
|
||||
yield new Transformation(new Pattern('/(japon|escoc|ingl|dinamarqu|fregu|portugu)eses$/i'), '\1ês');
|
||||
yield new Transformation(new Pattern('/(ae|ao|oe)s$/'), 'ao');
|
||||
yield new Transformation(new Pattern('/(ãe|ão|õe)s$/'), 'ão');
|
||||
yield new Transformation(new Pattern('/^(.*[^s]s)es$/i'), '\1');
|
||||
yield new Transformation(new Pattern('/sses$/i'), 'sse');
|
||||
yield new Transformation(new Pattern('/ns$/i'), 'm');
|
||||
yield new Transformation(new Pattern('/(r|t|f|v)is$/i'), '\1il');
|
||||
yield new Transformation(new Pattern('/uis$/i'), 'ul');
|
||||
yield new Transformation(new Pattern('/ois$/i'), 'ol');
|
||||
yield new Transformation(new Pattern('/eis$/i'), 'ei');
|
||||
yield new Transformation(new Pattern('/éis$/i'), 'el');
|
||||
yield new Transformation(new Pattern('/([^p])ais$/i'), '\1al');
|
||||
yield new Transformation(new Pattern('/(r|z)es$/i'), '\1');
|
||||
yield new Transformation(new Pattern('/^(á|gá)s$/i'), '\1s');
|
||||
yield new Transformation(new Pattern('/([^ê])s$/i'), '\1');
|
||||
}
|
||||
|
||||
/** @return Transformation[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/^(alem|c|p)ao$/i'), '\1aes');
|
||||
yield new Transformation(new Pattern('/^(irm|m)ao$/i'), '\1aos');
|
||||
yield new Transformation(new Pattern('/ao$/i'), 'oes');
|
||||
yield new Transformation(new Pattern('/^(alem|c|p)ão$/i'), '\1ães');
|
||||
yield new Transformation(new Pattern('/^(irm|m)ão$/i'), '\1ãos');
|
||||
yield new Transformation(new Pattern('/ão$/i'), 'ões');
|
||||
yield new Transformation(new Pattern('/^(|g)ás$/i'), '\1ases');
|
||||
yield new Transformation(new Pattern('/^(japon|escoc|ingl|dinamarqu|fregu|portugu)ês$/i'), '\1eses');
|
||||
yield new Transformation(new Pattern('/m$/i'), 'ns');
|
||||
yield new Transformation(new Pattern('/([^aeou])il$/i'), '\1is');
|
||||
yield new Transformation(new Pattern('/ul$/i'), 'uis');
|
||||
yield new Transformation(new Pattern('/ol$/i'), 'ois');
|
||||
yield new Transformation(new Pattern('/el$/i'), 'eis');
|
||||
yield new Transformation(new Pattern('/al$/i'), 'ais');
|
||||
yield new Transformation(new Pattern('/(z|r)$/i'), '\1es');
|
||||
yield new Transformation(new Pattern('/(s)$/i'), '\1');
|
||||
yield new Transformation(new Pattern('/$/'), 's');
|
||||
}
|
||||
|
||||
/** @return Substitution[] */
|
||||
public static function getIrregular(): iterable
|
||||
{
|
||||
yield new Substitution(new Word('abdomen'), new Word('abdomens'));
|
||||
yield new Substitution(new Word('alemão'), new Word('alemães'));
|
||||
yield new Substitution(new Word('artesã'), new Word('artesãos'));
|
||||
yield new Substitution(new Word('álcool'), new Word('álcoois'));
|
||||
yield new Substitution(new Word('árvore'), new Word('árvores'));
|
||||
yield new Substitution(new Word('bencão'), new Word('bencãos'));
|
||||
yield new Substitution(new Word('cão'), new Word('cães'));
|
||||
yield new Substitution(new Word('campus'), new Word('campi'));
|
||||
yield new Substitution(new Word('cadáver'), new Word('cadáveres'));
|
||||
yield new Substitution(new Word('capelão'), new Word('capelães'));
|
||||
yield new Substitution(new Word('capitão'), new Word('capitães'));
|
||||
yield new Substitution(new Word('chão'), new Word('chãos'));
|
||||
yield new Substitution(new Word('charlatão'), new Word('charlatães'));
|
||||
yield new Substitution(new Word('cidadão'), new Word('cidadãos'));
|
||||
yield new Substitution(new Word('consul'), new Word('consules'));
|
||||
yield new Substitution(new Word('cristão'), new Word('cristãos'));
|
||||
yield new Substitution(new Word('difícil'), new Word('difíceis'));
|
||||
yield new Substitution(new Word('email'), new Word('emails'));
|
||||
yield new Substitution(new Word('escrivão'), new Word('escrivães'));
|
||||
yield new Substitution(new Word('fóssil'), new Word('fósseis'));
|
||||
yield new Substitution(new Word('gás'), new Word('gases'));
|
||||
yield new Substitution(new Word('germens'), new Word('germen'));
|
||||
yield new Substitution(new Word('grão'), new Word('grãos'));
|
||||
yield new Substitution(new Word('hífen'), new Word('hífens'));
|
||||
yield new Substitution(new Word('irmão'), new Word('irmãos'));
|
||||
yield new Substitution(new Word('liquens'), new Word('liquen'));
|
||||
yield new Substitution(new Word('mal'), new Word('males'));
|
||||
yield new Substitution(new Word('mão'), new Word('mãos'));
|
||||
yield new Substitution(new Word('orfão'), new Word('orfãos'));
|
||||
yield new Substitution(new Word('país'), new Word('países'));
|
||||
yield new Substitution(new Word('pai'), new Word('pais'));
|
||||
yield new Substitution(new Word('pão'), new Word('pães'));
|
||||
yield new Substitution(new Word('projétil'), new Word('projéteis'));
|
||||
yield new Substitution(new Word('réptil'), new Word('répteis'));
|
||||
yield new Substitution(new Word('sacristão'), new Word('sacristães'));
|
||||
yield new Substitution(new Word('sotão'), new Word('sotãos'));
|
||||
yield new Substitution(new Word('tabelião'), new Word('tabeliães'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Portuguese;
|
||||
|
||||
use Doctrine\Inflector\GenericLanguageInflectorFactory;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
final class InflectorFactory extends GenericLanguageInflectorFactory
|
||||
{
|
||||
protected function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getSingularRuleset();
|
||||
}
|
||||
|
||||
protected function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getPluralRuleset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Portuguese;
|
||||
|
||||
use Doctrine\Inflector\Rules\Patterns;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
use Doctrine\Inflector\Rules\Substitutions;
|
||||
use Doctrine\Inflector\Rules\Transformations;
|
||||
|
||||
final class Rules
|
||||
{
|
||||
public static function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getSingular()),
|
||||
new Patterns(...Uninflected::getSingular()),
|
||||
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getPlural()),
|
||||
new Patterns(...Uninflected::getPlural()),
|
||||
new Substitutions(...Inflectible::getIrregular())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Portuguese;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
|
||||
final class Uninflected
|
||||
{
|
||||
/** @return Pattern[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
private static function getDefault(): iterable
|
||||
{
|
||||
yield new Pattern('tórax');
|
||||
yield new Pattern('tênis');
|
||||
yield new Pattern('ônibus');
|
||||
yield new Pattern('lápis');
|
||||
yield new Pattern('fênix');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules;
|
||||
|
||||
class Ruleset
|
||||
{
|
||||
/** @var Transformations */
|
||||
private $regular;
|
||||
|
||||
/** @var Patterns */
|
||||
private $uninflected;
|
||||
|
||||
/** @var Substitutions */
|
||||
private $irregular;
|
||||
|
||||
public function __construct(Transformations $regular, Patterns $uninflected, Substitutions $irregular)
|
||||
{
|
||||
$this->regular = $regular;
|
||||
$this->uninflected = $uninflected;
|
||||
$this->irregular = $irregular;
|
||||
}
|
||||
|
||||
public function getRegular(): Transformations
|
||||
{
|
||||
return $this->regular;
|
||||
}
|
||||
|
||||
public function getUninflected(): Patterns
|
||||
{
|
||||
return $this->uninflected;
|
||||
}
|
||||
|
||||
public function getIrregular(): Substitutions
|
||||
{
|
||||
return $this->irregular;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Spanish;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
use Doctrine\Inflector\Rules\Substitution;
|
||||
use Doctrine\Inflector\Rules\Transformation;
|
||||
use Doctrine\Inflector\Rules\Word;
|
||||
|
||||
class Inflectible
|
||||
{
|
||||
/** @return Transformation[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/ereses$/'), 'erés');
|
||||
yield new Transformation(new Pattern('/iones$/'), 'ión');
|
||||
yield new Transformation(new Pattern('/ces$/'), 'z');
|
||||
yield new Transformation(new Pattern('/es$/'), '');
|
||||
yield new Transformation(new Pattern('/s$/'), '');
|
||||
}
|
||||
|
||||
/** @return Transformation[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/ú([sn])$/i'), 'u\1es');
|
||||
yield new Transformation(new Pattern('/ó([sn])$/i'), 'o\1es');
|
||||
yield new Transformation(new Pattern('/í([sn])$/i'), 'i\1es');
|
||||
yield new Transformation(new Pattern('/é([sn])$/i'), 'e\1es');
|
||||
yield new Transformation(new Pattern('/á([sn])$/i'), 'a\1es');
|
||||
yield new Transformation(new Pattern('/z$/i'), 'ces');
|
||||
yield new Transformation(new Pattern('/([aeiou]s)$/i'), '\1');
|
||||
yield new Transformation(new Pattern('/([^aeéiou])$/i'), '\1es');
|
||||
yield new Transformation(new Pattern('/$/'), 's');
|
||||
}
|
||||
|
||||
/** @return Substitution[] */
|
||||
public static function getIrregular(): iterable
|
||||
{
|
||||
yield new Substitution(new Word('el'), new Word('los'));
|
||||
yield new Substitution(new Word('papá'), new Word('papás'));
|
||||
yield new Substitution(new Word('mamá'), new Word('mamás'));
|
||||
yield new Substitution(new Word('sofá'), new Word('sofás'));
|
||||
yield new Substitution(new Word('mes'), new Word('meses'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Spanish;
|
||||
|
||||
use Doctrine\Inflector\GenericLanguageInflectorFactory;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
final class InflectorFactory extends GenericLanguageInflectorFactory
|
||||
{
|
||||
protected function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getSingularRuleset();
|
||||
}
|
||||
|
||||
protected function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getPluralRuleset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Spanish;
|
||||
|
||||
use Doctrine\Inflector\Rules\Patterns;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
use Doctrine\Inflector\Rules\Substitutions;
|
||||
use Doctrine\Inflector\Rules\Transformations;
|
||||
|
||||
final class Rules
|
||||
{
|
||||
public static function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getSingular()),
|
||||
new Patterns(...Uninflected::getSingular()),
|
||||
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getPlural()),
|
||||
new Patterns(...Uninflected::getPlural()),
|
||||
new Substitutions(...Inflectible::getIrregular())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Spanish;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
|
||||
final class Uninflected
|
||||
{
|
||||
/** @return Pattern[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
private static function getDefault(): iterable
|
||||
{
|
||||
yield new Pattern('lunes');
|
||||
yield new Pattern('rompecabezas');
|
||||
yield new Pattern('crisis');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules;
|
||||
|
||||
final class Substitution
|
||||
{
|
||||
/** @var Word */
|
||||
private $from;
|
||||
|
||||
/** @var Word */
|
||||
private $to;
|
||||
|
||||
public function __construct(Word $from, Word $to)
|
||||
{
|
||||
$this->from = $from;
|
||||
$this->to = $to;
|
||||
}
|
||||
|
||||
public function getFrom(): Word
|
||||
{
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
public function getTo(): Word
|
||||
{
|
||||
return $this->to;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules;
|
||||
|
||||
use Doctrine\Inflector\WordInflector;
|
||||
|
||||
use function strtolower;
|
||||
use function strtoupper;
|
||||
use function substr;
|
||||
|
||||
class Substitutions implements WordInflector
|
||||
{
|
||||
/** @var Substitution[] */
|
||||
private $substitutions;
|
||||
|
||||
public function __construct(Substitution ...$substitutions)
|
||||
{
|
||||
foreach ($substitutions as $substitution) {
|
||||
$this->substitutions[$substitution->getFrom()->getWord()] = $substitution;
|
||||
}
|
||||
}
|
||||
|
||||
public function getFlippedSubstitutions(): Substitutions
|
||||
{
|
||||
$substitutions = [];
|
||||
|
||||
foreach ($this->substitutions as $substitution) {
|
||||
$substitutions[] = new Substitution(
|
||||
$substitution->getTo(),
|
||||
$substitution->getFrom()
|
||||
);
|
||||
}
|
||||
|
||||
return new Substitutions(...$substitutions);
|
||||
}
|
||||
|
||||
public function inflect(string $word): string
|
||||
{
|
||||
$lowerWord = strtolower($word);
|
||||
|
||||
if (isset($this->substitutions[$lowerWord])) {
|
||||
$firstLetterUppercase = $lowerWord[0] !== $word[0];
|
||||
|
||||
$toWord = $this->substitutions[$lowerWord]->getTo()->getWord();
|
||||
|
||||
if ($firstLetterUppercase) {
|
||||
return strtoupper($toWord[0]) . substr($toWord, 1);
|
||||
}
|
||||
|
||||
return $toWord;
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules;
|
||||
|
||||
use Doctrine\Inflector\WordInflector;
|
||||
|
||||
use function preg_replace;
|
||||
|
||||
final class Transformation implements WordInflector
|
||||
{
|
||||
/** @var Pattern */
|
||||
private $pattern;
|
||||
|
||||
/** @var string */
|
||||
private $replacement;
|
||||
|
||||
public function __construct(Pattern $pattern, string $replacement)
|
||||
{
|
||||
$this->pattern = $pattern;
|
||||
$this->replacement = $replacement;
|
||||
}
|
||||
|
||||
public function getPattern(): Pattern
|
||||
{
|
||||
return $this->pattern;
|
||||
}
|
||||
|
||||
public function getReplacement(): string
|
||||
{
|
||||
return $this->replacement;
|
||||
}
|
||||
|
||||
public function inflect(string $word): string
|
||||
{
|
||||
return (string) preg_replace($this->pattern->getRegex(), $this->replacement, $word);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules;
|
||||
|
||||
use Doctrine\Inflector\WordInflector;
|
||||
|
||||
class Transformations implements WordInflector
|
||||
{
|
||||
/** @var Transformation[] */
|
||||
private $transformations;
|
||||
|
||||
public function __construct(Transformation ...$transformations)
|
||||
{
|
||||
$this->transformations = $transformations;
|
||||
}
|
||||
|
||||
public function inflect(string $word): string
|
||||
{
|
||||
foreach ($this->transformations as $transformation) {
|
||||
if ($transformation->getPattern()->matches($word)) {
|
||||
return $transformation->inflect($word);
|
||||
}
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Turkish;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
use Doctrine\Inflector\Rules\Substitution;
|
||||
use Doctrine\Inflector\Rules\Transformation;
|
||||
use Doctrine\Inflector\Rules\Word;
|
||||
|
||||
class Inflectible
|
||||
{
|
||||
/** @return Transformation[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/l[ae]r$/i'), '');
|
||||
}
|
||||
|
||||
/** @return Transformation[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield new Transformation(new Pattern('/([eöiü][^aoıueöiü]{0,6})$/u'), '\1ler');
|
||||
yield new Transformation(new Pattern('/([aoıu][^aoıueöiü]{0,6})$/u'), '\1lar');
|
||||
}
|
||||
|
||||
/** @return Substitution[] */
|
||||
public static function getIrregular(): iterable
|
||||
{
|
||||
yield new Substitution(new Word('ben'), new Word('biz'));
|
||||
yield new Substitution(new Word('sen'), new Word('siz'));
|
||||
yield new Substitution(new Word('o'), new Word('onlar'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Turkish;
|
||||
|
||||
use Doctrine\Inflector\GenericLanguageInflectorFactory;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
final class InflectorFactory extends GenericLanguageInflectorFactory
|
||||
{
|
||||
protected function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getSingularRuleset();
|
||||
}
|
||||
|
||||
protected function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return Rules::getPluralRuleset();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Turkish;
|
||||
|
||||
use Doctrine\Inflector\Rules\Patterns;
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
use Doctrine\Inflector\Rules\Substitutions;
|
||||
use Doctrine\Inflector\Rules\Transformations;
|
||||
|
||||
final class Rules
|
||||
{
|
||||
public static function getSingularRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getSingular()),
|
||||
new Patterns(...Uninflected::getSingular()),
|
||||
(new Substitutions(...Inflectible::getIrregular()))->getFlippedSubstitutions()
|
||||
);
|
||||
}
|
||||
|
||||
public static function getPluralRuleset(): Ruleset
|
||||
{
|
||||
return new Ruleset(
|
||||
new Transformations(...Inflectible::getPlural()),
|
||||
new Patterns(...Uninflected::getPlural()),
|
||||
new Substitutions(...Inflectible::getIrregular())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules\Turkish;
|
||||
|
||||
use Doctrine\Inflector\Rules\Pattern;
|
||||
|
||||
final class Uninflected
|
||||
{
|
||||
/** @return Pattern[] */
|
||||
public static function getSingular(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
public static function getPlural(): iterable
|
||||
{
|
||||
yield from self::getDefault();
|
||||
}
|
||||
|
||||
/** @return Pattern[] */
|
||||
private static function getDefault(): iterable
|
||||
{
|
||||
yield new Pattern('lunes');
|
||||
yield new Pattern('rompecabezas');
|
||||
yield new Pattern('crisis');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector\Rules;
|
||||
|
||||
class Word
|
||||
{
|
||||
/** @var string */
|
||||
private $word;
|
||||
|
||||
public function __construct(string $word)
|
||||
{
|
||||
$this->word = $word;
|
||||
}
|
||||
|
||||
public function getWord(): string
|
||||
{
|
||||
return $this->word;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector;
|
||||
|
||||
use Doctrine\Inflector\Rules\Ruleset;
|
||||
|
||||
use function array_merge;
|
||||
|
||||
/**
|
||||
* Inflects based on multiple rulesets.
|
||||
*
|
||||
* Rules:
|
||||
* - If the word matches any uninflected word pattern, it is not inflected
|
||||
* - The first ruleset that returns a different value for an irregular word wins
|
||||
* - The first ruleset that returns a different value for a regular word wins
|
||||
* - If none of the above match, the word is left as-is
|
||||
*/
|
||||
class RulesetInflector implements WordInflector
|
||||
{
|
||||
/** @var Ruleset[] */
|
||||
private $rulesets;
|
||||
|
||||
public function __construct(Ruleset $ruleset, Ruleset ...$rulesets)
|
||||
{
|
||||
$this->rulesets = array_merge([$ruleset], $rulesets);
|
||||
}
|
||||
|
||||
public function inflect(string $word): string
|
||||
{
|
||||
if ($word === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ($this->rulesets as $ruleset) {
|
||||
if ($ruleset->getUninflected()->matches($word)) {
|
||||
return $word;
|
||||
}
|
||||
|
||||
$inflected = $ruleset->getIrregular()->inflect($word);
|
||||
|
||||
if ($inflected !== $word) {
|
||||
return $inflected;
|
||||
}
|
||||
|
||||
$inflected = $ruleset->getRegular()->inflect($word);
|
||||
|
||||
if ($inflected !== $word) {
|
||||
return $inflected;
|
||||
}
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Inflector;
|
||||
|
||||
interface WordInflector
|
||||
{
|
||||
public function inflect(string $word): string;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue