From 344426f81b8694be73d53f4439209ef3758dca7b Mon Sep 17 00:00:00 2001 From: Massi-X Date: Sat, 3 Feb 2024 11:49:01 +0100 Subject: [PATCH] Initial release --- .github/ISSUE_TEMPLATE/bug_report.md | 38 + .github/ISSUE_TEMPLATE/custom.md | 10 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .gitignore | 174 +++++ Backup.php | 23 + CoreInterface.php | 118 ++++ Job.php | 29 + LICENSE | 402 +++++++++++ Phonemiddleware.class.php | 798 +++++++++++++++++++++ README.md | 37 + Restore.php | 23 + assets/css/dragsort.css | 1 + assets/css/style.css | 809 ++++++++++++++++++++++ assets/css/tagify.css | 1 + assets/css/timepicker.css | 143 ++++ assets/images/icon.png | Bin 0 -> 65201 bytes assets/js/dragsort.js | 318 +++++++++ assets/js/dragsort.js_LICENSE | 21 + assets/js/magicconfig.js | 273 ++++++++ assets/js/scripts.js | 520 ++++++++++++++ assets/js/tagify.min.js | 26 + assets/js/tagify.min.js_LICENSE | 19 + assets/js/timepicker.js | 183 +++++ build | 570 +++++++++++++++ build_config | 44 ++ carddavtoxml.php | 23 + module.xml | 5 + numbertocnam.php | 42 ++ page.carddavmiddleware.php | 540 +++++++++++++++ utilities.php | 172 +++++ 30 files changed, 5382 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitignore create mode 100644 Backup.php create mode 100644 CoreInterface.php create mode 100644 Job.php create mode 100644 LICENSE create mode 100644 Phonemiddleware.class.php create mode 100644 README.md create mode 100644 Restore.php create mode 100644 assets/css/dragsort.css create mode 100644 assets/css/style.css create mode 100644 assets/css/tagify.css create mode 100644 assets/css/timepicker.css create mode 100644 assets/images/icon.png create mode 100644 assets/js/dragsort.js create mode 100644 assets/js/dragsort.js_LICENSE create mode 100644 assets/js/magicconfig.js create mode 100644 assets/js/scripts.js create mode 100644 assets/js/tagify.min.js create mode 100644 assets/js/tagify.min.js_LICENSE create mode 100644 assets/js/timepicker.js create mode 100755 build create mode 100644 build_config create mode 100644 carddavtoxml.php create mode 100644 module.xml create mode 100644 numbertocnam.php create mode 100644 page.carddavmiddleware.php create mode 100644 utilities.php diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000..48d5f81 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c9b122 --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +# Local History for Visual Studio Code +.history/ + +# Common credential files +**/credentials.json +**/client_secrets.json +**/client_secret.json +*creds* +*.dat +*password* +*.httr-oauth* + +# Private Node Modules +node_modules/ +creds.js + +# Private Files +*.json +*.csv +*.csv.gz +*.tsv +*.tsv.gz +*.xlsx + + +# Mac/OSX +.DS_Store + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ \ No newline at end of file diff --git a/Backup.php b/Backup.php new file mode 100644 index 0000000..178cef4 --- /dev/null +++ b/Backup.php @@ -0,0 +1,23 @@ + © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +namespace FreePBX\modules\PhoneMiddleware; + +use FreePBX\modules\Backup as Base; + +class Backup extends Base\BackupBase +{ + public function runBackup($id, $transaction) + { + $Core = $this->FreePBX->PhoneMiddleware->getCore(); //get instance beacuse I cannot access statically here + + if (method_exists($Core, 'run_backup')) { + $Core->run_backup($this); + } else + throw new \Exception(_('Backup is not implemented!')); + } +} diff --git a/CoreInterface.php b/CoreInterface.php new file mode 100644 index 0000000..5c58cf0 --- /dev/null +++ b/CoreInterface.php @@ -0,0 +1,118 @@ + © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +/******************************************************************************/ +/*** These are the methods to implement to create a working Core class ***/ +/******************************************************************************/ + +interface CoreInterface +{ + public static function getInstance(): self; //return a Core instance + public function init(): void; //initialize anything you might need to + + //getter and setter for various parameters settable in UI. BMO class will get the values and pass them back through these methods. Any logic should be implemented at your wish. + public static function get_url(): string; //server url + public static function set_url(string $url): void; + public static function get_carddav_addressbooks(): array; //addressbooks URIs + public static function set_carddav_addressbooks(array $carddav_addressbooks): void; + public static function get_auth(): array; //carddav auth info + public static function set_auth(string $username, string $password): void; + public static function get_cache_expire(): int; //cache duration + public static function set_cache_expire(int $expire): void; + public static function get_country_code(): string; //country code + public static function set_country_code(string $code): void; + public static function get_output_construct(): string; //format of the CNAM output + public static function set_output_construct(string $output_construct): void; + public static function get_max_cnam_length(): int; //CNAM max allowed length + public static function set_max_cnam_length(int $max_cnam_length): void; + public static function get_phone_type(): int; //user device type + public static function set_phone_type(int $phone_type): void; + public static function get_mail_level(): array; //importance level when notifications will be sent also via mail + public static function set_mail_level(array $types): void; + public static function get_superfecta_compat(): bool; //superfecta compatibility switch + public static function set_superfecta_compat(bool $superfecta_compat): void; + public static function get_spam_match(): bool; //spam match report + public static function set_spam_match(bool $spam_match): void; + + public function store_config(): bool; //called when the UI class wants to store the data (i.e. when the user clicks "Apply") + public static function delete_cache(): bool; //if you have any cache, this method is called to invalidate it if there is any breaking change. + + public function getXMLforPhones(bool $force = false): string; //returns a well formatted xml phonebook that can be read by a phone/other device. $force to force refresh + public function getCNFromPhone(string $number, bool $force = false); //returns a CNAM (name) given a phone number. You should really not use $force because the result must come as fast as possible - output type should be string|null (not supported in PHP7) + + public function discover_addressbooks_to_display(): array; //returns an array of addressbooks based on the current url, username and password. This s used by the UI in conjunction with get_carddav_addressbooks() to create a list of the current active/inactive addressbooks. + public static function sendUINotification(int $type, string $message, int $id, int $flags): void; //send a notification that will be displayed in the top right corner of the UI. $type = the type (see constants below), $message = the text of the notification, $id = unique id, $flags = flags (see constants below). + public static function retrieveUINotifications(): array; //retrieve an array of UI notifications so that UI can display it + public static function deleteUINotification(int $id): bool; //delete a notification by ID + public static function deleteAllUINotifications(): bool; //delete all the notifications + + /******************************************************************************/ + /*** Do not modify/delete constants! ***/ + /******************************************************************************/ + + //if new are added remember to update getXMLforPhones + BMO class with new languages. MUST BE CONSECUTIVE. MUST START AT 1 (NOT 0). + public const PHONE_TYPE_NO_LIMITS = 1; + public const PHONE_TYPE_FANVIL = 2; + public const PHONE_TYPE_SNOM = 3; + public const PHONE_TYPES = [self::PHONE_TYPE_NO_LIMITS, self::PHONE_TYPE_FANVIL, self::PHONE_TYPE_SNOM]; + + //notification type and options constants. You can put below fixed notification IDs (> 0 && < 1000) + public const NOTIFICATION_TYPE_VERBOSE = 1; //this is a verbose message + public const NOTIFICATION_TYPE_ERROR = 2; //this is an error message + public const NOTIFICATION_TYPE_INFO = 3; //this is an info message + public const NOTIFICATION_FLAG_NO_MAIL = 0b0001; //overwrite mail send if needed +} + +/******************************************************************************/ +/*** These are optional methods of your Core class ***/ +/******************************************************************************/ + +interface UIAddons +{ + public static function get_module_name(): string; //return here your module name to be displayed in UI + public static function get_author(): string; //return here your name to be displayed in UI + public static function get_readme_url(): string; //return here your readme URL if you have one. Must be a valid HTML + public static function get_license_to_display(): array; //if you have a license, you should have this method too. returns ['description' => intestation with '%linkstart' and '%Linkend' anchors to be replaced for license link, 'text' => full text of the license, 'title' => title of the dialog] + public static function get_libraries_to_display(): array; //any library you used. returns array of arrays containing ['name' => library name, 'url' => library url] + public static function get_additional_footer(): string; //any additional information to print in footer + public static function get_help(): array; //return an array of raw strings containing help informations to print for the user. You should use the sandard format: title:text... +} + +interface Activation +{ + public static function get_purchase_buttons(): string; //return a well formatted HTML string containing your purchase/donation button(s). Include any '; + + //submit settings handling + if (isset($_POST['submit'])) { + $this->Core->set_cache_expire(isset($_POST['cache_expire']) ? (int) $_POST['cache_expire'] : 0); + $this->Core->set_country_code(isset($_POST['country_code']) ? $_POST['country_code'] : ''); + $this->Core->set_output_construct(isset($_POST['output_construct']) ? $_POST['output_construct'] : ''); + if (isset($_POST['max_cnam_length_enable']) && $_POST['max_cnam_length_enable'] == 'on') + $this->Core->set_max_cnam_length(isset($_POST['max_cnam_length']) ? (int) $_POST['max_cnam_length'] : 0); + else + $this->Core->set_max_cnam_length(0); + $this->Core->set_phone_type(isset($_POST['phone_type']) ? (int) $_POST['phone_type'] : Core::PHONE_TYPE_NO_LIMITS); + $this->Core->set_mail_level(isset($_POST['mail_level']) ? $_POST['mail_level'] : []); + $this->Core->set_superfecta_compat(isset($_POST['superfecta_compat']) ? ($_POST['superfecta_compat'] == 'on' ? true : false) : false); + $this->Core->set_spam_match(isset($_POST['spam_match']) ? ($_POST['spam_match'] == 'on' ? true : false) : false); + + if (!$this->Core->store_config()) { + if (!is_array($_POST['errors'])) + $_POST['errors'] = []; + + array_push($_POST['errors'], _('Something went wrong! Failed to save settings.')); + } + } + } + + /** + * Return the resolved XML Phonebook URL + * + * @return string XML Phonebook URL + * @throws Exception If this doesn't run inside a page + */ + public static function getXmlPhonebookURL() + { + if (self::$xmlPhonebookURL == null) + throw new Exception(_('This is only available inside a page')); + + return self::$xmlPhonebookURL; + } + + /** + * Return the resolved NumberToCNAM URL + * + * @return string NumberToCNAM URL + * @throws Exception If this doesn't run inside a page + */ + public static function getNumberToCnamURL() + { + if (self::$numberToCnamURL == null) + throw new Exception(_('This is only available inside a page')); + + return self::$numberToCnamURL; + } + + /** + * Return the resolved email addresses + * + * @return array Array of email addresses with "To" (if available) and "From" + * @throws Exception If this doesn't run inside a page + */ + public static function getEmailAddresses() + { + if (self::$emailAddresses == null) + throw new Exception(_('This is only available inside a page')); + + return self::$emailAddresses; + } + + + /** + * Set which ajax requests should be allowed + * + * @param string $req The request string from JS + * @return void + */ + public function ajaxRequest($req, &$setting) + { + switch ($req) { + case 'savecarddav': + case 'validatecarddav': + case 'deletenotification': + case 'deleteallnotifications': + case 'superfectainit': + case 'superfectareorder': + case 'outcnamsetup': + case 'inboundroutesetup': + case 'createorder': + case 'validatepurchase': + case 'restorepurchase': + return true; + default: + return false; + } + } + + /** + * Handle ajax request and return a result back in JSON + * + * @return void + */ + public function ajaxHandler() + { + //set default timeout (10s) for those connections to stop the user wait too much in case of problems + ini_set('default_socket_timeout', 10); + + switch ($_REQUEST['command']) { + case 'savecarddav': + $this->Core->set_url($_POST['carddav_url']); + $this->Core->set_auth($_POST['carddav_user'], $_POST['carddav_psw']); + $this->Core->set_carddav_addressbooks($_POST['carddav_addressbooks']); + + try { + if (!$this->Core->store_config()) //store config with the updated values above + throw new Exception(); + } catch (Exception $e) { + throw new Exception(_('Failed to save the addressbook settings!')); + } + return true; + case 'validatecarddav': + if (empty($_POST['carddav_url'])) + throw new Exception(_('URL must be set.')); //url must be there + + //load all the values from the server + $this->Core->set_url($_POST['carddav_url']); + $this->Core->set_auth($_POST['carddav_user'], $_POST['carddav_psw']); + $this->Core->init(); //force new init + $result = $this->Core->discover_addressbooks_to_display(); + $uris = $this->Core->get_carddav_addressbooks(); + + //then reorder the array as we saved it. Position of not checked values will be obviously not be preserved as nobody cares. + for ($i = count($uris) - 1; $i >= 0; --$i) { + $uri = $uris[$i]; + if (array_key_exists($uri, $result)) { //if the book is still there (read: if not deleted from the server) + //check the element + $result[$uri]['checked'] = true; + + //move the element to the top. The array is iterated in reverse to correctly push the first value to the first place. + $temp = $result[$uri]; + unset($result[$uri]); + array_unshift($result, $temp); + } + } + + return array_values($result); //js doesn't care about array keys. It is simpler to give it a normal indexed array + case 'deletenotification': + try { + $this->Core->deleteUINotification($_POST['id']); + } catch (Exception $e) { + throw new Exception(_('Failed to delete the notification(s)!')); + } + return true; + case 'deleteallnotifications': + try { + $this->Core->deleteAllUINotifications(); + } catch (Exception $e) { + throw new Exception(_('Failed to delete the notification(s)!')); + } + return true; + case 'superfectainit': + //create scheme + $r = new \ReflectionObject($this->FreePBX->Superfecta); + $p = $r->getProperty('schemeDefaults'); + $p->setAccessible(true); + + $message = _('Updated scheme.'); + + $scheme = $p->getValue($this->FreePBX->Superfecta); + $scheme['scheme_name'] = Utilities::SUPERFECTA_SCHEME; + $scheme['destination'] = ''; //missing value in defaults. So add empty + $scheme['Curl_Timeout'] = 5; + $scheme['SPAM_Text'] = '[SPAM] '; + $scheme['SPAM_threshold'] = 1; + if (!$this->FreePBX->Superfecta->getScheme(Utilities::SUPERFECTA_SCHEME)) { + if (!$this->FreePBX->Superfecta->addScheme(Utilities::SUPERFECTA_SCHEME, $scheme)['status']) + return ['status' => false, 'message' => _('An unknown exception occured.')]; + else + $message = _('Created scheme.'); + } + + if ($this->FreePBX->Superfecta->updateScheme(Utilities::SUPERFECTA_SCHEME, $scheme)) + return ['status' => true, 'message' => $message]; + else + return ['status' => false, 'message' => _('An unknown exception occured.')]; + case 'superfectareorder': + //hacky way to get to the result. Beacuse in the next steps I set my scheme as the only one, this isn't really needed but I leave it here for completeness. + try { + //first get global var and unset everything that is inside to prevent any problem + global $_REQUEST; + global $_POST; + unset($_REQUEST); + unset($_POST); + + //put it on top + $_REQUEST['command'] = 'sort'; + $_REQUEST['scheme'] = Utilities::SUPERFECTA_SCHEME; + $_POST['position'] = 'up'; + $executions = 0; //prevent infinite loops + while ($this->FreePBX->Superfecta->getScheme(Utilities::SUPERFECTA_SCHEME)['order'] > 10) { + $this->FreePBX->Superfecta->ajaxHandler(); + + if ($executions >= 99) //I hope nobody has more than 100 schemes! + break; + ++$executions; + } + + return ['status' => true, 'message' => _('Moved scheme at the top.')]; + } catch (\Throwable $t) { + return ['status' => false, 'message' => _('Unable to move scheme at the top. Proceeding anyway...')]; + } + case 'outcnamsetup': + //hacky way to get to the result + $data['enable_cdr'] = 'CHECKED'; + $data['enable_rpid'] = 'CHECKED'; + $data['scheme'] = 'base_' . Utilities::SUPERFECTA_SCHEME; //for strange reasons superfecta prepends "base_" to the scheme name. Simply hardcode it here + $this->FreePBX->OutCNAM->editConfig(1, $data); //ID is always 1, see https://github.com/POSSA/freepbx-Outbound_CNAM/blob/master/Outcnam.class.php#L49 + return ['status' => true, 'message' => _('OutCNAM configured.')]; //outcnam does not return any useful information so as long as there is no errors this will be the return value + case 'inboundroutesetup': + //hacky way to get to the result. + $route = json_decode(file_get_contents("php://input")); + if (!$route) + throw new Exception(_('Data is invalid.')); //the data is not valid + + //reset cidlookup + $this->FreePBX->Modules->loadFunctionsInc('cidlookup'); + if (function_exists('cidlookup_did_del')) + cidlookup_did_del($route->extension, $route->cidnum); //viewing_itemid is unused here so doesn't matter + else + throw new Exception(_('Required function not found!')); + + //set superfecta + $settings['sf_enable'] = 'true'; + $settings['sf_scheme'] = Utilities::SUPERFECTA_SCHEME; + $settings['extension'] = $route->extension; + $settings['cidnum'] = $route->cidnum; + + $this->FreePBX->Superfecta->bulkhandler_superfecta_cfg($settings); //viewing_itemid is unused here so doesn't matter + + return ['status' => true, 'message' => _('Inbound route updated.')]; //sadly there is no way (simple enough) to know if this was successful or not + } + } + + /** + * Handle ajax request and return a result back in plain text + * + * @return void + */ + public function ajaxCustomHandler() + { + switch ($_REQUEST['command']) { + case 'createorder': + if (method_exists(Core::class, 'create_order')) { + $res = Core::create_order(); + echo $res['message']; + return $res['result']; + } else + throw new Exception(_('Command is not implemented')); + case 'validatepurchase': + if (method_exists(Core::class, 'validate_purchase')) { + $res = Core::validate_purchase($_POST, file_get_contents("php://input")); + echo $res['message']; + return $res['result']; + } else + throw new Exception(_('Command is not implemented')); + case 'restorepurchase': + if (method_exists(Core::class, 'restore_purchase')) { + $res = Core::restore_purchase($_POST, file_get_contents("php://input")); + echo $res['message']; + return $res['result']; + } else + throw new Exception(_('Command is not implemented')); + } + } + + /** + * Method that is executed on install/upgrade. die() using the red span from page.modules.php instead of throwing exceptions here to better present errors to the user + * + * @return void + */ + public function install() + { + //delete www folder + try { + //to support upgrade from previous versions i need to check both for is_link because I switched the two (is_link check always first) + if ((is_link($this->WWW_MODULE_DIR_OLD) && !unlink($this->WWW_MODULE_DIR_OLD)) || (file_exists($this->WWW_MODULE_DIR_OLD) && !Utilities::delete_dir($this->WWW_MODULE_DIR_OLD))) + throw new Exception(); + if ((is_link($this->WWW_MODULE_SYMLINK_OLD) && !unlink($this->WWW_MODULE_SYMLINK_OLD)) || (file_exists($this->WWW_MODULE_SYMLINK_OLD) && !Utilities::delete_dir($this->WWW_MODULE_SYMLINK_OLD))) + throw new Exception(); + if (file_exists($this->WWW_MODULE_DIR_NEW) && !Utilities::delete_dir($this->WWW_MODULE_DIR_NEW)) + throw new Exception(); + } catch (Exception $e) { + die('' . str_replace('%folder', 'root', _('Installation failed: Unable to delete %folder folder. Make sure the module has read/write permissions.')) . ''); + } + + //recreate www folder and symlinks + if ( + !mkdir($this->WWW_MODULE_DIR_NEW) || + !symlink($this->WWW_MODULE_DIR_NEW, $this->WWW_MODULE_DIR_OLD) || //symlink new folder to old ones + !symlink($this->WWW_MODULE_DIR_NEW, $this->WWW_MODULE_SYMLINK_OLD) || //symlink new folder to old ones + !symlink(__DIR__ . '/carddavtoxml.php', $this->WWW_MODULE_DIR_NEW . '/carddavtoxml.php') || //symlink carddavtoxml + !symlink(__DIR__ . '/carddavtoxml.php', $this->WWW_MODULE_DIR_NEW . '/carddavtoXML.php') || //symlink carddavToXML (for backward compatibility) + !symlink(__DIR__ . '/numbertocnam.php', $this->WWW_MODULE_DIR_NEW . '/numbertocnam.php') || //symlink numbertocnam + !symlink(__DIR__ . '/numbertocnam.php', $this->WWW_MODULE_DIR_NEW . '/numberToCNAM.php') || //symlink numberToCNAM (for backward compatibility) + !file_exists($this->ASSETS_SYMLINK) && !symlink(__DIR__ . '/assets/', $this->ASSETS_SYMLINK) //symlink assets folder if not already there (not done automatically by fpbx, and seems like it is right this way) + ) + die('' . _('Failed to initialize working directory. The module won\'t work. Make sure the module has read/write permissions.') . ''); + + //add Job + FreePBX::Job()->addClass('phonemiddleware', 'job', 'FreePBX\modules\PhoneMiddleware\Job', '* * * * *'); + + //post install hooks from core, if providen. Excpetions are not catched here, if you care you must catch them yourself. + if (method_exists(Core::class, 'post_install_hook')) + Core::post_install_hook($this->FreePBX); + } + + /** + * Method that is executed on uninstall + * + * @return void + */ + public function uninstall() + { + $isException = false; + + //delete main root folder (because this function is 2.0.0+ only, the folder is already lowercase only). + //also if this is executed in a non-consinsent state it doesn't matter because will be removed on next install. + try { + if (!Utilities::delete_dir($this->WWW_MODULE_DIR_NEW)) + throw new Exception(); + } catch (Exception $e) { + $isException = true; + out(str_replace('%folder', 'root', _('Unable to delete %folder folder.')) . ' ' . _('Try to delete it manually to completely remove the module.')); + } + + //delete www (symlink) folder + try { + //to support upgrade from previous versions i need to check both for is_link because I switched the two (is_link check always first) + if ((is_link($this->WWW_MODULE_DIR_OLD) && !unlink($this->WWW_MODULE_DIR_OLD)) || (file_exists($this->WWW_MODULE_DIR_OLD) && !Utilities::delete_dir($this->WWW_MODULE_DIR_OLD))) + throw new Exception(); + if ((is_link($this->WWW_MODULE_SYMLINK_OLD) && !unlink($this->WWW_MODULE_SYMLINK_OLD)) || (file_exists($this->WWW_MODULE_SYMLINK_OLD) && !Utilities::delete_dir($this->WWW_MODULE_SYMLINK_OLD))) + throw new Exception(); + } catch (Exception $e) { + $isException = true; + out(str_replace('%folder', 'symlink root', _('Unable to delete %folder folder.')) . ' ' . _('Try to delete it manually to completely remove the module.')); + } + + //pre uninstall hooks from core, if providen. Excpetions are catched here and a generic warning will be printed at the end. + try { + if (method_exists(Core::class, 'post_uninstall_hook')) + Core::post_uninstall_hook($this->FreePBX); + } catch (\Throwable $t) { + $isException = true; + } + + if ($isException) + throw new Exception(_('Some error(s) occurred during the process. Please check above for error messages.')); //this will be printed in red automatically and is not an exception that causes the page to die + + //symlink folder as well as Jobs are automatically removed by freepbx + } + + /** + * Return an associative array of all country codes. Thanks to https://gist.github.com/josephilipraja/8341837?permalink_comment_id=3883690#gistcomment-3883690 + * + * @return array Country code list with phone number and friendly name + */ + public static function get_country_codes() + { + return [ + 'AD' => ['name' => 'ANDORRA', 'code' => '376'], + 'AE' => ['name' => 'UNITED ARAB EMIRATES', 'code' => '971'], + 'AF' => ['name' => 'AFGHANISTAN', 'code' => '93'], + 'AG' => ['name' => 'ANTIGUA AND BARBUDA', 'code' => '1268'], + 'AI' => ['name' => 'ANGUILLA', 'code' => '1264'], + 'AL' => ['name' => 'ALBANIA', 'code' => '355'], + 'AM' => ['name' => 'ARMENIA', 'code' => '374'], + 'AN' => ['name' => 'NETHERLANDS ANTILLES', 'code' => '599'], + 'AO' => ['name' => 'ANGOLA', 'code' => '244'], + 'AQ' => ['name' => 'ANTARCTICA', 'code' => '672'], + 'AR' => ['name' => 'ARGENTINA', 'code' => '54'], + 'AS' => ['name' => 'AMERICAN SAMOA', 'code' => '1684'], + 'AT' => ['name' => 'AUSTRIA', 'code' => '43'], + 'AU' => ['name' => 'AUSTRALIA', 'code' => '61'], + 'AW' => ['name' => 'ARUBA', 'code' => '297'], + 'AX' => ['name' => 'ÅLAND ISLANDS', 'code' => '358'], + 'AZ' => ['name' => 'AZERBAIJAN', 'code' => '994'], + 'BA' => ['name' => 'BOSNIA AND HERZEGOVINA', 'code' => '387'], + 'BB' => ['name' => 'BARBADOS', 'code' => '1246'], + 'BD' => ['name' => 'BANGLADESH', 'code' => '880'], + 'BE' => ['name' => 'BELGIUM', 'code' => '32'], + 'BF' => ['name' => 'BURKINA FASO', 'code' => '226'], + 'BG' => ['name' => 'BULGARIA', 'code' => '359'], + 'BH' => ['name' => 'BAHRAIN', 'code' => '973'], + 'BI' => ['name' => 'BURUNDI', 'code' => '257'], + 'BJ' => ['name' => 'BENIN', 'code' => '229'], + 'BL' => ['name' => 'SAINT BARTHELEMY', 'code' => '590'], + 'BM' => ['name' => 'BERMUDA', 'code' => '1441'], + 'BN' => ['name' => 'BRUNEI DARUSSALAM', 'code' => '673'], + 'BO' => ['name' => 'BOLIVIA', 'code' => '591'], + 'BQ' => ['name' => 'CARIBEAN NETHERLANDS', 'code' => '599'], + 'BR' => ['name' => 'BRAZIL', 'code' => '55'], + 'BS' => ['name' => 'BAHAMAS', 'code' => '1242'], + 'BT' => ['name' => 'BHUTAN', 'code' => '975'], + 'BV' => ['name' => 'BOUVET ISLAND', 'code' => '55'], + 'BW' => ['name' => 'BOTSWANA', 'code' => '267'], + 'BY' => ['name' => 'BELARUS', 'code' => '375'], + 'BZ' => ['name' => 'BELIZE', 'code' => '501'], + 'CA' => ['name' => 'CANADA', 'code' => '1'], + 'CC' => ['name' => 'COCOS (KEELING) ISLANDS', 'code' => '61'], + 'CD' => ['name' => 'CONGO, THE DEMOCRATIC REPUBLIC OF THE', 'code' => '243'], + 'CF' => ['name' => 'CENTRAL AFRICAN REPUBLIC', 'code' => '236'], + 'CG' => ['name' => 'CONGO', 'code' => '242'], + 'CH' => ['name' => 'SWITZERLAND', 'code' => '41'], + 'CI' => ['name' => 'COTE D IVOIRE', 'code' => '225'], + 'CK' => ['name' => 'COOK ISLANDS', 'code' => '682'], + 'CL' => ['name' => 'CHILE', 'code' => '56'], + 'CM' => ['name' => 'CAMEROON', 'code' => '237'], + 'CN' => ['name' => 'CHINA', 'code' => '86'], + 'CO' => ['name' => 'COLOMBIA', 'code' => '57'], + 'CR' => ['name' => 'COSTA RICA', 'code' => '506'], + 'CU' => ['name' => 'CUBA', 'code' => '53'], + 'CV' => ['name' => 'CAPE VERDE', 'code' => '238'], + 'CW' => ['name' => 'CURAÇAO', 'code' => '599'], + 'CX' => ['name' => 'CHRISTMAS ISLAND', 'code' => '61'], + 'CY' => ['name' => 'CYPRUS', 'code' => '357'], + 'CZ' => ['name' => 'CZECH REPUBLIC', 'code' => '420'], + 'DE' => ['name' => 'GERMANY', 'code' => '49'], + 'DJ' => ['name' => 'DJIBOUTI', 'code' => '253'], + 'DK' => ['name' => 'DENMARK', 'code' => '45'], + 'DM' => ['name' => 'DOMINICA', 'code' => '1767'], + 'DO' => ['name' => 'DOMINICAN REPUBLIC', 'code' => '1809'], + 'DZ' => ['name' => 'ALGERIA', 'code' => '213'], + 'EC' => ['name' => 'ECUADOR', 'code' => '593'], + 'EE' => ['name' => 'ESTONIA', 'code' => '372'], + 'EG' => ['name' => 'EGYPT', 'code' => '20'], + 'EH' => ['name' => 'WESTERN SAHARA', 'code' => '212'], + 'ER' => ['name' => 'ERITREA', 'code' => '291'], + 'ES' => ['name' => 'SPAIN', 'code' => '34'], + 'ET' => ['name' => 'ETHIOPIA', 'code' => '251'], + 'FI' => ['name' => 'FINLAND', 'code' => '358'], + 'FJ' => ['name' => 'FIJI', 'code' => '679'], + 'FK' => ['name' => 'FALKLAND ISLANDS (MALVINAS)', 'code' => '500'], + 'FM' => ['name' => 'MICRONESIA, FEDERATED STATES OF', 'code' => '691'], + 'FO' => ['name' => 'FAROE ISLANDS', 'code' => '298'], + 'FR' => ['name' => 'FRANCE', 'code' => '33'], + 'GA' => ['name' => 'GABON', 'code' => '241'], + 'GB' => ['name' => 'UNITED KINGDOM', 'code' => '44'], + 'GD' => ['name' => 'GRENADA', 'code' => '1473'], + 'GE' => ['name' => 'GEORGIA', 'code' => '995'], + 'GF' => ['name' => 'FRENCH GUIANA', 'code' => '594'], + 'GG' => ['name' => 'GUERNSEY', 'code' => '44'], + 'GH' => ['name' => 'GHANA', 'code' => '233'], + 'GI' => ['name' => 'GIBRALTAR', 'code' => '350'], + 'GL' => ['name' => 'GREENLAND', 'code' => '299'], + 'GM' => ['name' => 'GAMBIA', 'code' => '220'], + 'GN' => ['name' => 'GUINEA', 'code' => '224'], + 'GP' => ['name' => 'GUADELOUPE', 'code' => '590'], + 'GQ' => ['name' => 'EQUATORIAL GUINEA', 'code' => '240'], + 'GR' => ['name' => 'GREECE', 'code' => '30'], + 'GS' => ['name' => 'SOUTH GEORGIA & SOUTH SANDWICH ISLANDS', 'code' => '500'], + 'GT' => ['name' => 'GUATEMALA', 'code' => '502'], + 'GU' => ['name' => 'GUAM', 'code' => '1671'], + 'GW' => ['name' => 'GUINEA-BISSAU', 'code' => '245'], + 'GY' => ['name' => 'GUYANA', 'code' => '592'], + 'HK' => ['name' => 'HONG KONG', 'code' => '852'], + 'HM' => ['name' => 'HEARD & MCDONALD ISLANDS', 'code' => '672'], + 'HN' => ['name' => 'HONDURAS', 'code' => '504'], + 'HR' => ['name' => 'CROATIA', 'code' => '385'], + 'HT' => ['name' => 'HAITI', 'code' => '509'], + 'HU' => ['name' => 'HUNGARY', 'code' => '36'], + 'ID' => ['name' => 'INDONESIA', 'code' => '62'], + 'IE' => ['name' => 'IRELAND', 'code' => '353'], + 'IL' => ['name' => 'ISRAEL', 'code' => '972'], + 'IM' => ['name' => 'ISLE OF MAN', 'code' => '44'], + 'IN' => ['name' => 'INDIA', 'code' => '91'], + 'IO' => ['name' => 'BRITISH INDIAN OCEAN TERRITORY', 'code' => '246'], + 'IQ' => ['name' => 'IRAQ', 'code' => '964'], + 'IR' => ['name' => 'IRAN, ISLAMIC REPUBLIC OF', 'code' => '98'], + 'IS' => ['name' => 'ICELAND', 'code' => '354'], + 'IT' => ['name' => 'ITALY', 'code' => '39'], + 'JE' => ['name' => 'JERSEY', 'code' => '44'], + 'JM' => ['name' => 'JAMAICA', 'code' => '1876'], + 'JO' => ['name' => 'JORDAN', 'code' => '962'], + 'JP' => ['name' => 'JAPAN', 'code' => '81'], + 'KE' => ['name' => 'KENYA', 'code' => '254'], + 'KG' => ['name' => 'KYRGYZSTAN', 'code' => '996'], + 'KH' => ['name' => 'CAMBODIA', 'code' => '855'], + 'KI' => ['name' => 'KIRIBATI', 'code' => '686'], + 'KM' => ['name' => 'COMOROS', 'code' => '269'], + 'KN' => ['name' => 'SAINT KITTS AND NEVIS', 'code' => '1869'], + 'KP' => ['name' => 'KOREA DEMOCRATIC PEOPLES REPUBLIC OF', 'code' => '850'], + 'KR' => ['name' => 'KOREA REPUBLIC OF', 'code' => '82'], + 'KW' => ['name' => 'KUWAIT', 'code' => '965'], + 'KY' => ['name' => 'CAYMAN ISLANDS', 'code' => '1345'], + 'KZ' => ['name' => 'KAZAKSTAN', 'code' => '7'], + 'LA' => ['name' => 'LAO PEOPLES DEMOCRATIC REPUBLIC', 'code' => '856'], + 'LB' => ['name' => 'LEBANON', 'code' => '961'], + 'LC' => ['name' => 'SAINT LUCIA', 'code' => '1758'], + 'LI' => ['name' => 'LIECHTENSTEIN', 'code' => '423'], + 'LK' => ['name' => 'SRI LANKA', 'code' => '94'], + 'LR' => ['name' => 'LIBERIA', 'code' => '231'], + 'LS' => ['name' => 'LESOTHO', 'code' => '266'], + 'LT' => ['name' => 'LITHUANIA', 'code' => '370'], + 'LU' => ['name' => 'LUXEMBOURG', 'code' => '352'], + 'LV' => ['name' => 'LATVIA', 'code' => '371'], + 'LY' => ['name' => 'LIBYAN ARAB JAMAHIRIYA', 'code' => '218'], + 'MA' => ['name' => 'MOROCCO', 'code' => '212'], + 'MC' => ['name' => 'MONACO', 'code' => '377'], + 'MD' => ['name' => 'MOLDOVA, REPUBLIC OF', 'code' => '373'], + 'ME' => ['name' => 'MONTENEGRO', 'code' => '382'], + 'MF' => ['name' => 'SAINT MARTIN', 'code' => '1599'], + 'MG' => ['name' => 'MADAGASCAR', 'code' => '261'], + 'MH' => ['name' => 'MARSHALL ISLANDS', 'code' => '692'], + 'MK' => ['name' => 'MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF', 'code' => '389'], + 'ML' => ['name' => 'MALI', 'code' => '223'], + 'MM' => ['name' => 'MYANMAR', 'code' => '95'], + 'MN' => ['name' => 'MONGOLIA', 'code' => '976'], + 'MO' => ['name' => 'MACAU', 'code' => '853'], + 'MP' => ['name' => 'NORTHERN MARIANA ISLANDS', 'code' => '1670'], + 'MQ' => ['name' => 'MARTINIQUE', 'code' => '596'], + 'MR' => ['name' => 'MAURITANIA', 'code' => '222'], + 'MS' => ['name' => 'MONTSERRAT', 'code' => '1664'], + 'MT' => ['name' => 'MALTA', 'code' => '356'], + 'MU' => ['name' => 'MAURITIUS', 'code' => '230'], + 'MV' => ['name' => 'MALDIVES', 'code' => '960'], + 'MW' => ['name' => 'MALAWI', 'code' => '265'], + 'MX' => ['name' => 'MEXICO', 'code' => '52'], + 'MY' => ['name' => 'MALAYSIA', 'code' => '60'], + 'MZ' => ['name' => 'MOZAMBIQUE', 'code' => '258'], + 'NA' => ['name' => 'NAMIBIA', 'code' => '264'], + 'NC' => ['name' => 'NEW CALEDONIA', 'code' => '687'], + 'NE' => ['name' => 'NIGER', 'code' => '227'], + 'NF' => ['name' => 'NORFOLK ISLAND', 'code' => '672'], + 'NG' => ['name' => 'NIGERIA', 'code' => '234'], + 'NI' => ['name' => 'NICARAGUA', 'code' => '505'], + 'NL' => ['name' => 'NETHERLANDS', 'code' => '31'], + 'NO' => ['name' => 'NORWAY', 'code' => '47'], + 'NP' => ['name' => 'NEPAL', 'code' => '977'], + 'NR' => ['name' => 'NAURU', 'code' => '674'], + 'NU' => ['name' => 'NIUE', 'code' => '683'], + 'NZ' => ['name' => 'NEW ZEALAND', 'code' => '64'], + 'OM' => ['name' => 'OMAN', 'code' => '968'], + 'PA' => ['name' => 'PANAMA', 'code' => '507'], + 'PE' => ['name' => 'PERU', 'code' => '51'], + 'PF' => ['name' => 'FRENCH POLYNESIA', 'code' => '689'], + 'PG' => ['name' => 'PAPUA NEW GUINEA', 'code' => '675'], + 'PH' => ['name' => 'PHILIPPINES', 'code' => '63'], + 'PK' => ['name' => 'PAKISTAN', 'code' => '92'], + 'PL' => ['name' => 'POLAND', 'code' => '48'], + 'PM' => ['name' => 'SAINT PIERRE AND MIQUELON', 'code' => '508'], + 'PN' => ['name' => 'PITCAIRN', 'code' => '870'], + 'PR' => ['name' => 'PUERTO RICO', 'code' => '1'], + 'PS' => ['name' => 'PALESTINE', 'code' => '970'], + 'PT' => ['name' => 'PORTUGAL', 'code' => '351'], + 'PW' => ['name' => 'PALAU', 'code' => '680'], + 'PY' => ['name' => 'PARAGUAY', 'code' => '595'], + 'QA' => ['name' => 'QATAR', 'code' => '974'], + 'RE' => ['name' => 'RÉUNION', 'code' => '262'], + 'RO' => ['name' => 'ROMANIA', 'code' => '40'], + 'RS' => ['name' => 'SERBIA', 'code' => '381'], + 'RU' => ['name' => 'RUSSIAN FEDERATION', 'code' => '7'], + 'RW' => ['name' => 'RWANDA', 'code' => '250'], + 'SA' => ['name' => 'SAUDI ARABIA', 'code' => '966'], + 'SB' => ['name' => 'SOLOMON ISLANDS', 'code' => '677'], + 'SC' => ['name' => 'SEYCHELLES', 'code' => '248'], + 'SD' => ['name' => 'SUDAN', 'code' => '249'], + 'SE' => ['name' => 'SWEDEN', 'code' => '46'], + 'SG' => ['name' => 'SINGAPORE', 'code' => '65'], + 'SH' => ['name' => 'SAINT HELENA', 'code' => '290'], + 'SI' => ['name' => 'SLOVENIA', 'code' => '386'], + 'SJ' => ['name' => 'SVALBARD & JAN MAYEN', 'code' => '47'], + 'SK' => ['name' => 'SLOVAKIA', 'code' => '421'], + 'SL' => ['name' => 'SIERRA LEONE', 'code' => '232'], + 'SM' => ['name' => 'SAN MARINO', 'code' => '378'], + 'SN' => ['name' => 'SENEGAL', 'code' => '221'], + 'SO' => ['name' => 'SOMALIA', 'code' => '252'], + 'SR' => ['name' => 'SURINAME', 'code' => '597'], + 'SS' => ['name' => 'SOUTH SUDAN', 'code' => '211'], + 'ST' => ['name' => 'SAO TOME AND PRINCIPE', 'code' => '239'], + 'SV' => ['name' => 'EL SALVADOR', 'code' => '503'], + 'SX' => ['name' => 'SINT MAARTEN', 'code' => '1721'], + 'SY' => ['name' => 'SYRIAN ARAB REPUBLIC', 'code' => '963'], + 'SZ' => ['name' => 'SWAZILAND', 'code' => '268'], + 'TC' => ['name' => 'TURKS AND CAICOS ISLANDS', 'code' => '1649'], + 'TD' => ['name' => 'CHAD', 'code' => '235'], + 'TF' => ['name' => 'FRENCH SOUTHERN TERRITORIES ', 'code' => '262'], + 'TG' => ['name' => 'TOGO', 'code' => '228'], + 'TH' => ['name' => 'THAILAND', 'code' => '66'], + 'TJ' => ['name' => 'TAJIKISTAN', 'code' => '992'], + 'TK' => ['name' => 'TOKELAU', 'code' => '690'], + 'TL' => ['name' => 'TIMOR-LESTE', 'code' => '670'], + 'TM' => ['name' => 'TURKMENISTAN', 'code' => '993'], + 'TN' => ['name' => 'TUNISIA', 'code' => '216'], + 'TO' => ['name' => 'TONGA', 'code' => '676'], + 'TR' => ['name' => 'TURKEY', 'code' => '90'], + 'TT' => ['name' => 'TRINIDAD AND TOBAGO', 'code' => '1868'], + 'TV' => ['name' => 'TUVALU', 'code' => '688'], + 'TW' => ['name' => 'TAIWAN, PROVINCE OF CHINA', 'code' => '886'], + 'TZ' => ['name' => 'TANZANIA, UNITED REPUBLIC OF', 'code' => '255'], + 'UA' => ['name' => 'UKRAINE', 'code' => '380'], + 'UG' => ['name' => 'UGANDA', 'code' => '256'], + 'UM' => ['name' => 'U.S. OUTLYING ISLANDS', 'code' => '1'], + 'US' => ['name' => 'UNITED STATES', 'code' => '1'], + 'UY' => ['name' => 'URUGUAY', 'code' => '598'], + 'UZ' => ['name' => 'UZBEKISTAN', 'code' => '998'], + 'VA' => ['name' => 'HOLY SEE (VATICAN CITY STATE)', 'code' => '39'], + 'VC' => ['name' => 'SAINT VINCENT AND THE GRENADINES', 'code' => '1784'], + 'VE' => ['name' => 'VENEZUELA', 'code' => '58'], + 'VG' => ['name' => 'VIRGIN ISLANDS, BRITISH', 'code' => '1284'], + 'VI' => ['name' => 'VIRGIN ISLANDS, U.S.', 'code' => '1340'], + 'VN' => ['name' => 'VIETNAM', 'code' => '84'], + 'VU' => ['name' => 'VANUATU', 'code' => '678'], + 'WF' => ['name' => 'WALLIS AND FUTUNA', 'code' => '681'], + 'WS' => ['name' => 'SAMOA', 'code' => '685'], + 'XK' => ['name' => 'KOSOVO', 'code' => '383'], + 'YE' => ['name' => 'YEMEN', 'code' => '967'], + 'YT' => ['name' => 'MAYOTTE', 'code' => '262'], + 'ZA' => ['name' => 'SOUTH AFRICA', 'code' => '27'], + 'ZM' => ['name' => 'ZAMBIA', 'code' => '260'], + 'ZW' => ['name' => 'ZIMBABWE', 'code' => '263'], + ]; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5161ba3 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ + + + + + +
+ Module icon + +

CardDAV Middleware UI for FreePBX (formerly PhoneMiddleware)

+
+ +# For the users: All the downloads moved to
Releases + +## About this +This is not the complete module and *will not work as is!* This is only meant for developers who wants to build their backends and release a module based on that. So if you are an end user and you still reading this, well.. you shouldn't! Download the ready-to-use module from here + +## Development +First pull the main repo or, even better, one of the tagged versions available, then start developing by having a look at the main files in the project: +- `core/CoreInterface.php` is the main interface that defines all the mandatory and optional mathods you can implement. Have a look inside for all the comments beside the declarations. Implement your logic following the requirements of the UI too. Your core class **MUST** be `Core` and the file **MUST** be `core.php` +- `numbertocnam.php` and `carddavtoxml.php` are the files that are exposed to the web server, the user will call those URLs to get a CNAM or a phonebook XML respectively +- When you have tested everything and ready to deploy the module, start the build script inside a shell, you will be guided step-by-step and you'll receive a nice packaged module back + +## Donation +If you like to support me, you can donate. Any help is greatly appreciated. Thank you! + +paypal + +**Bitcoin:** 1Pig6XJxXGBj1v3r6uLrFUSHwyX8GPopRs + +**Monero:** 89qdmpDsMm9MUvpsG69hbRMcGWeG2D26BdATw1iXXZwi8DqSEstJGcWNkenrXtThCAAJTpjkUNtZuQvxK1N5xSyb18eXzPD + +## License +`SPDX-License-Identifier: CC-BY-NC-ND-4.0`
+This work is licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International
+By using this module, building your own one by extending it or any other similar matter you agree to the terms. Licenses for the included libraries are available below, credits go to the original author only. +- [Tagify](https://github.com/yairEO/tagify) +- [DragSort](https://github.com/yairEO/dragsort) \ No newline at end of file diff --git a/Restore.php b/Restore.php new file mode 100644 index 0000000..43c65f0 --- /dev/null +++ b/Restore.php @@ -0,0 +1,23 @@ + © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +namespace FreePBX\modules\PhoneMiddleware; + +use FreePBX\modules\Backup as Base; + +class Restore extends Base\RestoreBase +{ + public function runRestore() + { + $Core = $this->FreePBX->PhoneMiddleware->getCore(); //get instance beacuse I cannot access statically here + + if (method_exists($Core, 'run_restore')) { + $Core->run_restore($this, $this->getVersion()); + } else + throw new \Exception(_('Retore is not implemented!')); + } +} diff --git a/assets/css/dragsort.css b/assets/css/dragsort.css new file mode 100644 index 0000000..8efaf00 --- /dev/null +++ b/assets/css/dragsort.css @@ -0,0 +1 @@ +.dragsort--noAnim,.dragsort--noAnim~[draggable]{transition:none!important}.dragsort--dragStart .dragsort--dragElem{opacity:0}.dragsort--dragStart .dragsort--hide{width:0!important;height:0!important;padding:0!important;margin:0!important;border-width:0!important}.dragsort--dragStart>*{transition:var(--dragsort-trans-dur, .18s) cubic-bezier(.6, .1, .4, 1.2)}.dragsort--dragStart>*>*{pointer-events:none} \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..b14a026 --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,809 @@ +/* + * CardDAV Middleware UI + * Written by Massi-X © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +/* override default dragsort styles to prevent glithes. See also js or https://github.com/yairEO/dragsort/issues/5#issuecomment-1004465926 */ +.dragsort--dragStart .dragsort--hide { + display: none !important; +} + +.dragsort--dragStart>* { + transition: margin .15s ease-in-out !important; +} + +/* set a maximum width for dialogs + prettier close icon */ +.ui-dialog { + max-width: 95vw; +} + +.ui-dialog-titlebar-close { + width: 20px !important; + padding: 0 !important; + height: 20px !important; + top: 17.5px !important; + right: 15px !important; + margin: 0 !important; + opacity: 1 !important; +} + +.ui-dialog-titlebar-close::before { + font-family: 'FontAwesome'; + content: "\f057"; + line-height: 20px; + font-size: 20px; + opacity: 1; + color: #f44336; +} + +.ui-dialog-titlebar-close:hover::before { + color: #d32f2f; +} + +.ui-dialog-titlebar-close:active::before { + color: #b71c1c; +} + +/* fix Sangoma's own footer... */ +#footer { + height: auto !important; + min-height: 100px; +} + +@media screen and (max-width: 991px) { + #footer #footer_content #footer_logo { + float: initial !important; + padding-right: 0 !important; + } +} + +/* fix row height */ +.element-container { + min-height: 42px; +} + +/* fix input hiding */ +input[hidden] { + display: none !important; +} + +/* fix button style per my taste */ +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: none; +} + +/* radioset tabulation selection/focus/active colors */ +.radioset label:focus-visible { + outline: none; +} + +.radioset>input:not(:checked)+label:focus-visible { + background: #d2ddd8 !important; +} + +.radioset>input:checked+label:focus-visible { + background: #4a7d64 !important; +} + +/* fix dialog height */ +.ui-dialog { + max-height: calc(100vh - 30px); + overflow-y: auto; +} + +/* keep dialog at center of screen */ +.ui-dialog { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%); +} + +/* adjust dialog list per my taste */ +.ui-dialog ul li { + margin-bottom: 5px; +} + +/* fix fpbx-tagify conflict see issue https://github.com/yairEO/tagify/issues/1110 */ +.tagify { + height: auto; + --tag-inset-shadow-size: 1.3em !important; +} + +/* adjust select (single-value mode) style of tagify per my needs */ +.phone_type__tagify .tagify__input::selection { + background: transparent; +} + +.tagify--select .tagify__tag__removeBtn { + display: none !important; +} + +/* overcome tagify visibility hidden after loading */ +.tagify { + visibility: visible !important; +} + +/* align tagify and inputs to the same styles */ +.tagify { + --tags-focus-border-color: rgba(94, 156, 125, 0.9) !important; + --tags-hover-border-color: rgba(94, 156, 125, 0.9) !important; + --tag-border-radius: 4px !important; +} + +:root { + --tagify-dd-color-primary: rgba(94, 156, 125, 0.9) !important; +} + +#page_body input.form-control, +.ui-dialog input.form-control { + min-height: 37px; + border-color: #ddd; +} + +#page_body input.form-control:hover:enabled, +.ui-dialog input.form-control:hover:enabled, +#page_body input.form-control:focus:enabled, +.ui-dialog input.form-control:focus:enabled { + box-shadow: none; + border-color: rgba(94, 156, 125, 0.9); +} + +#page_body input.form-control:focus, +.ui-dialog input.form-control:focus { + box-shadow: none; +} + +/* adjust input style of tagify per my needs */ +.phone_type__tagify, +.output_construct__tagify { + cursor: pointer !important; +} + +/* To be used together to place a button beside an input */ +.col-md-9-flex { + display: flex; +} + +.form-control-flex { + flex: 1; +} + +.btn-input-flex { + margin-left: 10px; +} + +/* custom checkbox */ +.ph-checkbox { + display: block; + user-select: none; + cursor: pointer; + height: 25px; + width: 25px; +} + +.ph-checkbox>input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +.ph-checkbox>span { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + border-radius: 0.3em; + border: 1px solid #9e9e9e; + background-color: #fafafa; +} + +.ph-checkbox>span:after { + font: normal normal normal 14px/1 FontAwesome; + content: "\f00c"; + position: absolute; + color: #9E9E9E; + left: 5px; + top: 5px; + width: 5px; + height: 10px; +} + +.ph-checkbox:hover input:not(:checked)~span { + background-color: #eee; +} + +.ph-checkbox:active input:not(:checked)~span, +.ph-checkbox>input:not(:checked):focus-visible~span { + background-color: #e0e0e0; +} + +.ph-checkbox>input:checked~span { + background-color: rgba(94, 156, 125, 0.9); + border-color: #4a7d64; +} + +.ph-checkbox:hover input:checked~span { + background-color: #5e9c7d; +} + +.ph-checkbox:active input:checked~span, +.ph-checkbox>input:checked:focus-visible~span { + background-color: #4a7d64; +} + +.ph-checkbox>input:checked~span:after { + display: block; + color: #fff; +} + +/* checkbox inside max cnam output */ +#max_cnam_container { + position: relative; +} + +#max_cnam_container>input[type="number"] { + padding-left: 37px; + transition: outline-color 0.25s ease; + outline: solid 1px transparent; +} + +.input-invalid-blink { + border-color: #F44336 !important; + outline-color: #F44336 !important; +} + +#max_cnam_container>.ph-checkbox { + position: absolute; + margin: 6px 6px; +} + +/* Other styles */ +pre { + white-space: pre-line; +} + +.notvisible { + visibility: hidden; +} + +details { + padding: 5px; + background: #fffde7; + border: 1px solid #fff59d; + border-radius: 4px; +} + +summary { + cursor: pointer; + color: #616161; +} + +summary::-webkit-details-marker { + display: none; +} + +summary:before { + content: "►"; + font-size: 10px; + padding-right: 5px; +} + +details[open] summary:before { + content: "▼"; +} + +.col-md-9.flexible { + display: flex; + align-items: center; +} + +.header-img { + height: 30px; +} + +.ph-header { + display: flex; + align-items: center; + margin-bottom: 3px; +} + +.ph-header>.title, +.ph-header>.activation { + display: flex; + align-items: center; +} + +.ph-header>div>*:not(script):not(style) { + display: inline-flex; + flex-shrink: 0; +} + +.ph-header>div>*:not(:first-child) { + margin-left: 8px; +} + +.ph-header>.title { + flex: 1; + order: 1; +} + +.ph-header>.activation { + order: 2; +} + +.ph-header>.title>h2 { + margin-bottom: 0; +} + +ul.numeric-list { + list-style: decimal; +} + +ul.numeric-list> ::marker { + font-weight: bold; +} + +.item-info { + font-size: 12px; + color: #9e9e9e; + margin-left: 10px; +} + +.element-container.flexible { + display: flex; + align-items: center; + justify-content: space-between; +} + +.btn-magic { + display: inline-block; + margin: 5px 5px; +} + +.btn-magic>* { + text-decoration: none !important; +} + +.btn-magic>i { + padding-right: 9px; + font-size: 18px; + line-height: 18px; + vertical-align: middle; +} + +.btn-popup { + display: inline-block; + font-weight: bold; + text-decoration: underline; + color: #2196f3; +} + +.btn-popup:visited { + color: #2196f3; +} + +.btn-popup>i { + text-decoration: underline; + font-weight: bold; +} + +.btn-submit { + float: right; + margin: 10px 10px 5px 0; +} + +#restoredonationPopup>form>input { + min-width: 300px; +} + +.help-section { + display: inline-flex; + margin: 5px; + background: #f1fbfc; + border: 1px solid #b2ebf2; + border-radius: 4px; +} + +.help-section>* { + padding: 8px; +} + +.help-section>*:nth-child(odd) { + border-right: 1px solid #b2ebf2; +} + +.footer { + width: 100%; + margin: 0; + padding: 10px 0; + border: none; + font-size: 11px; + text-align: center; +} + +.footer>b:first-of-type { + display: inline-block; + margin-bottom: 4px; +} + +a:hover>i.fa { + text-decoration: underline; +} + +/* Nice rotating animation for spinners */ +.fa.fa-spinner.letsspin { + animation: 1.4s linear; + animation-name: spinner-fa; + animation-iteration-count: infinite; + animation-direction: normal; +} + +@keyframes spinner-fa { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +/* Carddav setup popup custom styles */ +#setupCarddav input[type=text], +#setupCarddav input[type=password] { + width: 100%; + min-width: 300px; +} + +.ui-dialog-content>form>.btn { + float: right; + margin: 10px 0; + font-size: inherit; +} + +#carddav_result { + user-select: none; + width: calc(100% - 4px); + border-collapse: collapse; + margin: 0 2px; +} + +#carddav_result tr { + height: 23px; + cursor: default; +} + +#carddav_result th, +#carddav_result td { + border: 1px solid #bdbdbd; + padding: 0 5px; +} + +#carddav_result th:nth-child(1), +#carddav_result th:nth-child(2), +#carddav_result td:nth-child(1), +#carddav_result td:nth-child(2) { + width: 0; + text-align: center; +} + +#carddav_result th:last-child, +#carddav_result td:last-child { + word-break: break-all; +} + +#carddav_result td>i.fa { + line-height: 23px; + font-size: 16px; + vertical-align: middle; + cursor: grab; +} + +.carddav-setup-line label { + margin-bottom: 0; + margin-right: 10px; +} + +#carddav_parameters tbody tr { + width: 100%; + display: inline-grid; + margin-bottom: 10px; +} + +.carddav_info { + margin: 3px 5px 0 5px; + font-size: 12px; + color: #757575; +} + +/* Notification UI */ +#notification-ui { + display: flex; + order: 3; + position: relative; + margin: 0 5px; +} + +#notification-ui p { + margin: 0; +} + +#notification-header { + cursor: pointer; + font-size: 25px; + border-radius: 0.3em; +} + +#notification-count { + position: absolute; + pointer-events: none; + display: flex; + justify-content: center; + vertical-align: middle; + height: 17px; + width: 17px; + right: -5px; + top: -3px; + z-index: 101; + background: #f44336; + color: #fff; + font-size: 12px; + line-height: 17px; + font-weight: bold; + border-radius: 17px; +} + +#notification-container { + position: absolute; + display: flex; + flex-direction: column; + pointer-events: none; + z-index: 99; + top: 38px; + right: -4px; + width: 35vw; + min-width: 600px; + max-width: 800px; + max-height: 60vh; + opacity: 0; + visibility: hidden; + border: 1px solid #bdbdbd; + border-radius: 0.3em; + background: #fff; + transition: all .25s ease; +} + +#notification-container::after, +#notification-container::before { + content: ''; + position: absolute; + display: block; + right: 5px; + width: 0; + height: 0; + border-style: solid; +} + +#notification-container::after { + top: -20px; + border-color: transparent transparent #fff transparent; + border-width: 10px; +} + +#notification-container::before { + top: -21px; + border-color: transparent transparent #bdbdbd transparent; + border-width: 10px; +} + +#notification-container>.delete-all { + pointer-events: all; + padding: 10px 14px; +} + +.bubble-container { + pointer-events: all; + max-height: calc(60vh - 20px); + padding: 0 10px 7px 10px; + overflow: scroll; +} + +#notification-ui.open>#notification-container { + visibility: visible; + opacity: 1; +} + +.notification-bubble { + padding: 10px 15px; + margin-bottom: 3px; + border-radius: 0.3em; + font-size: 14px; + line-height: 25px; + border: 1px solid #f5f5f5; + word-break: break-word; +} + +.notification-bubble.info { + background-color: #f1f8e9; +} + +.notification-bubble.error { + background-color: #ffcdd2; +} + +.notification-bubble.verbose { + background-color: #ddd; +} + +.notification-bubble>button, +#notification-container>.delete-all>button { + float: right; + border-radius: 12.5px; + font-weight: bold; + color: #fff; +} + +.notification-bubble>button { + display: inline-flex; + justify-content: center; + align-items: center; + height: 26px; + width: 26px; + margin: 0 -6px; +} + +.notification-bubble.info>button { + background-color: #b0ca93; + border: 1px solid #a6bf8b; +} + +.notification-bubble.error>button, +#notification-container>.delete-all>button { + background-color: #e57373; + border: 1px solid #ef9a9a; +} + +.notification-bubble.verbose>button { + background-color: #9e9e9e; + border: 1px solid #919191; +} + +.notification-bubble>button:hover, +#notification-container>.delete-all>button:hover { + background: #f44336; + border: 1px solid #f44336; +} + +.notification-bubble>button:focus, +#notification-container>.delete-all>button:focus { + background: #c15c5c; + border: 1px solid #c15c5c; +} + +.bell-shake { + animation: 10s linear; + animation-name: bell-shake; + animation-iteration-count: infinite; + animation-direction: normal; +} + +@keyframes bell-shake { + 0% { + transform: rotate(0deg); + } + + 94% { + transform: rotate(0deg); + } + + 95% { + transform: rotate(-5deg); + } + + 96% { + transform: rotate(5deg); + } + + 97% { + transform: rotate(-5deg); + } + + 98% { + transform: rotate(5deg); + } + + 99% { + transform: rotate(-5deg); + } + + 100% { + transform: rotate(0deg); + } +} + +@media screen and (max-width: 1023px) { + + /* set a minimum width for dialogs on small screens to prevent them from being ridiculous */ + .ui-dialog { + min-width: 80vw; + } +} + +@media screen and (min-width: 768px) { + .visible-mobile { + display: none; + } +} + +@media screen and (max-width: 767px) { + .visible-desktop { + display: none; + } +} + +@media screen and (max-width: 991px) { + #notification-ui { + order: 2; + } + + .ph-header>.activation { + order: 3; + } + + .ph-header { + flex-wrap: wrap; + } + + .ph-header>.activation { + margin-top: 8px; + } + + .ph-header>.title, + .ph-header>.activation { + width: 100%; + } + + #notification-ui { + margin-left: auto; + } + + #notification-container { + width: 94.4vw; + min-width: 0; + } + + .help-section { + display: flex; + justify-content: space-around; + } + + .help-section>*:nth-child(odd) { + border: none; + } +} + +/* Autoselect trick used for config popup */ +.autoselect_container { + width: 100% !important; + margin-top: 5px !important; + padding: 4px 10px !important; + border-radius: 3em !important; + border: 1px solid #e0e0e0 !important; + background: #fafafa !important; + box-shadow: none !important; + outline: none !important; + font-family: inherit !important; + font-size: inherit !important; + color: inherit !important; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/assets/css/tagify.css b/assets/css/tagify.css new file mode 100644 index 0000000..3516030 --- /dev/null +++ b/assets/css/tagify.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--tagify-dd-color-primary:rgb(53,149,246);--tagify-dd-bg-color:white;--tagify-dd-item-pad:.3em .5em}.tagify{--tags-disabled-bg:#F1F1F1;--tags-border-color:#DDD;--tags-hover-border-color:#CCC;--tags-focus-border-color:#3595f6;--tag-border-radius:3px;--tag-bg:#E5E5E5;--tag-hover:#D3E2E2;--tag-text-color:black;--tag-text-color--edit:black;--tag-pad:0.3em 0.5em;--tag-inset-shadow-size:1.1em;--tag-invalid-color:#D39494;--tag-invalid-bg:rgba(211, 148, 148, 0.5);--tag-remove-bg:rgba(211, 148, 148, 0.3);--tag-remove-btn-color:black;--tag-remove-btn-bg:none;--tag-remove-btn-bg--hover:#c77777;--input-color:inherit;--tag--min-width:1ch;--tag--max-width:auto;--tag-hide-transition:0.3s;--placeholder-color:rgba(0, 0, 0, 0.4);--placeholder-color-focus:rgba(0, 0, 0, 0.25);--loader-size:.8em;--readonly-striped:1;display:inline-flex;align-items:flex-start;flex-wrap:wrap;border:1px solid var(--tags-border-color);padding:0;line-height:0;cursor:text;outline:0;position:relative;box-sizing:border-box;transition:.1s}@keyframes tags--bump{30%{transform:scale(1.2)}}@keyframes rotateLoader{to{transform:rotate(1turn)}}.tagify:hover:not(.tagify--focus):not(.tagify--invalid){--tags-border-color:var(--tags-hover-border-color)}.tagify[disabled]{background:var(--tags-disabled-bg);filter:saturate(0);opacity:.5;pointer-events:none}.tagify[disabled].tagify--select,.tagify[readonly].tagify--select{pointer-events:none}.tagify[disabled]:not(.tagify--mix):not(.tagify--select),.tagify[readonly]:not(.tagify--mix):not(.tagify--select){cursor:default}.tagify[disabled]:not(.tagify--mix):not(.tagify--select)>.tagify__input,.tagify[readonly]:not(.tagify--mix):not(.tagify--select)>.tagify__input{visibility:hidden;width:0;margin:5px 0}.tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div,.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div{padding:var(--tag-pad)}.tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div::before,.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div::before{animation:readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused}@keyframes readonlyStyles{0%{background:linear-gradient(45deg,var(--tag-bg) 25%,transparent 25%,transparent 50%,var(--tag-bg) 50%,var(--tag-bg) 75%,transparent 75%,transparent) 0/5px 5px;box-shadow:none;filter:brightness(.95)}}.tagify[disabled] .tagify__tag__removeBtn,.tagify[readonly] .tagify__tag__removeBtn{display:none}.tagify--loading .tagify__input>br:last-child{display:none}.tagify--loading .tagify__input::before{content:none}.tagify--loading .tagify__input::after{content:"";vertical-align:middle;opacity:1;width:.7em;height:.7em;width:var(--loader-size);height:var(--loader-size);min-width:0;border:3px solid;border-color:#eee #bbb #888 transparent;border-radius:50%;animation:rotateLoader .4s infinite linear;content:""!important;margin:-2px 0 -2px .5em}.tagify--loading .tagify__input:empty::after{margin-left:0}.tagify+input,.tagify+textarea{position:absolute!important;left:-9999em!important;transform:scale(0)!important}.tagify__tag{display:inline-flex;align-items:center;margin:5px 0 5px 5px;position:relative;z-index:1;outline:0;line-height:normal;cursor:default;transition:.13s ease-out}.tagify__tag>div{vertical-align:top;box-sizing:border-box;max-width:100%;padding:var(--tag-pad);color:var(--tag-text-color);line-height:inherit;border-radius:var(--tag-border-radius);white-space:nowrap;transition:.13s ease-out}.tagify__tag>div>*{white-space:pre-wrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;vertical-align:top;min-width:var(--tag--min-width);max-width:var(--tag--max-width);transition:.8s ease,.1s color}.tagify__tag>div>[contenteditable]{outline:0;-webkit-user-select:text;user-select:text;cursor:text;margin:-2px;padding:2px;max-width:350px}.tagify__tag>div::before{content:"";position:absolute;border-radius:inherit;inset:var(--tag-bg-inset,0);z-index:-1;pointer-events:none;transition:120ms ease;animation:tags--bump .3s ease-out 1;box-shadow:0 0 0 var(--tag-inset-shadow-size) var(--tag-bg) inset}.tagify__tag:focus div::before,.tagify__tag:hover:not([readonly]) div::before{--tag-bg-inset:-2.5px;--tag-bg:var(--tag-hover)}.tagify__tag--loading{pointer-events:none}.tagify__tag--loading .tagify__tag__removeBtn{display:none}.tagify__tag--loading::after{--loader-size:.4em;content:"";vertical-align:middle;opacity:1;width:.7em;height:.7em;width:var(--loader-size);height:var(--loader-size);min-width:0;border:3px solid;border-color:#eee #bbb #888 transparent;border-radius:50%;animation:rotateLoader .4s infinite linear;margin:0 .5em 0 -.1em}.tagify__tag--flash div::before{animation:none}.tagify__tag--hide{width:0!important;padding-left:0;padding-right:0;margin-left:0;margin-right:0;opacity:0;transform:scale(0);transition:var(--tag-hide-transition);pointer-events:none}.tagify__tag--hide>div>*{white-space:nowrap}.tagify__tag.tagify--noAnim>div::before{animation:none}.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div>span{opacity:.5}.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div::before{--tag-bg:var(--tag-invalid-bg);transition:.2s}.tagify__tag[readonly] .tagify__tag__removeBtn{display:none}.tagify__tag[readonly]>div::before{animation:readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused}@keyframes readonlyStyles{0%{background:linear-gradient(45deg,var(--tag-bg) 25%,transparent 25%,transparent 50%,var(--tag-bg) 50%,var(--tag-bg) 75%,transparent 75%,transparent) 0/5px 5px;box-shadow:none;filter:brightness(.95)}}.tagify__tag--editable>div{color:var(--tag-text-color--edit)}.tagify__tag--editable>div::before{box-shadow:0 0 0 2px var(--tag-hover) inset!important}.tagify__tag--editable>.tagify__tag__removeBtn{pointer-events:none}.tagify__tag--editable>.tagify__tag__removeBtn::after{opacity:0;transform:translateX(100%) translateX(5px)}.tagify__tag--editable.tagify--invalid>div::before{box-shadow:0 0 0 2px var(--tag-invalid-color) inset!important}.tagify__tag__removeBtn{order:5;display:inline-flex;align-items:center;justify-content:center;border-radius:50px;cursor:pointer;font:14px/1 Arial;background:var(--tag-remove-btn-bg);color:var(--tag-remove-btn-color);width:14px;height:14px;margin-right:4.6666666667px;margin-left:auto;overflow:hidden;transition:.2s ease-out}.tagify__tag__removeBtn::after{content:"×";transition:.3s,color 0s}.tagify__tag__removeBtn:hover{color:#fff;background:var(--tag-remove-btn-bg--hover)}.tagify__tag__removeBtn:hover+div>span{opacity:.5}.tagify__tag__removeBtn:hover+div::before{box-shadow:0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg,rgba(211,148,148,.3)) inset!important;transition:box-shadow .2s}.tagify:not(.tagify--mix) .tagify__input br{display:none}.tagify:not(.tagify--mix) .tagify__input *{display:inline;white-space:nowrap}.tagify__input{flex-grow:1;display:inline-block;min-width:110px;margin:5px;padding:var(--tag-pad);line-height:normal;position:relative;white-space:pre-wrap;color:var(--input-color);box-sizing:inherit}.tagify__input:empty::before{position:static}.tagify__input:focus{outline:0}.tagify__input:focus::before{transition:.2s ease-out;opacity:0;transform:translatex(6px)}@supports (-ms-ime-align:auto){.tagify__input:focus::before{display:none}}.tagify__input:focus:empty::before{transition:.2s ease-out;opacity:1;transform:none;color:rgba(0,0,0,.25);color:var(--placeholder-color-focus)}@-moz-document url-prefix(){.tagify__input:focus:empty::after{display:none}}.tagify__input::before{content:attr(data-placeholder);height:1em;line-height:1em;margin:auto 0;z-index:1;color:var(--placeholder-color);white-space:nowrap;pointer-events:none;opacity:0;position:absolute}.tagify__input::after{content:attr(data-suggest);display:inline-block;vertical-align:middle;position:absolute;min-width:calc(100% - 1.5em);text-overflow:ellipsis;overflow:hidden;white-space:pre;color:var(--tag-text-color);opacity:.3;pointer-events:none;max-width:100px}.tagify__input .tagify__tag{margin:0 1px}.tagify--mix{display:block}.tagify--mix .tagify__input{padding:5px;margin:0;width:100%;height:100%;line-height:1.5;display:block}.tagify--mix .tagify__input::before{height:auto;display:none;line-height:inherit}.tagify--mix .tagify__input::after{content:none}.tagify--select::after{content:">";opacity:.5;position:absolute;top:50%;right:0;bottom:0;font:16px monospace;line-height:8px;height:8px;pointer-events:none;transform:translate(-150%,-50%) scaleX(1.2) rotate(90deg);transition:.2s ease-in-out}.tagify--select[aria-expanded=true]::after{transform:translate(-150%,-50%) rotate(270deg) scaleY(1.2)}.tagify--select .tagify__tag{position:absolute;top:0;right:1.8em;bottom:0}.tagify--select .tagify__tag div{display:none}.tagify--select .tagify__input{width:100%}.tagify--empty .tagify__input::before{transition:.2s ease-out;opacity:1;transform:none;display:inline-block;width:auto}.tagify--mix .tagify--empty .tagify__input::before{display:inline-block}.tagify--focus{--tags-border-color:var(--tags-focus-border-color);transition:0s}.tagify--invalid{--tags-border-color:#D39494}.tagify__dropdown{position:absolute;z-index:9999;transform:translateY(1px);overflow:hidden}.tagify__dropdown[placement=top]{margin-top:0;transform:translateY(-100%)}.tagify__dropdown[placement=top] .tagify__dropdown__wrapper{border-top-width:1.1px;border-bottom-width:0}.tagify__dropdown[position=text]{box-shadow:0 0 0 3px rgba(var(--tagify-dd-color-primary),.1);font-size:.9em}.tagify__dropdown[position=text] .tagify__dropdown__wrapper{border-width:1px}.tagify__dropdown__wrapper{max-height:300px;overflow:auto;overflow-x:hidden;background:var(--tagify-dd-bg-color);border:1px solid;border-color:var(--tagify-dd-color-primary);border-bottom-width:1.5px;border-top-width:0;box-shadow:0 2px 4px -2px rgba(0,0,0,.2);transition:.25s cubic-bezier(0,1,.5,1)}.tagify__dropdown__header:empty{display:none}.tagify__dropdown__footer{display:inline-block;margin-top:.5em;padding:var(--tagify-dd-item-pad);font-size:.7em;font-style:italic;opacity:.5}.tagify__dropdown__footer:empty{display:none}.tagify__dropdown--initial .tagify__dropdown__wrapper{max-height:20px;transform:translateY(-1em)}.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper{transform:translateY(2em)}.tagify__dropdown__item{box-sizing:border-box;padding:var(--tagify-dd-item-pad);margin:1px;cursor:pointer;border-radius:2px;position:relative;outline:0;max-height:60px;max-width:100%}.tagify__dropdown__item--active{background:var(--tagify-dd-color-primary);color:#fff}.tagify__dropdown__item:active{filter:brightness(105%)}.tagify__dropdown__item--hidden{padding-top:0;padding-bottom:0;margin:0 1px;pointer-events:none;overflow:hidden;max-height:0;transition:var(--tagify-dd-item--hidden-duration,.3s)!important}.tagify__dropdown__item--hidden>*{transform:translateY(-100%);opacity:0;transition:inherit} \ No newline at end of file diff --git a/assets/css/timepicker.css b/assets/css/timepicker.css new file mode 100644 index 0000000..368a8ab --- /dev/null +++ b/assets/css/timepicker.css @@ -0,0 +1,143 @@ +/* + * CardDAV Middleware UI + * Written by Massi-X © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +.__timepicker-container { + display: flex; + height: 39px; + padding: 0; + background: #fff; + border: 1px solid #DDD; +} + +.__timepicker-container:hover, +.__timepicker-container:focus-within { + border-color: rgba(94, 156, 125, 0.9) !important; +} + +.__timepicker-div { + position: relative; + margin: 0; + flex: 1; + border-right: 1px solid #ddd; +} + +.__timepicker-div:last-child { + border-right: 0; +} + +.__timepicker-div>input { + width: 100%; + height: 100%; + pointer-events: none; + background: transparent; + border: 0; + padding-left: 66px; + outline: none !important; + box-shadow: none !important; + color: #000; +} + +.__timepicker-div>span { + position: absolute; + height: calc(100% - 3px); + cursor: default; + background: #fff; + right: 0; + margin-top: 3px; + margin-right: 1px; + padding: 0 6px; + color: #757575; +} + +.__timepicker-div>input::selection, +.__timepicker-div>span::selection { + background: transparent; +} + +.__timepicker-div>span:before { + content: ""; + display: inline-block; + height: 100%; + vertical-align: middle; +} + +.__timepicker-button-container { + position: absolute; + margin: 7px 9px; +} + +.__timepicker-button-container label { + display: inline-flex; + align-items: center; + justify-content: center; + height: 24px; + width: 24px; + cursor: pointer; + margin: 0; + border: 1px solid #959595; + border-radius: 0; + background: #fafafa; + color: #616161; +} + +.__timepicker-button-container label:hover { + background: #eee; +} + +.__timepicker-button-container label:active, +.__timepicker-button-container label:focus-visible { + outline: none; + background: #ddd; +} + +.__timepicker-button-container label:last-child { + border-left: 0; + border-top-right-radius: 0.2em; + border-bottom-right-radius: 0.2em; +} + +.__timepicker-button-container label:first-child { + border-top-left-radius: 0.2em; + border-bottom-left-radius: 0.2em; +} + +.__timepicker-arrow-down, +.__timepicker-arrow-up { + width: 0; + height: 0; + border-left: 7px solid transparent; + border-right: 7px solid transparent; +} + +.__timepicker-arrow-up { + border-bottom: 7px solid #616161; +} + +.__timepicker-arrow-down { + border-top: 7px solid #616161; +} + +@media screen and (max-width: 767px) { + .__timepicker-container { + display: block; + height: 117px; + } + + .__timepicker-div { + height: 39px; + border-bottom: 1px solid#ddd; + border-right: 0; + } + + .__timepicker-div:last-child { + border-bottom: 0; + } + + .__timepicker-div>span { + height: calc(100% - 9px); + margin-top: 5px; + } +} \ No newline at end of file diff --git a/assets/images/icon.png b/assets/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9609c5d648dbbd7b8f1ca7a2b90764e1381aca44 GIT binary patch literal 65201 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 literal 0 HcmV?d00001 diff --git a/assets/js/dragsort.js b/assets/js/dragsort.js new file mode 100644 index 0000000..1f7c551 --- /dev/null +++ b/assets/js/dragsort.js @@ -0,0 +1,318 @@ +;(function(root, factory){ + var define = define || {}; + if( typeof define === 'function' && define.amd ) + define([], factory); + else if( typeof exports === 'object' && typeof module === 'object' ) + module.exports = factory(); + else if(typeof exports === 'object') + exports["DragSort"] = factory() + else + root.DragSort = factory() +}(this, function(){ + var _id = 0, + _current = {}, // currently-dragged element + _instances = {} + + https://stackoverflow.com/a/14570614/104380 + var observeDOM = (function(){ + var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; + + return function( obj, callback ){ + if( !obj || obj.nodeType !== 1 ) return; // validation + + if( MutationObserver ){ + // define a new observer + var obs = new MutationObserver(function(mutations, observer){ + callback(mutations) + }) + + obs.observe(obj, {childList:true, subtree:false}) + } + + else if( window.addEventListener ) + obj.addEventListener('DOMNodeInserted', callback, false) + } + })() + + function DragSort(elm, settings) { + if (!elm) return this; + + settings = settings || {} + this.parentElm = elm; + this.uid = settings.uid; + + this.settings = { + selector: '*', + callbacks: {} + } + + Object.assign(this.settings, settings) + + this.setup() + observeDOM(this.parentElm, this.setup.bind(this)) + this.bindEvents() + } + + DragSort.prototype = { + namespace: 'dragsort', + + setup() { + // remove non-element nodes + [...this.parentElm.childNodes].forEach(elm => { + if (elm.nodeType != 1) + return elm.parentNode.removeChild(elm) + // set the "draggable" property on what's left + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable + + if( elm.matches(this.settings.selector) ) + elm.draggable = true + }) + + this.gap = this.getItemsGap(this.parentElm.firstElementChild) + }, + + throttle(cb, limit) { + var wait = false; + var that = this; + return function (args) { + if (!wait) { + cb.call(that, args); + wait = true; + setTimeout(() => wait = false, limit) + } + } + }, + + getDraggableElm(elm) { + if( !elm.closest ) return null + + var draggableElm = elm.closest('[draggable="true"]') + // only allow dragging/dropping inside the same parent element + return (this.uid == _current.uid) ? draggableElm : null + }, + + dragstart(e, elm) { + _current = this + + var draggableElm = this.getDraggableElm(elm), + clientRect; + + if (!draggableElm) { + _current = {} + return + } + + this.source = this.getInitialState() + this.target = this.getInitialState() + + clientRect = draggableElm.getBoundingClientRect() // more accurate than offsetWidth/offsetHeight (not rounded) + + this.source.elm = draggableElm + this.source.idx = this.getNodeIndex(draggableElm) + this.source.size.width = clientRect.width + this.source.size.height = clientRect.height + + // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/effectAllowed + e.dataTransfer.effectAllowed = 'move' + + this.settings.callbacks.dragStart && this.settings.callbacks.dragStart(this.source.elm, e) + + // https://stackoverflow.com/q/19639969/104380 + setTimeout(this.afterDragStart.bind(this)) + }, + + afterDragStart() { + var prop = this.settings.mode == 'vertical' ? 'height' : 'width' + + this.parentElm.classList.add(`${this.namespace}--dragStart`) + + // hiding the source element with transition, the initial "width" is set to occupy the same space + this.source.elm.style[prop] = this.source.size[prop] + 'px' + + this.source.elm.classList.add(`${this.namespace}--dragElem`) + }, + + dragover(e) { + e.preventDefault(); + e.stopPropagation(); + + var elm = e.target; + + elm = this.getDraggableElm(elm) + + if (!elm || !this.target) return; + + var prevTarget = { + elm: this.target.elm, + hoverDirection: this.target.hoverDirection + } + + e.dataTransfer.dropEffect = "move"; + + this.target.hoverDirection = this.getTargetDirection(e); + // Continue only if there was a reason for a change + if (prevTarget.elm != elm || prevTarget.hoverDirection != this.target.hoverDirection) + this.directionAwareDragEnter(e, elm); + }, + + dragenter(e, elm) { + elm = this.getDraggableElm(elm); + + if (!elm || !this.target) return; + + if (!this.isValidElm(elm) || this.source.elm == elm || !this.source.elm) + return; + + this.target.bounding = elm.getBoundingClientRect(); + }, + + // only gets called once the mouse direction is knowsn (entering from left/right) + directionAwareDragEnter(e, elm) { + e.preventDefault(); + e.stopPropagation(); + + var idxDelta; + + e.dataTransfer.dropEffect = 'none'; + + if (!this.isValidElm(elm) || this.source.elm == elm || !this.source.elm) + return; + + e.dataTransfer.dropEffect = 'move'; // See the section on the DataTransfer object. + this.cleanupLastTarget(); + this.target.elm = elm; + this.target.idx = this.getNodeIndex(elm); + elm.classList.add('over'); + + // if target is same as the source, un-hide the source + idxDelta = Math.abs(this.target.idx - this.source.idx); + + this.source.elm.classList.toggle(`${this.namespace}--hide`, idxDelta > 0); + + // if( this.isTargetLastChild() && this.target.hoverDirection ) + // return; + + if (this.settings.mode == 'vertical') + this.target.elm.style[this.target.hoverDirection ? 'marginBottom' : 'marginTop'] = this.source.size.height + this.gap + 'px'; + else + this.target.elm.style[this.target.hoverDirection ? 'marginRight' : 'marginLeft'] = this.source.size.width + this.gap + 'px'; + }, + + dragend(e) { + clearTimeout(this.dragoverTimeout) + this.dragoverTimeout = null + this.parentElm.classList.remove(`${this.namespace}--dragStart`) + + if (!this.isValidElm(this.target.elm)) { + return this.cleanup() + } + + var insertBeforeElm = this.target.hoverDirection ? this.target.elm.nextElementSibling : this.target.elm; + + + if (this.source.elm != this.target.elm && this.target.elm) { + this.target.elm.classList.add(`${this.namespace}--noAnim`) + this.cleanup(); + this.parentElm.insertBefore(this.source.elm, insertBeforeElm); + } + + this.source.elm && this.source.elm.classList.remove(`${this.namespace}--dragElem`, `${this.namespace}--hide`) + this.settings.callbacks.dragEnd && this.settings.callbacks.dragEnd(this.source.elm) + + return this + }, + + ///////////////////////////// + + isTargetLastChild() { + return this.parentElm.lastElementChild == this.target.elm; + }, + + getTargetDirection(e) { + if (!this.target.bounding) return; + return this.settings.mode == 'vertical' ? + e.pageY > (this.target.bounding.top + this.target.bounding.height / 2) ? 1 : 0 : + e.pageX > (this.target.bounding.left + this.target.bounding.width / 2) ? 1 : 0 + }, + + getNodeIndex(node) { + var index = 0; + while ((node = node.previousSibling)) + if (node.nodeType != 3 || !/^\s*$/.test(node.data)) + index++; + return index; + }, + + isValidElm(elm) { + return elm && elm.nodeType && elm.parentNode == this.parentElm; + }, + + cleanup() { + _current = {}; + + [...this.parentElm.children].forEach(elm => { + elm.removeAttribute('style') + setTimeout(() => { + elm.classList.remove(`${this.namespace}--over`, `${this.namespace}--noAnim`, `${this.namespace}--dragElem`) + }, 50) + }) + + return; + }, + + cleanupLastTarget() { + if (this.target.elm) { + this.target.elm.classList.remove(`${this.namespace}--hide`, `${this.namespace}--over`) + this.target.elm.removeAttribute('style'); + } + }, + + getInitialState() { + return { + elm: null, + size: {} + } + }, + + getItemsGap(elm) { + var itemStyles = getComputedStyle(elm), + parentStyles = getComputedStyle(elm.parentNode), + v = this.settings.mode == 'vertical', + gap = parseInt(parentStyles.gap) || 0, // parent might be a flexbox witha a gap + marginGap = parseInt(itemStyles[`margin${v ? 'Top' : 'Left'}`]) + + parseInt(itemStyles[`margin${v ? 'Bottom' : 'Right'}`]) + + return marginGap + gap; + }, + + bindEvents(unbind) { + this.listeners = this.listeners || { + dragstart: e => this.dragstart(e, e.target), + dragenter: e => this.dragenter(e, e.target), + dragend : e => this.dragend(e, e.target), + dragover : this.throttle(this.dragover, 0), // see https://github.com/yairEO/dragsort/issues/5#issuecomment-1004465926 + } + + for (var method in this.listeners) + this.parentElm[unbind ? "removeEventListener" : "addEventListener"](method, this.listeners[method]) + }, + + destroy() { + this.cleanup() + this.bindEvents(true) + delete _instances[this.uid] + } + } + + ///////////////////////////////////// + // Factory + return function (elm, settings) { + // if this "elm" has already been initialized with DragSort, return last DragSort instance and do not create a new one + _instances[++_id] = elm["DragSort"] + ? _instances[elm["DragSort"]] + : new DragSort(elm, {...settings, uid: _id}) + + elm["DragSort"] = _id + return _instances[_id] + } +})); \ No newline at end of file diff --git a/assets/js/dragsort.js_LICENSE b/assets/js/dragsort.js_LICENSE new file mode 100644 index 0000000..fb11f0b --- /dev/null +++ b/assets/js/dragsort.js_LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 vsync + +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. diff --git a/assets/js/magicconfig.js b/assets/js/magicconfig.js new file mode 100644 index 0000000..e19a6cf --- /dev/null +++ b/assets/js/magicconfig.js @@ -0,0 +1,273 @@ +/* + * CardDAV Middleware UI + * Written by Massi-X © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +//declaration: new TimePicker(target log container, language array) +class MagicConfig { + #popup; + #startButton; + #logContainer; + #language; + + //call it to start the magic + constructor(popup, language) { + if (window.magicIsRunning) + return; + + window.magicIsRunning = true; + this.#language = language; + this.#popup = popup; + + //disallow closing the popup + this.#popup.dialog({ + beforeClose: () => false + }); + + this.#startButton = popup[0].querySelector('#magic_start'); //no check for existence + this.#logContainer = popup[0].querySelector('#magic_pre_container'); //no check for existence + + this.#startButton.setAttribute('disabled', 'disabled'); //disable start button + this.#logContainer.innerHTML = ''; //reset container + + this._step1(); //start + } + + //create/update superfecta scheme and print message step 1 + _step1() { + this.#logContainer.innerHTML += this.#language['JS_magic_step1'] + '\n'; + this.#logContainer.innerHTML += this.#language['JS_magic_step1_1'] + '\n'; + this.#logContainer.scrollIntoView(false); //scroll to bottom + + //send request + fetch('ajax.php?' + new URLSearchParams({ + module: phonemiddleware['ajax_name'], + command: 'superfectainit' + }), []) + .then(response => { + return this._resp_handler(response); + }) + .then(data => { + this._step2(data); + }, error => { + this._error(error.error.message); + }); + } + + //move superfecta scheme at top + _step2(data) { + if (data.status) { + this.#logContainer.innerHTML += data.message + '\n'; + this.#logContainer.scrollIntoView(false); //scroll to bottom + + fetch('ajax.php?' + new URLSearchParams({ + module: phonemiddleware['ajax_name'], + command: 'superfectareorder' + }), []).then(response => { + return this._resp_handler(response); + }).then(data => { + this._step3(data); + }, error => { + this._error(error.error.message); + }); + } + else this._error(data.message); + } + + //enable superfecta regex_2 + _step3(data) { + this.#logContainer.innerHTML += data.message + '\n'; //don't care about errors. For info why see Phonemiddleware.class.php + this.#logContainer.scrollIntoView(false); //scroll to bottom + + let formData = new FormData(); + formData.append('data[]', 'Regular_Expressions_2'); + let options = { + method: 'post', + body: formData + }; + + fetch('ajax.php?' + new URLSearchParams({ + module: 'superfecta', + command: 'update_sources', + scheme: phonemiddleware['SUPERFECTA_SCHEME'] + }), options).then(response => { + return this._resp_handler(response); + }).then(data => { + this._step4(data); + }, error => { + this._error(error.error.message); + }); + } + + //setup superfecta regex_2 + _step4(data) { + if (data.success) { + this.#logContainer.innerHTML += this.#language['JS_magic_step1_2'] + '\n'; + this.#logContainer.scrollIntoView(false); //scroll to bottom + + let formData = new FormData(); + formData.append('URL', phonemiddleware['numberToCnamURL']); + formData.append('Enable_SPAM_Match', 'on'); + phonemiddleware['SUPERFECTA_SCHEME_CONFIG'].forEach(elem => { + switch (elem['key']) { + case 'POST_Data': + formData.append('POST_Data', elem['value']); + break; + case 'Regular_Expressions': + formData.append('Regular_Expressions', elem['value']); + break; + case 'SPAM_Regular_Expressions': + formData.append('SPAM_Regular_Expressions', elem['value']); + break; + } + }); + let options = { + method: 'post', + body: formData + }; + + fetch('ajax.php?' + new URLSearchParams({ + module: 'superfecta', + command: 'save_options', + scheme: phonemiddleware['SUPERFECTA_SCHEME'], + source: 'Regular_Expressions_2' + }), options).then(response => { + return this._resp_handler(response); + }).then(data => { + this._step5(data); + }, error => { + this._error(error.error.message); + }); + } + else this._error(data.message); + } + + //setup outcnam and print message step 2 + _step5(data) { + if (data.status) { + this.#logContainer.innerHTML += this.#language['JS_magic_step1_3'] + '\n'; + this.#logContainer.innerHTML += '' + this.#language['JS_magic_step1_4'] + '\n'; + this.#logContainer.innerHTML += this.#language['JS_magic_step2'] + '\n'; + this.#logContainer.scrollIntoView(false); //scroll to bottom + + fetch('ajax.php?' + new URLSearchParams({ + module: phonemiddleware['ajax_name'], + command: 'outcnamsetup' + }), []).then(response => { + return this._resp_handler(response); + }).then(data => { + this._step6(data); + }, error => { + this._error(error.error.message); + }); + } + else this._error(data.message); + } + + //retrieve all inbound routes, iterate over them and print step 3 + _step6(data) { + if (data.status) { + this.#logContainer.innerHTML += data.message + '\n'; + this.#logContainer.innerHTML += this.#language['JS_magic_step3'] + '\n'; + this.#logContainer.innerHTML += this.#language['JS_magic_step3_1'] + '\n'; + this.#logContainer.scrollIntoView(false); //scroll to bottom + + fetch('ajax.php?' + new URLSearchParams({ + module: 'core', + command: 'getJSON', + jdata: 'allDID' + }), []).then(response => { + return this._resp_handler(response); + }).then(async data => { + if (Array.isArray(data)) + for (let i = 0; i < data.length; i++) { + const res = await this._step7Recursive(data[i]); + if (!res) { + window.magicIsRunning = false; + return; + } + } + else + this.#logContainer.innerHTML += '' + this.#language['JS_magic_step3_notfound'] + '\n'; + + this.#logContainer.innerHTML += '' + this.#language['JS_magic_completed'] + '\n'; + + //standard freepbx function to apply changes + fpbx_reload(); + + //re-enable everything + window.magicIsRunning = false; + this.#popup.dialog({ + beforeClose: () => true + }); + this.#startButton.removeAttribute('disabled'); + }, error => { + this._error(error.error.message); + }); + } + else this._error(data.message); + } + + //apply changes to the current inbound route + async _step7Recursive(route) { + //decode first + route['cidnum'] = decodeURIComponent(route['cidnum']); + route['extension'] = decodeURIComponent(route['extension']); + + this.#logContainer.innerHTML += this.#language['JS_magic_step3_2'] + .replace('%cid', route['cidnum'] ? route['cidnum'] : this.#language['undefined']) + .replace('%did', route['extension'] ? route['extension'] : this.#language['undefined']) + '\n'; + + let options = { + method: 'post', + body: JSON.stringify(route) + }; + + const res = await fetch('ajax.php?' + new URLSearchParams({ + module: phonemiddleware['ajax_name'], + command: 'inboundroutesetup' + }), options).then(response => { + return this._resp_handler(response); + }).then(data => { + if (data.status) { + this.#logContainer.innerHTML += data.message + '\n'; + this.#logContainer.scrollIntoView(false); //scroll to bottom + return true; + } + else { + this._error(data.message); + return false; + } + }, error => { + this._error(error.error.message); + return false; + }); + + return res; + } + + //error handler: print message + disable lock + _error(msg) { + this.#logContainer.innerHTML += '' + msg + '\n'; + this.#logContainer.innerHTML += '' + this.#language['JS_magic_error'] + '\n'; + this.#logContainer.scrollIntoView(false); //scroll to bottom + + //re-enable everything + window.magicIsRunning = false; + this.#popup.dialog({ + beforeClose: () => true + }); + this.#startButton.removeAttribute('disabled'); + } + + //response handler, convenient shortcut to reduce code + _resp_handler(response) { + if (!response.ok) { + return response.json().then(json => { + return Promise.reject(json); + }); + } else + return response.json(); + } +} \ No newline at end of file diff --git a/assets/js/scripts.js b/assets/js/scripts.js new file mode 100644 index 0000000..40e95e7 --- /dev/null +++ b/assets/js/scripts.js @@ -0,0 +1,520 @@ +/* + * CardDAV Middleware UI + * Written by Massi-X © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +//no check is done on the pm_language or other necessary variables. Please make sure they are present when including this file +document.addEventListener('DOMContentLoaded', () => { + /******************** GET ELEMENTS ********************/ + carddav_url = document.getElementById('carddav_url'); + carddav_user = document.getElementById('carddav_user'); + carddav_psw = document.getElementById('carddav_psw'); + carddav_validate = document.getElementById('carddav_validate'); + carddav_display_url = document.getElementById('carddav_display_url'); + carddav_result_tbody = document.getElementById('carddav_result').getElementsByTagName('tbody')[0]; + + /******************** START TAGIFY ********************/ + document.addEventListener('dragover', e => e.preventDefault()); + + //tagify output_construct + var output_construct = new Tagify(document.getElementById('output_construct'), { + whitelist: [{ + "value": "fn", + "name": pm_language['JS_fn'] + " (fn)" + }, + { + "value": "n", + "name": pm_language['Name'] + " (n)" + }, + { + "value": "nickname", + "name": pm_language['Nickname'] + " (nickname)" + }, + { + "value": "bday", + "name": pm_language['Birthday'] + " (bday)" + }, + { + "value": "adr", + "name": pm_language['Address'] + " (adr)" + }, + { + "value": "email", + "name": pm_language['JS_email_adr'] + " (email)" + }, + { + "value": "title", + "name": pm_language['Title'] + " (title)" + }, + { + "value": "role", + "name": pm_language['Role'] + " (role)" + }, + { + "value": "org", + "name": pm_language['JS_org'] + " (org)" + }, + ], + tagTextProp: 'name', + dropdown: { + mapValueTo: 'name', + maxItems: Infinity + }, + enforceWhitelist: true, + backspace: false, + userInput: false, + originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(',') + }); + + //custom class for css styles + output_construct.toggleClass('output_construct__tagify'); + + //dragsort for output_construct + new DragSort(output_construct.DOM.scope, { + selector: '.' + output_construct.settings.classNames.tag, + callbacks: { + dragEnd: () => output_construct.updateValueByDOMTags() + } + }); + + //generate whitelist for phone_type + phone_type_whitelist = []; + let i = 1; + while (true) { + let name = pm_language['PHONE_TYPE_' + i]; + if (name === undefined) + break; + phone_type_whitelist.push({ 'value': i, 'name': name }); + ++i; + } + + //tagify phone_type + var tagify = new Tagify(document.getElementById('phone_type'), { + enforceWhitelist: true, + whitelist: phone_type_whitelist, + mode: "select", + backspace: false, + userInput: false, + tagTextProp: 'name', + dropdown: { + mapValueTo: 'name', + maxItems: Infinity + }, + originalInputValueFormat: valuesArr => valuesArr.map(item => item.value) + }); + + //custom class for css styles + tagify.toggleClass('phone_type__tagify'); + + tagify = new Tagify(document.getElementById('country_code'), { + enforceWhitelist: true, + whitelist: phonemiddleware['country_codes'], + callbacks: { + remove: e => { + e.detail.tagify['valueBeforeRemove'] = e.detail.data.value; + }, + blur: e => { + if (e.detail.tagify.value.length === 0) { + e.detail.tagify.state.dropdown.visible = false; //prevent dropdown from rifocusing everything up + e.detail.tagify.addTags(e.detail.tagify['valueBeforeRemove']); + } + }, + }, + mode: "select", + tagTextProp: 'name', + dropdown: { + mapValueTo: 'name', + searchKeys: ['name', 'value'], + maxItems: Infinity + }, + originalInputValueFormat: valuesArr => valuesArr.map(item => item.value) + }); + /******************** END TAGIFY ********************/ + + /******************** CARDDAV VALIDATION & SAVE ********************/ + //load data + validateCarddav(); + + //listener for sortable to set element width on window resize + window.resizeWidth = window.innerWidth; + window.addEventListener('resize', () => { + clearTimeout(window.resizeTimer); + window.resizeTimer = setTimeout(() => { + if (window.innerWidth !== window.resizeWidth) { + window.resizeWidth = window.innerWidth; + sortableListResize(); + } + }, 50); + }); + + //listener for sortable to set element width on dialog open + $('#setupCarddav').on('dialogopen', (event, ui) => { + sortableListResize(); + }); + /******************** END CARDDAV VALIDATION & SAVE ********************/ + + /******************** NOTIFICATION UI ********************/ + notificationHeader = document.getElementById('notification-header'); + notificationUI = document.getElementById('notification-ui'); + notificationCount = document.getElementById('notification-count'); + + //close notifications on click outside + window.addEventListener('click', ({ target }) => { + const popup = target.closest('#notification-ui'); + + if (notificationUI.classList.contains('open') && popup == null) + toggleNotification(true); + }); + + //parse timestamp into date-time + document.querySelectorAll('.notification-timestamp').forEach(elem => { + let timestamp = elem.innerHTML * 1000; //convert to millisecond + var date = new Date(timestamp); + + elem.innerHTML = date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); + }); + /******************** END NOTIFICATION UI ********************/ + + /******************** INIT POPUPS ********************/ + $('#setupCarddav').dialog({ + autoOpen: false, + modal: true, + resizable: false, + draggable: false, + height: 'auto', + width: 'auto' + }); + $('#errorPopup').dialog({ + autoOpen: false, + modal: true, + resizable: false, + draggable: false, + height: 'auto', + width: 'auto' + }); + $('#licensePopupUI').dialog({ + autoOpen: false, + modal: true, + resizable: false, + draggable: false, + height: 'auto', + width: 'auto' + }); + $('#licensePopupCore').dialog({ + autoOpen: false, + modal: true, + resizable: false, + draggable: false, + height: 'auto', + width: 'auto' + }); + $('#librariesPopup').dialog({ + autoOpen: false, + modal: true, + resizable: false, + draggable: false, + height: 'auto', + width: 'auto' + }); + window.magicPopup = $('#magicPopup').dialog({ + autoOpen: false, + modal: true, + resizable: false, + draggable: false, + height: 'auto', + width: 'auto' + }); + /******************** END INIT POPUPS ********************/ + + /******************** LISTEN MAX_CNAM_OUTPUT CHECKBOX ********************/ + document.getElementById('max_cnam_length').onblur = e => { + let elem = e.target; + let min = parseInt(elem.getAttribute('min')); + let max = parseInt(elem.getAttribute('max')); + + if (!min && !max) + return; + + if (elem.value < min || elem.value > max) { + if (elem.value < min) + elem.value = min; + else if (elem.value > max) + elem.value = max; + + elem.classList.add('input-invalid-blink'); + setTimeout(() => elem.classList.remove('input-invalid-blink'), 1000); + } + }; + + document.querySelectorAll('input[type="checkbox"][data-onchange]').forEach(elem => { + let target = document.getElementById(elem.getAttribute('data-onchange')); + + elem.onchange = e => { + e.target.checked ? target.removeAttribute('disabled') : target.setAttribute('disabled', 'disabled'); + target.value = target.getAttribute('min'); + } + }); + /******************** END LISTEN MAX_CNAM_OUTPUT CHECKBOX ********************/ + + /******************** MISC ********************/ + //enable timepicker on cache_expire + new TimePicker(document.getElementById('cache_expire'), 15); + + //update notification title text + updateNotificationText(); + + //tabindex helper + document.querySelectorAll('.radioset > label').forEach(elem => { + elem.onkeydown = e => { + if (e.key === 'Enter') + elem.click(); + }; + }); + + //disallow spaces inside addressbook dialog inputs + carddav_url.oninput = carddav_user.oninput = carddav_psw.oninput = e => e.target.value = e.target.value.replace(/\s/g, ''); +}, false); + +/******************** CARDDAV VALIDATION & SAVE ********************/ +function validateCarddav() { + let isSave = false; + let checkboxes = document.querySelectorAll('input[type=checkbox][name="carddav_addressbooks[]"]'); //this changes at every request + let request_type = 'ajax.php?module=' + phonemiddleware['ajax_name'] + '&command='; + let formData = new FormData(); + let options = { + method: 'post', + body: formData + }; + + checkboxes.forEach(elem => { //determine if this is save + if (elem.checked) + isSave = true; + }); + + //construct base formdata request + formData.append('carddav_url', carddav_url.value); + formData.append('carddav_user', carddav_user.value); + formData.append('carddav_psw', carddav_psw.value); + + if (isSave) { + carddav_validate.innerHTML = ' ' + pm_language['Saving_dots']; + request_type += 'savecarddav'; + + for (let i = 0; i < checkboxes.length; ++i) //add checked URLs to request + if (checkboxes[i].checked) + formData.append('carddav_addressbooks[]', checkboxes[i].getAttribute('data-uri')); + } else { + if (carddav_result_tbody.classList.contains('ui-sortable')) + $(carddav_result_tbody).sortable("destroy"); //detach sortable before updating content + carddav_validate.innerHTML = ' ' + pm_language['Validating_dots']; + carddav_result_tbody.innerHTML = '' + pm_language['Loading_dots'] + ''; + request_type += 'validatecarddav'; + } + + //disable every input + carddav_validate.setAttribute('disabled', 'disabled'); + carddav_url.disabled = carddav_user.disabled = carddav_psw.disabled = 'disabled'; + + //send request + fetch(request_type, options) //absolute path to www folder SAME AS UNINSTALL.PHP AND INSTALL.PHP + .then(response => { + if (!response.ok) { + return response.json().then(json => { + return Promise.reject(json); + }); + } else + return response.json(); + }) + .then(data => { + if (isSave) { + carddav_display_url.value = carddav_url.value; + $('#setupCarddav').dialog('close'); + } else { + carddav_result_tbody.innerHTML = ''; //reset content + + if (data.message) //if some error is raised from above + carddav_result_tbody.innerHTML = '' + data.message + ''; + else if (data.length == 0) //or if nothing is found + carddav_result_tbody.innerHTML = '' + pm_language['No_addresbook_found'] + ''; + else { + data.forEach(item => { + carddav_result_tbody.innerHTML += + '' + item['name'] + '' + item['uri'] + ''; + }); + sortableListResize(); + initSortableList(); + } + } + changeCarddavButton(); + }, error => { + if (isSave) //only if there is an error during save alert the user + alert(error.error.message); + else //else we only show a generic no address book found inside the table + carddav_result_tbody.innerHTML = '' + pm_language['No_addresbook_found'] + ''; + changeCarddavButton(); + }); +} + +//this changed the button according to the current situation +function changeCarddavButton() { + carddav_validate.innerHTML = pm_language['Validate']; + carddav_validate.removeAttribute('disabled'); + carddav_url.disabled = carddav_user.disabled = carddav_psw.disabled = ''; + + document.querySelectorAll('input[type=checkbox][name="carddav_addressbooks[]"]').forEach(elem => { //this change at every request + if (elem.checked) { + carddav_validate.innerHTML = pm_language['Save']; + carddav_url.disabled = carddav_user.disabled = carddav_psw.disabled = 'disabled'; + } + }); +} + +//init sortable UI on carddav addressbooks +function initSortableList() { + $(carddav_result_tbody).sortable({ + //fix row width unwanted shrink + start: (event, ui) => { + //thanks to someone on SO for this (sorry lost the link!) + var cellCount = 0; + $('td, th', ui.helper).each(() => { + var colspan = 1; + var colspanAttr = $(this).attr('colspan'); + if (colspanAttr > 1) + colspan = colspanAttr; + cellCount += colspan; + }); + ui.placeholder.html(' '); + + //fix table "jump" + var height = ui.helper.outerHeight(); + ui.placeholder.height(height); + }, + containment: "parent", //prevent the row from going outside the table + axis: 'y', //only y movement + tolerance: "pointer", //pointer on other row will trigger the switch + revert: 100, //animation when drag ends + handle: '.fa' //self explains + }).disableSelection(); +} + +//handle window resize + initialization to avoid jumps when moving rows +function sortableListResize() { + document.querySelectorAll('#carddav_result th, #carddav_result td').forEach(elem => { + elem.style.width = ''; + }); + document.querySelectorAll('#carddav_result th, #carddav_result td').forEach(elem => { + elem.style.width = elem.offsetWidth + 'px'; + }); + document.querySelectorAll('#carddav_result tr').forEach(elem => { + elem.style.height = ''; + }); + document.querySelectorAll('#carddav_result tr').forEach(elem => { + elem.style.height = elem.offsetHeight + 'px'; + }); +} +/******************** END CARDDAV VALIDATION & SAVE ********************/ + +/******************** NOTIFICATION UI ********************/ +function toggleNotification(close) { //open/close notifications + if (close === undefined) + close = notificationUI.classList.contains('open'); + + if (close) { + if (!notificationUI.classList.contains('open')) + return; + + notificationUI.classList.toggle('open'); + } + else /* if (open) */ { + if (parseInt(notificationCount.getAttribute('data-count')) == 0 || notificationUI.classList.contains('open')) + return; + + notificationUI.classList.toggle('open'); + } +} + +function updateNotificationText(reset, decrementValue) { //update title text according to current count + if (decrementValue === undefined) + decrementValue = 0; + + let notificationTotal = parseInt(notificationCount.getAttribute('data-count')); + if (reset) + notificationTotal = 0; + else if (decrementValue < 0) + notificationTotal += decrementValue; + + notificationCount.setAttribute('data-count', notificationTotal); + notificationHeader.classList.add('bell-shake'); + notificationCount.innerHTML = notificationTotal; + + if (notificationTotal == 0) { + notificationHeader.classList.remove('bell-shake'); + toggleNotification(true); //close notification UI if count == 0 + } else if (notificationTotal > 9) + notificationCount.innerHTML = '9+'; +} + +function deleteNotification(elem) { //delete notification + //no confirm + let formData = new FormData(); + + formData.append('id', elem.getAttribute('data-notificationid')); + + let options = { + method: 'post', + body: formData + }; + + //send request + fetch('ajax.php?' + new URLSearchParams({ + module: phonemiddleware['ajax_name'], + command: 'deletenotification' + }), options).then(response => { + if (!response.ok) { + return response.json().then(json => { + return Promise.reject(json); + }); + } else + return response.json(); + }).then(() => { + elem.closest('.notification-bubble').remove(); + updateNotificationText(false, -1); + }, error => { + alert(error.error.message); + }); +} + +function deleteAllNotifications() { //delete ALL notifications + if (!confirm(pm_language['JS_confirm_delete_notifications'])) + return; + + let formData = new FormData(); + let options = { + method: 'post', + body: formData + }; + + //send request + fetch('ajax.php?' + new URLSearchParams({ + module: phonemiddleware['ajax_name'], + command: 'deleteallnotifications' + }), options).then(response => { + if (!response.ok) { + return response.json().then(json => { + return Promise.reject(json); + }); + } else + return response.json(); + }).then(() => { + document.querySelectorAll('.notification-bubble').forEach(elem => { + elem.remove(); + }); + updateNotificationText(true); + }, error => { + alert(error.error.message); + }); +} +/******************** END NOTIFICATION UI ********************/ diff --git a/assets/js/tagify.min.js b/assets/js/tagify.min.js new file mode 100644 index 0000000..111b776 --- /dev/null +++ b/assets/js/tagify.min.js @@ -0,0 +1,26 @@ +/** + * Tagify (v 4.16.4) - tags input component + * By undefined + * https://github.com/yairEO/tagify + * 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. + * + * THE SOFTWARE IS NOT PERMISSIBLE TO BE SOLD. + */ + + !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Tagify=e()}(this,(function(){"use strict";function t(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,s)}return i}function e(e){for(var s=1;s(t=""+t,e=""+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase()),a=(t,e)=>t&&Array.isArray(t)&&t.map((t=>n(t,e)));function n(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function o(t){var e=document.createElement("div");return t.replace(/\&#?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function r(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild}function l(t,e){for(e=e||"previous";t=t[e+"Sibling"];)if(3==t.nodeType)return t}function d(t){return"string"==typeof t?t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/`|'/g,"'"):t}function h(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function g(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(h(e[i])){h(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(Array.isArray(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function p(){const t=[],e={};for(let i of arguments)for(let s of i)h(s)?e[s.value]||(t.push(s),e[s.value]=1):t.includes(s)||t.push(s);return t}function c(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var u=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);function m(){return([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)))}function v(t){return t&&t.classList&&t.classList.contains(this.settings.classNames.tag)}var f={delimiters:",",pattern:null,tagTextProp:"value",maxTags:1/0,callbacks:{},addTagOnBlur:!0,onChangeAfterBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,userInput:!0,keepInvalidTags:!1,createInvalidTags:!0,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,pasteAsTags:!0,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1},classNames:{namespace:"tagify",mixMode:"tagify--mix",selectMode:"tagify--select",input:"tagify__input",focus:"tagify--focus",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",scopeLoading:"tagify--loading",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty",inputInvalid:"tagify__input--invalid",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownHeader:"tagify__dropdown__header",dropdownFooter:"tagify__dropdown__footer",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownItemHidden:"tagify__dropdown__item--hidden",dropdownInital:"tagify__dropdown--initial",tag:"tagify__tag",tagText:"tagify__tag-text",tagX:"tagify__tag__removeBtn",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,includeSelectedTags:!1,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve()}};function T(){this.dropdown={};for(let t in this._dropdown)this.dropdown[t]="function"==typeof this._dropdown[t]?this._dropdown[t].bind(this):this._dropdown[t];this.dropdown.refs()}var w={refs(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-wrapper']")},getHeaderRef(){return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-header']")},getFooterRef(){return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-footer']")},getAllSuggestionsRefs(){return[...this.DOM.dropdown.content.querySelectorAll(this.settings.classNames.dropdownItemSelector)]},show(t){var e,i,a,n=this.settings,o="mix"==n.mode&&!n.enforceWhitelist,r=!n.whitelist||!n.whitelist.length,l="manual"==n.dropdown.position;if(t=void 0===t?this.state.inputText:t,!(r&&!o&&!n.templates.dropdownItemNoMatch||!1===n.dropdown.enable||this.state.isLoading||this.settings.readonly)){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems(t),t&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",t),n.templates.dropdownItemNoMatch&&(a=n.templates.dropdownItemNoMatch.call(this,{value:t}))),!a){if(this.suggestedListItems.length)t&&o&&!this.state.editing.scope&&!s(this.suggestedListItems[0].value,t)&&this.suggestedListItems.unshift({value:t});else{if(!t||!o||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide();this.suggestedListItems=[{value:t}]}i=""+(h(e=this.suggestedListItems[0])?e.value:e),n.autoComplete&&i&&0==i.indexOf(t)&&this.input.autocomplete.suggest.call(this,e)}this.dropdown.fill(a),n.dropdown.highlightFirst&&this.dropdown.highlightOption(this.DOM.dropdown.content.querySelector(n.classNames.dropdownItemSelector)),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=t||!0,this.state.dropdown.query=t,this.setStateSelection(),l||setTimeout((()=>{this.dropdown.position(),this.dropdown.render()})),setTimeout((()=>{this.trigger("dropdown:show",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a="manual"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",s),this},toggle(t){this.dropdown[this.state.dropdown.visible&&!t?"hide":"show"]()},render(){var t,e,i,s=(t=this.DOM.dropdown,(i=t.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e),a=this.settings;return"number"==typeof a.dropdown.enabled&&a.dropdown.enabled>=0?(this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(a.classNames.dropdownInital),this.dropdown.position(s),a.dropdown.appendTarget.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(a.classNames.dropdownInital)))),this):this},fill(t){t="string"==typeof t?t:this.dropdown.createListHTML(t||this.suggestedListItems);var e,i=this.settings.templates.dropdownContent.call(this,t);this.DOM.dropdown.content.innerHTML=(e=i)?e.replace(/\>[\r\n ]+\<").replace(/(<.*?>)|\s+/g,((t,e)=>e||" ")):""},fillHeaderFooter(){this.settings.templates;var t=this.dropdown.filterListItems(this.state.dropdown.query),e=this.parseTemplate("dropdownHeader",[t]),i=this.parseTemplate("dropdownFooter",[t]),s=this.dropdown.getHeaderRef(),a=this.dropdown.getFooterRef();e&&s?.parentNode.replaceChild(e,s),i&&a?.parentNode.replaceChild(i,a)},refilter(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems(t),this.dropdown.fill(),this.suggestedListItems.length||this.dropdown.hide(),this.trigger("dropdown:updated",this.DOM.dropdown)},position(t){var e=this.settings.dropdown;if("manual"!=e.position){var i,s,a,n,o,r,l=this.DOM.dropdown,d=e.placeAbove,h=e.appendTarget===document.body,g=h?window.pageYOffset:e.appendTarget.scrollTop,p=document.fullscreenElement||document.webkitFullscreenElement||document.documentElement,c=p.clientHeight,u=Math.max(p.clientWidth||0,window.innerWidth||0)>480?e.position:"all",m=this.DOM["input"==u?"input":"scope"];if(t=t||l.clientHeight,this.state.dropdown.visible){if("text"==u?(a=(i=this.getCaretGlobalPosition()).bottom,s=i.top,n=i.left,o="auto"):(r=function(t){for(var e=0,i=0;t&&t!=p;)e+=t.offsetLeft||0,i+=t.offsetTop||0,t=t.parentNode;return{left:e,top:i}}(e.appendTarget),s=(i=m.getBoundingClientRect()).top-r.top,a=i.bottom-1-r.top,n=i.left-r.left,o=i.width+"px"),!h){let t=function(){for(var t=0,i=e.appendTarget.parentNode;i;)t+=i.scrollTop||0,i=i.parentNode;return t}();s+=t,a+=t}s=Math.floor(s),a=Math.ceil(a),d=void 0===d?c-i.bottom0&&void 0!==arguments[0])||arguments[0];var e=this.dropdown.events.callbacks,i=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this,null),onKeyDown:e.onKeyDown.bind(this),onMouseOver:e.onMouseOver.bind(this),onMouseLeave:e.onMouseLeave.bind(this),onClick:e.onClick.bind(this),onScroll:e.onScroll.bind(this)},s=t?"addEventListener":"removeEventListener";"manual"!=this.settings.dropdown.position&&(document[s]("scroll",i.position,!0),window[s]("resize",i.position),window[s]("keydown",i.onKeyDown)),this.DOM.dropdown[s]("mouseover",i.onMouseOver),this.DOM.dropdown[s]("mouseleave",i.onMouseLeave),this.DOM.dropdown[s]("mousedown",i.onClick),this.DOM.dropdown.content[s]("scroll",i.onScroll)},callbacks:{onKeyDown(t){if(this.state.hasFocus&&!this.state.composing){var e=this.DOM.dropdown.querySelector(this.settings.classNames.dropdownItemActiveSelector),i=this.dropdown.getSuggestionDataByNode(e);switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault();var s=this.dropdown.getAllSuggestionsRefs(),a="ArrowUp"==t.key||"Up"==t.key;e&&(e=this.dropdown.getNextOrPrevOption(e,!a)),e&&e.matches(this.settings.classNames.dropdownItemSelector)||(e=s[a?s.length-1:0]),i=this.dropdown.getSuggestionDataByNode(e),this.dropdown.highlightOption(e,!0);break;case"Escape":case"Esc":this.dropdown.hide();break;case"ArrowRight":if(this.state.actions.ArrowLeft)return;case"Tab":if("mix"!=this.settings.mode&&e&&!this.settings.autoComplete.rightKey&&!this.state.editing){t.preventDefault();var n=this.dropdown.getMappedValue(i);return this.input.autocomplete.set.call(this,n),!1}return!0;case"Enter":t.preventDefault(),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{if(e)return this.dropdown.selectOption(e),e=this.dropdown.getNextOrPrevOption(e,!a),void this.dropdown.highlightOption(e);this.dropdown.hide(),"mix"!=this.settings.mode&&this.addTags(this.state.inputText.trim(),!0)})).catch((t=>t));break;case"Backspace":{if("mix"==this.settings.mode||this.state.editing.scope)return;const t=this.input.raw.call(this);""!=t&&8203!=t.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0))}}}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);e&&this.dropdown.highlightOption(e)},onMouseLeave(t){this.dropdown.highlightOption()},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode(e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption(e,t):this.dropdown.hide()})).catch((t=>console.warn(t)))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},getSuggestionDataByNode(t){var e=t&&t.getAttribute("value");return this.suggestedListItems.find((t=>t.value==e))||null},getNextOrPrevOption(t){let e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.dropdown.getAllSuggestionsRefs(),s=i.findIndex((e=>e===t));return e?i[s+1]:i[s-1]},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.dropdown.getSuggestionDataByNode(t),this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position())},selectOption(t,e){var i=this.settings.dropdown,s=i.clearOnSelect,a=i.closeOnSelect;if(!t)return this.addTags(this.state.inputText,!0),void(a&&this.dropdown.hide());e=e||{};var n=t.getAttribute("value"),o="noMatch"==n,r=this.suggestedListItems.find((t=>(t.value||t)==n));this.trigger("dropdown:select",{data:r,elm:t,event:e}),n&&(r||o)?(this.state.editing?this.onEditTagDone(null,g({__isValid:!0},this.normalizeTags([r])[0])):this["mix"==this.settings.mode?"addMixTags":"addTags"]([r||this.input.raw.call(this)],s),this.DOM.input.parentNode&&(setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0),this.setRangeAtStartEnd(!1)})),a&&setTimeout(this.dropdown.hide.bind(this)),t.addEventListener("transitionend",(()=>{this.dropdown.fillHeaderFooter(),setTimeout((()=>t.remove()),100)}),{once:!0}),t.classList.add(this.settings.classNames.dropdownItemHidden))):a&&setTimeout(this.dropdown.hide.bind(this))},selectAll(t){this.suggestedListItems.length=0,this.dropdown.hide(),this.dropdown.filterListItems("");var e=this.dropdown.filterListItems("");return t||(e=this.state.dropdown.suggestions),this.addTags(e,!0),this},filterListItems(t,e){var i,s,a,n,o,r=this.settings,l=r.dropdown,d=(e=e||{},[]),g=[],p=r.whitelist,u=l.maxItems>=0?l.maxItems:1/0,m=l.searchKeys,v=0;if(!(t="select"==r.mode&&this.value.length&&this.value[0][r.tagTextProp]==t?"":t)||!m.length)return d=l.includeSelectedTags?p:p.filter((t=>!this.isTagDuplicate(h(t)?t.value:t))),this.state.dropdown.suggestions=d,d.slice(0,u);function f(t,e){return e.toLowerCase().split(" ").every((e=>t.includes(e.toLowerCase())))}for(o=l.caseSensitive?""+t:(""+t).toLowerCase();vm.includes(t)))?["value"]:m;l.fuzzySearch&&!e.exact?(a=u.reduce(((t,e)=>t+" "+(i[e]||"")),"").toLowerCase().trim(),l.accentedSearch&&(a=c(a),o=c(o)),t=0==a.indexOf(o),r=a===o,s=f(a,o)):(t=!0,s=u.some((t=>{var s=""+(i[t]||"");return l.accentedSearch&&(s=c(s),o=c(o)),l.caseSensitive||(s=s.toLowerCase()),r=s===o,e.exact?s===o:0==s.indexOf(o)}))),n=!l.includeSelectedTags&&this.isTagDuplicate(h(i)?i.value:i),s&&!n&&(r&&t?g.push(i):"startsWith"==l.sortby&&t?d.unshift(i):d.push(i))}return this.state.dropdown.suggestions=g.concat(d),"function"==typeof l.sortby?l.sortby(g.concat(d),o):g.concat(d).slice(0,u)},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?"function"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return g([],t).map(((t,i)=>{"string"!=typeof t&&"number"!=typeof t||(t={value:t});var s=this.dropdown.getMappedValue(t);return s="string"==typeof s?d(s):s,this.settings.templates.dropdownItem.apply(this,[e(e({},t),{},{mappedValue:s}),this])})).join("")}};const b="@yaireo/tagify/";var y,x={empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},D={wrapper:(t,e)=>`\n \n ​\n `,tag(t,e){let i=e.settings;return`\n \n
\n ${t[i.tagTextProp]||t.value}\n
\n
`},dropdown(t){var e=t.dropdown,i="manual"==e.position,s=`${t.classNames.dropdown}`;return`
\n
\n
`},dropdownContent(t){var e=this.settings,i=this.state.dropdown.suggestions;return`\n ${e.templates.dropdownHeader.call(this,i)}\n ${t}\n ${e.templates.dropdownFooter.call(this,i)}\n `},dropdownItem(t){return`
${t.mappedValue||t.value}
`},dropdownHeader(t){return`
`},dropdownFooter(t){var e=t.length-this.settings.dropdown.maxItems;return e>0?`
\n ${e} more items. Refine your search.\n
`:""},dropdownItemNoMatch:null};var O={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];var e,i=this.events.callbacks,s=t?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!t){for(var a in this.state.mainEvents=t,t&&!this.listeners.main&&(this.events.bindGlobal.call(this),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",i.onFocusBlur.bind(this)],keydown:["input",i.onKeydown.bind(this)],click:["scope",i.onClickScope.bind(this)],dblclick:["scope",i.onDoubleClickScope.bind(this)],paste:["input",i.onPaste.bind(this)],drop:["input",i.onDrop.bind(this)],compositionstart:["input",i.onCompositionStart.bind(this)],compositionend:["input",i.onCompositionEnd.bind(this)]})this.DOM[e[a][0]][s](a,e[a][1]);clearInterval(this.listeners.main.originalInputValueObserverInterval),this.listeners.main.originalInputValueObserverInterval=setInterval(i.observeOriginalInputValue.bind(this),500);var n=this.listeners.main.inputMutationObserver||new MutationObserver(i.onInputDOMChange.bind(this));n&&n.disconnect(),"mix"==this.settings.mode&&n.observe(this.DOM.input,{childList:!0})}},bindGlobal(t){var e,i=this.events.callbacks,s=t?"removeEventListener":"addEventListener";if(t||!this.listeners.global)for(e of(this.listeners.global=this.listeners&&this.listeners.global||[{type:this.isIE?"keydown":"input",target:this.DOM.input,cb:i[this.isIE?"onInputIE":"onInput"].bind(this)},{type:"keydown",target:window,cb:i.onWindowKeyDown.bind(this)},{type:"blur",target:this.DOM.input,cb:i.onFocusBlur.bind(this)}],this.listeners.global))e.target[s](e.type,e.cb)},unbindGlobal(){this.events.bindGlobal.call(this,!0)},callbacks:{onFocusBlur(t){var e=this.settings,i=t.target?this.trim(t.target.textContent):"",s=this.value?.[0]?.[e.tagTextProp],a=t.type,n=e.dropdown.enabled>=0,o={relatedTarget:t.relatedTarget},r=this.state.actions.selectOption&&(n||!e.dropdown.closeOnSelect),l=this.state.actions.addNew&&n,d=t.relatedTarget&&v.call(this,t.relatedTarget)&&this.DOM.scope.contains(t.relatedTarget);if("blur"==a){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide(),void this.DOM.input.focus();this.postUpdate(),e.onChangeAfterBlur&&this.triggerChangeEvent()}if(!r&&!l)if(this.state.hasFocus="focus"==a&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=e.mode){if("focus"==a)return this.trigger("focus",o),void(0!==e.dropdown.enabled&&e.userInput||this.dropdown.show(this.value.length?"":void 0));"blur"==a&&(this.trigger("blur",o),this.loading(!1),"select"==e.mode&&(d&&(this.removeTags(),i=""),s===i&&(i="")),i&&!this.state.actions.selectOption&&e.addTagOnBlur&&this.addTags(i,!0)),this.DOM.input.removeAttribute("style"),this.dropdown.hide()}else"focus"==a?this.trigger("focus",o):"blur"==t.type&&(this.trigger("blur",o),this.loading(!1),this.dropdown.hide(),this.state.dropdown.visible=void 0,this.setStateSelection())},onCompositionStart(t){this.state.composing=!0},onCompositionEnd(t){this.state.composing=!1},onWindowKeyDown(t){var e,i=document.activeElement;if(v.call(this,i)&&this.DOM.scope.contains(document.activeElement))switch(e=i.nextElementSibling,t.key){case"Backspace":this.settings.readonly||(this.removeTags(i),(e||this.DOM.input).focus());break;case"Enter":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.settings;if(!this.state.composing&&e.userInput){"select"==e.mode&&e.enforceWhitelist&&this.value.length&&"Tab"!=t.key&&t.preventDefault();var i=this.trim(t.target.textContent);if(this.trigger("keydown",{event:t}),"mix"==e.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var s,a,n,r=document.getSelection(),d="Delete"==t.key&&r.anchorOffset==(r.anchorNode.length||0),h=r.anchorNode.previousSibling,g=1==r.anchorNode.nodeType||!r.anchorOffset&&h&&1==h.nodeType&&r.anchorNode.previousSibling,p=o(this.DOM.input.innerHTML),c=this.getTagElms();if("edit"==e.backspace&&g)return s=1==r.anchorNode.nodeType?null:r.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,s),void t.preventDefault();if(u()&&g)return n=l(g),g.hasAttribute("readonly")||g.remove(),this.DOM.input.focus(),void setTimeout((()=>{this.placeCaretAfterNode(n),this.DOM.input.click()}));if("BR"==r.anchorNode.nodeName)return;if((d||g)&&1==r.anchorNode.nodeType?a=0==r.anchorOffset?d?c[0]:null:c[Math.min(c.length,r.anchorOffset)-1]:d?a=r.anchorNode.nextElementSibling:g&&(a=g),3==r.anchorNode.nodeType&&!r.anchorNode.nodeValue&&r.anchorNode.previousElementSibling&&t.preventDefault(),(g||d)&&!e.backspace)return void t.preventDefault();if("Range"!=r.type&&!r.anchorOffset&&r.anchorNode==this.DOM.input&&"Delete"!=t.key)return void t.preventDefault();if("Range"!=r.type&&a&&a.hasAttribute("readonly"))return void this.placeCaretAfterNode(l(a));clearTimeout(y),y=setTimeout((()=>{var t=document.getSelection(),e=o(this.DOM.input.innerHTML),i=!d&&t.anchorNode.previousSibling;if(e.length>=p.length&&i)if(v.call(this,i)&&!i.hasAttribute("readonly")){if(this.removeTags(i),this.fixFirefoxLastTagNoCaret(),2==this.DOM.input.children.length&&"BR"==this.DOM.input.children[1].tagName)return this.DOM.input.innerHTML="",this.value.length=0,!0}else i.remove();this.value=[].map.call(c,((t,e)=>{var i=this.tagData(t);if(t.parentNode||i.readonly)return i;this.trigger("remove",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}switch(t.key){case"Backspace":"select"==e.mode&&e.enforceWhitelist&&this.value.length?this.removeTags():this.state.dropdown.visible&&"manual"!=e.dropdown.position||""!=t.target.textContent&&8203!=i.charCodeAt(0)||(!0===e.backspace?this.removeTags():"edit"==e.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show();break;case"ArrowRight":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&e.autoComplete.rightKey)return void this.addTags([t],!0);break}case"Tab":{let s="select"==e.mode;if(!i||s)return!0;t.preventDefault()}case"Enter":if(this.state.dropdown.visible&&"manual"!=e.dropdown.position)return;t.preventDefault(),setTimeout((()=>{this.state.actions.selectOption||this.addTags(i,!0)}))}}},onInput(t){this.postUpdate();var e=this.settings;if("mix"==e.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var i=this.input.normalize.call(this),s=i.length>=e.dropdown.enabled,a={value:i,inputElm:this.DOM.input},n=this.validateTag({value:i});"select"==e.mode&&this.toggleScopeValidation(n),a.isValid=n,this.state.inputText!=i&&(this.input.set.call(this,i,!1),-1!=i.search(e.delimiters)?this.addTags(i)&&this.input.set.call(this):e.dropdown.enabled>=0&&this.dropdown[s?"show":"hide"](i),this.trigger("input",a))},onMixTagsInput(t){var e,i,s,a,n,o,r,l,d=this.settings,h=this.value.length,p=this.getTagElms(),c=document.createDocumentFragment(),m=window.getSelection().getRangeAt(0),v=[].map.call(p,(t=>this.tagData(t).value));if("deleteContentBackward"==t.inputType&&u()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:"Backspace"}),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&c.appendChild(this.createTagElem(t))})),c.childNodes.length&&(m.insertNode(c),this.setRangeAtStartEnd(!1,c.lastChild)),p.length!=h)return this.value=[].map.call(this.getTagElms(),(t=>this.tagData(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((m=o.getRangeAt(0).cloneRange()).collapse(!0),m.setStart(o.focusNode,0),s=(e=m.toString().slice(0,m.endOffset)).split(d.pattern).length-1,(i=e.match(d.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(d.pattern)[0],value:a.replace(d.pattern,"")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,l=this.state.tag.value.match(d.delimiters))return this.state.tag.value=this.state.tag.value.replace(d.delimiters,""),this.state.tag.delimiters=l[0],this.addTags(this.state.tag.value,d.dropdown.clearOnSelect),void this.dropdown.hide();n=this.state.tag.value.length>=d.dropdown.enabled;try{r=(r=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&r.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(r||s{this.update({withoutChangeEvent:!0}),this.trigger("input",g({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?"show":"hide"](this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},observeOriginalInputValue(){this.DOM.originalInput.parentNode||this.destroy(),this.DOM.originalInput.value!=this.DOM.originalInput.tagifyValue&&this.loadOriginalValues()},onClickScope(t){var e=this.settings,i=t.target.closest("."+e.classNames.tag),s=+new Date-this.state.hasFocus;if(t.target!=this.DOM.scope){if(!t.target.classList.contains(e.classNames.tagX))return i?(this.trigger("click",{tag:i,index:this.getNodeIndex(i),data:this.tagData(i),event:t}),void(1!==e.editTags&&1!==e.editTags.clicks||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&("mix"==e.mode&&this.fixFirefoxLastTagNoCaret(),s>500)?this.state.dropdown.visible?this.dropdown.hide():0===e.dropdown.enabled&&"mix"!=e.mode&&this.dropdown.show(this.value.length?"":void 0):"select"==e.mode&&!this.state.dropdown.visible&&this.dropdown.show());this.removeTags(t.target.parentNode)}else this.state.hasFocus||this.DOM.input.focus()},onPaste(t){t.preventDefault();var e,i,s=this.settings;if("select"==s.mode&&s.enforceWhitelist||!s.userInput)return!1;s.readonly||(e=t.clipboardData||window.clipboardData,i=e.getData("Text"),s.hooks.beforePaste(t,{tagify:this,pastedText:i,clipboardData:e}).then((e=>{void 0===e&&(e=i),e&&(this.injectAtCaret(e,window.getSelection().getRangeAt(0)),"mix"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.settings.pasteAsTags?this.addTags(this.state.inputText+e,!0):this.state.inputText=e)})).catch((t=>t)))},onDrop(t){t.preventDefault()},onEditTagInput(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=this.tagData(i),n=this.input.normalize.call(this,t),o={[this.settings.tagTextProp]:n,__tagId:a.__tagId},r=this.validateTag(o);this.editTagChangeDetected(g(a,o))||!0!==t.originalIsValid||(r=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==r),a.__isValid=r,i.title=!0===r?a.title||a.value:r,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show(n)),this.trigger("edit:input",{tag:i,index:s,data:g({},this.value[s],{newValue:n}),event:e})},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur(t){if(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t)){var e,i,s=this.settings,a=t.closest("."+s.classNames.tag),n=this.input.normalize.call(this,t),o=this.tagData(a),r=o.__originalData,l=this.editTagChangeDetected(o),d=this.validateTag({[s.tagTextProp]:n,__tagId:o.__tagId});if(n)if(l){if(e=this.hasMaxTags(),i=g({},r,{[s.tagTextProp]:this.trim(n),__isValid:d}),s.transformTag.call(this,i,r),!0!==(d=(!e||!0===r.__isValid)&&this.validateTag(i))){if(this.trigger("invalid",{data:i,tag:a,message:d}),s.editTags.keepInvalid)return;s.keepInvalidTags?i.__isValid=d:i=r}else s.keepInvalidTags&&(delete i.title,delete i["aria-invalid"],delete i.class);this.onEditTagDone(a,i)}else this.onEditTagDone(a,r);else this.onEditTagDone(a)}},onEditTagkeydown(t,e){if(!this.state.composing)switch(this.trigger("edit:keydown",{event:t}),t.key){case"Esc":case"Escape":e.parentNode.replaceChild(e.__tagifyTagData.__originalHTML,e),this.state.editing=!1;case"Enter":case"Tab":t.preventDefault(),t.target.blur()}},onDoubleClickScope(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=this.tagData(s),n=this.settings;s&&n.userInput&&!1!==a.editable&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),"select"==n.mode||n.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0),this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:this.tagData(s)}))},onInputDOMChange(t){t.forEach((t=>{t.addedNodes.forEach((t=>{if("

"==t.outerHTML)t.replaceWith(document.createElement("br"));else if(1==t.nodeType&&t.querySelector(this.settings.classNames.tagSelector)){let e=document.createTextNode("");3==t.childNodes[0].nodeType&&"BR"!=t.previousSibling.nodeName&&(e=document.createTextNode("\n")),t.replaceWith(e,...[...t.childNodes].slice(0,-1)),this.placeCaretAfterNode(e)}else if(v.call(this,t)&&(3!=t.previousSibling?.nodeType||t.previousSibling.textContent||t.previousSibling.remove(),t.previousSibling&&"BR"==t.previousSibling.nodeName)){t.previousSibling.replaceWith("\n​");let e=t.nextSibling,i="";for(;e;)i+=e.textContent,e=e.nextSibling;i.trim()&&this.placeCaretAfterNode(t.previousSibling)}})),t.removedNodes.forEach((t=>{t&&"BR"==t.nodeName&&v.call(this,e)&&(this.removeTags(e),this.fixFirefoxLastTagNoCaret())}))}));var e=this.DOM.input.lastChild;e&&""==e.nodeValue&&e.remove(),e&&"BR"==e.nodeName||this.DOM.input.appendChild(document.createElement("br"))}}};function M(t,e){if(!t){console.warn("Tagify:","input element not found",t);const e=new Proxy(this,{get:()=>()=>e});return e}if(t.__tagify)return console.warn("Tagify: ","input element is already Tagified - Same instance is returned.",t),t.__tagify;var i;g(this,function(t){var e=document.createTextNode("");function i(t,i,s){s&&i.split(/\s+/g).forEach((i=>e[t+"EventListener"].call(e,i,s)))}return{off(t,e){return i("remove",t,e),this},on(t,e){return e&&"function"==typeof e&&i("add",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)"remove"==i&&(i="removeTag"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o="object"==typeof s?s:{value:s};if((o=a.cloneData?g({},o):o).tagify=this,s.event&&(o.event=this.cloneEvent(s.event)),s instanceof Object)for(var r in s)s[r]instanceof HTMLElement&&(o[r]=s[r]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox="undefined"!=typeof InstallTrigger,this.isIE=window.document.documentMode,e=e||{},this.getPersistedData=(i=e.id,t=>{let e,s="/"+t;if(1==localStorage.getItem(b+i+"/v",1))try{e=JSON.parse(localStorage[b+i+s])}catch(t){}return e}),this.setPersistedData=(t=>t?(localStorage.setItem(b+t+"/v",1),(e,i)=>{let s="/"+i,a=JSON.stringify(e);e&&i&&(localStorage.setItem(b+t+s,a),dispatchEvent(new Event("storage")))}):()=>{})(e.id),this.clearPersistedData=(t=>e=>{const i=b+"/"+t+"/";if(e)localStorage.removeItem(i+e);else for(let t in localStorage)t.includes(i)&&localStorage.removeItem(t)})(e.id),this.applySettings(t,e),this.state={inputText:"",editing:!1,composing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),T.call(this),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),t.autofocus&&this.DOM.input.focus(),t.__tagify=this}return M.prototype={_dropdown:w,helpers:{sameStr:s,removeCollectionProp:a,omit:n,isObject:h,parseHTML:r,escapeHTML:d,extend:g,concatWithoutDups:p,getUID:m,isNodeTag:v},customEventsList:["change","add","remove","invalid","input","click","keydown","focus","blur","edit:input","edit:beforeUpdate","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select","dropdown:updated","dropdown:noMatch","dropdown:scroll"],dataProps:["__isValid","__removed","__originalData","__originalHTML","__tagId"],trim(t){return this.settings.trim&&t&&"string"==typeof t?t.trim():t},parseHTML:r,templates:D,parseTemplate(t,e){return t=this.settings.templates[t]||t,this.parseHTML(t.apply(this,e))},set whitelist(t){const e=t&&Array.isArray(t);this.settings.whitelist=e?t:[],this.setPersistedData(e?t:[],"whitelist")},get whitelist(){return this.settings.whitelist},generateClassSelectors(t){for(let e in t){let i=e;Object.defineProperty(t,i+"Selector",{get(){return"."+this[i].split(" ")[0]}})}},applySettings(t,i){f.templates=this.templates;var s=this.settings=g({},f,i);if(s.disabled=t.hasAttribute("disabled"),s.readonly=s.readonly||t.hasAttribute("readonly"),s.placeholder=d(t.getAttribute("placeholder")||s.placeholder||""),s.required=t.hasAttribute("required"),this.generateClassSelectors(s.classNames),void 0===s.dropdown.includeSelectedTags&&(s.dropdown.includeSelectedTags=s.duplicates),this.isIE&&(s.autoComplete=!1),["whitelist","blacklist"].forEach((e=>{var i=t.getAttribute("data-"+e);i&&(i=i.split(s.delimiters))instanceof Array&&(s[e]=i)})),"autoComplete"in i&&!h(i.autoComplete)&&(s.autoComplete=f.autoComplete,s.autoComplete.enabled=i.autoComplete),"mix"==s.mode&&(s.autoComplete.rightKey=!0,s.delimiters=i.delimiters||null,s.tagTextProp&&!s.dropdown.searchKeys.includes(s.tagTextProp)&&s.dropdown.searchKeys.push(s.tagTextProp)),t.pattern)try{s.pattern=new RegExp(t.pattern)}catch(t){}if(s.delimiters){s._delimiters=s.delimiters;try{s.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}}s.disabled&&(s.userInput=!1),this.TEXTS=e(e({},x),s.texts||{}),"select"!=s.mode&&s.userInput||(s.dropdown.enabled=0),s.dropdown.appendTarget=i.dropdown&&i.dropdown.appendTarget?i.dropdown.appendTarget:document.body;let a=this.getPersistedData("whitelist");Array.isArray(a)&&(this.whitelist=Array.isArray(s.whitelist)?p(s.whitelist,a):a)},getAttributes(t){var e,i=this.getCustomAttributes(t),s="";for(e in i)s+=" "+e+(void 0!==t[e]?`="${i[e]}"`:"");return s},getCustomAttributes(t){if(!h(t))return"";var e,i={};for(e in t)"__"!=e.slice(0,2)&&"class"!=e&&t.hasOwnProperty(e)&&void 0!==t[e]&&(i[e]=d(t[e]));return i},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCaretGlobalPosition(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);var e;this.CSSVars={tagHideTransition:(t=>{let e=t.value;return"s"==t.unit?1e3*e:e})(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}((e="tag-hide-transition",t.getPropertyValue("--"+e))))}},build(t){var e=this.DOM;this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.originalInput_tabIndex=t.tabIndex,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t),t.tabIndex=-1)},destroy(){this.events.unbindGlobal.call(this),this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.DOM.originalInput.tabIndex=this.DOM.originalInput_tabIndex,delete this.DOM.originalInput.__tagify,this.dropdown.hide(!0),clearTimeout(this.dropdownHide__bindEventsTimeout),clearInterval(this.listeners.main.originalInputValueObserverInterval)},loadOriginalValues(t){var e,i=this.settings;if(this.state.blockChangeEvent=!0,void 0===t){const e=this.getPersistedData("value");t=e&&!this.DOM.originalInput.value?e:i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value}if(this.removeAllTags(),t)if("mix"==i.mode)this.parseMixTags(this.trim(t)),(e=this.DOM.input.lastChild)&&"BR"==e.tagName||this.DOM.input.insertAdjacentHTML("beforeend","
");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t,!0).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?"":this.DOM.originalInput.value,this.state.blockChangeEvent=!1},cloneEvent(t){var e={};for(var i in t)"path"!=i&&(e[i]=t[i]);return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleClass(t,e){"string"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleScopeValidation(t){var e=!0===t||void 0===t;!this.settings.required&&t&&t===this.TEXTS.empty&&(e=!0),this.toggleClass(this.settings.classNames.tagInvalid,!e),this.DOM.scope.title=e?"":t},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:O,fixFirefoxLastTagNoCaret(){},placeCaretAfterNode(t){if(t&&t.parentNode){var e=t,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartAfter(e||t),s.collapse(!0),i.removeAllRanges(),i.addRange(s))}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e="string"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTagChangeDetected(t){var e=t.__originalData;for(var i in e)if(!this.dataProps.includes(i)&&t[i]!=e[i])return!0;return!1},getTagTextNode(t){return t.querySelector(this.settings.classNames.tagTextSelector)},setTagTextNode(t,e){this.getTagTextNode(t).innerHTML=d(e)},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide();var i=this.settings,s=this.getTagTextNode(t),a=this.getNodeIndex(t),n=this.tagData(t),o=this.events.callbacks,r=this,l=!0;if(s){if(!(n instanceof Object&&"editable"in n)||n.editable)return n=this.tagData(t,{__originalData:g({},n),__originalHTML:t.cloneNode(!0)}),this.tagData(n.__originalHTML,n.__originalData),s.setAttribute("contenteditable",!0),t.classList.add(i.classNames.tagEditing),s.addEventListener("focus",o.onEditTagFocus.bind(this,t)),s.addEventListener("blur",(function(){setTimeout((()=>o.onEditTagBlur.call(r,r.getTagTextNode(t))))})),s.addEventListener("input",o.onEditTagInput.bind(this,s)),s.addEventListener("keydown",(e=>o.onEditTagkeydown.call(this,e,t))),s.addEventListener("compositionstart",o.onCompositionStart.bind(this)),s.addEventListener("compositionend",o.onCompositionEnd.bind(this)),e.skipValidation||(l=this.editTagToggleValidity(t)),s.originalIsValid=l,this.trigger("edit:start",{tag:t,index:a,data:n,isValid:l}),s.focus(),this.setRangeAtStartEnd(!1,s),this}else console.warn("Cannot find element in Tag template: .",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i;if(e=e||this.tagData(t))return(i=!("__isValid"in e)||!0===e.__isValid)||this.removeTagsFromValue(t),this.update(),t.classList.toggle(this.settings.classNames.tagNotAllowed,!i),e.__isValid;console.warn("tag has no data: ",t,e)},onEditTagDone(t,e){e=e||{};var i={tag:t=t||this.state.editing.scope,index:this.getNodeIndex(t),previousData:this.tagData(t),data:e};this.trigger("edit:beforeUpdate",i,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&e[this.settings.tagTextProp]?(t=this.replaceTag(t,e),this.editTagToggleValidity(t,e),this.settings.a11y.focusableTags?t.focus():this.placeCaretAfterNode(t)):t&&this.removeTags(t),this.trigger("edit:updated",i),this.dropdown.hide(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&g(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(" ")[0])||this.value.push(this.tagData(t))})),this.update()},setRangeAtStartEnd(t,e){t="number"==typeof t?t:!!t,e=(e=e||this.DOM.input).lastChild||e;var i=document.getSelection();try{i.rangeCount>=1&&["Start","End"].forEach((s=>i.getRangeAt(0)["set"+s](e,t||e.length)))}catch(t){}},injectAtCaret(t,e){return!(e=e||this.state.selection?.range)&&t?(this.appendMixTags(t),this):("string"==typeof t&&(t=document.createTextNode(t)),e.deleteContents(),e.insertNode(t),this.setRangeAtStartEnd(!1,t),this.updateValueByDOMTags(),this.update(),this)},input:{set(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=d(""+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},raw(){return this.DOM.input.textContent},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t){var e=t||this.DOM.input,i=[];e.childNodes.forEach((t=>3==t.nodeType&&i.push(t.nodeValue))),i=i.join("\n");try{i=i.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return i=i.replace(/\s/g," "),this.trim(i)},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){"string"==typeof(t=t||{})&&(t={value:t});var e=t.value?""+t.value:"",i=e.substr(0,this.state.inputText.length).toLowerCase(),s=e.substring(this.state.inputText.length);e&&this.state.inputText&&i==this.state.inputText.toLowerCase()?(this.DOM.input.setAttribute("data-suggest",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}},set(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.state.inputText+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd()),this.input.autocomplete.suggest.call(this),this.dropdown.hide(),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(){for(var t=arguments.length,e=new Array(t),i=0;it?(e&&(t.__tagifyTagData=i?e:g({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn("tag element doesn't exist",t,e),e),isTagDuplicate(t,e,i){var a=0;if("select"==this.settings.mode)return!1;for(let n of this.value){s(this.trim(""+t),n.value,e)&&i!=n.__tagId&&a++}return a},getTagIndexByValue(t){var e=[];return this.getTagElms().forEach(((i,a)=>{s(this.trim(i.textContent),t,this.settings.dropdown.caseSensitive)&&e.push(a)})),e},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(""+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(t,e,i){e=e||"value";var a,n=this.settings;return(i=i||n.whitelist).some((i=>{var o="string"==typeof i?i:i[e]||i.value;if(s(o,t,n.dropdown.caseSensitive,n.trim))return a="string"==typeof i?{value:i}:i,!0})),a||"value"!=e||"value"==n.tagTextProp||(a=this.getWhitelistItem(t,n.tagTextProp,i)),a},validateTag(t){var e=this.settings,i="value"in t?"value":e.tagTextProp,s=this.trim(t[i]+"");return(t[i]+"").trim()?e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,e.dropdown.caseSensitive,t.__tagId)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{"aria-invalid":!0,class:`${t.class||""} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t,e){var i=this.settings;document.activeElement.blur(),i[e||"readonly"]=t,this.DOM.scope[(t?"set":"remove")+"Attribute"](e||"readonly",!0),this.setContentEditable(!t)},setContentEditable(t){this.settings.userInput&&(this.DOM.input.contentEditable=t,this.DOM.input.tabIndex=t?0:-1)},setDisabled(t){this.setReadonly(t,"disabled")},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp;e.enforceWhitelist;var o=[],r=!!i&&i[0]instanceof Object,l=Array.isArray(t),d=l&&t[0].value,h=t=>(t+"").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=h(t)}else l&&(t=[].concat(...t.map((t=>t.value?t:h(t)))));return r&&!d&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0});this.settings.duplicates||(i=i.filter((t=>!e.includes(t.value))));var s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):"mix"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),o.length&&(t=o)),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];return t=t.split(i[0]).map(((t,e)=>{var d,h,g,p=t.split(i[1]),c=p[0],u=l.length==o;try{if(c==+c)throw Error;h=JSON.parse(c)}catch(t){h=this.normalizeTags(c)[0]||{value:c}}if(a.call(this,h),u||!(p.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else h[d=h[r]?r:"value"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),p[0]=g.outerHTML,this.value.push(h);return p.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize(),this.getTagElms().forEach(((t,e)=>this.tagData(t,l[e]))),this.update({withoutChangeEvent:!0}),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=this.state.selection||window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),-1==(i=n.nodeValue.lastIndexOf(e))?!0:(s=n.splitText(i),t&&n.parentNode.replaceChild(t,s),!0)}},selectTag(t,e){var i=this.settings;if(!i.enforceWhitelist||this.isTagWhitelisted(e.value)){this.input.set.call(this,e[i.tagTextProp]||e.value,!0),this.state.actions.selectOption&&setTimeout(this.setRangeAtStartEnd.bind(this));var s=this.getLastTag();return s?this.replaceTag(s,e):this.appendTag(t),this.value[0]=e,this.update(),this.trigger("add",{tag:t,data:e}),[t]}},addEmptyTag(t){var e=g({value:""},t||{}),i=this.createTagElem(e);this.tagData(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0})},addTags(t,e,i){var s=[],a=this.settings,n=[],o=document.createDocumentFragment();if(i=i||a.skipInvalid,!t||0==t.length)return s;switch(t=this.normalizeTags(t),a.mode){case"mix":return this.addMixTags(t);case"select":e=!1,this.removeAllTags()}return this.DOM.input.removeAttribute("style"),t.forEach((t=>{var e,r={},l=Object.assign({},t,{value:t.value+""});if(t=Object.assign({},l),a.transformTag.call(this,t),t.__isValid=this.hasMaxTags()||this.validateTag(t),!0!==t.__isValid){if(i)return;if(g(r,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:l}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value)),!a.createInvalidTags)return void n.push(t.value)}if("readonly"in t&&(t.readonly?r["aria-readonly"]=!0:delete t.readonly),e=this.createTagElem(t,r),s.push(e),"select"==a.mode)return this.selectTag(e,t);o.appendChild(e),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.trigger("add",{tag:e,index:this.value.length-1,data:t})):(this.trigger("invalid",{data:t,index:this.value.length,tag:e,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(e,!0)),1e3)),this.dropdown.position()})),this.appendTag(o),this.update(),t.length&&e&&(this.input.set.call(this,a.createInvalidTags?"":n.join(a._delimiters)),this.setRangeAtStartEnd()),a.dropdown.enabled&&this.dropdown.refilter(),s},addMixTags(t){if((t=this.normalizeTags(t))[0].prefix||this.state.tag)return this.prefixedTextToTag(t[0]);"string"==typeof t&&(t=[{value:t}]),this.state.selection;var e=document.createDocumentFragment();return t.forEach((t=>{var i=this.createTagElem(t);e.appendChild(i),this.insertAfterTag(i)})),this.appendMixTags(e),e},appendMixTags(t){var e=!!this.state.selection;e?this.injectAtCaret(t):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(t),this.updateValueByDOMTags(),this.update())},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag.delimiters;if(i.transformTag.call(this,t),t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.createTagElem(t),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;this.placeCaretAfterNode(a)}return this.state.tag=null,this.trigger("add",g({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.input;i===e.input?e.scope.insertBefore(t,i):e.scope.appendChild(t)},createTagElem(t,i){t.__tagId=m();var s,a=g({},t,e({value:d(t.value+"")},i));return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(s=this.parseTemplate("tag",[a,this])),this.tagData(s,t),s},reCheckInvalidTags(){var t=this.settings;this.getTagElms(t.classNames.tagNotAllowed).forEach(((e,i)=>{var s=this.tagData(e),a=this.hasMaxTags(),n=this.validateTag(s),o=!0===n&&!a;if("select"==t.mode&&this.toggleScopeValidation(n),o)return s=s.__preInvalidData?s.__preInvalidData:{value:s.value},this.replaceTag(e,s);e.title=a||n}))},removeTags(t,e,i){var s,a=this.settings;if(t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>{e&&"string"==typeof e&&(e=this.getTagElmByValue(e));var i=this.tagData(e);return e&&i&&!i.readonly&&t.push({node:e,idx:this.getTagIdx(i),data:this.tagData(e,{__removed:!0})}),t}),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==a.mode&&(i=0,this.input.set.call(this)),1==s.length&&"select"!=a.mode&&s[0].node.classList.contains(a.classNames.tagNotAllowed)&&(e=!0),s.length)return a.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?a.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter(),this.dropdown.position(),this.DOM.input.normalize(),a.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(a.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(this.removeTagsFromValue(s.map((t=>t.node))),this.update(),"select"==a.mode&&this.setContentEditable(!0))})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeTagsFromValue(t){(t=Array.isArray(t)?t:[t]).forEach((t=>{var e=this.tagData(t),i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)}))},removeAllTags(t){t=t||{},this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":this.removeTagsFromDOM(),this.dropdown.refilter(),this.dropdown.position(),this.state.dropdown.visible&&setTimeout((()=>{this.DOM.input.focus()})),"select"==this.settings.mode&&(this.input.set.call(this),this.setContentEditable(!0)),this.update(t)},postUpdate(){var t=this.settings,e=t.classNames,i="mix"==t.mode?t.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value.trim():this.value.length+this.input.raw.call(this).length;this.toggleClass(e.hasMaxTags,this.value.length>=t.maxTags),this.toggleClass(e.hasNoTags,!this.value.length),this.toggleClass(e.empty,!i),"select"==t.mode&&this.toggleScopeValidation(this.value?.[0]?.__isValid)},setOriginalInputValue(t){var e=this.DOM.originalInput;this.settings.mixMode.integrated||(e.value=t,e.tagifyValue=e.value,this.setPersistedData(t,"value"))},update(t){var e=this.getInputValue();this.setOriginalInputValue(e),this.postUpdate(),this.settings.onChangeAfterBlur&&(t||{}).withoutChangeEvent||this.state.blockChangeEvent||this.triggerChangeEvent()},getInputValue(){var t=this.getCleanValue();return"mix"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):""},getCleanValue(t){return a(t||this.value,this.dataProps)},getMixedTagsAsString(){var t="",e=this,i=this.settings,s=i.originalInputValueFormat||JSON.stringify,a=i.mixTagsInterpolator;return function i(o){o.childNodes.forEach((o=>{if(1==o.nodeType){const r=e.tagData(o);if("BR"==o.tagName&&(t+="\r\n"),r&&v.call(e,o)){if(r.__removed)return;t+=a[0]+s(n(r,e.dataProps))+a[1]}else o.getAttribute("style")||["B","I","U"].includes(o.tagName)?t+=o.textContent:"DIV"!=o.tagName&&"P"!=o.tagName||(t+="\r\n",i(o))}else t+=o.textContent}))}(this.DOM.input),t}},M.prototype.removeTag=M.prototype.removeTags,M})); \ No newline at end of file diff --git a/assets/js/tagify.min.js_LICENSE b/assets/js/tagify.min.js_LICENSE new file mode 100644 index 0000000..c95c06b --- /dev/null +++ b/assets/js/tagify.min.js_LICENSE @@ -0,0 +1,19 @@ +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. + +THE SOFTWARE IS NOT PERMISSIBLE TO BE SOLD. \ No newline at end of file diff --git a/assets/js/timepicker.js b/assets/js/timepicker.js new file mode 100644 index 0000000..45bb7ba --- /dev/null +++ b/assets/js/timepicker.js @@ -0,0 +1,183 @@ +/* + * CardDAV Middleware UI + * Written by Massi-X © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +//declaration: new TimePicker(target element, -optional- minutesStep) +class TimePicker { + #hiddenInput; + #daysInput; + #hoursInput; + #minutesInput; + #minutesStep; + #previousMinutes; + + constructor(elem, minutesStep) { + if (!(/^input$/i).test(elem.nodeName)) //only allow inputs + return; + + if (minutesStep == undefined) + minutesStep = 1; + + this.#minutesStep = minutesStep; + this.#hiddenInput = elem; + + let parts = this._calculatePiecesFromValue(this.#hiddenInput.value); + + parts[2] = Math.round(parts[2] / minutesStep) * minutesStep; //normalize to step + this.#previousMinutes = parts[2]; + + //create new element + let timepicker = document.createElement('div'); + timepicker.className = '__timepicker-container'; + if (elem.classList != null) //add existing classes if != null + timepicker.classList.add(elem.classList); + + timepicker.insertAdjacentHTML('beforeend', + '
' + + '
' + + '' + + '' + + '
' + + '' + + 'Days' + + '
'); + timepicker.insertAdjacentHTML('beforeend', + '
' + + '
' + + '' + + '' + + '
' + + '' + + 'Hours' + + '
'); + timepicker.insertAdjacentHTML('beforeend', + '
' + + '
' + + '' + + '' + + '
' + + '' + + 'Minutes' + + '
'); + + elem.parentNode.insertBefore(timepicker, elem.nextSibling); //indert into DOM + elem.type = 'hidden'; //hide original input + + //save inputs + this.#daysInput = timepicker.querySelectorAll('[data-timepicker-days]')[0]; + this.#hoursInput = timepicker.querySelectorAll('[data-timepicker-hours]')[0]; + this.#minutesInput = timepicker.querySelectorAll('[data-timepicker-minutes]')[0]; + + //listeners + timepicker.querySelectorAll('[data-timepicker-function]').forEach((elem) => { + var func = elem.getAttribute('data-timepicker-function').split(" "); + var self = this; + + elem.intervalTime = 600; + elem.onmouseup = elem.onmouseleave = elem.onkeyup = () => { + clearTimeout(elem.timeoutId) + elem.intervalTime = 600; + }; + elem.onkeydown = e => { + if (e.key === 'Enter') + elem.dispatchEvent(new Event('mousedown')); + }; + + var repeat = (type, decrease = false) => { + if (elem.intervalTime > 100) + elem.intervalTime /= 3.5; + + if (type == "days") + decrease ? self.#daysInput.value-- : self.#daysInput.value++; + else if (type == "hours") + decrease ? self.#hoursInput.value-- : self.#hoursInput.value++; + else if (type == "minutes") + decrease ? self.#minutesInput.value-- : self.#minutesInput.value++; + + timepicker.dispatchEvent(new Event('input')); + }; + + if ("increment" == func[0]) + elem.onmousedown = () => { + elem.timeoutId = setTimeout(() => { elem.dispatchEvent(new Event('mousedown')) }, elem.intervalTime); + repeat(func[1]); + }; + else if ("decrement" == func[0]) + elem.onmousedown = () => { + elem.timeoutId = setTimeout(() => { elem.dispatchEvent(new Event('mousedown')) }, elem.intervalTime); + repeat(func[1], true); + }; + }); + timepicker.oninput = () => { + //calculate step + if (this.#minutesInput.value != -1) { + if (this.#minutesInput.value - this.#previousMinutes == 1) //increment + this.#minutesInput.value = +this.#minutesInput.value + this.#minutesStep - 1; + else if (this.#minutesInput.value - this.#previousMinutes == -1) //decrement + this.#minutesInput.value = +this.#minutesInput.value - this.#minutesStep + 1; + } + + //thanks to https://codepen.io/denilsonsa/pen/ZGYEEpD + if (this.#minutesInput.value == -1) { + if (this.#hoursInput.value > 0 || this.#daysInput.value > 0) { + this.#hoursInput.value--; + this.#minutesInput.value = 60 - this.#minutesStep; + } else { + this.#minutesInput.value = 0; + } + } else if (this.#minutesInput.value == 60) { + this.#hoursInput.value++; + this.#minutesInput.value = 0; + } + + if (this.#hoursInput.value == -1) { + if (this.#daysInput.value > 0) { + this.#daysInput.value--; + this.#hoursInput.value = 23; + } else { + this.#hoursInput.value = 0; + } + } else if (this.#hoursInput.value == 24) { + this.#daysInput.value++; + this.#hoursInput.value = 0; + } + + if (this.#daysInput.value == -1) { + this.#daysInput.value = 0; + } + + this._updateHiddenInput(); + + //check if it's over the max + if (+this.#hiddenInput.value > +this.#hiddenInput.max) { + let parts = this._calculatePiecesFromValue(this.#hiddenInput.max); + + this.#daysInput.value = parts[0]; + this.#hoursInput.value = parts[1]; + this.#minutesInput.value = parts[2]; + + this._updateHiddenInput(); + } + + this.#previousMinutes = this.#minutesInput.value; + }; + } + + _updateHiddenInput() { + this.#hiddenInput.value = this.#daysInput.value * 24 * 60 + this.#hoursInput.value * 60 + this.#minutesInput.value * 1; //by 1 to convert to integer + if (this.#hiddenInput.value < 0) + this.#hiddenInput.value = 0; + } + + _calculatePiecesFromValue(val) { + let days = val / (24 * 60); + let daysTruncated = Math.trunc(days); + let hours = (days - daysTruncated) * 24; + let hoursTruncated = Math.trunc(hours); + let minutesTruncated = Math.round((hours - hoursTruncated) * 60); //needs rounding for precision errors + + return [daysTruncated, hoursTruncated, minutesTruncated]; + } +} \ No newline at end of file diff --git a/build b/build new file mode 100755 index 0000000..01acf82 --- /dev/null +++ b/build @@ -0,0 +1,570 @@ +#!/usr/bin/env php + © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +/** + * This build script handles package creation, versioning, signing and everything. + */ + +require 'build_config'; +$rawname = 'temp'; //rawname is needed for create the tar archive (will change later to the chosen one) + +//Thanks to https://stackoverflow.com/a/15322457 for the interactive shell hint +//Note that $allowed_answers is incompatible with $multiline +function interactive_shell($question, $type = null, $allowed_answers = null, $multiline = false) +{ + $str = $question . ' '; + $res = ''; + + //print allowed answers + if (!empty($allowed_answers)) { + $str .= '('; + foreach ($allowed_answers as $answer) + $str .= $answer . '/'; + $str = substr($str, 0, -1) . ') '; + } + + //echo message + echo colorLog($str, $type); + + //read input + if (!$multiline) + $res = readline(); + else + while ($tmp = readline()) + $res .= $tmp . "\n"; + + //keep looping till we get an allowed answer + if (!$multiline && !empty($allowed_answers)) { + foreach ($allowed_answers as $answer) { + if (strcasecmp($res, $answer) == 0) + return $res; + } + echo colorLog("I did not understand, please try again.\n", 'e'); + return interactive_shell($question, $type, $allowed_answers); + } + + return $res; +} + +//Thanks to https://stackoverflow.com/a/66075475 +function colorLog($str, $type) +{ + switch ($type) { + case 'e': //error + echo "\033[31m$str\033[0m"; + break; + case 's': //success + echo "\033[32m$str\033[0m"; + break; + case 'w': //warning + echo "\033[33m$str\033[0m"; + break; + case 'i': //info + echo "\033[36m$str\033[0m"; + break; + case 'b': //boring + echo "\033[37m$str\033[0m"; + break; + default: + echo $str; + break; + } +} + +//redefined copy to recurse folders +function copy_i($from, $to, $exclude_list = []) +{ + //don't copy hidden files + if (substr($from, 0, 1) == '.') + return true; + + if (is_dir($from)) { + //don't need to array_diff with '.', '..' because hidden files will be removed anyway + $fileList = scandir($from); + if (!$fileList || !mkdir("$to")) + return false; + + $fileList = array_diff($fileList, $exclude_list); + foreach ($fileList as $file) { + if (substr($file, 0, 1) == '.') + continue; + + if (!copy_i("$from/$file", "$to/$file")) + return false; + } + + return true; + } else + return copy($from, $to); +} + +//die with error color +function die_i($msg) +{ + die(colorLog($msg, 'e')); +} + +/*****************************************************************/ +/* ACTUAL SCRIPT */ +/*****************************************************************/ + +echo colorLog("Welcome to the build script. A compress file will be created containing the module ready to be published.\n", 'i'); +echo colorLog("REMEMBER THAT HIDDEN FILES WILL NOT BE INCLUDED IN THE OUTPUT!\n", 'i'); +sleep(1); + +//first change to the correct dir +if (!chdir(__DIR__)) + die_i("Failed to change dir!\n"); + +if (!file_exists('core/core.php')) + die_i("Unable to find core class! ABORTING!\n"); + +if (file_exists('output.tar.gz')) { + $res = interactive_shell('output.tar.gz already exists, if you continue it will be overwritten! Are you sure?', 'w', ['Y', 'N']); + + if ($res != 'Y' && $res != 'y') + die_i("ABORTING!\n"); + + echo "Deleting output.tar.gz...\n"; + if (!unlink(__DIR__ . '/output.tar.gz')) + die_i("Cannot delete output.tar.gz!\n"); +} + +//create random folder +$tmpFolder = 'ph_' . uniqid(); +while (true) { + if (file_exists($tmpFolder) || is_link($tmpFolder)) { + $tmpFolder = 'ph_' . uniqid(); + continue; + } + + $tmpFolder = sys_get_temp_dir() . '/' . $tmpFolder; + if (!@mkdir($tmpFolder . '/' . $rawname, 0777, true) || !@is_dir($tmpFolder . '/' . $rawname)) + die_i("Cannot create tmp dir!\n"); + + break; +} + +//list all the files in this dir +$fileList = scandir(__DIR__); +if (!$fileList) + die_i("Cannot scan dir!\n"); + +//remove excluded files from root (these are only mine, so $excludedLibraryFiles) +$fileList = array_diff($fileList, $excludedLibraryFiles); + +//now copy everything and pass $excludedGenericFiles here to exclude unnecessary files in core too +foreach ($fileList as $file) + copy_i($file, "$tmpFolder/$rawname/$file", $excludedGenericFiles); + +echo colorLog("Copied temporary files.\n", 's'); + +//ask some questions about the module details (that will be included in module.xml) +echo colorLog("It's now time to get some information about the module... If you want to skip any section press enter.\n", 'i'); +echo colorLog("(PS. If you want to skip this for future releases, you can create a file called config (inside \"core\" directory) and it will be automatically loaded!)\n", 'i'); + +//if config exists, preload all the values +$config = false; +if (file_exists("$tmpFolder/$rawname/core/config")) { + $config = @include "$tmpFolder/$rawname/core/config"; + if ($config === false) + die_i('Unable to include config file!'); + + if (!unlink("$tmpFolder/$rawname/core/config")) + die_i("Unable to delete config file!\n"); + + echo colorLog("Using the provded config file\n", 's'); +} + +//container for the xml below +$output_xml = new SimpleXMLElement(''); + +//add meta tag with UI version +$output_xml->addChild('meta-cmui-version', $UI_version); + +//rawname +if (!$config) + $res = interactive_shell('What is the rawname of the module?'); +else + $res = @$config['rawname']; + +if (!empty($res)) { + echo colorLog("Setting rawname to \"$res\"\n", 'b'); +} else { + $res = (empty($modulexml['rawname']) ? 'none' : $modulexml['rawname']); + echo colorLog("Setting rawname to default \"$res\"\n", 'b'); +} + +$output_xml->addChild('rawname', htmlspecialchars(trim($res))); + +//now move the files to the new directory +if (!@rename("$tmpFolder/$rawname", $tmpFolder . '/' . trim($res))) + die_i("Unable to rename tmp folder!\n"); +$rawname = trim($res); + +//name +if (!$config) + $res = interactive_shell('What is the name (to display) of the module?'); +else + $res = @$config['name']; + +if (!empty($res)) { + echo colorLog("Setting name to \"$res\"\n", 'b'); +} else { + $res = (empty($modulexml['name']) ? 'none' : $modulexml['name']); + echo colorLog("Setting name to default \"$res\"\n", 'b'); +} + +$output_xml->addChild('name', htmlspecialchars(trim($res))); + +//version +if (!$config) + $res = interactive_shell('Which version is this? (use three digits, ex. 1.0.0):'); +else + $res = @$config['version']; + +if (!empty($res)) + echo colorLog("Setting version to \"$res\"\n", 'b'); +else { + $res = (empty($modulexml['version']) ? 'none' : $modulexml['version']); + echo colorLog("Setting version to default \"$res\"\n", 'b'); +} + +$output_xml->addChild('version', htmlspecialchars(trim($res))); + +//category +$categories = [ + 0 => 'Uncategorized', + 1 => 'Admin', + 2 => 'Applications', + 3 => 'Connectivity', + 4 => 'Reports', + 5 => 'Settings' +]; + +if (!$config) { + echo "Which category does your module belongs to?\n"; + foreach ($categories as $index => $cat) + echo "[$index] $cat\n"; + $res = interactive_shell('Input your choice:', null, array_keys($categories)); +} else { + $res = @$config['category']; + if (!empty($res) && is_string($res)) { + $key = array_search($res, $categories); + if (is_int($key)) + $res = $key; + else { + $res = null; + echo colorLog("Config file contains an invalid value for category, skipping.\n", 'e'); + } + } +} + +if (!empty($res) && strcasecmp('0', trim($res)) != 0) { + echo colorLog('Setting category to "' . $categories[$res] . "\"\n", 'b'); + $output_xml->addChild('category', htmlspecialchars($categories[$res])); +} else + echo colorLog("Skipping category\n", 'b'); + +//menu items +$menu_to_add = []; + +if (isset($modulexml['menuitems'])) + $menu_to_add = array_merge($modulexml['menuitems'], []); //add default values + +if (!$config) { + //print info for user (adding default menu items) + if (!empty($menu_to_add)) { + echo "Adding default menu items...\n"; + foreach ($menu_to_add as $key => $val) + echo colorLog("Adding \"$key\" => \"$val\"\n", 'b'); + } + + $res = 'y'; + while ($res != null) { + $res = interactive_shell('Does your module have any menu items? Then input here the (php) page name or skip with enter:'); + if (!empty($res)) { + $key = $res; + $res = interactive_shell("OK, what is the name for the entry \"$res\"?"); + if (!empty($res)) { + echo colorLog((isset($menu_to_add[$key]) ? 'Overwriting' : 'Adding') . " menu item \"$key\" => \"$res\"\n", 'b'); + $menu_to_add[$key] = $res; + } else + echo colorLog("Invalid value, please retry.\n", 'w'); + } else + $res = null; + } +} else { + $arr = @$config['menuitems']; + if (!is_array($arr)) + echo colorLog("Config file contains an invalid array for menuitems, skipping.\n", 'e'); + else { + foreach ($arr as $key => $val) + $menu_to_add[$key] = $val; + } +} + +//add all the values declared before (if any) +if (count($menu_to_add)) { + $menuitems_xml = $output_xml->addChild('menuitems'); //subnode for menu items + + foreach ($menu_to_add as $key => $val) { + echo colorLog("Adding menuitem \"$key\" => \"$val\"\n", 'b'); + $menuitems_xml->addChild(trim($key), htmlspecialchars(trim($val))); + } + + echo colorLog("Menu items set.\n", 'b'); +} else + echo colorLog("Skipping menu items\n", 'b'); + +//publisher +if (!$config) + $res = interactive_shell('Do you want to be added as a publisher? Input your name then:'); +else + $res = @$config['publisher']; + +if (!empty($res)) { + $res = $res . ' w/ ' . $modulexml['publisher']; + echo colorLog("Setting publisher to \"$res\"\n", 'b'); +} else { + $res = (empty($modulexml['publisher']) ? 'none' : $modulexml['publisher']); + echo colorLog("Setting publisher to default \"$res\"\n", 'b'); +} + +$output_xml->addChild('publisher', htmlspecialchars(trim($res))); + +//description +if (!$config) + $res = interactive_shell('What is the short description of the module?'); +else + $res = @$config['description']; + +if (!empty($res)) + echo colorLog("Setting description to \"$res\"\n", 'b'); +else { + $res = (empty($modulexml['description']) ? 'none' : $modulexml['description']); + echo colorLog("Setting description to default \"$res\"\n", 'b'); +} + +$output_xml->addChild('description', htmlspecialchars(trim($res))); + +//license +if (!$config) + $res = interactive_shell('What license are you using?'); +else + $res = @$config['license']; + +if (!empty($res)) { + echo colorLog("Setting license to \"$res\"\n", 'b'); + $output_xml->addChild('license', htmlspecialchars(trim($res))); + + //license link is subject to license itself + if (!$config) + $res = interactive_shell("Please now input a link pointing to the license text of $res:"); + else + $res = @$config['licenselink']; + + if (!empty($res)) { + echo colorLog("Setting license link to \"$res\"\n", 'b'); + $output_xml->addChild('licenselink', htmlspecialchars(trim($res))); + } else + die_i("You MUST include a license link in case you choose a different license.\n"); +} else { + $license = (empty($modulexml['license']) ? 'none' : $modulexml['license']); + echo colorLog("Setting license to default \"$license\"\n", 'b'); + + $output_xml->addChild('license', htmlspecialchars(trim($license))); + $output_xml->addChild('licenselink', htmlspecialchars(trim($modulexml['licenselink']))); +} + +//more-info +if (!$config) + $res = interactive_shell('Do you have a link with more info about the module? Please input it then:'); +else + $res = @$config['more-info']; + +if (!empty($res)) + echo colorLog("Setting more-info to \"$res\"\n", 'b'); +else { + $res = (empty($modulexml['more-info']) ? 'none' : $modulexml['more-info']); + echo colorLog("Setting more-info to default \"$res\"\n", 'b'); +} + +$output_xml->addChild('more-info', htmlspecialchars(trim($res))); + +//updateurl +if (!$config) + $res = interactive_shell('Do you have an update json for automatic updates? Please input the link then:'); +else + $res = @$config['updateurl']; + +if (!empty($res)) + echo colorLog("Setting updateurl to \"$res\"\n", 'b'); +else { + $res = (empty($modulexml['updateurl']) ? 'none' : $modulexml['updateurl']); + echo colorLog("Setting updateurl to default \"$res\"\n", 'b'); +} + +$output_xml->addChild('updateurl', htmlspecialchars(trim($res))); + +//changelog +if (!$config) + $res = interactive_shell('Do you want to insert any release notes? Please input them now (to finish hit enter twice):', null, null, true); +else + $res = @$config['changelog']; + +if (!empty($res)) + $modulexml['changelog'] .= $res; + +echo colorLog("Setting changelog to:\n" . trim(preg_replace('/\t+/', '', (empty($modulexml['changelog']) ? 'none' : $modulexml['changelog']))) . "\n", 'b'); +$output_xml->addChild('changelog', htmlspecialchars(trim($modulexml['changelog']))); + +//dependencies +$depends_xml = $output_xml->addChild('depends'); //subnode for depends + +$depends_to_add = []; +if (isset($modulexml['depends'])) + $depends_to_add = array_merge($modulexml['depends'], []); //store the used keys to print an info for the user (except 'module' that can have multiple ones) + set default keys + +if (!$config) { + //print info for user (adding default dependencies) + echo "Adding default dependecies...\n"; + foreach ($depends_to_add as $key => $val) + echo colorLog("Adding \"$key\" => \"$val\"\n", 'b'); + + $res = 'y'; + while ($res != null) { + $res = interactive_shell('Does your module have any dependecies? If yes input it here (version/module/phpversion...) or skip with enter:'); + if (!empty($res)) { + $key = $res; + $res = interactive_shell("OK, what is the description/version for dependecy \"$res\"?"); + if (!empty($res)) { + if (strcasecmp($key, 'version') == 0 && version_compare($minimum_fpbx, $res) == 1) //disallow versions < $minimum_fpbx + echo colorLog("Minimum version is $minimum_fpbx, please retry.\n", 'e'); + else { + if (strcasecmp($key, 'module') == 0) { //module is the only one that accepts multiple entries + echo colorLog("Adding dependecy \"$key\" => \"$res\"\n", 'b'); + $depends_xml->addChild(trim($key), htmlspecialchars(trim($res))); + } else { + echo colorLog((isset($depends_to_add[$key]) ? 'Overwriting' : 'Adding') . " dependecy \"$key\" => \"$res\"\n", 'b'); + $depends_to_add[$key] = $res; + } + } + } else + echo colorLog("Invalid value, please retry.\n", 'w'); + } else + $res = null; + } + + //add all the values declared before (apart from module) + foreach ($depends_to_add as $key => $val) + $depends_xml->addChild(trim($key), htmlspecialchars(trim($val))); +} else { + //build the xml from an array + function arraywalk($arr, $is_default = false) + { + global $depends_xml; + global $minimum_fpbx; + + foreach ($arr as $key => $value) { + if (!empty($key)) { + if (!empty($value)) { + if (strcasecmp($key, 'version') == 0 && version_compare($minimum_fpbx, $value) == 1) //disallow versions < $minimum_fpbx + echo colorLog("You were trying to set a minimum version below $minimum_fpbx but this is not allowed, skipping.\n", 'e'); + else { + if (strcasecmp($key, 'module') == 0) { //module is the only one that accepts multiple entries + if (!is_array($value)) + $value = [$value]; + + foreach ($value as $val) { + echo colorLog("Adding " . ($is_default ? 'default ' : '') . "dependecy \"module\" => \"$val\"\n", 'b'); + $depends_xml->addChild('module', htmlspecialchars(trim($val))); + } + } else { //no need to check for duplicates here. It is impossible for the array to have any + echo colorLog("Adding " . ($is_default ? 'default ' : '') . "dependecy \"$key\" => \"$value\"\n", 'b'); + $depends_xml->addChild(trim($key), htmlspecialchars(trim($value))); + } + } + } else + echo colorLog("$key is empty, skipping.\n", 'w'); + } + } + } + + if (!is_array(@$config['depends'])) //fail if depends is not an array + echo colorLog("Config file contains an invalid array for depends, skipping.\n", 'e'); + else { + //remove overwritten default values + foreach ($depends_to_add as $key => $val) { + if (array_key_exists($key, $config['depends'])) + unset($depends_to_add[$key]); + } + + arraywalk($depends_to_add, true); //add default dependencies + arraywalk($config['depends']); //add 'core' dependencies + } +} + +echo colorLog("Dependencies set.\n", 'b'); //no need to skip this if no dependency is set (it will never happen because version is always there) + +//remove module.xml from output (if present)... +if (file_exists("$tmpFolder/$rawname/module.xml") && !unlink("$tmpFolder/$rawname/module.xml")) + die_i("Unable to delete module.xml!\n"); + +//...and replace it with our generated one +try { + $result = $output_xml->asXML("$tmpFolder/$rawname/module.xml"); + file_put_contents("$tmpFolder/$rawname/module.xml", file_get_contents("$tmpFolder/$rawname/module.xml") . ""); //put comment at the end for xml parser +} catch (Throwable $t) { + die_i("Unable to write module.xml!\n"); +} + +if ($config) + echo colorLog("Finished loading values from config file. Please read above to assure everything is correct\n", 's'); + +//signing is optional +$res = interactive_shell('Do you want to sign the module?', null, ['Y', 'N']); +if ($res == 'Y' || $res == 'y') { + //checks to be performed before everything else + if (!`which gpg`) + die_i('Please run `brew install gpg` to enable signing.'); + + if (!file_exists('devtools') || !is_dir('devtools')) + die_i("Unable to find devtools folder. Please download it from https://github.com/FreePBX/devtools and unzip it in the main directory (the same as this script) to continue.\n"); + + //a key ID is needed + $key = interactive_shell('Please provide now your GPG key ID to sign the module:'); + + if (empty($key)) + die_i("GPG key cannot be empty! ABORTING!\n"); + + //exec the sign and echo every output + echo colorLog(exec("GPG_TTY=$(tty) && export GPG_TTY && php devtools/sign.php $tmpFolder/$rawname " . escapeshellarg($key)) . "\n", 'i'); +} + +//tar.gz the file (thanks to https://stackoverflow.com/a/20062628) +try { + $phar = new PharData(__DIR__ . "/output.tar"); + + $phar->buildFromDirectory("$tmpFolder"); + + //tar archive will be created only after closing object + $phar->compress(Phar::GZ); + + // NOTE THAT BOTH FILES WILL EXISTS. SO IF YOU WANT YOU CAN UNLINK archive.tar + unlink(__DIR__ . "/output.tar"); +} catch (Exception $e) { + die_i("Failed to create tar file!\n"); +} + +//delete tmp folder (don't care about exeptions) +@exec("rm -rf $tmpFolder"); + +//open output directory +@exec('open ' . escapeshellarg(__DIR__)); //don't care about exceptions +echo colorLog("Done, you can find the tar inside the source directory.\n", 's'); diff --git a/build_config b/build_config new file mode 100644 index 0000000..a20e53f --- /dev/null +++ b/build_config @@ -0,0 +1,44 @@ + © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +//Dear Reader, DO NOT EDIT VALUES HERE USE INSTEAD THE CONFIG FILE! + +$minimum_fpbx = '15.0'; //minimum compatible FreePBX +$UI_version = '1.0.0'; //version to change in case of new releases +$readme = 'https://github.com/Massi-X/freepbx-phonemiddleware/#readme'; //readme URL + +//list of files from this library that should not end up in the finished module +$excludedLibraryFiles = [ + 'build', + 'build_config', + 'devtools' +]; + +//list of files that are not needed even if in core +$excludedGenericFiles = [ + //not used for now + //'composer.json', + //'composer.lock' + //config is automatically removed after loading values, no need to include it here +]; + +//default module xml array +$modulexml = [ + 'rawname' => 'carddavmiddleware', + 'name' => 'CardDAV Middleware UI', + 'version' => '1.0.0', + 'publisher' => 'Massi-X', + 'license' => 'CC-BY-NC-ND-4.0', + 'licenselink' => 'https://github.com/Massi-X/freepbx-phonemiddleware/blob/main/LICENSE', + 'description' => 'This is the UI for CardDAV Middleware. Please change this when releasing.', + 'more-info' => $readme, + 'updateurl' => '', + 'changelog' => "*Powered by CardDAV Middleware UI v.$UI_version* More info at $readme\n", //changelog is one line. Do not add anything more than that + 'depends' => [ + 'version' => $minimum_fpbx + ] +]; diff --git a/carddavtoxml.php b/carddavtoxml.php new file mode 100644 index 0000000..86e2b83 --- /dev/null +++ b/carddavtoxml.php @@ -0,0 +1,23 @@ + © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +header('Content-type: text/xml'); +require __DIR__ . '/core/core.php'; + +use Core; + +try { + $instance = Core::getInstance(); + echo $instance->getXMLforPhones($instance::get_cache_expire() == 0); +} catch (Exception $e) { + //send real message to the UI + Core::sendUINotification(Core::NOTIFICATION_TYPE_ERROR, $e->getMessage()); + //and print a generic one here + $xml = new SimpleXMLElement(''); + $xml->addChild('error', _('Something went wrong while retrieving the addressbook(s). Please log into the UI to see a more detailed error.')); + echo $xml->asXML(); +} diff --git a/module.xml b/module.xml new file mode 100644 index 0000000..05ab7e8 --- /dev/null +++ b/module.xml @@ -0,0 +1,5 @@ + + + 2.0.0 + \ No newline at end of file diff --git a/numbertocnam.php b/numbertocnam.php new file mode 100644 index 0000000..f774944 --- /dev/null +++ b/numbertocnam.php @@ -0,0 +1,42 @@ + © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +header('Content-type: text/xml'); +require __DIR__ . '/core/core.php'; + +use Core; + +//handy shortcut to print a basic xml message +function printBasicXML($key, $text) +{ + $xml = new SimpleXMLElement(''); + $xml->addChild($key, $text); + echo $xml->asXML(); +} + +//legacy warning so users will switch to POST +if (isset($_GET['number'])) { + //send real message to the UI + Core::sendUINotification(Core::NOTIFICATION_TYPE_ERROR, _('Passing parameters to numbertocnam with GET is deprecated. Please use "Auto Configure" again to fix this for you.')); + //and print a generic one here + printBasicXML('name', '[W!] ' . _('GET is deprecated, please switch to POST.')); +} + +//actual code +try { + if (isset($_POST['number'])) { + $instance = Core::getInstance(); + echo $instance->getCNFromPhone($_POST['number'], $instance::get_cache_expire() == 0); + } else if (!Core::get_superfecta_compat()) + printBasicXML('name', _('Unknown')); +} catch (Exception $e) { + //send real message to the UI + Core::sendUINotification(Core::NOTIFICATION_TYPE_ERROR, $e->getMessage()); + //and print a generic [W!] with number + if (!Core::get_superfecta_compat()) + printBasicXML('name', '[W!] ' . $_POST['number']); +} diff --git a/page.carddavmiddleware.php b/page.carddavmiddleware.php new file mode 100644 index 0000000..5b996ee --- /dev/null +++ b/page.carddavmiddleware.php @@ -0,0 +1,540 @@ + + + +
+
+ getCore(); + //print all the errors (if any) + if (isset($_POST['errors']) && is_array($_POST['errors'])) { + echo ''; + } + ?> + + +
+
+ +

+ +

+
+ +
+ + +
+ + +
+ retrieveUINotifications(); + + echo ''; //count handled by js + echo '
'; + echo '
'; + echo '
'; + + foreach ($notifications as $notification) { + $type = 'info'; + switch ($notification['level']) { + case Core::NOTIFICATION_TYPE_ERROR: + $type = 'error'; + break; + case Core::NOTIFICATION_TYPE_VERBOSE: + $type = 'verbose'; + break; + } + + //timestamp is converted to locale in js + echo '

' . $notification['timestamp'] . ''; + //repetitions + if ($notification['repeated'] > 1 && $notification['repeated'] < 10) + echo ' [Repeated ' . $notification['repeated'] . ' times]'; + else if ($notification['repeated'] >= 10) + echo ' [Repeated many times]'; + //message + close div + echo '

' . $notification['message'] . '
'; + } + + echo '
'; + ?> +
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ + +
+
+
+ + get_max_cnam_length() == 0 ? 'disabled' : ''; ?>> +
+
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ + You can try with some other manufacturer if yours is not listed, but if this still isn\'t working reach out to the developer.'); ?> + +
+
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ + (not reccomended). Max 30 days.'); ?> + +
+
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ + ' . _('ISO format') . '', _('Default country code used for parsing the numbers in %url. This is only used as a fallback if the number contains no country code.')); ?> + +
+
+
+ +
+
+
+
+
+
+ + +
+
+ + get_superfecta_compat()) echo 'checked'; ?>> + + get_superfecta_compat()) echo 'checked'; ?>> + + +
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ + +
+
+ + get_spam_match()) echo 'checked'; ?>> + + get_spam_match()) echo 'checked'; ?>> + + +
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ + +
+
+ + get_mail_level())) echo 'checked' ?>> + + get_mail_level())) echo 'checked' ?>> + + + + getEmailAddresses()['To']; ?> + +
+
+
+
+
+
+
+ + The email will be sent to the registered address in "Module Admin", section "Scheduler and Alerts", the "From" address is that of "System Admin" section "Notifications Settings". This requires a well set and working postfix configuration or the use of the commercial "System Admin" module. If something is missing or misconfigured, e-mail won\'t work!'); ?> + +
+
+
+ +
+ + +
+
+
+
+
+ + +
+ + +
+ + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/utilities.php b/utilities.php new file mode 100644 index 0000000..2478960 --- /dev/null +++ b/utilities.php @@ -0,0 +1,172 @@ + © 2023 + * This file is protected under CC-BY-NC-ND-4.0, please see "LICENSE" file for more information + */ + +/** + * Helper class to manage some BMO functions and other small useful things. + **/ + +class Utilities +{ + //list of strings that will be replaced by the script when sending an email (for now only one) + public const MAIL_SERVER_NAME = '%server_name'; + + //other constants + const SUPERFECTA_SCHEME = 'carddavmiddleware'; + const SUPERFECTA_SCHEME_CONFIG = [ + 'POST_Data' => '"number" = "$thenumber"', + 'Regular_Expressions' => '/(.*?)<\\/name>/m', + 'SPAM_Regular_Expressions' => '/(true)<\\/spam>[\\s\\S]*(.*?)<\\/threshold>/m' + ]; + + /** + * Get module version. Returns 0 in case of unknown failure + * + * @return string version number + */ + public static function get_version() + { + $xml = simplexml_load_file(__DIR__ . "/module.xml"); //this loads the value set by build script in CM UI + return $xml ? $xml->version : 0; + } + + /** + * Same as file_put_contents, but automatically handle folder creation and related errors. + * + * @param string $filename See @file_put_contents for description + * @param mixed $data See @file_put_contents for description + * @param int $flags See @file_put_contents for description + * @param $context See @file_put_contents for description + * @return int|false The function returns false in case of failure creating the folder structure or returns the result of @file_put_contents. + */ + public static function file_put_contents_i($filename, $data, $flags = 0, $context = null) + { + $dirname = dirname($filename); + + if (!is_dir($dirname) && !mkdir($dirname)) + return false; + + return file_put_contents($filename, $data, $flags, $context); + } + + /** + * Deletes a directory recursively. Does not follow symlinks. + * + * @param string $dirPath Path to dir + * @return boolean true if success, false otherwise + * @throws Exception If the provided path is not a directory + */ + public static function delete_dir($dirPath) + { + if (!is_dir($dirPath) || is_link($dirPath)) + throw new Exception("$dirPath must be a directory"); + + if (substr($dirPath, strlen($dirPath) - 1, 1) != '/') + $dirPath .= '/'; + + $files = array_diff(scandir($dirPath), array('..', '.')); + + foreach ($files as $file) { + $file = $dirPath . $file; + if (!is_link($file) && is_dir($file)) + self::delete_dir($file); + else + unlink($file); + } + return rmdir($dirPath); + } + + /** + * BMO Function + * Get email "To" and "From" addresses from fpbx config + * + * @param FreePBX $FreePBX BMO object + * @return array the registered email address in the format ["To" => "...", "From" => "..."] + * @throws Exception If the "To" (and only "To") email address is not set + */ + public static function get_fpbx_registered_email_config($FreePBX) + { + if (!is_a($FreePBX, 'FreePBX', true)) + throw new Exception(_('Not given a FreePBX Object')); + + $to = (new FreePBX\Builtin\UpdateManager())->getCurrentUpdateSettings()['notification_emails']; + if (empty($to)) + throw new Exception(_('"To" field is empty.')); + + $from = $FreePBX->Config()->get('AMPUSERMANEMAILFROM'); + if (empty($from)) + $from = null; //make sure to null it so the caller will have a consistent result + + return ["To" => $to, "From" => $from]; + } + + /** + * BMO Function + * Returns the server name extracted from shell. + * + * @param FreePBX $FreePBX BMO object + * @return string Server name or 'Unknown' in case of errors + */ + public static function get_server_name($FreePBX) + { + if (!is_a($FreePBX, 'FreePBX', true)) + throw new Exception(_('Not given a FreePBX Object')); + + $serverName = trim($FreePBX->Config()->get('FREEPBX_SYSTEM_IDENT')); + + if (empty($serverName)) + $serverName = 'Unknown'; + + return $serverName; + } + + /** + * BMO Function + * Construct the email body. + * + * @param FreePBX $FreePBX BMO object + * @param string $to "To" address + * @param string $subject Subject of email + * @param string $html_txt Message of email formatted in html + * @param string $from_name "From" name. Defaults to the email itself (WARNING! only the name, not the formatted "name ") + * @return boolean True in case of success, false otherwise + */ + public static function send_mail($FreePBX, $to, $subject, $html_txt, $from_name = '') + { + if (!is_a($FreePBX, 'FreePBX', true)) + throw new Exception(_('Not given a FreePBX Object')); + + //this is all taken from BMO/Mail.class.php + $from_email = get_current_user() . '@' . gethostname(); + + //sysadmin allows to change "from" address + if (function_exists('sysadmin_get_storage_email')) { + $emails = call_user_func('sysadmin_get_storage_email'); + //Check that what we got back above is a email address + if (!empty($emails['fromemail']) && filter_var($emails['fromemail'], FILTER_VALIDATE_EMAIL)) { + //Fallback address + $from_email = $emails['fromemail']; + } + } + + //set sender name to the address if nothing provided + if (empty($from_name)) + $from_name = $from_email; + + //replace strings matching the pattern (yeah for now I have only this so it's rather simple...) + $to = str_replace(self::MAIL_SERVER_NAME, self::get_server_name($FreePBX), $to); + $subject = str_replace(self::MAIL_SERVER_NAME, self::get_server_name($FreePBX), $subject); + $html_txt = str_replace(self::MAIL_SERVER_NAME, self::get_server_name($FreePBX), $html_txt); + $from_name = str_replace(self::MAIL_SERVER_NAME, self::get_server_name($FreePBX), $from_name); + + $headers = + "MIME-Version: 1.0\r\n" . + "Content-type: text/html; charset=UTF-8\r\n" . + "From: $from_name <$from_email>\r\n"; + + return mail($to, $subject, $html_txt, $headers); + } +}