Compare commits

...

123 Commits

Author SHA1 Message Date
osaajani 064d6fd941 Fix url parsing 2024-02-26 12:07:19 +01:00
osaajani 6e6c51a9ee Add support for automatic response to SMS stop 2024-02-25 11:11:25 +01:00
osaajani 6321899e02 Update descartes framework to improve env.php constants handling 2024-02-25 11:04:45 +01:00
osaajani cf4dd2f075 Fix sms stop that whose ignored due to a bug 2023-11-20 15:42:09 +01:00
osaajani 5d1015e190 Fix static function on inbound callback and endcallback for adapters 2023-09-22 18:27:17 +02:00
osaajani 3b2dddbea3 up version 2023-09-19 18:35:13 +02:00
osaajani 490c6499e2 Fix sms settings check on api 2023-09-19 18:34:59 +02:00
osaajani 4e165ec32d Version 3.8.0 add support for http link shortening in sms 2023-09-17 16:27:13 +02:00
osaajani 241d079ffb Add link shortening for http links in sms, using YOURLS 2023-09-17 16:16:35 +02:00
osaajani fb3f9425d1 User setting update now create setting if it doesn't exists yet instead of returning an error 2023-09-17 16:12:31 +02:00
osaajani 9aa3eca812 Add verification on phone number on contact import 2023-07-18 17:16:34 +02:00
osaajani 347084b5c4 improve perfs on status update by sizing down uid and adding an index 2023-06-06 20:32:04 +02:00
osaajani aaeb7b64e9 Add index on sms timestamps for perfs 2023-06-06 18:39:48 +02:00
Your Name 7c94c24192 update composer 2023-06-06 12:52:20 +02:00
osaajani 03f7c463a2 jquery v3 incompatible with magicsuggest 2023-06-06 12:22:40 +02:00
osaajani e95677aec5 . 2023-05-31 18:43:18 +02:00
osaajani c90da4bd5d Fix type detection in router invocation descartes for php >= 8 2023-05-31 18:34:46 +02:00
osaajani 946e03e500 v3.7.0 add stats about sended sms status, add better support for haproxy, improve gammu ream sms with deletion of read sms 2023-05-30 18:09:06 +02:00
osaajani 4e80a6a3a1 Improve dashboard stats to show sended sms status stats 2023-05-30 18:05:38 +02:00
osaajani 552300a971 fix version check 2023-05-30 17:53:04 +02:00
Pierre-Lin Bonnemaison 01dcd164ec
Merge pull request #195 from deajan/improve-gammu-receiver
Improve Gammu SMS Receiver
2023-05-30 17:50:01 +02:00
osaajani 4fe4d662b7 up dependabot 2023-05-29 21:57:16 +02:00
osaajani 7014f3da68 clean http_pwd forging 2023-05-29 21:53:09 +02:00
Pierre-Lin Bonnemaison 62eb897589
Merge pull request #199 from deajan/make_https_easier
Make HTTPS proxies work
2023-05-29 21:27:16 +02:00
osaajani 49af8f7d94 pump version 2023-03-17 16:10:08 +01:00
osaajani e7a6c486ee add status limit_reached to phone and check raspisms sms limit when updating phone status 2023-03-17 16:09:32 +01:00
osaajani b8ab352deb Fix bad offset on invalid csv file while importing contact 2023-03-01 19:34:51 +01:00
osaajani 572a243a7b Fix scheduled no with no phone on PHP < 8 2023-03-01 19:20:02 +01:00
osaajani c999318460 fix preview conditional group 2023-03-01 12:58:31 +01:00
osaajani e0c49ea055 fix preview conditional group 2023-03-01 12:58:07 +01:00
osaajani eba0b83b87 Add multiple new functions :
- A new adapter for Odyssey Messaging
- Feature to associate a tag to sms campaign
- Support for phone groups
- Support of phone status
- Support for phone priority
- Support for phone limits
- Settings to enable phone priority and phone limits
2023-02-24 19:23:51 +01:00
osaajani c3b2f9d764 Accept update status phone for phone without real status 2023-02-24 19:08:48 +01:00
osaajani 7600f096ae combine account and user for odyssey messaging adapter 2023-02-24 18:38:44 +01:00
osaajani 7483b9a8ae Add support for tag in sms campaigns 2023-02-24 16:29:10 +01:00
osaajani 2f74fa6173 Add adapter for odyssey messaging 2023-02-24 13:32:36 +01:00
osaajani b825bd6d6e Add settings to enable/disable phone priority and phone limits 2023-02-20 15:48:47 +01:00
osaajani 4ea624b0d9 update font awesome 2023-02-20 15:45:53 +01:00
osaajani 9203e2426b Remove file change ownership on file upload 2023-02-20 03:37:23 +01:00
osaajani 7c3bb65f8b Add phone group support 2023-02-20 03:17:53 +01:00
osaajani 22e5149193 add support for phone status during sms sending 2023-02-18 17:18:36 +01:00
osaajani 38d350dfc2 start add support for phone status + improve edit of phone for hidden phones 2023-02-18 16:39:07 +01:00
osaajani 85b64ada1a fix mms phone not set 2023-02-18 05:44:42 +01:00
osaajani f9e64aee65 add preview of group members 2023-02-17 05:18:57 +01:00
osaajani 59d3e28489 Add a setting to force gsm alphabet conversion 2023-02-06 20:18:35 +01:00
osaajani fdbc6a0878 fix date format 2023-02-06 05:52:49 +01:00
osaajani 4f0c585f78 finally, we will just do limit checking all over again during sending phase 2023-02-06 05:32:30 +01:00
osaajani 55fe91619b see to use forcefail 2023-02-06 05:05:18 +01:00
osaajani 47b81c1af3 fix a few error, undefined vars, update phpstan, fix adapter data preset on edit phone 2023-02-06 04:35:08 +01:00
osaajani 715afd79ec Update sending functions to correctly use phone limits and priority 2023-02-06 03:42:03 +01:00
osaajani 9b7907ad18 Start work to update get_smss_to_send to use priority and phone limits 2023-02-06 03:19:36 +01:00
osaajani 69619d0bef Add notion of priority to phones 2023-02-05 23:11:58 +01:00
osaajani 6353d5115b Add phone limits to list 2023-02-04 01:45:59 +01:00
osaajani fb58802240 Add update to phones 2023-02-04 01:15:36 +01:00
osaajani 298bba0c39 Add phone limit creation to phone creation 2023-02-02 01:12:30 +01:00
osaajani fd1e7b5519 Merge branch 'master' of https://github.com/RaspbianFrance/RaspiSMS 2023-01-31 23:13:39 +01:00
osaajani 1c7a84def0 Only start daemons for phones of active users 2023-01-31 23:11:25 +01:00
Orsiris de Jong c202806755
Make descartes work with HTTPS proxies 2022-11-04 20:00:27 +01:00
Orsiris de Jong f76977e021
Make sure we allow HTTPS request upgrades when behind https proxy 2022-11-04 19:41:53 +01:00
Orsiris de Jong 185d7772f7
Improve logging 2022-10-26 00:07:29 +02:00
Orsiris de Jong cd5f674164
Move comment to proper line 2022-10-23 11:21:11 +02:00
Orsiris de Jong 3c8061dbbb
Allow get_gamm_version() failure 2022-10-23 11:19:22 +02:00
Orsiris de Jong 2309a0e031
Improve Gammu SMS Receiver
This as a rewrite of `gammu_get_unread_sms.py` script that adds:
- Support for long SMS
- Added proper CLI interface (see --help)
- Added optional --delete parameter which deletes SMS after printing them as JSON
- Added optional --show-read parameter which shows all not Unread marked SMS
- Added logging and --debug option
- Retain retrocompatibility with earlier versions of this script
- Retain retrocompatibility with Python 2.7+ (hopefully)

Fixes #181.
Btw, the interface with RaspiSMS could be improved with a temporary JSON file, using stdout seems lossy.
2022-10-20 23:15:04 +02:00
Pierre-Lin Bonnemaison d6b650147a
Merge pull request #192 from deajan/gammu-fix-unlock-sms
Don't bother to check SIM security status on successful unlock
2022-10-19 18:30:11 +02:00
Pierre-Lin Bonnemaison 9691ee45a0
Merge pull request #194 from deajan/gammu-utf8
Make sure Gammu always sends as UTF-8 format
2022-10-19 18:23:05 +02:00
Orsiris de Jong 4776b147e5
Make sure Gammu always sends as UTF-8 format 2022-10-19 00:46:29 +02:00
Pierre-Lin Bonnemaison 61bec81c44
Merge pull request #191 from deajan/gammu-fixes
Make sure Gammu speaks english
2022-10-18 19:26:52 +02:00
Orsiris de Jong 82021648a3
Don't bother to check SIM security status on successful unlock 2022-10-17 19:19:28 +02:00
Orsiris de Jong f790adc6a1
Make sure Gammu speaks english
Gammu returns localized messages whereas GammuAdapter searches for string "nothing".
Force Gammu to return english messages so word searches will work properly.
This fixes #172
2022-10-17 19:15:32 +02:00
osaajani ad1f798ae6 fix trailing $ 2022-09-28 20:10:12 +02:00
osaajani 3dd5e099e8 Change addressing system for message queue of phones to fix issue #189 on 32 bits systems 2022-09-28 20:02:35 +02:00
osaajani ea744d31e2 Check phone exists on callback reception 2022-09-26 17:31:32 +02:00
osaajani 5c5571d38a up version 2022-09-26 17:18:15 +02:00
osaajani 11b481aebd add limit check to size of sms 2022-09-26 17:17:41 +02:00
osaajani 99349a35c5 Bump version 2022-07-12 13:16:49 +02:00
osaajani a0f3784baa Change phone adapter meta number to type phone_number + add support for boolean adapter data + add a noStaupClose to ovh adapter 2022-07-12 13:16:22 +02:00
osaajani 9c9f99c87a fix discussion send message not working after introduction of data in numbers 2022-04-25 18:48:18 +02:00
osaajani 65f74dc27b fix discussion send message not working after introduction of data in numbers 2022-04-25 18:47:55 +02:00
osaajani f1a4eb008b Add update of phone from api 2022-03-30 02:16:08 +02:00
osaajani 8889eb3ded update version 2022-03-28 01:54:59 +02:00
osaajani a226139630 Add support for hidding adapter datas 2022-03-28 01:54:38 +02:00
osaajani 5c69237169 add actual usage of data on numbers 2022-03-19 22:56:56 +01:00
osaajani cf7fa2784b add support for sms to rich number + add support for sms to csv 2022-03-19 20:06:11 +01:00
osaajani 81fb987740 Add support for numbers with data on scheduled + add support for sending sms to a csv file 2022-03-15 02:24:28 +01:00
osaajani 7fc7a8f245 Add button to show api key and add qrcode 2022-01-20 02:52:31 +01:00
osaajani 85fb487d84 Hide api key by default and add qrcode 2022-01-20 02:51:41 +01:00
osaajani bd3da73711 Change UTF8 to utf8mb4 to properly support utf8 in mysql 2021-12-29 02:54:21 +01:00
osaajani fadffdab10 extend suppression of smsstops to all users 2021-12-27 20:25:23 +01:00
osaajani b5d1483048 Add delay on direct sms reading to prevent api rate limit exceed 2021-12-21 15:03:10 +01:00
osaajani 85c09673b1 remove useless log 2021-12-21 15:01:46 +01:00
osaajani c69527a5ad fix counting tick for read phone 2021-12-21 15:00:34 +01:00
osaajani cc188f3118 Add delay between two read operation on daemon phones to prevent too many api call 2021-12-21 14:43:07 +01:00
osaajani 3f632e9db7 Catch php error in ruler evaluate. Unify all dates functions by using real DateTime objects and function date -> date_create and date_from_format -> date_create_from_format. 2021-12-02 01:03:42 +01:00
osaajani 6bd18c95cc change is_birthdate to a simpler is_birthday always using current date. Add date to get current date to the wanted format. Add intval and boolval 2021-12-02 00:30:21 +01:00
osaajani 14808eb4b0 Add first version of is_birthdate to ruler engine 2021-12-01 23:53:50 +01:00
osaajani e80638dd2e Up version 2021-11-26 19:41:13 +01:00
osaajani 7538e2e60d silence expression notice to prevent notice when using a non defined variable/property 2021-11-26 19:40:26 +01:00
osaajani 4218b0e353 Fix unexpected behavior when calling api with numbers being an array of array 2021-11-26 19:27:51 +01:00
osaajani d826762e9d fix utf8 chars in sms and add tool to quota to check if a text is gsm0338 compatible 2021-11-05 23:59:38 +01:00
osaajani a39c9577b1 Add adapter for kannel phones 2021-11-05 23:26:09 +01:00
osaajani 69bf62f115 ensure text and at are string in api to create scheduled 2021-10-28 17:38:46 +02:00
osaajani 87a04742a3 fix version 2021-10-18 03:42:34 +02:00
osaajani d0fa0a299c Add a new webhook for sended sms status change. Add status and originating scheduled's id to webhook send_sms 2021-10-18 03:40:06 +02:00
osaajani ec108d8543 add status to webhook sms_send 2021-10-17 02:52:46 +02:00
osaajani 1dd8756d3b Fix bug where scheduled message was not removed when only target number has a stop sms or message is empty. 2021-09-17 22:31:23 +02:00
osaajani e2bb00cc0c Fix few missing indexes on get_smss_to_send 2021-09-17 21:50:44 +02:00
osaajani 48b2ba5684 Fix js 2021-08-29 05:37:52 +02:00
osaajani 54f3567714 few js fixes 2021-08-28 02:07:30 +02:00
osaajani 850923c1e6 Fix console command to update/create a user. 2021-08-05 17:14:53 +02:00
osaajani 85095ef995 add server side computing for event listing + fix ordering for some entities 2021-07-21 16:42:25 +02:00
osaajani 8e43c53498 Add serverside list for contacts 2021-07-19 21:14:56 +02:00
osaajani 3084288e5d remove return type on get model to make it more widely compatible 2021-07-19 20:56:44 +02:00
osaajani 169cbfde2d remove return type on get model to make it more widely compatible 2021-07-19 20:55:45 +02:00
osaajani e957c9feb7 use correct model and fix style 2021-07-19 17:32:23 +02:00
osaajani e21b89cc7c Fix smsstop list on non-admin 2021-07-19 17:03:31 +02:00
osaajani 231efc736b Fix smsstop list on non-admin 2021-07-19 17:02:37 +02:00
osaajani 8f3634b921 fix get model 2021-07-19 15:57:18 +02:00
osaajani 651c428ed7 move received listing to serverside processing 2021-07-16 22:53:33 +02:00
osaajani 170a00e30a Change sended list to use server side datatable 2021-07-14 04:38:52 +02:00
osaajani 30386f8caf Revert limit on sended list 2021-07-13 17:16:35 +02:00
osaajani 919b81bdf1 fix smsstop 2021-07-13 02:37:45 +02:00
osaajani 1f46b3ad57 fix smsstop 2021-07-13 02:36:22 +02:00
osaajani 8492da652a re-enable smsstops 2021-07-13 02:13:40 +02:00
osaajani 8f7868cae7 Limit to last 10k the sended sms to see 2021-07-13 01:36:22 +02:00
153 changed files with 11878 additions and 1821 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
.php_cs.cache
.credentials
.credentials*
.vscode/
vendor/
scripts/
composer.lock

View File

@ -3,3 +3,7 @@ RewriteRule ^assets - [L]
RewriteRule ^.well-known - [L]
RewriteRule ^data/public/ - [L]
RewriteRule . index.php
<IfModule headers_module>
Header always set Content-Security-Policy "upgrade-insecure-requests;"
</ifModule>

View File

@ -1 +1 @@
v3.1.6
v3.9.1

View File

@ -44,6 +44,12 @@ interface AdapterInterface
*/
public static function meta_hidden(): bool;
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool;
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
@ -73,6 +79,11 @@ interface AdapterInterface
*/
public static function meta_support_read(): bool;
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool;
/**
* Does the implemented service support reception callback.
*/
@ -113,9 +124,9 @@ interface AdapterInterface
* @param array $medias : Array of medias to link to the MMS, [['http_url' => HTTP public url of the media et 'local_uri' => local uri to media file]]
*
* @return array : [
* bool 'error' => false if no error, true else
* ?string 'error_message' => null if no error, else error message
* array 'uid' => Uid of the sms created on success
* bool 'error' => false if no error, true else,
* ?string 'error_message' => null if no error, else error message,
* array 'uid' => Uid of the sms created on success,
* ]
*/
public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array;
@ -146,6 +157,15 @@ interface AdapterInterface
*/
public function test(): bool;
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit', 'limit_reached'
*/
public function check_phone_status(): string;
/**
* Method called on reception of a status update notification for a SMS.
*
@ -187,7 +207,7 @@ interface AdapterInterface
* ]
* ]
*/
public function inbound_call_callback(): array;
public static function inbound_call_callback(): array;
/**
* Method called on reception of a end call notification.
@ -201,5 +221,5 @@ interface AdapterInterface
* ]
* ]
*/
public function end_call_callback(): array;
public static function end_call_callback(): array;
}

View File

@ -66,6 +66,15 @@ namespace adapters;
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
@ -102,6 +111,14 @@ namespace adapters;
return false;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
@ -172,6 +189,7 @@ namespace adapters;
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_POST, true);
@ -216,6 +234,16 @@ namespace adapters;
return [];
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public static function status_change_callback()
{
return null;
@ -231,12 +259,12 @@ namespace adapters;
return true;
}
public function inbound_call_callback(): array
public static function inbound_call_callback(): array
{
return [];
}
public function end_call_callback(): array
public static function end_call_callback(): array
{
return [];
}

View File

@ -61,6 +61,15 @@ namespace adapters;
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
@ -112,6 +121,14 @@ namespace adapters;
return true;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
@ -179,13 +196,14 @@ namespace adapters;
}
$command_parts = [
'LC_ALL=C',
'gammu',
'--config',
escapeshellarg($this->data['config_file']),
'sendsms',
'TEXT',
escapeshellarg($destination),
'-text',
'-textutf8',
escapeshellarg($text),
'-validity',
'MAX',
@ -262,6 +280,7 @@ namespace adapters;
$command_parts = [
PWD . '/bin/gammu_get_unread_sms.py',
escapeshellarg($this->data['config_file']),
'--delete'
];
$return = $this->exec_command($command_parts);
@ -291,6 +310,16 @@ namespace adapters;
return $response;
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public function test(): bool
{
//Always return true as we cannot test because we would be needing a root account
@ -307,12 +336,12 @@ namespace adapters;
return [];
}
public function inbound_call_callback(): array
public static function inbound_call_callback(): array
{
return [];
}
public function end_call_callback(): array
public static function end_call_callback(): array
{
return [];
}
@ -328,8 +357,11 @@ namespace adapters;
{
return true;
}
// The command returns 123 on failed execution (even if SIM is already unlocked), and returns 0 if unlock was successful
// We can directly return true if command was succesful
$command_parts = [
'LC_ALL=C',
'gammu',
'--config',
escapeshellarg($this->data['config_file']),
@ -339,9 +371,15 @@ namespace adapters;
];
$result = $this->exec_command($command_parts);
if (0 === $result['return'])
{
return true;
}
//Check security status
// The command returns 0 regardless of the SIM security state
$command_parts = [
'LC_ALL=C',
'gammu',
'--config',
escapeshellarg($this->data['config_file']),

520
adapters/KannelAdapter.php Normal file
View File

@ -0,0 +1,520 @@
<?php
/*
* This file is part of RaspiSMS.
*
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
*
* This source file is subject to the GPL-3.0 license that is bundled
* with this source code in the file LICENSE.
*/
namespace adapters;
use controllers\internals\Quota;
use controllers\internals\Tool;
/**
* Kannel adapter.
*/
class KannelAdapter implements AdapterInterface
{
const KANNEL_SENDSMS_RESULTS_ACCEPTED = 0;
const KANNEL_SENDSMS_RESULTS_QUEUED = 3;
const KANNEL_SENDSMS_HTTP_CODE_ACCEPTED = 202;
const KANNEL_SENDSMS_HTTP_CODE_QUEUED = 202;
const KANNEL_CODING_7_BITS = 0;
const KANNEL_CODING_8_BITS = 1;
const KANNEL_CODING_UCS_2 = 2;
/**
* DLR mask to transmit to kannel.
*
* 1 -> Delivered to phone
* 2 -> not delivered
* 16 -> non delivered to SMSC
*
* (see https://gist.github.com/grantpullen/3d550f31c454e80fda8fc0d5b9105fd0)
*/
const KANNEL_DLR_BITMASK = 1 + 2 + 16;
/**
* Data used to configure interaction with the implemented service. (e.g : Api credentials, ports numbers, etc.).
*/
private $data;
/**
* Kannel send-sms service url.
*/
private $kannel_sendsms_url;
/**
* Kannel send-sms username.
*/
private $username;
/**
* Kannel send-sms password.
*/
private $password;
/**
* Phone number of the sender, this number may or may not actually be overrided by the SMSC.
*/
private $from;
/**
* SMSC's id to use for sending the message.
*/
private $smsc;
/**
* SMS Delivery Report Url.
*/
private $dlr_url;
/**
* Adapter constructor, called when instanciated by RaspiSMS.
*
* @param string $number : Phone number the adapter is used for
* @param json string $data : JSON string of the data to configure interaction with the implemented service
*/
public function __construct(string $data)
{
$this->data = json_decode($data, true);
$this->kannel_sendsms_url = $this->data['kannel_sendsms_url'];
$this->username = $this->data['username'];
$this->password = $this->data['password'];
$this->from = $this->data['from'];
$this->dlr_url = $this->data['dlr_url'];
$this->smsc = $this->data['smsc'] ?? null;
}
/**
* Classname of the adapter.
*/
public static function meta_classname(): string
{
return __CLASS__;
}
/**
* Uniq name of the adapter
* It should be the classname of the adapter un snakecase.
*/
public static function meta_uid(): string
{
return 'kannel_adapter';
}
/**
* Should this adapter be hidden in user interface for phone creation and
* available to creation through API only.
*/
public static function meta_hidden(): bool
{
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
*/
public static function meta_name(): string
{
return 'Kannel';
}
/**
* Description of the adapter.
* A short description of the service the adapter implements.
*/
public static function meta_description(): string
{
$kannel_homepage = 'https://www.kannel.org';
return '
Envoi de SMS via le logiciel Kannel, pour plus d\'information sur Kannel, voir <a target="_blank" href="' . $kannel_homepage . '">le site du projet.</a><br/>
Pour plus d\'information sur l\'utilisation de ce type de téléphone, reportez-vous à <a href="https://documentation.raspisms.fr/users/adapters/kannel.html" target="_blank">la documentation sur le téléphone "Kannel".</a>
';
}
/**
* List of entries we want in data for the adapter.
*
* @return array : Every line is a field as an array with keys : name, title, description, required
*/
public static function meta_data_fields(): array
{
return [
[
'name' => 'kannel_sendsms_url',
'title' => 'Adresse URL du service kannel sendsms',
'description' => 'Adresse URL du service sendsms de Kannel (ex : http://smsbox.host.name:13013/cgi-bin/sendsms)',
'required' => true,
],
[
'name' => 'username',
'title' => 'Nom de l\'utilisateur',
'description' => 'Nom d\'utilisateur du service send-sms de Kannel.',
'required' => true,
],
[
'name' => 'password',
'title' => 'Mot de passe de l\'utilisateur',
'description' => 'Mot de passe de l\'utilisateur du service send-sms de Kannel.',
'required' => true,
],
[
'name' => 'from',
'title' => 'Numéro de téléphone émetteur ou nom de l\'émetteur',
'description' => 'Numéro de téléphone à transmettre au SMS Center, ou nom à afficher à la place du numéro (dans ce cas, entre 3 et 11 caractères), dans la très grande majorité des cas, ce numéro ou ce nom sera écrasé par le SMSC.',
'required' => true,
],
[
'name' => 'dlr_url',
'title' => 'Adresse URL de livraison du Delivery Report du SMS',
'description' => 'Adresse URL de livraison du Delivery Report du SMS qui sera transmis à Kannel. Vous devriez probablement laisser ce champs tel quel.',
'required' => true,
'default_value' => \descartes\Router::url('Callback', 'update_sended_status', ['adapter_uid' => self::meta_uid()], ['api_key' => $_SESSION['user']['api_key'] ?? '']),
],
[
'name' => 'smsc',
'title' => 'Identifiant unique du SMSC',
'description' => 'Identifiant du SMSC (sms-id) à utiliser pour envoyer le message.<br/>
<b>Laissez vide pour laisser Kannel décider du routage vers le SMSC.</b>',
'required' => false,
],
];
}
/**
* Does the implemented service support reading smss.
*/
public static function meta_support_read(): bool
{
return false;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
public static function meta_support_flash(): bool
{
return false;
}
/**
* Does the implemented service support status change.
*/
public static function meta_support_status_change(): bool
{
return true;
}
/**
* Does the implemented service support reception callback.
*/
public static function meta_support_reception(): bool
{
return true;
}
/**
* Does the implemented service support mms reception.
*/
public static function meta_support_mms_reception(): bool
{
return false;
}
/**
* Does the implemented service support mms sending.
*/
public static function meta_support_mms_sending(): bool
{
return false;
}
public static function meta_support_inbound_call_callback(): bool
{
return false;
}
public static function meta_support_end_call_callback(): bool
{
return false;
}
public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array
{
$response = [
'error' => false,
'error_message' => null,
'uid' => null,
];
try
{
//As kannel does not return uid of the SMS when sending it, we create our own uid and will pass it to kannel's delivery report url
//in order to retrieve it in raspisms and update the status
$sms_uid = Tool::random_uuid();
//Forge dlr Url by adding new query parts to url provided within phone settings
$dlr_url_parts = parse_url($this->dlr_url);
//Append sms uid and delivery report value to the original dlr_url query parts
$dlr_url_parts['query'] = $dlr_url_parts['query'] ?? '';
$dlr_url_query_parts = [];
parse_str($dlr_url_parts['query'], $dlr_url_query_parts);
unset($dlr_url_query_parts['type']);
$dlr_url_query_parts['sms_uid'] = $sms_uid; //Pass uid as param so raspisms can identify sms to update
$dlr_url_parts['query'] = http_build_query($dlr_url_query_parts) . '&type=%d'; //Kannel will replace %d by the delivery report value. We cannot set type in bild query or it get double encoded
$forged_dlr_url = Tool::unparse_url($dlr_url_parts);
$data = [
'username' => $this->username,
'password' => $this->password,
'text' => $text,
'to' => $destination,
'from' => $this->from,
'dlr-mask' => self::KANNEL_DLR_BITMASK,
'dlr-url' => $forged_dlr_url,
];
//If necessary, use utf8 sms to represent special chars
$use_utf8_sms = !Quota::is_gsm0338($text);
if ($use_utf8_sms)
{
$data['coding'] = self::KANNEL_CODING_8_BITS;
}
if ($this->smsc)
{
$data['smsc'] = $this->smsc;
}
$endpoint = $this->kannel_sendsms_url . '?' . http_build_query($data);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
$curl_response = curl_exec($curl);
$http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if (false === $curl_response)
{
$response['error'] = true;
$response['error_message'] = 'HTTP query failed.';
return $response;
}
if (!in_array($http_code, [self::KANNEL_SENDSMS_HTTP_CODE_ACCEPTED, self::KANNEL_SENDSMS_HTTP_CODE_QUEUED]))
{
$response['error'] = true;
$response['error_message'] = 'Response error with HTTP code : ' . $http_code . ' -> ' . $curl_response;
return $response;
}
$response['uid'] = $sms_uid;
return $response;
}
catch (\Throwable $t)
{
$response['error'] = true;
$response['error_message'] = $t->getMessage();
return $response;
}
}
public function read(): array
{
return [];
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public function test(): bool
{
try
{
if (!$this->username || !$this->password || !$this->from || !$this->dlr_url)
{
return false;
}
//Check kannel url is a valid http/https url to protect against ssrf
//This is mainly cosmetic, the real protection is in CURLOPT_PROTOCOLS
if (!mb_ereg_match('^http(s?)://', $this->kannel_sendsms_url))
{
return false;
}
//Check credentials and kannel url
$data = [
'username' => $this->username,
'password' => $this->password,
];
$endpoint = $this->kannel_sendsms_url . '?' . http_build_query($data);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); //Protect curl against non http(s) queries and redirects
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
$curl_response = curl_exec($curl);
$http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if (false === $curl_response)
{
return false;
}
switch (true)
{
case 403 == $http_code: //Bad credentials
case 404 == $http_code: //Cannot find url
return false;
case $http_code >= 500: //Server error
return false;
}
if (!filter_var($this->dlr_url, FILTER_VALIDATE_URL))
{
return false;
}
return true;
}
catch (\Throwable $t)
{
return false;
}
}
public static function status_change_callback()
{
$status = $_GET['type'] ?? false;
$uid = $_GET['sms_uid'] ?? false;
if (!$status || !$uid)
{
return false;
}
switch ((int) $status)
{
case 1:
$status = \models\Sended::STATUS_DELIVERED;
break;
case 2:
case 16:
$status = \models\Sended::STATUS_FAILED;
break;
default:
$status = \models\Sended::STATUS_UNKNOWN;
break;
}
return ['uid' => $uid, 'status' => $status];
}
public static function reception_callback(): array
{
$response = [
'error' => false,
'error_message' => null,
'sms' => null,
];
header('Connection: close');
header('Content-Encoding: none');
header('Content-Length: 0');
$text = file_get_contents('php://input');
$number = $_SERVER['HTTP_X_KANNEL_TO'] ?? false;
$at = $_SERVER['HTTP_X_KANNEL_TIME'] ?? false;
if (!$number || !$text || !$at)
{
$response['error'] = true;
$response['error_message'] = 'One required data of the callback is missing.';
return $response;
}
$origin = \controllers\internals\Tool::parse_phone($number);
if (!$origin)
{
$response['error'] = true;
$response['error_message'] = 'Invalid origin number : ' . $number;
return $response;
}
$response['sms'] = [
'at' => $at,
'text' => $text,
'origin' => $origin,
];
return $response;
}
public static function inbound_call_callback(): array
{
return [];
}
public static function end_call_callback(): array
{
return [];
}
}

View File

@ -38,9 +38,9 @@ class OctopushShortcodeAdapter implements AdapterInterface
* Sender name to use instead of shortcode.
*/
private $sender;
/**
* Octopush SMS type
* Octopush SMS type.
*/
private $sms_type;
@ -49,7 +49,6 @@ class OctopushShortcodeAdapter implements AdapterInterface
*/
private $api_url = 'https://api.octopush.com/v1/public';
/**
* Adapter constructor, called when instanciated by RaspiSMS.
*
@ -64,7 +63,7 @@ class OctopushShortcodeAdapter implements AdapterInterface
$this->api_key = $this->data['api_key'];
$this->sms_type = self::SMS_TYPE_LOWCOST;
if (($this->data['sms_type'] ?? false) && $this->data['sms_type'] === 'premium')
if (($this->data['sms_type'] ?? false) && 'premium' === $this->data['sms_type'])
{
$this->sms_type = self::SMS_TYPE_PREMIUM;
}
@ -98,6 +97,15 @@ class OctopushShortcodeAdapter implements AdapterInterface
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
@ -166,6 +174,14 @@ class OctopushShortcodeAdapter implements AdapterInterface
return false;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
@ -234,7 +250,7 @@ class OctopushShortcodeAdapter implements AdapterInterface
$data = [
'text' => $text,
'recipients' => [['phone_number' => $destination]],
'recipients' => [['phone_number' => $destination]],
'sms_type' => $this->sms_type,
'purpose' => 'alert',
];
@ -245,22 +261,22 @@ class OctopushShortcodeAdapter implements AdapterInterface
}
else
{
$data['with_replies'] = "True";
$data['with_replies'] = 'True';
}
$data = json_encode($data);
$endpoint = $this->api_url . '/sms-campaign/send';
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$curl_response = curl_exec($curl);
$http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
@ -317,6 +333,16 @@ class OctopushShortcodeAdapter implements AdapterInterface
return [];
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public function test(): bool
{
try
@ -332,7 +358,7 @@ class OctopushShortcodeAdapter implements AdapterInterface
{
return false;
}
$headers = [
'api-login: ' . $this->login,
'api-key: ' . $this->api_key,
@ -343,6 +369,7 @@ class OctopushShortcodeAdapter implements AdapterInterface
$endpoint = $this->api_url . '/wallet/check-balance';
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
@ -350,7 +377,7 @@ class OctopushShortcodeAdapter implements AdapterInterface
$http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($http_code !== 200)
if (200 !== $http_code)
{
return false;
}
@ -369,14 +396,13 @@ class OctopushShortcodeAdapter implements AdapterInterface
header('Content-Encoding: none');
header('Content-Length: 0');
$input = file_get_contents('php://input');
$content = json_decode($input, true);
if (null === $content)
{
return false;
}
$uid = $content['message_id'] ?? false;
$status = $content['status'] ?? false;
@ -385,7 +411,6 @@ class OctopushShortcodeAdapter implements AdapterInterface
return false;
}
switch ($status)
{
case 'DELIVERED':
@ -420,14 +445,14 @@ class OctopushShortcodeAdapter implements AdapterInterface
header('Connection: close');
header('Content-Encoding: none');
header('Content-Length: 0');
$input = file_get_contents('php://input');
$content = json_decode($input, true);
if (null === $content)
{
$response['error'] = true;
$response['error_message'] = 'Cannot read input data from callback request.';
return $response;
}
@ -461,12 +486,12 @@ class OctopushShortcodeAdapter implements AdapterInterface
return $response;
}
public function inbound_call_callback(): array
public static function inbound_call_callback(): array
{
return [];
}
public function end_call_callback(): array
public static function end_call_callback(): array
{
return [];
}

View File

@ -35,7 +35,7 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
private $api_key;
/**
* Octopush SMS type
* Octopush SMS type.
*/
private $sms_type;
@ -45,11 +45,10 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
private $api_url = 'https://api.octopush.com/v1/public';
/**
* Octopush phone number
* Octopush phone number.
*/
private $number;
/**
* Adapter constructor, called when instanciated by RaspiSMS.
*
@ -65,7 +64,7 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
$this->number = $this->data['number'];
$this->sms_type = self::SMS_TYPE_LOWCOST;
if (($this->data['sms_type'] ?? false) && $this->data['sms_type'] === 'premium')
if (($this->data['sms_type'] ?? false) && 'premium' === $this->data['sms_type'])
{
$this->sms_type = self::SMS_TYPE_PREMIUM;
}
@ -97,6 +96,16 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
@ -118,7 +127,6 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
Envoi de SMS avec un numéro virtuel en utilisant <a target="_blank" href="https://www.octopush.com/">Octopush</a>. Pour trouver vos clés API Octopush <a target="_blank" href="' . $credentials_url . '">cliquez ici.</a><br/>
Pour plus d\'information sur l\'utilisation de ce téléphone, reportez-vous à <a href="https://documentation.raspisms.fr/users/adapters/octopush_virtual_number.html" target="_blank">la documentation sur les téléphones "Octopush Numéro Virtuel".</a>
';
}
/**
@ -146,7 +154,7 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
'title' => 'Numéro de téléphone virtuel',
'description' => 'Numéro de téléphone virtuel Octopush à utiliser.',
'required' => true,
'number' => true,
'type' => 'phone_number',
],
[
'name' => 'sms_type',
@ -154,7 +162,7 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
'description' => 'Type de SMS à employer coté Octopush, rentrez "low cost" ou "premium" selon le type de SMS que vous souhaitez employer. Laissez vide pour utiliser par défaut des SMS low cost.',
'required' => false,
],
];
];
}
/**
@ -165,6 +173,14 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
return false;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
@ -233,26 +249,26 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
$data = [
'text' => $text,
'recipients' => [['phone_number' => $destination]],
'recipients' => [['phone_number' => $destination]],
'sms_type' => $this->sms_type,
'purpose' => 'alert',
'sender' => $this->number,
'with_replies' => "True",
'with_replies' => 'True',
];
$data = json_encode($data);
$endpoint = $this->api_url . '/sms-campaign/send';
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$curl_response = curl_exec($curl);
$http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
@ -309,6 +325,16 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
return [];
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public function test(): bool
{
try
@ -325,7 +351,7 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
{
return false;
}
$headers = [
'api-login: ' . $this->login,
'api-key: ' . $this->api_key,
@ -336,6 +362,7 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
$endpoint = $this->api_url . '/wallet/check-balance';
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
@ -343,7 +370,7 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
$http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($http_code !== 200)
if (200 !== $http_code)
{
return false;
}
@ -362,14 +389,13 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
header('Content-Encoding: none');
header('Content-Length: 0');
$input = file_get_contents('php://input');
$content = json_decode($input, true);
if (null === $content)
{
return false;
}
$uid = $content['message_id'] ?? false;
$status = $content['status'] ?? false;
@ -378,7 +404,6 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
return false;
}
switch ($status)
{
case 'DELIVERED':
@ -413,14 +438,14 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
header('Connection: close');
header('Content-Encoding: none');
header('Content-Length: 0');
$input = file_get_contents('php://input');
$content = json_decode($input, true);
if (null === $content)
{
$response['error'] = true;
$response['error_message'] = 'Cannot read input data from callback request.';
return $response;
}
@ -454,12 +479,12 @@ class OctopushVirtualNumberAdapter implements AdapterInterface
return $response;
}
public function inbound_call_callback(): array
public static function inbound_call_callback(): array
{
return [];
}
public function end_call_callback(): array
public static function end_call_callback(): array
{
return [];
}

View File

@ -0,0 +1,511 @@
<?php
/*
* This file is part of RaspiSMS.
*
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
*
* This source file is subject to the GPL-3.0 license that is bundled
* with this source code in the file LICENSE.
*/
namespace adapters;
use DateTime;
/**
* Odyssey Messaging SMS service
*/
class OdysseyMessagingAdapter implements AdapterInterface
{
const EVENT_TYPES = [
'OPT_OUT' => 1,
'SYSTEM_ERROR' => 2,
'END_OF_ITEM' => 3,
'END_OF_JOB' => 4,
'JOB_STATUS_CHANGED' => 5,
'REAL_TIME_STATUS' => 6,
'RETRIEVE_FILE' => 7,
'INBOUND_SMS' => 8,
'ITEM_STATUS_CHANGED' => 9,
'DATA_COLLECTION_FILLED' => 10,
];
/**
* Data used to configure interaction with the implemented service. (e.g : Api credentials, ports numbers, etc.).
*/
private $data;
/**
* Odyssey login.
*/
private $login;
/**
* Odyssey password.
*/
private $password;
/**
* Sender name to use instead of shortcode.
*/
private $sender;
/**
* Odyssey api baseurl.
*/
private $api_url = 'https://api.odyssey-services.fr/api/v1';
/**
* Adapter constructor, called when instanciated by RaspiSMS.
*
* @param json string $data : JSON string of the data to configure interaction with the implemented service
*/
public function __construct(string $data)
{
$this->data = json_decode($data, true);
$this->login = $this->data['login'];
$this->password = $this->data['password'];
$this->sender = $this->data['sender'] ?? null;
}
/**
* Classname of the adapter.
*/
public static function meta_classname(): string
{
return __CLASS__;
}
/**
* Uniq name of the adapter
* It should be the classname of the adapter un snakecase.
*/
public static function meta_uid(): string
{
return 'odyssey_messaging_adapter';
}
/**
* Should this adapter be hidden in user interface for phone creation and
* available to creation through API only.
*/
public static function meta_hidden(): bool
{
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
*/
public static function meta_name(): string
{
return 'Odyssey Messaging';
}
/**
* Description of the adapter.
* A short description of the service the adapter implements.
*/
public static function meta_description(): string
{
return '
Envoi de SMS avec <a target="_blank" href="https://www.odyssey-messaging.com/">Odyssey Messaging</a>.
Pour plus d\'information sur l\'utilisation de ce type de téléphone, reportez-vous à <a href="https://documentation.raspisms.fr/users/adapters/odyssey_messaging.html" target="_blank">la documentation sur le téléphone "Odyssey Messaging".</a>
';
}
/**
* List of entries we want in data for the adapter.
*
* @return array : Every line is a field as an array with keys : name, title, description, required
*/
public static function meta_data_fields(): array
{
return [
[
'name' => 'login',
'title' => 'Odyssey login',
'description' => 'Login du compte Odyssey à employer.',
'required' => true,
],
[
'name' => 'password',
'title' => 'Mot de passe',
'description' => 'Mot de passe du compte Odyssey à employer.',
'required' => true,
],
[
'name' => 'sender',
'title' => 'Nom de l\'expéditeur',
'description' => 'Nom de l\'expéditeur à afficher à la place du numéro (11 caractères max).<br/>
<b>Laissez vide pour ne pas utiliser d\'expéditeur nommé.</b><br/>
<b>Si vous utilisez un expéditeur nommé, le destinataire ne pourra pas répondre.</b>',
'required' => false,
],
];
}
/**
* Does the implemented service support reading smss.
*/
public static function meta_support_read(): bool
{
return false;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
public static function meta_support_flash(): bool
{
return false;
}
/**
* Does the implemented service support status change.
*/
public static function meta_support_status_change(): bool
{
return true;
}
/**
* Does the implemented service support reception callback.
*/
public static function meta_support_reception(): bool
{
return true;
}
/**
* Does the implemented service support mms reception.
*/
public static function meta_support_mms_reception(): bool
{
return false;
}
/**
* Does the implemented service support mms sending.
*/
public static function meta_support_mms_sending(): bool
{
return false;
}
public static function meta_support_inbound_call_callback(): bool
{
return false;
}
public static function meta_support_end_call_callback(): bool
{
return false;
}
public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array
{
$response = [
'error' => false,
'error_message' => null,
'uid' => null,
];
try
{
$credentials = base64_encode($this->login . ':' . $this->password);
$headers = [
'Authorization: Basic ' . $credentials,
'Content-Type: application/json',
];
$data = [
'JobType' => 'SMS',
'Text' => $text,
'TrackingID' => uniqid(),
'AdhocRecipients' => [['Name' => uniqid(), 'Address' => str_replace('+', '00', $destination)]],
];
if ($this->sender)
{
$data['Parameter'] = ['Sender' => $this->sender, 'Media' => 1];
}
$data = json_encode($data);
$endpoint = $this->api_url . '/SMSJobs';
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$curl_response = curl_exec($curl);
$http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if (false === $curl_response)
{
$response['error'] = true;
$response['error_message'] = 'HTTP query failed.';
return $response;
}
$response_decode = json_decode($curl_response, true);
if (null === $response_decode)
{
$response['error'] = true;
$response['error_message'] = 'Invalid JSON for response.';
return $response;
}
if (200 !== $http_code)
{
$response['error'] = true;
$response['error_message'] = 'Response indicate error : ' . $response_decode['Message'] . ' -> """' . json_encode($response_decode['ModelState']) . '""" AND HTTP CODE -> ' . $http_code;
return $response;
}
$uid = $response_decode['JobNumber'] ?? false;
if (!$uid)
{
$response['error'] = true;
$response['error_message'] = 'Cannot extract SMS uid';
return $response;
}
$response['uid'] = $uid;
return $response;
}
catch (\Throwable $t)
{
$response['error'] = true;
$response['error_message'] = $t->getMessage();
return $response;
}
}
public function read(): array
{
return [];
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public function test(): bool
{
try
{
if ($this->data['sender'] && (mb_strlen($this->data['sender']) < 3 || mb_strlen($this->data['sender'] > 11)))
{
return false;
}
if (!empty($this->data['sms_type']) && !in_array($this->data['sms_type'], ['premium', 'low cost']))
{
return false;
}
$credentials = base64_encode($this->login . ':' . $this->password);
$headers = [
'Authorization: Basic ' . $credentials,
'Content-Type: application/json',
];
//Check service name
$endpoint = $this->api_url . '/JobTypes';
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $endpoint);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($curl);
$http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if (200 !== $http_code)
{
return false;
}
return true;
}
catch (\Throwable $t)
{
return false;
}
}
public static function status_change_callback()
{
header('Connection: close');
header('Content-Encoding: none');
header('Content-Length: 0');
$input = file_get_contents('php://input');
$content = json_decode($input, true);
if (null === $content)
{
return false;
}
$event_type = $content['EventType'] ?? false;
if ($event_type != self::EVENT_TYPES['ITEM_STATUS_CHANGED'])
{
return false;
}
$uid = $content['JobNumber'] ?? false;
$status = $content['Outcome'] ?? false;
if (false === $uid || false === $status)
{
return false;
}
switch ($status)
{
case 'S':
$status = \models\Sended::STATUS_DELIVERED;
break;
case 'B':
$status = \models\Sended::STATUS_UNKNOWN;
break;
default:
$status = \models\Sended::STATUS_FAILED;
break;
}
return ['uid' => $uid, 'status' => $status];
}
public static function reception_callback(): array
{
$response = [
'error' => false,
'error_message' => null,
'sms' => null,
];
header('Connection: close');
header('Content-Encoding: none');
header('Content-Length: 0');
$input = file_get_contents('php://input');
$content = json_decode($input, true);
if (null === $content)
{
$response['error'] = true;
$response['error_message'] = 'Cannot read input data from callback request.';
return $response;
}
$event_type = $content['EventType'] ?? false;
if ($event_type != self::EVENT_TYPES['INBOUND_SMS'])
{
$response['error'] = true;
$response['error_message'] = 'Invalid event type : ' . $event_type . '.';
return $response;
}
$number = $content['From'] ?? false;
$text = $content['Message'] ?? false;
$at = $content['EventDateTime'] ?? false;
if (!$number || !$text || !$at)
{
$response['error'] = true;
$response['error_message'] = 'One required data of the callback is missing.';
return $response;
}
$matches = null;
$match = preg_match('#/Date\(([0-9]+)\+([0-9]+)\)/#', $at, $matches);
$timestamp = ($matches[1] ?? null);
if (!$match || !$timestamp)
{
$response['error'] = true;
$response['error_message'] = 'Invalid date.';
return $response;
}
$at = DateTime::createFromFormat('U', $timestamp / 1000);
$at = $at->format('Y-m-d H:i:s');
$origin = \controllers\internals\Tool::parse_phone($number);
if (!$origin)
{
$response['error'] = true;
$response['error_message'] = 'Invalid origin number : ' . $number;
return $response;
}
$response['sms'] = [
'at' => $at,
'text' => $text,
'origin' => $origin,
];
return $response;
}
public static function inbound_call_callback(): array
{
return [];
}
public static function end_call_callback(): array
{
return [];
}
}

View File

@ -72,6 +72,15 @@ namespace adapters;
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
@ -135,6 +144,13 @@ namespace adapters;
'description' => 'Paramètre "Consumer Key" obtenu lors de la génération de la clef API OVH.',
'required' => true,
],
[
'name' => 'no_stop_clause',
'title' => 'Désactiver la clause "STOP SMS" automatique',
'description' => 'En cochant ce paramètre, la clause "STOP SMS" ne sera pas ajoutée automatiquement au SMS par OVH.',
'required' => false,
'type' => 'boolean'
],
];
}
@ -146,6 +162,14 @@ namespace adapters;
return true;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
@ -213,6 +237,7 @@ namespace adapters;
'message' => $text,
'receivers' => [$destination],
'senderForResponse' => true,
'noStopClause' => (bool) ($this->data['no_stop_clause'] ?? false),
];
if ($this->data['sender'])
@ -310,13 +335,23 @@ namespace adapters;
}
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public function test(): bool
{
try
{
$success = true;
if ($this->data['sender'] && (mb_strlen($this->data['sender']) < 3 || mb_strlen($this->data['sender'] > 11)))
if ($this->data['sender'] && (mb_strlen($this->data['sender']) < 3 || mb_strlen($this->data['sender']) > 11))
{
return false;
}
@ -325,6 +360,7 @@ namespace adapters;
$endpoint = '/sms/' . $this->data['service_name'];
$response = $this->api->get($endpoint);
return $success && (bool) $response;
}
catch (\Throwable $t)
@ -370,12 +406,12 @@ namespace adapters;
return [];
}
public function inbound_call_callback(): array
public static function inbound_call_callback(): array
{
return [];
}
public function end_call_callback(): array
public static function end_call_callback(): array
{
return [];
}

View File

@ -84,6 +84,15 @@ namespace adapters;
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
@ -126,7 +135,7 @@ namespace adapters;
'title' => 'Numéro',
'description' => 'Numéro de téléphone virtuel chez OVH.',
'required' => true,
'number' => true,
'type' => 'phone_number',
],
[
'name' => 'app_key',
@ -157,6 +166,14 @@ namespace adapters;
return true;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
@ -308,6 +325,16 @@ namespace adapters;
}
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public function test(): bool
{
try
@ -369,12 +396,12 @@ namespace adapters;
return [];
}
public function inbound_call_callback(): array
public static function inbound_call_callback(): array
{
return [];
}
public function end_call_callback(): array
public static function end_call_callback(): array
{
return [];
}

View File

@ -71,6 +71,15 @@ namespace adapters;
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
@ -107,6 +116,14 @@ namespace adapters;
return true;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
@ -278,6 +295,16 @@ namespace adapters;
}
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public function test(): bool
{
return true;
@ -324,7 +351,7 @@ namespace adapters;
return [];
}
public function inbound_call_callback(): array
public static function inbound_call_callback(): array
{
$response = [
'error' => false,
@ -353,7 +380,7 @@ namespace adapters;
return $response;
}
public function end_call_callback(): array
public static function end_call_callback(): array
{
$response = [
'error' => false,

View File

@ -83,6 +83,15 @@ class TwilioVirtualNumberAdapter implements AdapterInterface
return false;
}
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials.
*/
public static function meta_hide_data(): bool
{
return false;
}
/**
* Name of the adapter.
* It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.).
@ -131,7 +140,7 @@ class TwilioVirtualNumberAdapter implements AdapterInterface
'title' => 'Numéro de téléphone virtuel',
'description' => 'Numéro de téléphone virtuel Twilio à utiliser parmis les numéro actifs (format international), <a href="https://www.twilio.com/console/phone-numbers/incoming" target="_blank">voir la liste ici</a>.',
'required' => true,
'number' => true,
'type' => 'phone_number',
],
[
'name' => 'status_change_callback',
@ -151,6 +160,14 @@ class TwilioVirtualNumberAdapter implements AdapterInterface
return true;
}
/**
* Does the implemented service support updating phone status.
*/
public static function meta_support_phone_status(): bool
{
return false;
}
/**
* Does the implemented service support flash smss.
*/
@ -286,6 +303,16 @@ class TwilioVirtualNumberAdapter implements AdapterInterface
}
}
/**
* Method called to verify phone status
*
* @return string : Return one phone status among 'available', 'unavailable', 'no_credit'
*/
public function check_phone_status(): string
{
return \models\Phone::STATUS_AVAILABLE;
}
public function test(): bool
{
try
@ -347,12 +374,12 @@ class TwilioVirtualNumberAdapter implements AdapterInterface
return [];
}
public function inbound_call_callback(): array
public static function inbound_call_callback(): array
{
return [];
}
public function end_call_callback(): array
public static function end_call_callback(): array
{
return [];
}

View File

@ -311,7 +311,8 @@ footer img
}
/* SCHEDULEDS */
.add-number-button
.add-number-button,
.add-phone-limit-button
{
display: inline-block;
color: #DADFE1;
@ -319,7 +320,8 @@ footer img
vertical-align: top;
}
.add-number-button:hover
.add-number-button:hover,
.add-phone-limit-button:hover
{
color: #3498DB;
cursor: pointer;
@ -373,6 +375,49 @@ footer img
text-align: right;
}
.scheduleds-number-groupe,
.phone-limits-group
{
padding-top: 15px;
padding-bottom: 15px;
background-color: #eee;
border-radius: 4px;
margin-bottom: 15px;
position: relative;
}
.scheduleds-number-groupe-remove,
.phone-limits-group-remove
{
position: absolute;
top: 15px;
right: 15px;
color: #888;
}
.scheduleds-number-groupe-remove:hover,
.phone-limits-group-remove:hover
{
color: #555;
}
.scheduleds-number-data-container .form-group:last-of-type
{
margin-bottom: 0;
}
.scheduled-number-data-name
{
width: 30%;
display: inline-block;
}
.scheduled-number-data-value
{
width: 65%;
display: inline-block;
}
/* AUDIO RECEPTION MESSAGE */
#reception-sound
{
@ -414,11 +459,28 @@ footer img
width: 50%;
}
.contact-data-container .contact-data-remove
.contact-data-container .contact-data-remove,
.scheduled-number-data-remove
{
color: #c9302c;
}
.contact-data-container .contact-data-remove:hover,
.scheduled-number-data-remove:hover
{
color: #9b2420;
}
/* PREVIEW CONTACT */
.preview-contact-name
{
font-weight: bold;
}
.preview-contact-number
{
font-style: italic;
}
/* PHONE */
#adapter-data-container
@ -437,6 +499,54 @@ footer img
padding-left: 15px;
}
#adapter-data-fields input[type="checkbox"]
{
display: block;
width: auto;
}
#adapter-data-fields input[type=checkbox]{
height: 0;
width: 0;
visibility: hidden;
}
#adapter-data-fields .switch {
cursor: pointer;
text-indent: -9999px;
width: 44px;
height: 24px;
background: #aaa;
display: block;
border-radius: 100px;
position: relative;
}
#adapter-data-fields .switch:after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: #fff;
border-radius: 20px;
transition: 0.3s;
}
#adapter-data-fields input:checked + .switch {
background: #5cb85c;
}
#adapter-data-fields input:checked + .switch:after {
left: calc(100% - 2px);
transform: translateX(-100%);
}
#adapter-data-fields .switch:active:after {
width: 30px;
}
/* DATATABLES */
.dataTables_paginate
{

View File

@ -1,13 +1,13 @@
/*!
* Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
/* FONT PATH
* -------------------------- */
@font-face {
font-family: 'FontAwesome';
src: url('../fonts/fontawesome-webfont.eot?v=4.2.0');
src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');
src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal;
font-style: normal;
}
@ -64,6 +64,19 @@
border: solid 0.08em #eeeeee;
border-radius: .1em;
}
.fa-pull-left {
float: left;
}
.fa-pull-right {
float: right;
}
.fa.fa-pull-left {
margin-right: .3em;
}
.fa.fa-pull-right {
margin-left: .3em;
}
/* Deprecated as of 4.4.0 */
.pull-right {
float: right;
}
@ -80,6 +93,10 @@
-webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear;
}
.fa-pulse {
-webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8);
}
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
@ -101,31 +118,31 @@
}
}
.fa-rotate-90 {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
.fa-rotate-180 {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg);
}
.fa-rotate-270 {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
-webkit-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg);
}
.fa-flip-horizontal {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
-webkit-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
transform: scale(-1, 1);
}
.fa-flip-vertical {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
-webkit-transform: scale(1, -1);
-ms-transform: scale(1, -1);
transform: scale(1, -1);
@ -610,6 +627,7 @@
.fa-twitter:before {
content: "\f099";
}
.fa-facebook-f:before,
.fa-facebook:before {
content: "\f09a";
}
@ -622,6 +640,7 @@
.fa-credit-card:before {
content: "\f09d";
}
.fa-feed:before,
.fa-rss:before {
content: "\f09e";
}
@ -1259,7 +1278,8 @@
.fa-male:before {
content: "\f183";
}
.fa-gittip:before {
.fa-gittip:before,
.fa-gratipay:before {
content: "\f184";
}
.fa-sun-o:before {
@ -1363,7 +1383,7 @@
.fa-digg:before {
content: "\f1a6";
}
.fa-pied-piper:before {
.fa-pied-piper-pp:before {
content: "\f1a7";
}
.fa-pied-piper-alt:before {
@ -1489,6 +1509,7 @@
content: "\f1ce";
}
.fa-ra:before,
.fa-resistance:before,
.fa-rebel:before {
content: "\f1d0";
}
@ -1502,6 +1523,8 @@
.fa-git:before {
content: "\f1d3";
}
.fa-y-combinator-square:before,
.fa-yc-square:before,
.fa-hacker-news:before {
content: "\f1d4";
}
@ -1670,3 +1693,645 @@
.fa-meanpath:before {
content: "\f20c";
}
.fa-buysellads:before {
content: "\f20d";
}
.fa-connectdevelop:before {
content: "\f20e";
}
.fa-dashcube:before {
content: "\f210";
}
.fa-forumbee:before {
content: "\f211";
}
.fa-leanpub:before {
content: "\f212";
}
.fa-sellsy:before {
content: "\f213";
}
.fa-shirtsinbulk:before {
content: "\f214";
}
.fa-simplybuilt:before {
content: "\f215";
}
.fa-skyatlas:before {
content: "\f216";
}
.fa-cart-plus:before {
content: "\f217";
}
.fa-cart-arrow-down:before {
content: "\f218";
}
.fa-diamond:before {
content: "\f219";
}
.fa-ship:before {
content: "\f21a";
}
.fa-user-secret:before {
content: "\f21b";
}
.fa-motorcycle:before {
content: "\f21c";
}
.fa-street-view:before {
content: "\f21d";
}
.fa-heartbeat:before {
content: "\f21e";
}
.fa-venus:before {
content: "\f221";
}
.fa-mars:before {
content: "\f222";
}
.fa-mercury:before {
content: "\f223";
}
.fa-intersex:before,
.fa-transgender:before {
content: "\f224";
}
.fa-transgender-alt:before {
content: "\f225";
}
.fa-venus-double:before {
content: "\f226";
}
.fa-mars-double:before {
content: "\f227";
}
.fa-venus-mars:before {
content: "\f228";
}
.fa-mars-stroke:before {
content: "\f229";
}
.fa-mars-stroke-v:before {
content: "\f22a";
}
.fa-mars-stroke-h:before {
content: "\f22b";
}
.fa-neuter:before {
content: "\f22c";
}
.fa-genderless:before {
content: "\f22d";
}
.fa-facebook-official:before {
content: "\f230";
}
.fa-pinterest-p:before {
content: "\f231";
}
.fa-whatsapp:before {
content: "\f232";
}
.fa-server:before {
content: "\f233";
}
.fa-user-plus:before {
content: "\f234";
}
.fa-user-times:before {
content: "\f235";
}
.fa-hotel:before,
.fa-bed:before {
content: "\f236";
}
.fa-viacoin:before {
content: "\f237";
}
.fa-train:before {
content: "\f238";
}
.fa-subway:before {
content: "\f239";
}
.fa-medium:before {
content: "\f23a";
}
.fa-yc:before,
.fa-y-combinator:before {
content: "\f23b";
}
.fa-optin-monster:before {
content: "\f23c";
}
.fa-opencart:before {
content: "\f23d";
}
.fa-expeditedssl:before {
content: "\f23e";
}
.fa-battery-4:before,
.fa-battery:before,
.fa-battery-full:before {
content: "\f240";
}
.fa-battery-3:before,
.fa-battery-three-quarters:before {
content: "\f241";
}
.fa-battery-2:before,
.fa-battery-half:before {
content: "\f242";
}
.fa-battery-1:before,
.fa-battery-quarter:before {
content: "\f243";
}
.fa-battery-0:before,
.fa-battery-empty:before {
content: "\f244";
}
.fa-mouse-pointer:before {
content: "\f245";
}
.fa-i-cursor:before {
content: "\f246";
}
.fa-object-group:before {
content: "\f247";
}
.fa-object-ungroup:before {
content: "\f248";
}
.fa-sticky-note:before {
content: "\f249";
}
.fa-sticky-note-o:before {
content: "\f24a";
}
.fa-cc-jcb:before {
content: "\f24b";
}
.fa-cc-diners-club:before {
content: "\f24c";
}
.fa-clone:before {
content: "\f24d";
}
.fa-balance-scale:before {
content: "\f24e";
}
.fa-hourglass-o:before {
content: "\f250";
}
.fa-hourglass-1:before,
.fa-hourglass-start:before {
content: "\f251";
}
.fa-hourglass-2:before,
.fa-hourglass-half:before {
content: "\f252";
}
.fa-hourglass-3:before,
.fa-hourglass-end:before {
content: "\f253";
}
.fa-hourglass:before {
content: "\f254";
}
.fa-hand-grab-o:before,
.fa-hand-rock-o:before {
content: "\f255";
}
.fa-hand-stop-o:before,
.fa-hand-paper-o:before {
content: "\f256";
}
.fa-hand-scissors-o:before {
content: "\f257";
}
.fa-hand-lizard-o:before {
content: "\f258";
}
.fa-hand-spock-o:before {
content: "\f259";
}
.fa-hand-pointer-o:before {
content: "\f25a";
}
.fa-hand-peace-o:before {
content: "\f25b";
}
.fa-trademark:before {
content: "\f25c";
}
.fa-registered:before {
content: "\f25d";
}
.fa-creative-commons:before {
content: "\f25e";
}
.fa-gg:before {
content: "\f260";
}
.fa-gg-circle:before {
content: "\f261";
}
.fa-tripadvisor:before {
content: "\f262";
}
.fa-odnoklassniki:before {
content: "\f263";
}
.fa-odnoklassniki-square:before {
content: "\f264";
}
.fa-get-pocket:before {
content: "\f265";
}
.fa-wikipedia-w:before {
content: "\f266";
}
.fa-safari:before {
content: "\f267";
}
.fa-chrome:before {
content: "\f268";
}
.fa-firefox:before {
content: "\f269";
}
.fa-opera:before {
content: "\f26a";
}
.fa-internet-explorer:before {
content: "\f26b";
}
.fa-tv:before,
.fa-television:before {
content: "\f26c";
}
.fa-contao:before {
content: "\f26d";
}
.fa-500px:before {
content: "\f26e";
}
.fa-amazon:before {
content: "\f270";
}
.fa-calendar-plus-o:before {
content: "\f271";
}
.fa-calendar-minus-o:before {
content: "\f272";
}
.fa-calendar-times-o:before {
content: "\f273";
}
.fa-calendar-check-o:before {
content: "\f274";
}
.fa-industry:before {
content: "\f275";
}
.fa-map-pin:before {
content: "\f276";
}
.fa-map-signs:before {
content: "\f277";
}
.fa-map-o:before {
content: "\f278";
}
.fa-map:before {
content: "\f279";
}
.fa-commenting:before {
content: "\f27a";
}
.fa-commenting-o:before {
content: "\f27b";
}
.fa-houzz:before {
content: "\f27c";
}
.fa-vimeo:before {
content: "\f27d";
}
.fa-black-tie:before {
content: "\f27e";
}
.fa-fonticons:before {
content: "\f280";
}
.fa-reddit-alien:before {
content: "\f281";
}
.fa-edge:before {
content: "\f282";
}
.fa-credit-card-alt:before {
content: "\f283";
}
.fa-codiepie:before {
content: "\f284";
}
.fa-modx:before {
content: "\f285";
}
.fa-fort-awesome:before {
content: "\f286";
}
.fa-usb:before {
content: "\f287";
}
.fa-product-hunt:before {
content: "\f288";
}
.fa-mixcloud:before {
content: "\f289";
}
.fa-scribd:before {
content: "\f28a";
}
.fa-pause-circle:before {
content: "\f28b";
}
.fa-pause-circle-o:before {
content: "\f28c";
}
.fa-stop-circle:before {
content: "\f28d";
}
.fa-stop-circle-o:before {
content: "\f28e";
}
.fa-shopping-bag:before {
content: "\f290";
}
.fa-shopping-basket:before {
content: "\f291";
}
.fa-hashtag:before {
content: "\f292";
}
.fa-bluetooth:before {
content: "\f293";
}
.fa-bluetooth-b:before {
content: "\f294";
}
.fa-percent:before {
content: "\f295";
}
.fa-gitlab:before {
content: "\f296";
}
.fa-wpbeginner:before {
content: "\f297";
}
.fa-wpforms:before {
content: "\f298";
}
.fa-envira:before {
content: "\f299";
}
.fa-universal-access:before {
content: "\f29a";
}
.fa-wheelchair-alt:before {
content: "\f29b";
}
.fa-question-circle-o:before {
content: "\f29c";
}
.fa-blind:before {
content: "\f29d";
}
.fa-audio-description:before {
content: "\f29e";
}
.fa-volume-control-phone:before {
content: "\f2a0";
}
.fa-braille:before {
content: "\f2a1";
}
.fa-assistive-listening-systems:before {
content: "\f2a2";
}
.fa-asl-interpreting:before,
.fa-american-sign-language-interpreting:before {
content: "\f2a3";
}
.fa-deafness:before,
.fa-hard-of-hearing:before,
.fa-deaf:before {
content: "\f2a4";
}
.fa-glide:before {
content: "\f2a5";
}
.fa-glide-g:before {
content: "\f2a6";
}
.fa-signing:before,
.fa-sign-language:before {
content: "\f2a7";
}
.fa-low-vision:before {
content: "\f2a8";
}
.fa-viadeo:before {
content: "\f2a9";
}
.fa-viadeo-square:before {
content: "\f2aa";
}
.fa-snapchat:before {
content: "\f2ab";
}
.fa-snapchat-ghost:before {
content: "\f2ac";
}
.fa-snapchat-square:before {
content: "\f2ad";
}
.fa-pied-piper:before {
content: "\f2ae";
}
.fa-first-order:before {
content: "\f2b0";
}
.fa-yoast:before {
content: "\f2b1";
}
.fa-themeisle:before {
content: "\f2b2";
}
.fa-google-plus-circle:before,
.fa-google-plus-official:before {
content: "\f2b3";
}
.fa-fa:before,
.fa-font-awesome:before {
content: "\f2b4";
}
.fa-handshake-o:before {
content: "\f2b5";
}
.fa-envelope-open:before {
content: "\f2b6";
}
.fa-envelope-open-o:before {
content: "\f2b7";
}
.fa-linode:before {
content: "\f2b8";
}
.fa-address-book:before {
content: "\f2b9";
}
.fa-address-book-o:before {
content: "\f2ba";
}
.fa-vcard:before,
.fa-address-card:before {
content: "\f2bb";
}
.fa-vcard-o:before,
.fa-address-card-o:before {
content: "\f2bc";
}
.fa-user-circle:before {
content: "\f2bd";
}
.fa-user-circle-o:before {
content: "\f2be";
}
.fa-user-o:before {
content: "\f2c0";
}
.fa-id-badge:before {
content: "\f2c1";
}
.fa-drivers-license:before,
.fa-id-card:before {
content: "\f2c2";
}
.fa-drivers-license-o:before,
.fa-id-card-o:before {
content: "\f2c3";
}
.fa-quora:before {
content: "\f2c4";
}
.fa-free-code-camp:before {
content: "\f2c5";
}
.fa-telegram:before {
content: "\f2c6";
}
.fa-thermometer-4:before,
.fa-thermometer:before,
.fa-thermometer-full:before {
content: "\f2c7";
}
.fa-thermometer-3:before,
.fa-thermometer-three-quarters:before {
content: "\f2c8";
}
.fa-thermometer-2:before,
.fa-thermometer-half:before {
content: "\f2c9";
}
.fa-thermometer-1:before,
.fa-thermometer-quarter:before {
content: "\f2ca";
}
.fa-thermometer-0:before,
.fa-thermometer-empty:before {
content: "\f2cb";
}
.fa-shower:before {
content: "\f2cc";
}
.fa-bathtub:before,
.fa-s15:before,
.fa-bath:before {
content: "\f2cd";
}
.fa-podcast:before {
content: "\f2ce";
}
.fa-window-maximize:before {
content: "\f2d0";
}
.fa-window-minimize:before {
content: "\f2d1";
}
.fa-window-restore:before {
content: "\f2d2";
}
.fa-times-rectangle:before,
.fa-window-close:before {
content: "\f2d3";
}
.fa-times-rectangle-o:before,
.fa-window-close-o:before {
content: "\f2d4";
}
.fa-bandcamp:before {
content: "\f2d5";
}
.fa-grav:before {
content: "\f2d6";
}
.fa-etsy:before {
content: "\f2d7";
}
.fa-imdb:before {
content: "\f2d8";
}
.fa-ravelry:before {
content: "\f2d9";
}
.fa-eercast:before {
content: "\f2da";
}
.fa-microchip:before {
content: "\f2db";
}
.fa-snowflake-o:before {
content: "\f2dc";
}
.fa-superpowers:before {
content: "\f2dd";
}
.fa-wpexplorer:before {
content: "\f2de";
}
.fa-meetup:before {
content: "\f2e0";
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.sr-only-focusable:active,
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -1,4 +1,4 @@
// Spinning Icons
// Animated Icons
// --------------------------
.@{fa-css-prefix}-spin {
@ -6,6 +6,11 @@
animation: fa-spin 2s infinite linear;
}
.@{fa-css-prefix}-pulse {
-webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8);
}
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);

View File

@ -7,6 +7,15 @@
border-radius: .1em;
}
.@{fa-css-prefix}-pull-left { float: left; }
.@{fa-css-prefix}-pull-right { float: right; }
.@{fa-css-prefix} {
&.@{fa-css-prefix}-pull-left { margin-right: .3em; }
&.@{fa-css-prefix}-pull-right { margin-left: .3em; }
}
/* Deprecated as of 4.4.0 */
.pull-right { float: right; }
.pull-left { float: left; }

View File

@ -3,9 +3,10 @@
.@{fa-css-prefix} {
display: inline-block;
font: normal normal normal 14px/1 FontAwesome; // shortening font declaration
font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -1,5 +1,5 @@
/*!
* Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
@ -11,7 +11,8 @@
@import "fixed-width.less";
@import "list.less";
@import "bordered-pulled.less";
@import "spinning.less";
@import "animated.less";
@import "rotated-flipped.less";
@import "stacked.less";
@import "icons.less";
@import "screen-reader.less";

View File

@ -158,10 +158,12 @@
.@{fa-css-prefix}-bookmark-o:before { content: @fa-var-bookmark-o; }
.@{fa-css-prefix}-phone-square:before { content: @fa-var-phone-square; }
.@{fa-css-prefix}-twitter:before { content: @fa-var-twitter; }
.@{fa-css-prefix}-facebook-f:before,
.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook; }
.@{fa-css-prefix}-github:before { content: @fa-var-github; }
.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; }
.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; }
.@{fa-css-prefix}-feed:before,
.@{fa-css-prefix}-rss:before { content: @fa-var-rss; }
.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; }
.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; }
@ -397,7 +399,8 @@
.@{fa-css-prefix}-trello:before { content: @fa-var-trello; }
.@{fa-css-prefix}-female:before { content: @fa-var-female; }
.@{fa-css-prefix}-male:before { content: @fa-var-male; }
.@{fa-css-prefix}-gittip:before { content: @fa-var-gittip; }
.@{fa-css-prefix}-gittip:before,
.@{fa-css-prefix}-gratipay:before { content: @fa-var-gratipay; }
.@{fa-css-prefix}-sun-o:before { content: @fa-var-sun-o; }
.@{fa-css-prefix}-moon-o:before { content: @fa-var-moon-o; }
.@{fa-css-prefix}-archive:before { content: @fa-var-archive; }
@ -435,7 +438,7 @@
.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; }
.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; }
.@{fa-css-prefix}-digg:before { content: @fa-var-digg; }
.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; }
.@{fa-css-prefix}-pied-piper-pp:before { content: @fa-var-pied-piper-pp; }
.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; }
.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; }
.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; }
@ -485,11 +488,14 @@
.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; }
.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; }
.@{fa-css-prefix}-ra:before,
.@{fa-css-prefix}-resistance:before,
.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; }
.@{fa-css-prefix}-ge:before,
.@{fa-css-prefix}-empire:before { content: @fa-var-empire; }
.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; }
.@{fa-css-prefix}-git:before { content: @fa-var-git; }
.@{fa-css-prefix}-y-combinator-square:before,
.@{fa-css-prefix}-yc-square:before,
.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; }
.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; }
.@{fa-css-prefix}-qq:before { content: @fa-var-qq; }
@ -550,3 +556,234 @@
.@{fa-css-prefix}-sheqel:before,
.@{fa-css-prefix}-ils:before { content: @fa-var-ils; }
.@{fa-css-prefix}-meanpath:before { content: @fa-var-meanpath; }
.@{fa-css-prefix}-buysellads:before { content: @fa-var-buysellads; }
.@{fa-css-prefix}-connectdevelop:before { content: @fa-var-connectdevelop; }
.@{fa-css-prefix}-dashcube:before { content: @fa-var-dashcube; }
.@{fa-css-prefix}-forumbee:before { content: @fa-var-forumbee; }
.@{fa-css-prefix}-leanpub:before { content: @fa-var-leanpub; }
.@{fa-css-prefix}-sellsy:before { content: @fa-var-sellsy; }
.@{fa-css-prefix}-shirtsinbulk:before { content: @fa-var-shirtsinbulk; }
.@{fa-css-prefix}-simplybuilt:before { content: @fa-var-simplybuilt; }
.@{fa-css-prefix}-skyatlas:before { content: @fa-var-skyatlas; }
.@{fa-css-prefix}-cart-plus:before { content: @fa-var-cart-plus; }
.@{fa-css-prefix}-cart-arrow-down:before { content: @fa-var-cart-arrow-down; }
.@{fa-css-prefix}-diamond:before { content: @fa-var-diamond; }
.@{fa-css-prefix}-ship:before { content: @fa-var-ship; }
.@{fa-css-prefix}-user-secret:before { content: @fa-var-user-secret; }
.@{fa-css-prefix}-motorcycle:before { content: @fa-var-motorcycle; }
.@{fa-css-prefix}-street-view:before { content: @fa-var-street-view; }
.@{fa-css-prefix}-heartbeat:before { content: @fa-var-heartbeat; }
.@{fa-css-prefix}-venus:before { content: @fa-var-venus; }
.@{fa-css-prefix}-mars:before { content: @fa-var-mars; }
.@{fa-css-prefix}-mercury:before { content: @fa-var-mercury; }
.@{fa-css-prefix}-intersex:before,
.@{fa-css-prefix}-transgender:before { content: @fa-var-transgender; }
.@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; }
.@{fa-css-prefix}-venus-double:before { content: @fa-var-venus-double; }
.@{fa-css-prefix}-mars-double:before { content: @fa-var-mars-double; }
.@{fa-css-prefix}-venus-mars:before { content: @fa-var-venus-mars; }
.@{fa-css-prefix}-mars-stroke:before { content: @fa-var-mars-stroke; }
.@{fa-css-prefix}-mars-stroke-v:before { content: @fa-var-mars-stroke-v; }
.@{fa-css-prefix}-mars-stroke-h:before { content: @fa-var-mars-stroke-h; }
.@{fa-css-prefix}-neuter:before { content: @fa-var-neuter; }
.@{fa-css-prefix}-genderless:before { content: @fa-var-genderless; }
.@{fa-css-prefix}-facebook-official:before { content: @fa-var-facebook-official; }
.@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; }
.@{fa-css-prefix}-whatsapp:before { content: @fa-var-whatsapp; }
.@{fa-css-prefix}-server:before { content: @fa-var-server; }
.@{fa-css-prefix}-user-plus:before { content: @fa-var-user-plus; }
.@{fa-css-prefix}-user-times:before { content: @fa-var-user-times; }
.@{fa-css-prefix}-hotel:before,
.@{fa-css-prefix}-bed:before { content: @fa-var-bed; }
.@{fa-css-prefix}-viacoin:before { content: @fa-var-viacoin; }
.@{fa-css-prefix}-train:before { content: @fa-var-train; }
.@{fa-css-prefix}-subway:before { content: @fa-var-subway; }
.@{fa-css-prefix}-medium:before { content: @fa-var-medium; }
.@{fa-css-prefix}-yc:before,
.@{fa-css-prefix}-y-combinator:before { content: @fa-var-y-combinator; }
.@{fa-css-prefix}-optin-monster:before { content: @fa-var-optin-monster; }
.@{fa-css-prefix}-opencart:before { content: @fa-var-opencart; }
.@{fa-css-prefix}-expeditedssl:before { content: @fa-var-expeditedssl; }
.@{fa-css-prefix}-battery-4:before,
.@{fa-css-prefix}-battery:before,
.@{fa-css-prefix}-battery-full:before { content: @fa-var-battery-full; }
.@{fa-css-prefix}-battery-3:before,
.@{fa-css-prefix}-battery-three-quarters:before { content: @fa-var-battery-three-quarters; }
.@{fa-css-prefix}-battery-2:before,
.@{fa-css-prefix}-battery-half:before { content: @fa-var-battery-half; }
.@{fa-css-prefix}-battery-1:before,
.@{fa-css-prefix}-battery-quarter:before { content: @fa-var-battery-quarter; }
.@{fa-css-prefix}-battery-0:before,
.@{fa-css-prefix}-battery-empty:before { content: @fa-var-battery-empty; }
.@{fa-css-prefix}-mouse-pointer:before { content: @fa-var-mouse-pointer; }
.@{fa-css-prefix}-i-cursor:before { content: @fa-var-i-cursor; }
.@{fa-css-prefix}-object-group:before { content: @fa-var-object-group; }
.@{fa-css-prefix}-object-ungroup:before { content: @fa-var-object-ungroup; }
.@{fa-css-prefix}-sticky-note:before { content: @fa-var-sticky-note; }
.@{fa-css-prefix}-sticky-note-o:before { content: @fa-var-sticky-note-o; }
.@{fa-css-prefix}-cc-jcb:before { content: @fa-var-cc-jcb; }
.@{fa-css-prefix}-cc-diners-club:before { content: @fa-var-cc-diners-club; }
.@{fa-css-prefix}-clone:before { content: @fa-var-clone; }
.@{fa-css-prefix}-balance-scale:before { content: @fa-var-balance-scale; }
.@{fa-css-prefix}-hourglass-o:before { content: @fa-var-hourglass-o; }
.@{fa-css-prefix}-hourglass-1:before,
.@{fa-css-prefix}-hourglass-start:before { content: @fa-var-hourglass-start; }
.@{fa-css-prefix}-hourglass-2:before,
.@{fa-css-prefix}-hourglass-half:before { content: @fa-var-hourglass-half; }
.@{fa-css-prefix}-hourglass-3:before,
.@{fa-css-prefix}-hourglass-end:before { content: @fa-var-hourglass-end; }
.@{fa-css-prefix}-hourglass:before { content: @fa-var-hourglass; }
.@{fa-css-prefix}-hand-grab-o:before,
.@{fa-css-prefix}-hand-rock-o:before { content: @fa-var-hand-rock-o; }
.@{fa-css-prefix}-hand-stop-o:before,
.@{fa-css-prefix}-hand-paper-o:before { content: @fa-var-hand-paper-o; }
.@{fa-css-prefix}-hand-scissors-o:before { content: @fa-var-hand-scissors-o; }
.@{fa-css-prefix}-hand-lizard-o:before { content: @fa-var-hand-lizard-o; }
.@{fa-css-prefix}-hand-spock-o:before { content: @fa-var-hand-spock-o; }
.@{fa-css-prefix}-hand-pointer-o:before { content: @fa-var-hand-pointer-o; }
.@{fa-css-prefix}-hand-peace-o:before { content: @fa-var-hand-peace-o; }
.@{fa-css-prefix}-trademark:before { content: @fa-var-trademark; }
.@{fa-css-prefix}-registered:before { content: @fa-var-registered; }
.@{fa-css-prefix}-creative-commons:before { content: @fa-var-creative-commons; }
.@{fa-css-prefix}-gg:before { content: @fa-var-gg; }
.@{fa-css-prefix}-gg-circle:before { content: @fa-var-gg-circle; }
.@{fa-css-prefix}-tripadvisor:before { content: @fa-var-tripadvisor; }
.@{fa-css-prefix}-odnoklassniki:before { content: @fa-var-odnoklassniki; }
.@{fa-css-prefix}-odnoklassniki-square:before { content: @fa-var-odnoklassniki-square; }
.@{fa-css-prefix}-get-pocket:before { content: @fa-var-get-pocket; }
.@{fa-css-prefix}-wikipedia-w:before { content: @fa-var-wikipedia-w; }
.@{fa-css-prefix}-safari:before { content: @fa-var-safari; }
.@{fa-css-prefix}-chrome:before { content: @fa-var-chrome; }
.@{fa-css-prefix}-firefox:before { content: @fa-var-firefox; }
.@{fa-css-prefix}-opera:before { content: @fa-var-opera; }
.@{fa-css-prefix}-internet-explorer:before { content: @fa-var-internet-explorer; }
.@{fa-css-prefix}-tv:before,
.@{fa-css-prefix}-television:before { content: @fa-var-television; }
.@{fa-css-prefix}-contao:before { content: @fa-var-contao; }
.@{fa-css-prefix}-500px:before { content: @fa-var-500px; }
.@{fa-css-prefix}-amazon:before { content: @fa-var-amazon; }
.@{fa-css-prefix}-calendar-plus-o:before { content: @fa-var-calendar-plus-o; }
.@{fa-css-prefix}-calendar-minus-o:before { content: @fa-var-calendar-minus-o; }
.@{fa-css-prefix}-calendar-times-o:before { content: @fa-var-calendar-times-o; }
.@{fa-css-prefix}-calendar-check-o:before { content: @fa-var-calendar-check-o; }
.@{fa-css-prefix}-industry:before { content: @fa-var-industry; }
.@{fa-css-prefix}-map-pin:before { content: @fa-var-map-pin; }
.@{fa-css-prefix}-map-signs:before { content: @fa-var-map-signs; }
.@{fa-css-prefix}-map-o:before { content: @fa-var-map-o; }
.@{fa-css-prefix}-map:before { content: @fa-var-map; }
.@{fa-css-prefix}-commenting:before { content: @fa-var-commenting; }
.@{fa-css-prefix}-commenting-o:before { content: @fa-var-commenting-o; }
.@{fa-css-prefix}-houzz:before { content: @fa-var-houzz; }
.@{fa-css-prefix}-vimeo:before { content: @fa-var-vimeo; }
.@{fa-css-prefix}-black-tie:before { content: @fa-var-black-tie; }
.@{fa-css-prefix}-fonticons:before { content: @fa-var-fonticons; }
.@{fa-css-prefix}-reddit-alien:before { content: @fa-var-reddit-alien; }
.@{fa-css-prefix}-edge:before { content: @fa-var-edge; }
.@{fa-css-prefix}-credit-card-alt:before { content: @fa-var-credit-card-alt; }
.@{fa-css-prefix}-codiepie:before { content: @fa-var-codiepie; }
.@{fa-css-prefix}-modx:before { content: @fa-var-modx; }
.@{fa-css-prefix}-fort-awesome:before { content: @fa-var-fort-awesome; }
.@{fa-css-prefix}-usb:before { content: @fa-var-usb; }
.@{fa-css-prefix}-product-hunt:before { content: @fa-var-product-hunt; }
.@{fa-css-prefix}-mixcloud:before { content: @fa-var-mixcloud; }
.@{fa-css-prefix}-scribd:before { content: @fa-var-scribd; }
.@{fa-css-prefix}-pause-circle:before { content: @fa-var-pause-circle; }
.@{fa-css-prefix}-pause-circle-o:before { content: @fa-var-pause-circle-o; }
.@{fa-css-prefix}-stop-circle:before { content: @fa-var-stop-circle; }
.@{fa-css-prefix}-stop-circle-o:before { content: @fa-var-stop-circle-o; }
.@{fa-css-prefix}-shopping-bag:before { content: @fa-var-shopping-bag; }
.@{fa-css-prefix}-shopping-basket:before { content: @fa-var-shopping-basket; }
.@{fa-css-prefix}-hashtag:before { content: @fa-var-hashtag; }
.@{fa-css-prefix}-bluetooth:before { content: @fa-var-bluetooth; }
.@{fa-css-prefix}-bluetooth-b:before { content: @fa-var-bluetooth-b; }
.@{fa-css-prefix}-percent:before { content: @fa-var-percent; }
.@{fa-css-prefix}-gitlab:before { content: @fa-var-gitlab; }
.@{fa-css-prefix}-wpbeginner:before { content: @fa-var-wpbeginner; }
.@{fa-css-prefix}-wpforms:before { content: @fa-var-wpforms; }
.@{fa-css-prefix}-envira:before { content: @fa-var-envira; }
.@{fa-css-prefix}-universal-access:before { content: @fa-var-universal-access; }
.@{fa-css-prefix}-wheelchair-alt:before { content: @fa-var-wheelchair-alt; }
.@{fa-css-prefix}-question-circle-o:before { content: @fa-var-question-circle-o; }
.@{fa-css-prefix}-blind:before { content: @fa-var-blind; }
.@{fa-css-prefix}-audio-description:before { content: @fa-var-audio-description; }
.@{fa-css-prefix}-volume-control-phone:before { content: @fa-var-volume-control-phone; }
.@{fa-css-prefix}-braille:before { content: @fa-var-braille; }
.@{fa-css-prefix}-assistive-listening-systems:before { content: @fa-var-assistive-listening-systems; }
.@{fa-css-prefix}-asl-interpreting:before,
.@{fa-css-prefix}-american-sign-language-interpreting:before { content: @fa-var-american-sign-language-interpreting; }
.@{fa-css-prefix}-deafness:before,
.@{fa-css-prefix}-hard-of-hearing:before,
.@{fa-css-prefix}-deaf:before { content: @fa-var-deaf; }
.@{fa-css-prefix}-glide:before { content: @fa-var-glide; }
.@{fa-css-prefix}-glide-g:before { content: @fa-var-glide-g; }
.@{fa-css-prefix}-signing:before,
.@{fa-css-prefix}-sign-language:before { content: @fa-var-sign-language; }
.@{fa-css-prefix}-low-vision:before { content: @fa-var-low-vision; }
.@{fa-css-prefix}-viadeo:before { content: @fa-var-viadeo; }
.@{fa-css-prefix}-viadeo-square:before { content: @fa-var-viadeo-square; }
.@{fa-css-prefix}-snapchat:before { content: @fa-var-snapchat; }
.@{fa-css-prefix}-snapchat-ghost:before { content: @fa-var-snapchat-ghost; }
.@{fa-css-prefix}-snapchat-square:before { content: @fa-var-snapchat-square; }
.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; }
.@{fa-css-prefix}-first-order:before { content: @fa-var-first-order; }
.@{fa-css-prefix}-yoast:before { content: @fa-var-yoast; }
.@{fa-css-prefix}-themeisle:before { content: @fa-var-themeisle; }
.@{fa-css-prefix}-google-plus-circle:before,
.@{fa-css-prefix}-google-plus-official:before { content: @fa-var-google-plus-official; }
.@{fa-css-prefix}-fa:before,
.@{fa-css-prefix}-font-awesome:before { content: @fa-var-font-awesome; }
.@{fa-css-prefix}-handshake-o:before { content: @fa-var-handshake-o; }
.@{fa-css-prefix}-envelope-open:before { content: @fa-var-envelope-open; }
.@{fa-css-prefix}-envelope-open-o:before { content: @fa-var-envelope-open-o; }
.@{fa-css-prefix}-linode:before { content: @fa-var-linode; }
.@{fa-css-prefix}-address-book:before { content: @fa-var-address-book; }
.@{fa-css-prefix}-address-book-o:before { content: @fa-var-address-book-o; }
.@{fa-css-prefix}-vcard:before,
.@{fa-css-prefix}-address-card:before { content: @fa-var-address-card; }
.@{fa-css-prefix}-vcard-o:before,
.@{fa-css-prefix}-address-card-o:before { content: @fa-var-address-card-o; }
.@{fa-css-prefix}-user-circle:before { content: @fa-var-user-circle; }
.@{fa-css-prefix}-user-circle-o:before { content: @fa-var-user-circle-o; }
.@{fa-css-prefix}-user-o:before { content: @fa-var-user-o; }
.@{fa-css-prefix}-id-badge:before { content: @fa-var-id-badge; }
.@{fa-css-prefix}-drivers-license:before,
.@{fa-css-prefix}-id-card:before { content: @fa-var-id-card; }
.@{fa-css-prefix}-drivers-license-o:before,
.@{fa-css-prefix}-id-card-o:before { content: @fa-var-id-card-o; }
.@{fa-css-prefix}-quora:before { content: @fa-var-quora; }
.@{fa-css-prefix}-free-code-camp:before { content: @fa-var-free-code-camp; }
.@{fa-css-prefix}-telegram:before { content: @fa-var-telegram; }
.@{fa-css-prefix}-thermometer-4:before,
.@{fa-css-prefix}-thermometer:before,
.@{fa-css-prefix}-thermometer-full:before { content: @fa-var-thermometer-full; }
.@{fa-css-prefix}-thermometer-3:before,
.@{fa-css-prefix}-thermometer-three-quarters:before { content: @fa-var-thermometer-three-quarters; }
.@{fa-css-prefix}-thermometer-2:before,
.@{fa-css-prefix}-thermometer-half:before { content: @fa-var-thermometer-half; }
.@{fa-css-prefix}-thermometer-1:before,
.@{fa-css-prefix}-thermometer-quarter:before { content: @fa-var-thermometer-quarter; }
.@{fa-css-prefix}-thermometer-0:before,
.@{fa-css-prefix}-thermometer-empty:before { content: @fa-var-thermometer-empty; }
.@{fa-css-prefix}-shower:before { content: @fa-var-shower; }
.@{fa-css-prefix}-bathtub:before,
.@{fa-css-prefix}-s15:before,
.@{fa-css-prefix}-bath:before { content: @fa-var-bath; }
.@{fa-css-prefix}-podcast:before { content: @fa-var-podcast; }
.@{fa-css-prefix}-window-maximize:before { content: @fa-var-window-maximize; }
.@{fa-css-prefix}-window-minimize:before { content: @fa-var-window-minimize; }
.@{fa-css-prefix}-window-restore:before { content: @fa-var-window-restore; }
.@{fa-css-prefix}-times-rectangle:before,
.@{fa-css-prefix}-window-close:before { content: @fa-var-window-close; }
.@{fa-css-prefix}-times-rectangle-o:before,
.@{fa-css-prefix}-window-close-o:before { content: @fa-var-window-close-o; }
.@{fa-css-prefix}-bandcamp:before { content: @fa-var-bandcamp; }
.@{fa-css-prefix}-grav:before { content: @fa-var-grav; }
.@{fa-css-prefix}-etsy:before { content: @fa-var-etsy; }
.@{fa-css-prefix}-imdb:before { content: @fa-var-imdb; }
.@{fa-css-prefix}-ravelry:before { content: @fa-var-ravelry; }
.@{fa-css-prefix}-eercast:before { content: @fa-var-eercast; }
.@{fa-css-prefix}-microchip:before { content: @fa-var-microchip; }
.@{fa-css-prefix}-snowflake-o:before { content: @fa-var-snowflake-o; }
.@{fa-css-prefix}-superpowers:before { content: @fa-var-superpowers; }
.@{fa-css-prefix}-wpexplorer:before { content: @fa-var-wpexplorer; }
.@{fa-css-prefix}-meetup:before { content: @fa-var-meetup; }

View File

@ -3,23 +3,58 @@
.fa-icon() {
display: inline-block;
font: normal normal normal 14px/1 FontAwesome; // shortening font declaration
font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.fa-icon-rotate(@degrees, @rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})";
-webkit-transform: rotate(@degrees);
-ms-transform: rotate(@degrees);
transform: rotate(@degrees);
}
.fa-icon-flip(@horiz, @vert, @rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)";
-webkit-transform: scale(@horiz, @vert);
-ms-transform: scale(@horiz, @vert);
transform: scale(@horiz, @vert);
}
// Only display content to screen readers. A la Bootstrap 4.
//
// See: http://a11yproject.com/posts/how-to-hide-content/
.sr-only() {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
// Use in conjunction with .sr-only to only display content when it's focused.
//
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
//
// Credit: HTML5 Boilerplate
.sr-only-focusable() {
&:active,
&:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
}

View File

@ -5,10 +5,11 @@
font-family: 'FontAwesome';
src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}');
src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'),
url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'),
url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'),
url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'),
url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg');
// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
font-weight: normal;
font-style: normal;
}

View File

@ -0,0 +1,5 @@
// Screen Readers
// -------------------------
.sr-only { .sr-only(); }
.sr-only-focusable { .sr-only-focusable(); }

View File

@ -2,20 +2,29 @@
// --------------------------
@fa-font-path: "../fonts";
//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts"; // for referencing Bootstrap CDN font files directly
@fa-font-size-base: 14px;
@fa-line-height-base: 1;
//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts"; // for referencing Bootstrap CDN font files directly
@fa-css-prefix: fa;
@fa-version: "4.2.0";
@fa-version: "4.7.0";
@fa-border-color: #eee;
@fa-inverse: #fff;
@fa-li-width: (30em / 14);
@fa-var-500px: "\f26e";
@fa-var-address-book: "\f2b9";
@fa-var-address-book-o: "\f2ba";
@fa-var-address-card: "\f2bb";
@fa-var-address-card-o: "\f2bc";
@fa-var-adjust: "\f042";
@fa-var-adn: "\f170";
@fa-var-align-center: "\f037";
@fa-var-align-justify: "\f039";
@fa-var-align-left: "\f036";
@fa-var-align-right: "\f038";
@fa-var-amazon: "\f270";
@fa-var-ambulance: "\f0f9";
@fa-var-american-sign-language-interpreting: "\f2a3";
@fa-var-anchor: "\f13d";
@fa-var-android: "\f17b";
@fa-var-angellist: "\f209";
@ -46,16 +55,35 @@
@fa-var-arrows-alt: "\f0b2";
@fa-var-arrows-h: "\f07e";
@fa-var-arrows-v: "\f07d";
@fa-var-asl-interpreting: "\f2a3";
@fa-var-assistive-listening-systems: "\f2a2";
@fa-var-asterisk: "\f069";
@fa-var-at: "\f1fa";
@fa-var-audio-description: "\f29e";
@fa-var-automobile: "\f1b9";
@fa-var-backward: "\f04a";
@fa-var-balance-scale: "\f24e";
@fa-var-ban: "\f05e";
@fa-var-bandcamp: "\f2d5";
@fa-var-bank: "\f19c";
@fa-var-bar-chart: "\f080";
@fa-var-bar-chart-o: "\f080";
@fa-var-barcode: "\f02a";
@fa-var-bars: "\f0c9";
@fa-var-bath: "\f2cd";
@fa-var-bathtub: "\f2cd";
@fa-var-battery: "\f240";
@fa-var-battery-0: "\f244";
@fa-var-battery-1: "\f243";
@fa-var-battery-2: "\f242";
@fa-var-battery-3: "\f241";
@fa-var-battery-4: "\f240";
@fa-var-battery-empty: "\f244";
@fa-var-battery-full: "\f240";
@fa-var-battery-half: "\f242";
@fa-var-battery-quarter: "\f243";
@fa-var-battery-three-quarters: "\f241";
@fa-var-bed: "\f236";
@fa-var-beer: "\f0fc";
@fa-var-behance: "\f1b4";
@fa-var-behance-square: "\f1b5";
@ -69,12 +97,17 @@
@fa-var-bitbucket: "\f171";
@fa-var-bitbucket-square: "\f172";
@fa-var-bitcoin: "\f15a";
@fa-var-black-tie: "\f27e";
@fa-var-blind: "\f29d";
@fa-var-bluetooth: "\f293";
@fa-var-bluetooth-b: "\f294";
@fa-var-bold: "\f032";
@fa-var-bolt: "\f0e7";
@fa-var-bomb: "\f1e2";
@fa-var-book: "\f02d";
@fa-var-bookmark: "\f02e";
@fa-var-bookmark-o: "\f097";
@fa-var-braille: "\f2a1";
@fa-var-briefcase: "\f0b1";
@fa-var-btc: "\f15a";
@fa-var-bug: "\f188";
@ -83,10 +116,15 @@
@fa-var-bullhorn: "\f0a1";
@fa-var-bullseye: "\f140";
@fa-var-bus: "\f207";
@fa-var-buysellads: "\f20d";
@fa-var-cab: "\f1ba";
@fa-var-calculator: "\f1ec";
@fa-var-calendar: "\f073";
@fa-var-calendar-check-o: "\f274";
@fa-var-calendar-minus-o: "\f272";
@fa-var-calendar-o: "\f133";
@fa-var-calendar-plus-o: "\f271";
@fa-var-calendar-times-o: "\f273";
@fa-var-camera: "\f030";
@fa-var-camera-retro: "\f083";
@fa-var-car: "\f1b9";
@ -98,9 +136,13 @@
@fa-var-caret-square-o-right: "\f152";
@fa-var-caret-square-o-up: "\f151";
@fa-var-caret-up: "\f0d8";
@fa-var-cart-arrow-down: "\f218";
@fa-var-cart-plus: "\f217";
@fa-var-cc: "\f20a";
@fa-var-cc-amex: "\f1f3";
@fa-var-cc-diners-club: "\f24c";
@fa-var-cc-discover: "\f1f2";
@fa-var-cc-jcb: "\f24b";
@fa-var-cc-mastercard: "\f1f1";
@fa-var-cc-paypal: "\f1f4";
@fa-var-cc-stripe: "\f1f5";
@ -122,12 +164,14 @@
@fa-var-chevron-right: "\f054";
@fa-var-chevron-up: "\f077";
@fa-var-child: "\f1ae";
@fa-var-chrome: "\f268";
@fa-var-circle: "\f111";
@fa-var-circle-o: "\f10c";
@fa-var-circle-o-notch: "\f1ce";
@fa-var-circle-thin: "\f1db";
@fa-var-clipboard: "\f0ea";
@fa-var-clock-o: "\f017";
@fa-var-clone: "\f24d";
@fa-var-close: "\f00d";
@fa-var-cloud: "\f0c2";
@fa-var-cloud-download: "\f0ed";
@ -136,19 +180,26 @@
@fa-var-code: "\f121";
@fa-var-code-fork: "\f126";
@fa-var-codepen: "\f1cb";
@fa-var-codiepie: "\f284";
@fa-var-coffee: "\f0f4";
@fa-var-cog: "\f013";
@fa-var-cogs: "\f085";
@fa-var-columns: "\f0db";
@fa-var-comment: "\f075";
@fa-var-comment-o: "\f0e5";
@fa-var-commenting: "\f27a";
@fa-var-commenting-o: "\f27b";
@fa-var-comments: "\f086";
@fa-var-comments-o: "\f0e6";
@fa-var-compass: "\f14e";
@fa-var-compress: "\f066";
@fa-var-connectdevelop: "\f20e";
@fa-var-contao: "\f26d";
@fa-var-copy: "\f0c5";
@fa-var-copyright: "\f1f9";
@fa-var-creative-commons: "\f25e";
@fa-var-credit-card: "\f09d";
@fa-var-credit-card-alt: "\f283";
@fa-var-crop: "\f125";
@fa-var-crosshairs: "\f05b";
@fa-var-css3: "\f13c";
@ -157,27 +208,39 @@
@fa-var-cut: "\f0c4";
@fa-var-cutlery: "\f0f5";
@fa-var-dashboard: "\f0e4";
@fa-var-dashcube: "\f210";
@fa-var-database: "\f1c0";
@fa-var-deaf: "\f2a4";
@fa-var-deafness: "\f2a4";
@fa-var-dedent: "\f03b";
@fa-var-delicious: "\f1a5";
@fa-var-desktop: "\f108";
@fa-var-deviantart: "\f1bd";
@fa-var-diamond: "\f219";
@fa-var-digg: "\f1a6";
@fa-var-dollar: "\f155";
@fa-var-dot-circle-o: "\f192";
@fa-var-download: "\f019";
@fa-var-dribbble: "\f17d";
@fa-var-drivers-license: "\f2c2";
@fa-var-drivers-license-o: "\f2c3";
@fa-var-dropbox: "\f16b";
@fa-var-drupal: "\f1a9";
@fa-var-edge: "\f282";
@fa-var-edit: "\f044";
@fa-var-eercast: "\f2da";
@fa-var-eject: "\f052";
@fa-var-ellipsis-h: "\f141";
@fa-var-ellipsis-v: "\f142";
@fa-var-empire: "\f1d1";
@fa-var-envelope: "\f0e0";
@fa-var-envelope-o: "\f003";
@fa-var-envelope-open: "\f2b6";
@fa-var-envelope-open-o: "\f2b7";
@fa-var-envelope-square: "\f199";
@fa-var-envira: "\f299";
@fa-var-eraser: "\f12d";
@fa-var-etsy: "\f2d7";
@fa-var-eur: "\f153";
@fa-var-euro: "\f153";
@fa-var-exchange: "\f0ec";
@ -185,16 +248,21 @@
@fa-var-exclamation-circle: "\f06a";
@fa-var-exclamation-triangle: "\f071";
@fa-var-expand: "\f065";
@fa-var-expeditedssl: "\f23e";
@fa-var-external-link: "\f08e";
@fa-var-external-link-square: "\f14c";
@fa-var-eye: "\f06e";
@fa-var-eye-slash: "\f070";
@fa-var-eyedropper: "\f1fb";
@fa-var-fa: "\f2b4";
@fa-var-facebook: "\f09a";
@fa-var-facebook-f: "\f09a";
@fa-var-facebook-official: "\f230";
@fa-var-facebook-square: "\f082";
@fa-var-fast-backward: "\f049";
@fa-var-fast-forward: "\f050";
@fa-var-fax: "\f1ac";
@fa-var-feed: "\f09e";
@fa-var-female: "\f182";
@fa-var-fighter-jet: "\f0fb";
@fa-var-file: "\f15b";
@ -220,6 +288,8 @@
@fa-var-filter: "\f0b0";
@fa-var-fire: "\f06d";
@fa-var-fire-extinguisher: "\f134";
@fa-var-firefox: "\f269";
@fa-var-first-order: "\f2b0";
@fa-var-flag: "\f024";
@fa-var-flag-checkered: "\f11e";
@fa-var-flag-o: "\f11d";
@ -232,8 +302,13 @@
@fa-var-folder-open: "\f07c";
@fa-var-folder-open-o: "\f115";
@fa-var-font: "\f031";
@fa-var-font-awesome: "\f2b4";
@fa-var-fonticons: "\f280";
@fa-var-fort-awesome: "\f286";
@fa-var-forumbee: "\f211";
@fa-var-forward: "\f04e";
@fa-var-foursquare: "\f180";
@fa-var-free-code-camp: "\f2c5";
@fa-var-frown-o: "\f119";
@fa-var-futbol-o: "\f1e3";
@fa-var-gamepad: "\f11b";
@ -242,45 +317,87 @@
@fa-var-ge: "\f1d1";
@fa-var-gear: "\f013";
@fa-var-gears: "\f085";
@fa-var-genderless: "\f22d";
@fa-var-get-pocket: "\f265";
@fa-var-gg: "\f260";
@fa-var-gg-circle: "\f261";
@fa-var-gift: "\f06b";
@fa-var-git: "\f1d3";
@fa-var-git-square: "\f1d2";
@fa-var-github: "\f09b";
@fa-var-github-alt: "\f113";
@fa-var-github-square: "\f092";
@fa-var-gitlab: "\f296";
@fa-var-gittip: "\f184";
@fa-var-glass: "\f000";
@fa-var-glide: "\f2a5";
@fa-var-glide-g: "\f2a6";
@fa-var-globe: "\f0ac";
@fa-var-google: "\f1a0";
@fa-var-google-plus: "\f0d5";
@fa-var-google-plus-circle: "\f2b3";
@fa-var-google-plus-official: "\f2b3";
@fa-var-google-plus-square: "\f0d4";
@fa-var-google-wallet: "\f1ee";
@fa-var-graduation-cap: "\f19d";
@fa-var-gratipay: "\f184";
@fa-var-grav: "\f2d6";
@fa-var-group: "\f0c0";
@fa-var-h-square: "\f0fd";
@fa-var-hacker-news: "\f1d4";
@fa-var-hand-grab-o: "\f255";
@fa-var-hand-lizard-o: "\f258";
@fa-var-hand-o-down: "\f0a7";
@fa-var-hand-o-left: "\f0a5";
@fa-var-hand-o-right: "\f0a4";
@fa-var-hand-o-up: "\f0a6";
@fa-var-hand-paper-o: "\f256";
@fa-var-hand-peace-o: "\f25b";
@fa-var-hand-pointer-o: "\f25a";
@fa-var-hand-rock-o: "\f255";
@fa-var-hand-scissors-o: "\f257";
@fa-var-hand-spock-o: "\f259";
@fa-var-hand-stop-o: "\f256";
@fa-var-handshake-o: "\f2b5";
@fa-var-hard-of-hearing: "\f2a4";
@fa-var-hashtag: "\f292";
@fa-var-hdd-o: "\f0a0";
@fa-var-header: "\f1dc";
@fa-var-headphones: "\f025";
@fa-var-heart: "\f004";
@fa-var-heart-o: "\f08a";
@fa-var-heartbeat: "\f21e";
@fa-var-history: "\f1da";
@fa-var-home: "\f015";
@fa-var-hospital-o: "\f0f8";
@fa-var-hotel: "\f236";
@fa-var-hourglass: "\f254";
@fa-var-hourglass-1: "\f251";
@fa-var-hourglass-2: "\f252";
@fa-var-hourglass-3: "\f253";
@fa-var-hourglass-end: "\f253";
@fa-var-hourglass-half: "\f252";
@fa-var-hourglass-o: "\f250";
@fa-var-hourglass-start: "\f251";
@fa-var-houzz: "\f27c";
@fa-var-html5: "\f13b";
@fa-var-i-cursor: "\f246";
@fa-var-id-badge: "\f2c1";
@fa-var-id-card: "\f2c2";
@fa-var-id-card-o: "\f2c3";
@fa-var-ils: "\f20b";
@fa-var-image: "\f03e";
@fa-var-imdb: "\f2d8";
@fa-var-inbox: "\f01c";
@fa-var-indent: "\f03c";
@fa-var-industry: "\f275";
@fa-var-info: "\f129";
@fa-var-info-circle: "\f05a";
@fa-var-inr: "\f156";
@fa-var-instagram: "\f16d";
@fa-var-institution: "\f19c";
@fa-var-internet-explorer: "\f26b";
@fa-var-intersex: "\f224";
@fa-var-ioxhost: "\f208";
@fa-var-italic: "\f033";
@fa-var-joomla: "\f1aa";
@ -294,6 +411,7 @@
@fa-var-lastfm: "\f202";
@fa-var-lastfm-square: "\f203";
@fa-var-leaf: "\f06c";
@fa-var-leanpub: "\f212";
@fa-var-legal: "\f0e3";
@fa-var-lemon-o: "\f094";
@fa-var-level-down: "\f149";
@ -307,6 +425,7 @@
@fa-var-link: "\f0c1";
@fa-var-linkedin: "\f0e1";
@fa-var-linkedin-square: "\f08c";
@fa-var-linode: "\f2b8";
@fa-var-linux: "\f17c";
@fa-var-list: "\f03a";
@fa-var-list-alt: "\f022";
@ -318,32 +437,58 @@
@fa-var-long-arrow-left: "\f177";
@fa-var-long-arrow-right: "\f178";
@fa-var-long-arrow-up: "\f176";
@fa-var-low-vision: "\f2a8";
@fa-var-magic: "\f0d0";
@fa-var-magnet: "\f076";
@fa-var-mail-forward: "\f064";
@fa-var-mail-reply: "\f112";
@fa-var-mail-reply-all: "\f122";
@fa-var-male: "\f183";
@fa-var-map: "\f279";
@fa-var-map-marker: "\f041";
@fa-var-map-o: "\f278";
@fa-var-map-pin: "\f276";
@fa-var-map-signs: "\f277";
@fa-var-mars: "\f222";
@fa-var-mars-double: "\f227";
@fa-var-mars-stroke: "\f229";
@fa-var-mars-stroke-h: "\f22b";
@fa-var-mars-stroke-v: "\f22a";
@fa-var-maxcdn: "\f136";
@fa-var-meanpath: "\f20c";
@fa-var-medium: "\f23a";
@fa-var-medkit: "\f0fa";
@fa-var-meetup: "\f2e0";
@fa-var-meh-o: "\f11a";
@fa-var-mercury: "\f223";
@fa-var-microchip: "\f2db";
@fa-var-microphone: "\f130";
@fa-var-microphone-slash: "\f131";
@fa-var-minus: "\f068";
@fa-var-minus-circle: "\f056";
@fa-var-minus-square: "\f146";
@fa-var-minus-square-o: "\f147";
@fa-var-mixcloud: "\f289";
@fa-var-mobile: "\f10b";
@fa-var-mobile-phone: "\f10b";
@fa-var-modx: "\f285";
@fa-var-money: "\f0d6";
@fa-var-moon-o: "\f186";
@fa-var-mortar-board: "\f19d";
@fa-var-motorcycle: "\f21c";
@fa-var-mouse-pointer: "\f245";
@fa-var-music: "\f001";
@fa-var-navicon: "\f0c9";
@fa-var-neuter: "\f22c";
@fa-var-newspaper-o: "\f1ea";
@fa-var-object-group: "\f247";
@fa-var-object-ungroup: "\f248";
@fa-var-odnoklassniki: "\f263";
@fa-var-odnoklassniki-square: "\f264";
@fa-var-opencart: "\f23d";
@fa-var-openid: "\f19b";
@fa-var-opera: "\f26a";
@fa-var-optin-monster: "\f23c";
@fa-var-outdent: "\f03b";
@fa-var-pagelines: "\f18c";
@fa-var-paint-brush: "\f1fc";
@ -353,19 +498,24 @@
@fa-var-paragraph: "\f1dd";
@fa-var-paste: "\f0ea";
@fa-var-pause: "\f04c";
@fa-var-pause-circle: "\f28b";
@fa-var-pause-circle-o: "\f28c";
@fa-var-paw: "\f1b0";
@fa-var-paypal: "\f1ed";
@fa-var-pencil: "\f040";
@fa-var-pencil-square: "\f14b";
@fa-var-pencil-square-o: "\f044";
@fa-var-percent: "\f295";
@fa-var-phone: "\f095";
@fa-var-phone-square: "\f098";
@fa-var-photo: "\f03e";
@fa-var-picture-o: "\f03e";
@fa-var-pie-chart: "\f200";
@fa-var-pied-piper: "\f1a7";
@fa-var-pied-piper: "\f2ae";
@fa-var-pied-piper-alt: "\f1a8";
@fa-var-pied-piper-pp: "\f1a7";
@fa-var-pinterest: "\f0d2";
@fa-var-pinterest-p: "\f231";
@fa-var-pinterest-square: "\f0d3";
@fa-var-plane: "\f072";
@fa-var-play: "\f04b";
@ -376,28 +526,36 @@
@fa-var-plus-circle: "\f055";
@fa-var-plus-square: "\f0fe";
@fa-var-plus-square-o: "\f196";
@fa-var-podcast: "\f2ce";
@fa-var-power-off: "\f011";
@fa-var-print: "\f02f";
@fa-var-product-hunt: "\f288";
@fa-var-puzzle-piece: "\f12e";
@fa-var-qq: "\f1d6";
@fa-var-qrcode: "\f029";
@fa-var-question: "\f128";
@fa-var-question-circle: "\f059";
@fa-var-question-circle-o: "\f29c";
@fa-var-quora: "\f2c4";
@fa-var-quote-left: "\f10d";
@fa-var-quote-right: "\f10e";
@fa-var-ra: "\f1d0";
@fa-var-random: "\f074";
@fa-var-ravelry: "\f2d9";
@fa-var-rebel: "\f1d0";
@fa-var-recycle: "\f1b8";
@fa-var-reddit: "\f1a1";
@fa-var-reddit-alien: "\f281";
@fa-var-reddit-square: "\f1a2";
@fa-var-refresh: "\f021";
@fa-var-registered: "\f25d";
@fa-var-remove: "\f00d";
@fa-var-renren: "\f18b";
@fa-var-reorder: "\f0c9";
@fa-var-repeat: "\f01e";
@fa-var-reply: "\f112";
@fa-var-reply-all: "\f122";
@fa-var-resistance: "\f1d0";
@fa-var-retweet: "\f079";
@fa-var-rmb: "\f157";
@fa-var-road: "\f018";
@ -410,13 +568,18 @@
@fa-var-rub: "\f158";
@fa-var-ruble: "\f158";
@fa-var-rupee: "\f156";
@fa-var-s15: "\f2cd";
@fa-var-safari: "\f267";
@fa-var-save: "\f0c7";
@fa-var-scissors: "\f0c4";
@fa-var-scribd: "\f28a";
@fa-var-search: "\f002";
@fa-var-search-minus: "\f010";
@fa-var-search-plus: "\f00e";
@fa-var-sellsy: "\f213";
@fa-var-send: "\f1d8";
@fa-var-send-o: "\f1d9";
@fa-var-server: "\f233";
@fa-var-share: "\f064";
@fa-var-share-alt: "\f1e0";
@fa-var-share-alt-square: "\f1e1";
@ -425,16 +588,29 @@
@fa-var-shekel: "\f20b";
@fa-var-sheqel: "\f20b";
@fa-var-shield: "\f132";
@fa-var-ship: "\f21a";
@fa-var-shirtsinbulk: "\f214";
@fa-var-shopping-bag: "\f290";
@fa-var-shopping-basket: "\f291";
@fa-var-shopping-cart: "\f07a";
@fa-var-shower: "\f2cc";
@fa-var-sign-in: "\f090";
@fa-var-sign-language: "\f2a7";
@fa-var-sign-out: "\f08b";
@fa-var-signal: "\f012";
@fa-var-signing: "\f2a7";
@fa-var-simplybuilt: "\f215";
@fa-var-sitemap: "\f0e8";
@fa-var-skyatlas: "\f216";
@fa-var-skype: "\f17e";
@fa-var-slack: "\f198";
@fa-var-sliders: "\f1de";
@fa-var-slideshare: "\f1e7";
@fa-var-smile-o: "\f118";
@fa-var-snapchat: "\f2ab";
@fa-var-snapchat-ghost: "\f2ac";
@fa-var-snapchat-square: "\f2ad";
@fa-var-snowflake-o: "\f2dc";
@fa-var-soccer-ball-o: "\f1e3";
@fa-var-sort: "\f0dc";
@fa-var-sort-alpha-asc: "\f15d";
@ -467,13 +643,20 @@
@fa-var-step-backward: "\f048";
@fa-var-step-forward: "\f051";
@fa-var-stethoscope: "\f0f1";
@fa-var-sticky-note: "\f249";
@fa-var-sticky-note-o: "\f24a";
@fa-var-stop: "\f04d";
@fa-var-stop-circle: "\f28d";
@fa-var-stop-circle-o: "\f28e";
@fa-var-street-view: "\f21d";
@fa-var-strikethrough: "\f0cc";
@fa-var-stumbleupon: "\f1a4";
@fa-var-stumbleupon-circle: "\f1a3";
@fa-var-subscript: "\f12c";
@fa-var-subway: "\f239";
@fa-var-suitcase: "\f0f2";
@fa-var-sun-o: "\f185";
@fa-var-superpowers: "\f2dd";
@fa-var-superscript: "\f12b";
@fa-var-support: "\f1cd";
@fa-var-table: "\f0ce";
@ -483,6 +666,8 @@
@fa-var-tags: "\f02c";
@fa-var-tasks: "\f0ae";
@fa-var-taxi: "\f1ba";
@fa-var-telegram: "\f2c6";
@fa-var-television: "\f26c";
@fa-var-tencent-weibo: "\f1d5";
@fa-var-terminal: "\f120";
@fa-var-text-height: "\f034";
@ -490,6 +675,18 @@
@fa-var-th: "\f00a";
@fa-var-th-large: "\f009";
@fa-var-th-list: "\f00b";
@fa-var-themeisle: "\f2b2";
@fa-var-thermometer: "\f2c7";
@fa-var-thermometer-0: "\f2cb";
@fa-var-thermometer-1: "\f2ca";
@fa-var-thermometer-2: "\f2c9";
@fa-var-thermometer-3: "\f2c8";
@fa-var-thermometer-4: "\f2c7";
@fa-var-thermometer-empty: "\f2cb";
@fa-var-thermometer-full: "\f2c7";
@fa-var-thermometer-half: "\f2c9";
@fa-var-thermometer-quarter: "\f2ca";
@fa-var-thermometer-three-quarters: "\f2c8";
@fa-var-thumb-tack: "\f08d";
@fa-var-thumbs-down: "\f165";
@fa-var-thumbs-o-down: "\f088";
@ -499,6 +696,8 @@
@fa-var-times: "\f00d";
@fa-var-times-circle: "\f057";
@fa-var-times-circle-o: "\f05c";
@fa-var-times-rectangle: "\f2d3";
@fa-var-times-rectangle-o: "\f2d4";
@fa-var-tint: "\f043";
@fa-var-toggle-down: "\f150";
@fa-var-toggle-left: "\f191";
@ -506,10 +705,15 @@
@fa-var-toggle-on: "\f205";
@fa-var-toggle-right: "\f152";
@fa-var-toggle-up: "\f151";
@fa-var-trademark: "\f25c";
@fa-var-train: "\f238";
@fa-var-transgender: "\f224";
@fa-var-transgender-alt: "\f225";
@fa-var-trash: "\f1f8";
@fa-var-trash-o: "\f014";
@fa-var-tree: "\f1bb";
@fa-var-trello: "\f181";
@fa-var-tripadvisor: "\f262";
@fa-var-trophy: "\f091";
@fa-var-truck: "\f0d1";
@fa-var-try: "\f195";
@ -517,26 +721,45 @@
@fa-var-tumblr: "\f173";
@fa-var-tumblr-square: "\f174";
@fa-var-turkish-lira: "\f195";
@fa-var-tv: "\f26c";
@fa-var-twitch: "\f1e8";
@fa-var-twitter: "\f099";
@fa-var-twitter-square: "\f081";
@fa-var-umbrella: "\f0e9";
@fa-var-underline: "\f0cd";
@fa-var-undo: "\f0e2";
@fa-var-universal-access: "\f29a";
@fa-var-university: "\f19c";
@fa-var-unlink: "\f127";
@fa-var-unlock: "\f09c";
@fa-var-unlock-alt: "\f13e";
@fa-var-unsorted: "\f0dc";
@fa-var-upload: "\f093";
@fa-var-usb: "\f287";
@fa-var-usd: "\f155";
@fa-var-user: "\f007";
@fa-var-user-circle: "\f2bd";
@fa-var-user-circle-o: "\f2be";
@fa-var-user-md: "\f0f0";
@fa-var-user-o: "\f2c0";
@fa-var-user-plus: "\f234";
@fa-var-user-secret: "\f21b";
@fa-var-user-times: "\f235";
@fa-var-users: "\f0c0";
@fa-var-vcard: "\f2bb";
@fa-var-vcard-o: "\f2bc";
@fa-var-venus: "\f221";
@fa-var-venus-double: "\f226";
@fa-var-venus-mars: "\f228";
@fa-var-viacoin: "\f237";
@fa-var-viadeo: "\f2a9";
@fa-var-viadeo-square: "\f2aa";
@fa-var-video-camera: "\f03d";
@fa-var-vimeo: "\f27d";
@fa-var-vimeo-square: "\f194";
@fa-var-vine: "\f1ca";
@fa-var-vk: "\f189";
@fa-var-volume-control-phone: "\f2a0";
@fa-var-volume-down: "\f027";
@fa-var-volume-off: "\f026";
@fa-var-volume-up: "\f028";
@ -544,17 +767,33 @@
@fa-var-wechat: "\f1d7";
@fa-var-weibo: "\f18a";
@fa-var-weixin: "\f1d7";
@fa-var-whatsapp: "\f232";
@fa-var-wheelchair: "\f193";
@fa-var-wheelchair-alt: "\f29b";
@fa-var-wifi: "\f1eb";
@fa-var-wikipedia-w: "\f266";
@fa-var-window-close: "\f2d3";
@fa-var-window-close-o: "\f2d4";
@fa-var-window-maximize: "\f2d0";
@fa-var-window-minimize: "\f2d1";
@fa-var-window-restore: "\f2d2";
@fa-var-windows: "\f17a";
@fa-var-won: "\f159";
@fa-var-wordpress: "\f19a";
@fa-var-wpbeginner: "\f297";
@fa-var-wpexplorer: "\f2de";
@fa-var-wpforms: "\f298";
@fa-var-wrench: "\f0ad";
@fa-var-xing: "\f168";
@fa-var-xing-square: "\f169";
@fa-var-y-combinator: "\f23b";
@fa-var-y-combinator-square: "\f1d4";
@fa-var-yahoo: "\f19e";
@fa-var-yc: "\f23b";
@fa-var-yc-square: "\f1d4";
@fa-var-yelp: "\f1e9";
@fa-var-yen: "\f157";
@fa-var-yoast: "\f2b1";
@fa-var-youtube: "\f167";
@fa-var-youtube-play: "\f16a";
@fa-var-youtube-square: "\f166";

View File

@ -6,6 +6,11 @@
animation: fa-spin 2s infinite linear;
}
.#{$fa-css-prefix}-pulse {
-webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8);
}
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);

View File

@ -7,6 +7,15 @@
border-radius: .1em;
}
.#{$fa-css-prefix}-pull-left { float: left; }
.#{$fa-css-prefix}-pull-right { float: right; }
.#{$fa-css-prefix} {
&.#{$fa-css-prefix}-pull-left { margin-right: .3em; }
&.#{$fa-css-prefix}-pull-right { margin-left: .3em; }
}
/* Deprecated as of 4.4.0 */
.pull-right { float: right; }
.pull-left { float: left; }

View File

@ -3,9 +3,10 @@
.#{$fa-css-prefix} {
display: inline-block;
font: normal normal normal 14px/1 FontAwesome; // shortening font declaration
font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -158,10 +158,12 @@
.#{$fa-css-prefix}-bookmark-o:before { content: $fa-var-bookmark-o; }
.#{$fa-css-prefix}-phone-square:before { content: $fa-var-phone-square; }
.#{$fa-css-prefix}-twitter:before { content: $fa-var-twitter; }
.#{$fa-css-prefix}-facebook-f:before,
.#{$fa-css-prefix}-facebook:before { content: $fa-var-facebook; }
.#{$fa-css-prefix}-github:before { content: $fa-var-github; }
.#{$fa-css-prefix}-unlock:before { content: $fa-var-unlock; }
.#{$fa-css-prefix}-credit-card:before { content: $fa-var-credit-card; }
.#{$fa-css-prefix}-feed:before,
.#{$fa-css-prefix}-rss:before { content: $fa-var-rss; }
.#{$fa-css-prefix}-hdd-o:before { content: $fa-var-hdd-o; }
.#{$fa-css-prefix}-bullhorn:before { content: $fa-var-bullhorn; }
@ -397,7 +399,8 @@
.#{$fa-css-prefix}-trello:before { content: $fa-var-trello; }
.#{$fa-css-prefix}-female:before { content: $fa-var-female; }
.#{$fa-css-prefix}-male:before { content: $fa-var-male; }
.#{$fa-css-prefix}-gittip:before { content: $fa-var-gittip; }
.#{$fa-css-prefix}-gittip:before,
.#{$fa-css-prefix}-gratipay:before { content: $fa-var-gratipay; }
.#{$fa-css-prefix}-sun-o:before { content: $fa-var-sun-o; }
.#{$fa-css-prefix}-moon-o:before { content: $fa-var-moon-o; }
.#{$fa-css-prefix}-archive:before { content: $fa-var-archive; }
@ -435,7 +438,7 @@
.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; }
.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; }
.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; }
.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; }
.#{$fa-css-prefix}-pied-piper-pp:before { content: $fa-var-pied-piper-pp; }
.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; }
.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; }
.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; }
@ -485,11 +488,14 @@
.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; }
.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; }
.#{$fa-css-prefix}-ra:before,
.#{$fa-css-prefix}-resistance:before,
.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; }
.#{$fa-css-prefix}-ge:before,
.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; }
.#{$fa-css-prefix}-git-square:before { content: $fa-var-git-square; }
.#{$fa-css-prefix}-git:before { content: $fa-var-git; }
.#{$fa-css-prefix}-y-combinator-square:before,
.#{$fa-css-prefix}-yc-square:before,
.#{$fa-css-prefix}-hacker-news:before { content: $fa-var-hacker-news; }
.#{$fa-css-prefix}-tencent-weibo:before { content: $fa-var-tencent-weibo; }
.#{$fa-css-prefix}-qq:before { content: $fa-var-qq; }
@ -550,3 +556,234 @@
.#{$fa-css-prefix}-sheqel:before,
.#{$fa-css-prefix}-ils:before { content: $fa-var-ils; }
.#{$fa-css-prefix}-meanpath:before { content: $fa-var-meanpath; }
.#{$fa-css-prefix}-buysellads:before { content: $fa-var-buysellads; }
.#{$fa-css-prefix}-connectdevelop:before { content: $fa-var-connectdevelop; }
.#{$fa-css-prefix}-dashcube:before { content: $fa-var-dashcube; }
.#{$fa-css-prefix}-forumbee:before { content: $fa-var-forumbee; }
.#{$fa-css-prefix}-leanpub:before { content: $fa-var-leanpub; }
.#{$fa-css-prefix}-sellsy:before { content: $fa-var-sellsy; }
.#{$fa-css-prefix}-shirtsinbulk:before { content: $fa-var-shirtsinbulk; }
.#{$fa-css-prefix}-simplybuilt:before { content: $fa-var-simplybuilt; }
.#{$fa-css-prefix}-skyatlas:before { content: $fa-var-skyatlas; }
.#{$fa-css-prefix}-cart-plus:before { content: $fa-var-cart-plus; }
.#{$fa-css-prefix}-cart-arrow-down:before { content: $fa-var-cart-arrow-down; }
.#{$fa-css-prefix}-diamond:before { content: $fa-var-diamond; }
.#{$fa-css-prefix}-ship:before { content: $fa-var-ship; }
.#{$fa-css-prefix}-user-secret:before { content: $fa-var-user-secret; }
.#{$fa-css-prefix}-motorcycle:before { content: $fa-var-motorcycle; }
.#{$fa-css-prefix}-street-view:before { content: $fa-var-street-view; }
.#{$fa-css-prefix}-heartbeat:before { content: $fa-var-heartbeat; }
.#{$fa-css-prefix}-venus:before { content: $fa-var-venus; }
.#{$fa-css-prefix}-mars:before { content: $fa-var-mars; }
.#{$fa-css-prefix}-mercury:before { content: $fa-var-mercury; }
.#{$fa-css-prefix}-intersex:before,
.#{$fa-css-prefix}-transgender:before { content: $fa-var-transgender; }
.#{$fa-css-prefix}-transgender-alt:before { content: $fa-var-transgender-alt; }
.#{$fa-css-prefix}-venus-double:before { content: $fa-var-venus-double; }
.#{$fa-css-prefix}-mars-double:before { content: $fa-var-mars-double; }
.#{$fa-css-prefix}-venus-mars:before { content: $fa-var-venus-mars; }
.#{$fa-css-prefix}-mars-stroke:before { content: $fa-var-mars-stroke; }
.#{$fa-css-prefix}-mars-stroke-v:before { content: $fa-var-mars-stroke-v; }
.#{$fa-css-prefix}-mars-stroke-h:before { content: $fa-var-mars-stroke-h; }
.#{$fa-css-prefix}-neuter:before { content: $fa-var-neuter; }
.#{$fa-css-prefix}-genderless:before { content: $fa-var-genderless; }
.#{$fa-css-prefix}-facebook-official:before { content: $fa-var-facebook-official; }
.#{$fa-css-prefix}-pinterest-p:before { content: $fa-var-pinterest-p; }
.#{$fa-css-prefix}-whatsapp:before { content: $fa-var-whatsapp; }
.#{$fa-css-prefix}-server:before { content: $fa-var-server; }
.#{$fa-css-prefix}-user-plus:before { content: $fa-var-user-plus; }
.#{$fa-css-prefix}-user-times:before { content: $fa-var-user-times; }
.#{$fa-css-prefix}-hotel:before,
.#{$fa-css-prefix}-bed:before { content: $fa-var-bed; }
.#{$fa-css-prefix}-viacoin:before { content: $fa-var-viacoin; }
.#{$fa-css-prefix}-train:before { content: $fa-var-train; }
.#{$fa-css-prefix}-subway:before { content: $fa-var-subway; }
.#{$fa-css-prefix}-medium:before { content: $fa-var-medium; }
.#{$fa-css-prefix}-yc:before,
.#{$fa-css-prefix}-y-combinator:before { content: $fa-var-y-combinator; }
.#{$fa-css-prefix}-optin-monster:before { content: $fa-var-optin-monster; }
.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; }
.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; }
.#{$fa-css-prefix}-battery-4:before,
.#{$fa-css-prefix}-battery:before,
.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; }
.#{$fa-css-prefix}-battery-3:before,
.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; }
.#{$fa-css-prefix}-battery-2:before,
.#{$fa-css-prefix}-battery-half:before { content: $fa-var-battery-half; }
.#{$fa-css-prefix}-battery-1:before,
.#{$fa-css-prefix}-battery-quarter:before { content: $fa-var-battery-quarter; }
.#{$fa-css-prefix}-battery-0:before,
.#{$fa-css-prefix}-battery-empty:before { content: $fa-var-battery-empty; }
.#{$fa-css-prefix}-mouse-pointer:before { content: $fa-var-mouse-pointer; }
.#{$fa-css-prefix}-i-cursor:before { content: $fa-var-i-cursor; }
.#{$fa-css-prefix}-object-group:before { content: $fa-var-object-group; }
.#{$fa-css-prefix}-object-ungroup:before { content: $fa-var-object-ungroup; }
.#{$fa-css-prefix}-sticky-note:before { content: $fa-var-sticky-note; }
.#{$fa-css-prefix}-sticky-note-o:before { content: $fa-var-sticky-note-o; }
.#{$fa-css-prefix}-cc-jcb:before { content: $fa-var-cc-jcb; }
.#{$fa-css-prefix}-cc-diners-club:before { content: $fa-var-cc-diners-club; }
.#{$fa-css-prefix}-clone:before { content: $fa-var-clone; }
.#{$fa-css-prefix}-balance-scale:before { content: $fa-var-balance-scale; }
.#{$fa-css-prefix}-hourglass-o:before { content: $fa-var-hourglass-o; }
.#{$fa-css-prefix}-hourglass-1:before,
.#{$fa-css-prefix}-hourglass-start:before { content: $fa-var-hourglass-start; }
.#{$fa-css-prefix}-hourglass-2:before,
.#{$fa-css-prefix}-hourglass-half:before { content: $fa-var-hourglass-half; }
.#{$fa-css-prefix}-hourglass-3:before,
.#{$fa-css-prefix}-hourglass-end:before { content: $fa-var-hourglass-end; }
.#{$fa-css-prefix}-hourglass:before { content: $fa-var-hourglass; }
.#{$fa-css-prefix}-hand-grab-o:before,
.#{$fa-css-prefix}-hand-rock-o:before { content: $fa-var-hand-rock-o; }
.#{$fa-css-prefix}-hand-stop-o:before,
.#{$fa-css-prefix}-hand-paper-o:before { content: $fa-var-hand-paper-o; }
.#{$fa-css-prefix}-hand-scissors-o:before { content: $fa-var-hand-scissors-o; }
.#{$fa-css-prefix}-hand-lizard-o:before { content: $fa-var-hand-lizard-o; }
.#{$fa-css-prefix}-hand-spock-o:before { content: $fa-var-hand-spock-o; }
.#{$fa-css-prefix}-hand-pointer-o:before { content: $fa-var-hand-pointer-o; }
.#{$fa-css-prefix}-hand-peace-o:before { content: $fa-var-hand-peace-o; }
.#{$fa-css-prefix}-trademark:before { content: $fa-var-trademark; }
.#{$fa-css-prefix}-registered:before { content: $fa-var-registered; }
.#{$fa-css-prefix}-creative-commons:before { content: $fa-var-creative-commons; }
.#{$fa-css-prefix}-gg:before { content: $fa-var-gg; }
.#{$fa-css-prefix}-gg-circle:before { content: $fa-var-gg-circle; }
.#{$fa-css-prefix}-tripadvisor:before { content: $fa-var-tripadvisor; }
.#{$fa-css-prefix}-odnoklassniki:before { content: $fa-var-odnoklassniki; }
.#{$fa-css-prefix}-odnoklassniki-square:before { content: $fa-var-odnoklassniki-square; }
.#{$fa-css-prefix}-get-pocket:before { content: $fa-var-get-pocket; }
.#{$fa-css-prefix}-wikipedia-w:before { content: $fa-var-wikipedia-w; }
.#{$fa-css-prefix}-safari:before { content: $fa-var-safari; }
.#{$fa-css-prefix}-chrome:before { content: $fa-var-chrome; }
.#{$fa-css-prefix}-firefox:before { content: $fa-var-firefox; }
.#{$fa-css-prefix}-opera:before { content: $fa-var-opera; }
.#{$fa-css-prefix}-internet-explorer:before { content: $fa-var-internet-explorer; }
.#{$fa-css-prefix}-tv:before,
.#{$fa-css-prefix}-television:before { content: $fa-var-television; }
.#{$fa-css-prefix}-contao:before { content: $fa-var-contao; }
.#{$fa-css-prefix}-500px:before { content: $fa-var-500px; }
.#{$fa-css-prefix}-amazon:before { content: $fa-var-amazon; }
.#{$fa-css-prefix}-calendar-plus-o:before { content: $fa-var-calendar-plus-o; }
.#{$fa-css-prefix}-calendar-minus-o:before { content: $fa-var-calendar-minus-o; }
.#{$fa-css-prefix}-calendar-times-o:before { content: $fa-var-calendar-times-o; }
.#{$fa-css-prefix}-calendar-check-o:before { content: $fa-var-calendar-check-o; }
.#{$fa-css-prefix}-industry:before { content: $fa-var-industry; }
.#{$fa-css-prefix}-map-pin:before { content: $fa-var-map-pin; }
.#{$fa-css-prefix}-map-signs:before { content: $fa-var-map-signs; }
.#{$fa-css-prefix}-map-o:before { content: $fa-var-map-o; }
.#{$fa-css-prefix}-map:before { content: $fa-var-map; }
.#{$fa-css-prefix}-commenting:before { content: $fa-var-commenting; }
.#{$fa-css-prefix}-commenting-o:before { content: $fa-var-commenting-o; }
.#{$fa-css-prefix}-houzz:before { content: $fa-var-houzz; }
.#{$fa-css-prefix}-vimeo:before { content: $fa-var-vimeo; }
.#{$fa-css-prefix}-black-tie:before { content: $fa-var-black-tie; }
.#{$fa-css-prefix}-fonticons:before { content: $fa-var-fonticons; }
.#{$fa-css-prefix}-reddit-alien:before { content: $fa-var-reddit-alien; }
.#{$fa-css-prefix}-edge:before { content: $fa-var-edge; }
.#{$fa-css-prefix}-credit-card-alt:before { content: $fa-var-credit-card-alt; }
.#{$fa-css-prefix}-codiepie:before { content: $fa-var-codiepie; }
.#{$fa-css-prefix}-modx:before { content: $fa-var-modx; }
.#{$fa-css-prefix}-fort-awesome:before { content: $fa-var-fort-awesome; }
.#{$fa-css-prefix}-usb:before { content: $fa-var-usb; }
.#{$fa-css-prefix}-product-hunt:before { content: $fa-var-product-hunt; }
.#{$fa-css-prefix}-mixcloud:before { content: $fa-var-mixcloud; }
.#{$fa-css-prefix}-scribd:before { content: $fa-var-scribd; }
.#{$fa-css-prefix}-pause-circle:before { content: $fa-var-pause-circle; }
.#{$fa-css-prefix}-pause-circle-o:before { content: $fa-var-pause-circle-o; }
.#{$fa-css-prefix}-stop-circle:before { content: $fa-var-stop-circle; }
.#{$fa-css-prefix}-stop-circle-o:before { content: $fa-var-stop-circle-o; }
.#{$fa-css-prefix}-shopping-bag:before { content: $fa-var-shopping-bag; }
.#{$fa-css-prefix}-shopping-basket:before { content: $fa-var-shopping-basket; }
.#{$fa-css-prefix}-hashtag:before { content: $fa-var-hashtag; }
.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; }
.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; }
.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; }
.#{$fa-css-prefix}-gitlab:before { content: $fa-var-gitlab; }
.#{$fa-css-prefix}-wpbeginner:before { content: $fa-var-wpbeginner; }
.#{$fa-css-prefix}-wpforms:before { content: $fa-var-wpforms; }
.#{$fa-css-prefix}-envira:before { content: $fa-var-envira; }
.#{$fa-css-prefix}-universal-access:before { content: $fa-var-universal-access; }
.#{$fa-css-prefix}-wheelchair-alt:before { content: $fa-var-wheelchair-alt; }
.#{$fa-css-prefix}-question-circle-o:before { content: $fa-var-question-circle-o; }
.#{$fa-css-prefix}-blind:before { content: $fa-var-blind; }
.#{$fa-css-prefix}-audio-description:before { content: $fa-var-audio-description; }
.#{$fa-css-prefix}-volume-control-phone:before { content: $fa-var-volume-control-phone; }
.#{$fa-css-prefix}-braille:before { content: $fa-var-braille; }
.#{$fa-css-prefix}-assistive-listening-systems:before { content: $fa-var-assistive-listening-systems; }
.#{$fa-css-prefix}-asl-interpreting:before,
.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: $fa-var-american-sign-language-interpreting; }
.#{$fa-css-prefix}-deafness:before,
.#{$fa-css-prefix}-hard-of-hearing:before,
.#{$fa-css-prefix}-deaf:before { content: $fa-var-deaf; }
.#{$fa-css-prefix}-glide:before { content: $fa-var-glide; }
.#{$fa-css-prefix}-glide-g:before { content: $fa-var-glide-g; }
.#{$fa-css-prefix}-signing:before,
.#{$fa-css-prefix}-sign-language:before { content: $fa-var-sign-language; }
.#{$fa-css-prefix}-low-vision:before { content: $fa-var-low-vision; }
.#{$fa-css-prefix}-viadeo:before { content: $fa-var-viadeo; }
.#{$fa-css-prefix}-viadeo-square:before { content: $fa-var-viadeo-square; }
.#{$fa-css-prefix}-snapchat:before { content: $fa-var-snapchat; }
.#{$fa-css-prefix}-snapchat-ghost:before { content: $fa-var-snapchat-ghost; }
.#{$fa-css-prefix}-snapchat-square:before { content: $fa-var-snapchat-square; }
.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; }
.#{$fa-css-prefix}-first-order:before { content: $fa-var-first-order; }
.#{$fa-css-prefix}-yoast:before { content: $fa-var-yoast; }
.#{$fa-css-prefix}-themeisle:before { content: $fa-var-themeisle; }
.#{$fa-css-prefix}-google-plus-circle:before,
.#{$fa-css-prefix}-google-plus-official:before { content: $fa-var-google-plus-official; }
.#{$fa-css-prefix}-fa:before,
.#{$fa-css-prefix}-font-awesome:before { content: $fa-var-font-awesome; }
.#{$fa-css-prefix}-handshake-o:before { content: $fa-var-handshake-o; }
.#{$fa-css-prefix}-envelope-open:before { content: $fa-var-envelope-open; }
.#{$fa-css-prefix}-envelope-open-o:before { content: $fa-var-envelope-open-o; }
.#{$fa-css-prefix}-linode:before { content: $fa-var-linode; }
.#{$fa-css-prefix}-address-book:before { content: $fa-var-address-book; }
.#{$fa-css-prefix}-address-book-o:before { content: $fa-var-address-book-o; }
.#{$fa-css-prefix}-vcard:before,
.#{$fa-css-prefix}-address-card:before { content: $fa-var-address-card; }
.#{$fa-css-prefix}-vcard-o:before,
.#{$fa-css-prefix}-address-card-o:before { content: $fa-var-address-card-o; }
.#{$fa-css-prefix}-user-circle:before { content: $fa-var-user-circle; }
.#{$fa-css-prefix}-user-circle-o:before { content: $fa-var-user-circle-o; }
.#{$fa-css-prefix}-user-o:before { content: $fa-var-user-o; }
.#{$fa-css-prefix}-id-badge:before { content: $fa-var-id-badge; }
.#{$fa-css-prefix}-drivers-license:before,
.#{$fa-css-prefix}-id-card:before { content: $fa-var-id-card; }
.#{$fa-css-prefix}-drivers-license-o:before,
.#{$fa-css-prefix}-id-card-o:before { content: $fa-var-id-card-o; }
.#{$fa-css-prefix}-quora:before { content: $fa-var-quora; }
.#{$fa-css-prefix}-free-code-camp:before { content: $fa-var-free-code-camp; }
.#{$fa-css-prefix}-telegram:before { content: $fa-var-telegram; }
.#{$fa-css-prefix}-thermometer-4:before,
.#{$fa-css-prefix}-thermometer:before,
.#{$fa-css-prefix}-thermometer-full:before { content: $fa-var-thermometer-full; }
.#{$fa-css-prefix}-thermometer-3:before,
.#{$fa-css-prefix}-thermometer-three-quarters:before { content: $fa-var-thermometer-three-quarters; }
.#{$fa-css-prefix}-thermometer-2:before,
.#{$fa-css-prefix}-thermometer-half:before { content: $fa-var-thermometer-half; }
.#{$fa-css-prefix}-thermometer-1:before,
.#{$fa-css-prefix}-thermometer-quarter:before { content: $fa-var-thermometer-quarter; }
.#{$fa-css-prefix}-thermometer-0:before,
.#{$fa-css-prefix}-thermometer-empty:before { content: $fa-var-thermometer-empty; }
.#{$fa-css-prefix}-shower:before { content: $fa-var-shower; }
.#{$fa-css-prefix}-bathtub:before,
.#{$fa-css-prefix}-s15:before,
.#{$fa-css-prefix}-bath:before { content: $fa-var-bath; }
.#{$fa-css-prefix}-podcast:before { content: $fa-var-podcast; }
.#{$fa-css-prefix}-window-maximize:before { content: $fa-var-window-maximize; }
.#{$fa-css-prefix}-window-minimize:before { content: $fa-var-window-minimize; }
.#{$fa-css-prefix}-window-restore:before { content: $fa-var-window-restore; }
.#{$fa-css-prefix}-times-rectangle:before,
.#{$fa-css-prefix}-window-close:before { content: $fa-var-window-close; }
.#{$fa-css-prefix}-times-rectangle-o:before,
.#{$fa-css-prefix}-window-close-o:before { content: $fa-var-window-close-o; }
.#{$fa-css-prefix}-bandcamp:before { content: $fa-var-bandcamp; }
.#{$fa-css-prefix}-grav:before { content: $fa-var-grav; }
.#{$fa-css-prefix}-etsy:before { content: $fa-var-etsy; }
.#{$fa-css-prefix}-imdb:before { content: $fa-var-imdb; }
.#{$fa-css-prefix}-ravelry:before { content: $fa-var-ravelry; }
.#{$fa-css-prefix}-eercast:before { content: $fa-var-eercast; }
.#{$fa-css-prefix}-microchip:before { content: $fa-var-microchip; }
.#{$fa-css-prefix}-snowflake-o:before { content: $fa-var-snowflake-o; }
.#{$fa-css-prefix}-superpowers:before { content: $fa-var-superpowers; }
.#{$fa-css-prefix}-wpexplorer:before { content: $fa-var-wpexplorer; }
.#{$fa-css-prefix}-meetup:before { content: $fa-var-meetup; }

View File

@ -3,23 +3,58 @@
@mixin fa-icon() {
display: inline-block;
font: normal normal normal 14px/1 FontAwesome; // shortening font declaration
font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@mixin fa-icon-rotate($degrees, $rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})";
-webkit-transform: rotate($degrees);
-ms-transform: rotate($degrees);
transform: rotate($degrees);
}
@mixin fa-icon-flip($horiz, $vert, $rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)";
-webkit-transform: scale($horiz, $vert);
-ms-transform: scale($horiz, $vert);
transform: scale($horiz, $vert);
}
// Only display content to screen readers. A la Bootstrap 4.
//
// See: http://a11yproject.com/posts/how-to-hide-content/
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
// Use in conjunction with .sr-only to only display content when it's focused.
//
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
//
// Credit: HTML5 Boilerplate
@mixin sr-only-focusable {
&:active,
&:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
}

View File

@ -5,10 +5,11 @@
font-family: 'FontAwesome';
src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}');
src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'),
url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'),
url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'),
url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'),
url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg');
//src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
font-weight: normal;
font-style: normal;
}

View File

@ -0,0 +1,5 @@
// Screen Readers
// -------------------------
.sr-only { @include sr-only(); }
.sr-only-focusable { @include sr-only-focusable(); }

View File

@ -2,20 +2,29 @@
// --------------------------
$fa-font-path: "../fonts" !default;
//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts" !default; // for referencing Bootstrap CDN font files directly
$fa-font-size-base: 14px !default;
$fa-line-height-base: 1 !default;
//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly
$fa-css-prefix: fa !default;
$fa-version: "4.2.0" !default;
$fa-version: "4.7.0" !default;
$fa-border-color: #eee !default;
$fa-inverse: #fff !default;
$fa-li-width: (30em / 14) !default;
$fa-var-500px: "\f26e";
$fa-var-address-book: "\f2b9";
$fa-var-address-book-o: "\f2ba";
$fa-var-address-card: "\f2bb";
$fa-var-address-card-o: "\f2bc";
$fa-var-adjust: "\f042";
$fa-var-adn: "\f170";
$fa-var-align-center: "\f037";
$fa-var-align-justify: "\f039";
$fa-var-align-left: "\f036";
$fa-var-align-right: "\f038";
$fa-var-amazon: "\f270";
$fa-var-ambulance: "\f0f9";
$fa-var-american-sign-language-interpreting: "\f2a3";
$fa-var-anchor: "\f13d";
$fa-var-android: "\f17b";
$fa-var-angellist: "\f209";
@ -46,16 +55,35 @@ $fa-var-arrows: "\f047";
$fa-var-arrows-alt: "\f0b2";
$fa-var-arrows-h: "\f07e";
$fa-var-arrows-v: "\f07d";
$fa-var-asl-interpreting: "\f2a3";
$fa-var-assistive-listening-systems: "\f2a2";
$fa-var-asterisk: "\f069";
$fa-var-at: "\f1fa";
$fa-var-audio-description: "\f29e";
$fa-var-automobile: "\f1b9";
$fa-var-backward: "\f04a";
$fa-var-balance-scale: "\f24e";
$fa-var-ban: "\f05e";
$fa-var-bandcamp: "\f2d5";
$fa-var-bank: "\f19c";
$fa-var-bar-chart: "\f080";
$fa-var-bar-chart-o: "\f080";
$fa-var-barcode: "\f02a";
$fa-var-bars: "\f0c9";
$fa-var-bath: "\f2cd";
$fa-var-bathtub: "\f2cd";
$fa-var-battery: "\f240";
$fa-var-battery-0: "\f244";
$fa-var-battery-1: "\f243";
$fa-var-battery-2: "\f242";
$fa-var-battery-3: "\f241";
$fa-var-battery-4: "\f240";
$fa-var-battery-empty: "\f244";
$fa-var-battery-full: "\f240";
$fa-var-battery-half: "\f242";
$fa-var-battery-quarter: "\f243";
$fa-var-battery-three-quarters: "\f241";
$fa-var-bed: "\f236";
$fa-var-beer: "\f0fc";
$fa-var-behance: "\f1b4";
$fa-var-behance-square: "\f1b5";
@ -69,12 +97,17 @@ $fa-var-birthday-cake: "\f1fd";
$fa-var-bitbucket: "\f171";
$fa-var-bitbucket-square: "\f172";
$fa-var-bitcoin: "\f15a";
$fa-var-black-tie: "\f27e";
$fa-var-blind: "\f29d";
$fa-var-bluetooth: "\f293";
$fa-var-bluetooth-b: "\f294";
$fa-var-bold: "\f032";
$fa-var-bolt: "\f0e7";
$fa-var-bomb: "\f1e2";
$fa-var-book: "\f02d";
$fa-var-bookmark: "\f02e";
$fa-var-bookmark-o: "\f097";
$fa-var-braille: "\f2a1";
$fa-var-briefcase: "\f0b1";
$fa-var-btc: "\f15a";
$fa-var-bug: "\f188";
@ -83,10 +116,15 @@ $fa-var-building-o: "\f0f7";
$fa-var-bullhorn: "\f0a1";
$fa-var-bullseye: "\f140";
$fa-var-bus: "\f207";
$fa-var-buysellads: "\f20d";
$fa-var-cab: "\f1ba";
$fa-var-calculator: "\f1ec";
$fa-var-calendar: "\f073";
$fa-var-calendar-check-o: "\f274";
$fa-var-calendar-minus-o: "\f272";
$fa-var-calendar-o: "\f133";
$fa-var-calendar-plus-o: "\f271";
$fa-var-calendar-times-o: "\f273";
$fa-var-camera: "\f030";
$fa-var-camera-retro: "\f083";
$fa-var-car: "\f1b9";
@ -98,9 +136,13 @@ $fa-var-caret-square-o-left: "\f191";
$fa-var-caret-square-o-right: "\f152";
$fa-var-caret-square-o-up: "\f151";
$fa-var-caret-up: "\f0d8";
$fa-var-cart-arrow-down: "\f218";
$fa-var-cart-plus: "\f217";
$fa-var-cc: "\f20a";
$fa-var-cc-amex: "\f1f3";
$fa-var-cc-diners-club: "\f24c";
$fa-var-cc-discover: "\f1f2";
$fa-var-cc-jcb: "\f24b";
$fa-var-cc-mastercard: "\f1f1";
$fa-var-cc-paypal: "\f1f4";
$fa-var-cc-stripe: "\f1f5";
@ -122,12 +164,14 @@ $fa-var-chevron-left: "\f053";
$fa-var-chevron-right: "\f054";
$fa-var-chevron-up: "\f077";
$fa-var-child: "\f1ae";
$fa-var-chrome: "\f268";
$fa-var-circle: "\f111";
$fa-var-circle-o: "\f10c";
$fa-var-circle-o-notch: "\f1ce";
$fa-var-circle-thin: "\f1db";
$fa-var-clipboard: "\f0ea";
$fa-var-clock-o: "\f017";
$fa-var-clone: "\f24d";
$fa-var-close: "\f00d";
$fa-var-cloud: "\f0c2";
$fa-var-cloud-download: "\f0ed";
@ -136,19 +180,26 @@ $fa-var-cny: "\f157";
$fa-var-code: "\f121";
$fa-var-code-fork: "\f126";
$fa-var-codepen: "\f1cb";
$fa-var-codiepie: "\f284";
$fa-var-coffee: "\f0f4";
$fa-var-cog: "\f013";
$fa-var-cogs: "\f085";
$fa-var-columns: "\f0db";
$fa-var-comment: "\f075";
$fa-var-comment-o: "\f0e5";
$fa-var-commenting: "\f27a";
$fa-var-commenting-o: "\f27b";
$fa-var-comments: "\f086";
$fa-var-comments-o: "\f0e6";
$fa-var-compass: "\f14e";
$fa-var-compress: "\f066";
$fa-var-connectdevelop: "\f20e";
$fa-var-contao: "\f26d";
$fa-var-copy: "\f0c5";
$fa-var-copyright: "\f1f9";
$fa-var-creative-commons: "\f25e";
$fa-var-credit-card: "\f09d";
$fa-var-credit-card-alt: "\f283";
$fa-var-crop: "\f125";
$fa-var-crosshairs: "\f05b";
$fa-var-css3: "\f13c";
@ -157,27 +208,39 @@ $fa-var-cubes: "\f1b3";
$fa-var-cut: "\f0c4";
$fa-var-cutlery: "\f0f5";
$fa-var-dashboard: "\f0e4";
$fa-var-dashcube: "\f210";
$fa-var-database: "\f1c0";
$fa-var-deaf: "\f2a4";
$fa-var-deafness: "\f2a4";
$fa-var-dedent: "\f03b";
$fa-var-delicious: "\f1a5";
$fa-var-desktop: "\f108";
$fa-var-deviantart: "\f1bd";
$fa-var-diamond: "\f219";
$fa-var-digg: "\f1a6";
$fa-var-dollar: "\f155";
$fa-var-dot-circle-o: "\f192";
$fa-var-download: "\f019";
$fa-var-dribbble: "\f17d";
$fa-var-drivers-license: "\f2c2";
$fa-var-drivers-license-o: "\f2c3";
$fa-var-dropbox: "\f16b";
$fa-var-drupal: "\f1a9";
$fa-var-edge: "\f282";
$fa-var-edit: "\f044";
$fa-var-eercast: "\f2da";
$fa-var-eject: "\f052";
$fa-var-ellipsis-h: "\f141";
$fa-var-ellipsis-v: "\f142";
$fa-var-empire: "\f1d1";
$fa-var-envelope: "\f0e0";
$fa-var-envelope-o: "\f003";
$fa-var-envelope-open: "\f2b6";
$fa-var-envelope-open-o: "\f2b7";
$fa-var-envelope-square: "\f199";
$fa-var-envira: "\f299";
$fa-var-eraser: "\f12d";
$fa-var-etsy: "\f2d7";
$fa-var-eur: "\f153";
$fa-var-euro: "\f153";
$fa-var-exchange: "\f0ec";
@ -185,16 +248,21 @@ $fa-var-exclamation: "\f12a";
$fa-var-exclamation-circle: "\f06a";
$fa-var-exclamation-triangle: "\f071";
$fa-var-expand: "\f065";
$fa-var-expeditedssl: "\f23e";
$fa-var-external-link: "\f08e";
$fa-var-external-link-square: "\f14c";
$fa-var-eye: "\f06e";
$fa-var-eye-slash: "\f070";
$fa-var-eyedropper: "\f1fb";
$fa-var-fa: "\f2b4";
$fa-var-facebook: "\f09a";
$fa-var-facebook-f: "\f09a";
$fa-var-facebook-official: "\f230";
$fa-var-facebook-square: "\f082";
$fa-var-fast-backward: "\f049";
$fa-var-fast-forward: "\f050";
$fa-var-fax: "\f1ac";
$fa-var-feed: "\f09e";
$fa-var-female: "\f182";
$fa-var-fighter-jet: "\f0fb";
$fa-var-file: "\f15b";
@ -220,6 +288,8 @@ $fa-var-film: "\f008";
$fa-var-filter: "\f0b0";
$fa-var-fire: "\f06d";
$fa-var-fire-extinguisher: "\f134";
$fa-var-firefox: "\f269";
$fa-var-first-order: "\f2b0";
$fa-var-flag: "\f024";
$fa-var-flag-checkered: "\f11e";
$fa-var-flag-o: "\f11d";
@ -232,8 +302,13 @@ $fa-var-folder-o: "\f114";
$fa-var-folder-open: "\f07c";
$fa-var-folder-open-o: "\f115";
$fa-var-font: "\f031";
$fa-var-font-awesome: "\f2b4";
$fa-var-fonticons: "\f280";
$fa-var-fort-awesome: "\f286";
$fa-var-forumbee: "\f211";
$fa-var-forward: "\f04e";
$fa-var-foursquare: "\f180";
$fa-var-free-code-camp: "\f2c5";
$fa-var-frown-o: "\f119";
$fa-var-futbol-o: "\f1e3";
$fa-var-gamepad: "\f11b";
@ -242,45 +317,87 @@ $fa-var-gbp: "\f154";
$fa-var-ge: "\f1d1";
$fa-var-gear: "\f013";
$fa-var-gears: "\f085";
$fa-var-genderless: "\f22d";
$fa-var-get-pocket: "\f265";
$fa-var-gg: "\f260";
$fa-var-gg-circle: "\f261";
$fa-var-gift: "\f06b";
$fa-var-git: "\f1d3";
$fa-var-git-square: "\f1d2";
$fa-var-github: "\f09b";
$fa-var-github-alt: "\f113";
$fa-var-github-square: "\f092";
$fa-var-gitlab: "\f296";
$fa-var-gittip: "\f184";
$fa-var-glass: "\f000";
$fa-var-glide: "\f2a5";
$fa-var-glide-g: "\f2a6";
$fa-var-globe: "\f0ac";
$fa-var-google: "\f1a0";
$fa-var-google-plus: "\f0d5";
$fa-var-google-plus-circle: "\f2b3";
$fa-var-google-plus-official: "\f2b3";
$fa-var-google-plus-square: "\f0d4";
$fa-var-google-wallet: "\f1ee";
$fa-var-graduation-cap: "\f19d";
$fa-var-gratipay: "\f184";
$fa-var-grav: "\f2d6";
$fa-var-group: "\f0c0";
$fa-var-h-square: "\f0fd";
$fa-var-hacker-news: "\f1d4";
$fa-var-hand-grab-o: "\f255";
$fa-var-hand-lizard-o: "\f258";
$fa-var-hand-o-down: "\f0a7";
$fa-var-hand-o-left: "\f0a5";
$fa-var-hand-o-right: "\f0a4";
$fa-var-hand-o-up: "\f0a6";
$fa-var-hand-paper-o: "\f256";
$fa-var-hand-peace-o: "\f25b";
$fa-var-hand-pointer-o: "\f25a";
$fa-var-hand-rock-o: "\f255";
$fa-var-hand-scissors-o: "\f257";
$fa-var-hand-spock-o: "\f259";
$fa-var-hand-stop-o: "\f256";
$fa-var-handshake-o: "\f2b5";
$fa-var-hard-of-hearing: "\f2a4";
$fa-var-hashtag: "\f292";
$fa-var-hdd-o: "\f0a0";
$fa-var-header: "\f1dc";
$fa-var-headphones: "\f025";
$fa-var-heart: "\f004";
$fa-var-heart-o: "\f08a";
$fa-var-heartbeat: "\f21e";
$fa-var-history: "\f1da";
$fa-var-home: "\f015";
$fa-var-hospital-o: "\f0f8";
$fa-var-hotel: "\f236";
$fa-var-hourglass: "\f254";
$fa-var-hourglass-1: "\f251";
$fa-var-hourglass-2: "\f252";
$fa-var-hourglass-3: "\f253";
$fa-var-hourglass-end: "\f253";
$fa-var-hourglass-half: "\f252";
$fa-var-hourglass-o: "\f250";
$fa-var-hourglass-start: "\f251";
$fa-var-houzz: "\f27c";
$fa-var-html5: "\f13b";
$fa-var-i-cursor: "\f246";
$fa-var-id-badge: "\f2c1";
$fa-var-id-card: "\f2c2";
$fa-var-id-card-o: "\f2c3";
$fa-var-ils: "\f20b";
$fa-var-image: "\f03e";
$fa-var-imdb: "\f2d8";
$fa-var-inbox: "\f01c";
$fa-var-indent: "\f03c";
$fa-var-industry: "\f275";
$fa-var-info: "\f129";
$fa-var-info-circle: "\f05a";
$fa-var-inr: "\f156";
$fa-var-instagram: "\f16d";
$fa-var-institution: "\f19c";
$fa-var-internet-explorer: "\f26b";
$fa-var-intersex: "\f224";
$fa-var-ioxhost: "\f208";
$fa-var-italic: "\f033";
$fa-var-joomla: "\f1aa";
@ -294,6 +411,7 @@ $fa-var-laptop: "\f109";
$fa-var-lastfm: "\f202";
$fa-var-lastfm-square: "\f203";
$fa-var-leaf: "\f06c";
$fa-var-leanpub: "\f212";
$fa-var-legal: "\f0e3";
$fa-var-lemon-o: "\f094";
$fa-var-level-down: "\f149";
@ -307,6 +425,7 @@ $fa-var-line-chart: "\f201";
$fa-var-link: "\f0c1";
$fa-var-linkedin: "\f0e1";
$fa-var-linkedin-square: "\f08c";
$fa-var-linode: "\f2b8";
$fa-var-linux: "\f17c";
$fa-var-list: "\f03a";
$fa-var-list-alt: "\f022";
@ -318,32 +437,58 @@ $fa-var-long-arrow-down: "\f175";
$fa-var-long-arrow-left: "\f177";
$fa-var-long-arrow-right: "\f178";
$fa-var-long-arrow-up: "\f176";
$fa-var-low-vision: "\f2a8";
$fa-var-magic: "\f0d0";
$fa-var-magnet: "\f076";
$fa-var-mail-forward: "\f064";
$fa-var-mail-reply: "\f112";
$fa-var-mail-reply-all: "\f122";
$fa-var-male: "\f183";
$fa-var-map: "\f279";
$fa-var-map-marker: "\f041";
$fa-var-map-o: "\f278";
$fa-var-map-pin: "\f276";
$fa-var-map-signs: "\f277";
$fa-var-mars: "\f222";
$fa-var-mars-double: "\f227";
$fa-var-mars-stroke: "\f229";
$fa-var-mars-stroke-h: "\f22b";
$fa-var-mars-stroke-v: "\f22a";
$fa-var-maxcdn: "\f136";
$fa-var-meanpath: "\f20c";
$fa-var-medium: "\f23a";
$fa-var-medkit: "\f0fa";
$fa-var-meetup: "\f2e0";
$fa-var-meh-o: "\f11a";
$fa-var-mercury: "\f223";
$fa-var-microchip: "\f2db";
$fa-var-microphone: "\f130";
$fa-var-microphone-slash: "\f131";
$fa-var-minus: "\f068";
$fa-var-minus-circle: "\f056";
$fa-var-minus-square: "\f146";
$fa-var-minus-square-o: "\f147";
$fa-var-mixcloud: "\f289";
$fa-var-mobile: "\f10b";
$fa-var-mobile-phone: "\f10b";
$fa-var-modx: "\f285";
$fa-var-money: "\f0d6";
$fa-var-moon-o: "\f186";
$fa-var-mortar-board: "\f19d";
$fa-var-motorcycle: "\f21c";
$fa-var-mouse-pointer: "\f245";
$fa-var-music: "\f001";
$fa-var-navicon: "\f0c9";
$fa-var-neuter: "\f22c";
$fa-var-newspaper-o: "\f1ea";
$fa-var-object-group: "\f247";
$fa-var-object-ungroup: "\f248";
$fa-var-odnoklassniki: "\f263";
$fa-var-odnoklassniki-square: "\f264";
$fa-var-opencart: "\f23d";
$fa-var-openid: "\f19b";
$fa-var-opera: "\f26a";
$fa-var-optin-monster: "\f23c";
$fa-var-outdent: "\f03b";
$fa-var-pagelines: "\f18c";
$fa-var-paint-brush: "\f1fc";
@ -353,19 +498,24 @@ $fa-var-paperclip: "\f0c6";
$fa-var-paragraph: "\f1dd";
$fa-var-paste: "\f0ea";
$fa-var-pause: "\f04c";
$fa-var-pause-circle: "\f28b";
$fa-var-pause-circle-o: "\f28c";
$fa-var-paw: "\f1b0";
$fa-var-paypal: "\f1ed";
$fa-var-pencil: "\f040";
$fa-var-pencil-square: "\f14b";
$fa-var-pencil-square-o: "\f044";
$fa-var-percent: "\f295";
$fa-var-phone: "\f095";
$fa-var-phone-square: "\f098";
$fa-var-photo: "\f03e";
$fa-var-picture-o: "\f03e";
$fa-var-pie-chart: "\f200";
$fa-var-pied-piper: "\f1a7";
$fa-var-pied-piper: "\f2ae";
$fa-var-pied-piper-alt: "\f1a8";
$fa-var-pied-piper-pp: "\f1a7";
$fa-var-pinterest: "\f0d2";
$fa-var-pinterest-p: "\f231";
$fa-var-pinterest-square: "\f0d3";
$fa-var-plane: "\f072";
$fa-var-play: "\f04b";
@ -376,28 +526,36 @@ $fa-var-plus: "\f067";
$fa-var-plus-circle: "\f055";
$fa-var-plus-square: "\f0fe";
$fa-var-plus-square-o: "\f196";
$fa-var-podcast: "\f2ce";
$fa-var-power-off: "\f011";
$fa-var-print: "\f02f";
$fa-var-product-hunt: "\f288";
$fa-var-puzzle-piece: "\f12e";
$fa-var-qq: "\f1d6";
$fa-var-qrcode: "\f029";
$fa-var-question: "\f128";
$fa-var-question-circle: "\f059";
$fa-var-question-circle-o: "\f29c";
$fa-var-quora: "\f2c4";
$fa-var-quote-left: "\f10d";
$fa-var-quote-right: "\f10e";
$fa-var-ra: "\f1d0";
$fa-var-random: "\f074";
$fa-var-ravelry: "\f2d9";
$fa-var-rebel: "\f1d0";
$fa-var-recycle: "\f1b8";
$fa-var-reddit: "\f1a1";
$fa-var-reddit-alien: "\f281";
$fa-var-reddit-square: "\f1a2";
$fa-var-refresh: "\f021";
$fa-var-registered: "\f25d";
$fa-var-remove: "\f00d";
$fa-var-renren: "\f18b";
$fa-var-reorder: "\f0c9";
$fa-var-repeat: "\f01e";
$fa-var-reply: "\f112";
$fa-var-reply-all: "\f122";
$fa-var-resistance: "\f1d0";
$fa-var-retweet: "\f079";
$fa-var-rmb: "\f157";
$fa-var-road: "\f018";
@ -410,13 +568,18 @@ $fa-var-rss-square: "\f143";
$fa-var-rub: "\f158";
$fa-var-ruble: "\f158";
$fa-var-rupee: "\f156";
$fa-var-s15: "\f2cd";
$fa-var-safari: "\f267";
$fa-var-save: "\f0c7";
$fa-var-scissors: "\f0c4";
$fa-var-scribd: "\f28a";
$fa-var-search: "\f002";
$fa-var-search-minus: "\f010";
$fa-var-search-plus: "\f00e";
$fa-var-sellsy: "\f213";
$fa-var-send: "\f1d8";
$fa-var-send-o: "\f1d9";
$fa-var-server: "\f233";
$fa-var-share: "\f064";
$fa-var-share-alt: "\f1e0";
$fa-var-share-alt-square: "\f1e1";
@ -425,16 +588,29 @@ $fa-var-share-square-o: "\f045";
$fa-var-shekel: "\f20b";
$fa-var-sheqel: "\f20b";
$fa-var-shield: "\f132";
$fa-var-ship: "\f21a";
$fa-var-shirtsinbulk: "\f214";
$fa-var-shopping-bag: "\f290";
$fa-var-shopping-basket: "\f291";
$fa-var-shopping-cart: "\f07a";
$fa-var-shower: "\f2cc";
$fa-var-sign-in: "\f090";
$fa-var-sign-language: "\f2a7";
$fa-var-sign-out: "\f08b";
$fa-var-signal: "\f012";
$fa-var-signing: "\f2a7";
$fa-var-simplybuilt: "\f215";
$fa-var-sitemap: "\f0e8";
$fa-var-skyatlas: "\f216";
$fa-var-skype: "\f17e";
$fa-var-slack: "\f198";
$fa-var-sliders: "\f1de";
$fa-var-slideshare: "\f1e7";
$fa-var-smile-o: "\f118";
$fa-var-snapchat: "\f2ab";
$fa-var-snapchat-ghost: "\f2ac";
$fa-var-snapchat-square: "\f2ad";
$fa-var-snowflake-o: "\f2dc";
$fa-var-soccer-ball-o: "\f1e3";
$fa-var-sort: "\f0dc";
$fa-var-sort-alpha-asc: "\f15d";
@ -467,13 +643,20 @@ $fa-var-steam-square: "\f1b7";
$fa-var-step-backward: "\f048";
$fa-var-step-forward: "\f051";
$fa-var-stethoscope: "\f0f1";
$fa-var-sticky-note: "\f249";
$fa-var-sticky-note-o: "\f24a";
$fa-var-stop: "\f04d";
$fa-var-stop-circle: "\f28d";
$fa-var-stop-circle-o: "\f28e";
$fa-var-street-view: "\f21d";
$fa-var-strikethrough: "\f0cc";
$fa-var-stumbleupon: "\f1a4";
$fa-var-stumbleupon-circle: "\f1a3";
$fa-var-subscript: "\f12c";
$fa-var-subway: "\f239";
$fa-var-suitcase: "\f0f2";
$fa-var-sun-o: "\f185";
$fa-var-superpowers: "\f2dd";
$fa-var-superscript: "\f12b";
$fa-var-support: "\f1cd";
$fa-var-table: "\f0ce";
@ -483,6 +666,8 @@ $fa-var-tag: "\f02b";
$fa-var-tags: "\f02c";
$fa-var-tasks: "\f0ae";
$fa-var-taxi: "\f1ba";
$fa-var-telegram: "\f2c6";
$fa-var-television: "\f26c";
$fa-var-tencent-weibo: "\f1d5";
$fa-var-terminal: "\f120";
$fa-var-text-height: "\f034";
@ -490,6 +675,18 @@ $fa-var-text-width: "\f035";
$fa-var-th: "\f00a";
$fa-var-th-large: "\f009";
$fa-var-th-list: "\f00b";
$fa-var-themeisle: "\f2b2";
$fa-var-thermometer: "\f2c7";
$fa-var-thermometer-0: "\f2cb";
$fa-var-thermometer-1: "\f2ca";
$fa-var-thermometer-2: "\f2c9";
$fa-var-thermometer-3: "\f2c8";
$fa-var-thermometer-4: "\f2c7";
$fa-var-thermometer-empty: "\f2cb";
$fa-var-thermometer-full: "\f2c7";
$fa-var-thermometer-half: "\f2c9";
$fa-var-thermometer-quarter: "\f2ca";
$fa-var-thermometer-three-quarters: "\f2c8";
$fa-var-thumb-tack: "\f08d";
$fa-var-thumbs-down: "\f165";
$fa-var-thumbs-o-down: "\f088";
@ -499,6 +696,8 @@ $fa-var-ticket: "\f145";
$fa-var-times: "\f00d";
$fa-var-times-circle: "\f057";
$fa-var-times-circle-o: "\f05c";
$fa-var-times-rectangle: "\f2d3";
$fa-var-times-rectangle-o: "\f2d4";
$fa-var-tint: "\f043";
$fa-var-toggle-down: "\f150";
$fa-var-toggle-left: "\f191";
@ -506,10 +705,15 @@ $fa-var-toggle-off: "\f204";
$fa-var-toggle-on: "\f205";
$fa-var-toggle-right: "\f152";
$fa-var-toggle-up: "\f151";
$fa-var-trademark: "\f25c";
$fa-var-train: "\f238";
$fa-var-transgender: "\f224";
$fa-var-transgender-alt: "\f225";
$fa-var-trash: "\f1f8";
$fa-var-trash-o: "\f014";
$fa-var-tree: "\f1bb";
$fa-var-trello: "\f181";
$fa-var-tripadvisor: "\f262";
$fa-var-trophy: "\f091";
$fa-var-truck: "\f0d1";
$fa-var-try: "\f195";
@ -517,26 +721,45 @@ $fa-var-tty: "\f1e4";
$fa-var-tumblr: "\f173";
$fa-var-tumblr-square: "\f174";
$fa-var-turkish-lira: "\f195";
$fa-var-tv: "\f26c";
$fa-var-twitch: "\f1e8";
$fa-var-twitter: "\f099";
$fa-var-twitter-square: "\f081";
$fa-var-umbrella: "\f0e9";
$fa-var-underline: "\f0cd";
$fa-var-undo: "\f0e2";
$fa-var-universal-access: "\f29a";
$fa-var-university: "\f19c";
$fa-var-unlink: "\f127";
$fa-var-unlock: "\f09c";
$fa-var-unlock-alt: "\f13e";
$fa-var-unsorted: "\f0dc";
$fa-var-upload: "\f093";
$fa-var-usb: "\f287";
$fa-var-usd: "\f155";
$fa-var-user: "\f007";
$fa-var-user-circle: "\f2bd";
$fa-var-user-circle-o: "\f2be";
$fa-var-user-md: "\f0f0";
$fa-var-user-o: "\f2c0";
$fa-var-user-plus: "\f234";
$fa-var-user-secret: "\f21b";
$fa-var-user-times: "\f235";
$fa-var-users: "\f0c0";
$fa-var-vcard: "\f2bb";
$fa-var-vcard-o: "\f2bc";
$fa-var-venus: "\f221";
$fa-var-venus-double: "\f226";
$fa-var-venus-mars: "\f228";
$fa-var-viacoin: "\f237";
$fa-var-viadeo: "\f2a9";
$fa-var-viadeo-square: "\f2aa";
$fa-var-video-camera: "\f03d";
$fa-var-vimeo: "\f27d";
$fa-var-vimeo-square: "\f194";
$fa-var-vine: "\f1ca";
$fa-var-vk: "\f189";
$fa-var-volume-control-phone: "\f2a0";
$fa-var-volume-down: "\f027";
$fa-var-volume-off: "\f026";
$fa-var-volume-up: "\f028";
@ -544,17 +767,33 @@ $fa-var-warning: "\f071";
$fa-var-wechat: "\f1d7";
$fa-var-weibo: "\f18a";
$fa-var-weixin: "\f1d7";
$fa-var-whatsapp: "\f232";
$fa-var-wheelchair: "\f193";
$fa-var-wheelchair-alt: "\f29b";
$fa-var-wifi: "\f1eb";
$fa-var-wikipedia-w: "\f266";
$fa-var-window-close: "\f2d3";
$fa-var-window-close-o: "\f2d4";
$fa-var-window-maximize: "\f2d0";
$fa-var-window-minimize: "\f2d1";
$fa-var-window-restore: "\f2d2";
$fa-var-windows: "\f17a";
$fa-var-won: "\f159";
$fa-var-wordpress: "\f19a";
$fa-var-wpbeginner: "\f297";
$fa-var-wpexplorer: "\f2de";
$fa-var-wpforms: "\f298";
$fa-var-wrench: "\f0ad";
$fa-var-xing: "\f168";
$fa-var-xing-square: "\f169";
$fa-var-y-combinator: "\f23b";
$fa-var-y-combinator-square: "\f1d4";
$fa-var-yahoo: "\f19e";
$fa-var-yc: "\f23b";
$fa-var-yc-square: "\f1d4";
$fa-var-yelp: "\f1e9";
$fa-var-yen: "\f157";
$fa-var-yoast: "\f2b1";
$fa-var-youtube: "\f167";
$fa-var-youtube-play: "\f16a";
$fa-var-youtube-square: "\f166";

View File

@ -1,5 +1,5 @@
/*!
* Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
@ -11,7 +11,8 @@
@import "fixed-width";
@import "list";
@import "bordered-pulled";
@import "spinning";
@import "animated";
@import "rotated-flipped";
@import "stacked";
@import "icons";
@import "screen-reader";

1
assets/js/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,56 +1,236 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# vim: expandtab sw=4 ts=4 sts=4:
#
# (C) 2003 - 2018 Michal Čihař <michal@cihar.com> - python-gammu
# (C) 2015 - 2021 Raspian France <raspbianfrance@gmail.com> - RaspianFrance/raspisms
# (C) 2022 - Orsiris de Jong <orsiris.dejong@netperfect.fr> - NetInvent SASU
from __future__ import print_function
__intname__ = "gammu_get_unread_sms.py"
__author__ = "Orsiris de Jong - <orsiris.dejong@netperfect.fr>"
__version__ = "2.0.2"
__build__ = "2022102501"
__compat__ = "python2.7+"
import os
import gammu
import sys
import json
import logging
from logging.handlers import RotatingFileHandler
import tempfile
from argparse import ArgumentParser
import subprocess
import re
def main():
LOG_FILE = "/var/log/{}.log".format(__intname__)
_DEBUG = os.environ.get("_DEBUG", False)
logger = logging.getLogger(__name__)
def get_logger(log_file):
# We would normally use ofunctions.logger_utils here with logger_get_logger(), but let's keep no dependencies
try:
try:
filehandler = RotatingFileHandler(
log_file, mode="a", encoding="utf-8", maxBytes=1048576, backupCount=3
)
except OSError:
try:
temp_log_file = tempfile.gettempdir() + os.sep + __name__ + ".log"
filehandler = RotatingFileHandler(
temp_log_file,
mode="a",
encoding="utf-8",
maxBytes=1048576,
backupCount=3,
)
except OSError as exc:
print("Cannot create log file: %s" % exc.__str__())
filehandler = None
_logger = logging.getLogger()
if _DEBUG:
_logger.setLevel(logging.DEBUG)
else:
_logger.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s :: %(levelname)s :: %(message)s")
if filehandler:
filehandler.setFormatter(formatter)
_logger.addHandler(filehandler)
consolehandler = logging.StreamHandler()
consolehandler.setFormatter(formatter)
_logger.addHandler(consolehandler)
return _logger
except Exception as exc:
print("Cannot create logger instance: %s" % exc.__str__())
def get_gammu_version():
# Quite badly coded, i'd use command_runner but I try to not have dependencies here
try:
proc = subprocess.Popen(
["LC_ALL=C gammu", "--version"], shell=True, stdout=subprocess.PIPE
)
stdout, _ = proc.communicate()
version = re.search(r"Gammu version ([0-9]+)\.([0-9]+)\.([0-9]+)", str(stdout))
# dont' bother to return version[0] since it's the whole match
return (int(version[1]), int(version[2]), int(version[3]))
except Exception as exc:
logger.error("Cannot get gammu version: %s" % exc.__str__())
return None
def get_gammu_handle(config_file):
state_machine = gammu.StateMachine()
if len(sys.argv) < 2:
sys.exit(1)
else :
state_machine.ReadConfig(Filename=sys.argv[1])
del sys.argv[1]
if config_file:
state_machine.ReadConfig(Filename=config_file)
else:
state_machine.Readconfig()
state_machine.Init()
return state_machine
def load_sms_from_gammu(state_machine):
"""
The actual function that retrieves SMS via GAMMU from your modem / phone
Also concatenates multiple SMS into single long SMS
"""
status = state_machine.GetSMSStatus()
remain = status['SIMUsed'] + status['PhoneUsed'] + status['TemplatesUsed']
start = True
remaining_sms = status["SIMUsed"] + status["PhoneUsed"] + status["TemplatesUsed"]
logger.debug("Found %s sms" % remaining_sms)
sms_list = []
try:
while remain > 0:
if start:
sms = state_machine.GetNextSMS(Start=True, Folder=0)
start = False
is_first_message = True
while remaining_sms > 0:
if is_first_message:
sms = state_machine.GetNextSMS(Start=is_first_message, Folder=0)
is_first_message = False
else:
sms = state_machine.GetNextSMS(
Location=sms[0]['Location'], Folder=0
)
remain = remain - len(sms)
for m in sms :
if m['State'] != 'UnRead' :
continue
print(json.dumps({
'number': m['Number'],
'at': str(m['DateTime']),
'status': m['State'],
'text': m['Text'],
}))
sms = state_machine.GetNextSMS(Location=sms[0]["Location"], Folder=0)
remaining_sms = remaining_sms - len(sms)
sms_list.append(sms)
except gammu.ERR_EMPTY:
#do noting
return True
logger.debug("Finished reading all messages")
# Concat multiple SMS into list of sms that go together using LinkSMS
return gammu.LinkSMS(sms_list)
if __name__ == '__main__':
main()
def render_sms_as_json(state_machine, sms_list, delete_sms, show_read_sms):
"""
Provided sms_list is a list of lists of sms, eg
sms_list = [
[sms],
[sms1, sms2], # When two sms are in the same list, they form a long sms
[sms],
]
Concatenate long SMS from multiple sends and print them as JSON on stdout
"""
for sms in sms_list:
if sms[0]["State"] == "UnRead" or show_read_sms:
sms_text = ""
for to_concat_sms in sms:
sms_text += to_concat_sms["Text"]
print(
json.dumps(
{
"number": sms[0]["Number"],
"at": str(sms[0]["DateTime"]),
"status": sms[0]["State"],
"text": sms_text,
}
)
)
if delete_sms:
for to_concat_sms in sms:
try:
state_machine.DeleteSMS(
to_concat_sms["Folder"], to_concat_sms["Location"]
)
except Exception as exc:
logger.error("Cannot delete sms: %s" % exc.__str__())
def main(config_file, delete_sms, show_read):
# type: (bool, bool) -> None
logger.debug("Running gammu receiver with config {}".format(config_file))
try:
# Mandatory modem config file
# config_file = sys.argv[1]
state_machine = get_gammu_handle(config_file)
sms_list = load_sms_from_gammu(state_machine)
render_sms_as_json(state_machine, sms_list, delete_sms, show_read)
except Exception as exc:
logger.error("Could not retrieve SMS from Gammu: %s" % exc.__str__())
logger.debug("Trace:", exc_info=True)
if __name__ == "__main__":
parser = ArgumentParser("Gammu SMS retriever")
parser.add_argument(
"gammu_config_file", type=str, nargs="?", help="Gammu config file"
)
parser.add_argument("--debug", action="store_true", help="Activate debugging")
parser.add_argument(
"-l",
"--log-file",
type=str,
dest="log_file",
default=None,
help="Optional path to log file, defaults to /var/log",
)
parser.add_argument(
"--delete", action="store_true", help="Delete messages after they've been read"
)
parser.add_argument(
"--show-read", action="store_true", help="Also show already read messages"
)
args = parser.parse_args()
config_file = args.gammu_config_file
if args.log_file:
LOG_FILE = args.log_file
if args.debug:
_DEBUG = args.debug
_logger = get_logger(LOG_FILE)
if _logger:
logger = _logger
delete = False
if args.delete:
# We need to check if we have gammu >= 1.42.0 since deleting sms with lower versions fail with:
# Cannot delete sms: {'Text': 'The type of memory is not available or has been disabled.', 'Where': 'DeleteSMS', 'Code': 81}
# see https://github.com/gammu/gammu/issues/460
try:
gammu_version = get_gammu_version()
if gammu_version[0] > 1 or (gammu_version[0] == 1 and gammu_version[1] >= 42):
delete = True
else:
logger.warning("Cannot delete SMS. You need gammu >= 1.42.0.")
except TypeError:
logger.warning("Cannot get gammu version. SMS Deleting might not work properly.")
show_read = args.show_read
main(config_file, delete, show_read)

View File

@ -13,9 +13,8 @@
"twilio/sdk": "^6.1",
"symfony/yaml": "^5.0",
"phpmailer/phpmailer": "^6.1",
"ralouphie/mimey": "^2.1",
"kreait/firebase-php": "^5.14"
},
"require-dev": {
"xantios/mimey": ">=2.1",
"kreait/firebase-php": "^5.14",
"benmorel/gsm-charset-converter": "^0.3.0"
}
}

Binary file not shown.

View File

@ -136,7 +136,7 @@ namespace controllers\internals;
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Call
{
$this->model = $this->model ?? new \models\Call($this->bdd);

View File

@ -150,7 +150,7 @@ namespace controllers\internals;
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Command
{
$this->model = $this->model ?? new \models\Command($this->bdd);

View File

@ -134,7 +134,7 @@ namespace controllers\internals;
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\ConditionalGroup
{
$this->model = $this->model ?? new \models\ConditionalGroup($this->bdd);

View File

@ -11,6 +11,8 @@
namespace controllers\internals;
use DateInterval;
/**
* Class to call the console scripts.
*/
@ -55,7 +57,7 @@ namespace controllers\internals;
*/
public function phone($id_phone)
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_phone = new \controllers\internals\Phone($bdd);
$phone = $internal_phone->get($id_phone);
@ -74,7 +76,7 @@ namespace controllers\internals;
*/
public function user_exists(string $email)
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_user = new \controllers\internals\User($bdd);
$user = $internal_user->get_by_email($email);
@ -82,6 +84,21 @@ namespace controllers\internals;
exit($user ? 0 : 1);
}
/**
* Check if a user exists based on id.
*
* @param string $id : User id
*/
public function user_id_exists(string $id)
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_user = new \controllers\internals\User($bdd);
$user = $internal_user->get($id);
exit($user ? 0 : 1);
}
/**
* Create a user or update an existing user.
*
@ -96,14 +113,14 @@ namespace controllers\internals;
*/
public function create_update_user(string $email, string $password, bool $admin, ?string $api_key = null, string $status = \models\User::STATUS_ACTIVE, bool $encrypt_password = true)
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_user = new \controllers\internals\User($bdd);
$user = $internal_user->get_by_email($email);
if ($user)
{
$api_key = $api_key ?? $internal_user->generate_random_api_key();
$user = [
$update_datas = [
'email' => $email,
'password' => $encrypt_password ? password_hash($password, PASSWORD_DEFAULT) : $password,
'admin' => $admin,
@ -111,7 +128,7 @@ namespace controllers\internals;
'status' => $status,
];
$success = $internal_user->update($user['id'], $user);
$success = $internal_user->update($user['id'], $update_datas);
echo json_encode(['id' => $user['id']]);
exit($success ? 0 : 1);
@ -131,7 +148,7 @@ namespace controllers\internals;
*/
public function update_user_status(string $id, string $status)
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_user = new \controllers\internals\User($bdd);
$user = $internal_user->get($id);
@ -152,7 +169,7 @@ namespace controllers\internals;
*/
public function delete_user(string $id)
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_user = new \controllers\internals\User($bdd);
$success = $internal_user->delete($id);
@ -165,7 +182,7 @@ namespace controllers\internals;
*/
public function clean_unused_medias()
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_media = new \controllers\internals\Media($bdd);
$medias = $internal_media->gets_unused();
@ -183,7 +200,7 @@ namespace controllers\internals;
*/
public function quota_limit_alerting()
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_quota = new \controllers\internals\Quota($bdd);
$internal_quota->alerting_for_limit_close_and_reached();
}
@ -193,8 +210,50 @@ namespace controllers\internals;
*/
public function renew_quotas()
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_quota = new \controllers\internals\Quota($bdd);
$internal_quota->renew_quotas();
}
/**
* Do some fake population renewal.
*/
public function f()
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$internal_sended = new \controllers\internals\Sended($bdd);
$destinations = ['+33612345678','+33612345679','+33612345680',];
$statuses = [\models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED, \models\Sended::STATUS_UNKNOWN];
$day = new \DateTime();
$day->sub(new DateInterval('P30D'));
for ($i = 0; $i < 30; $i++)
{
$day->add(new DateInterval('P1D'));
$n = rand(0, 100);
for ($j = 0; $j < $n; $j++)
{
$id_user = 1;
$id_phone = rand(1, 2);
$destination = $destinations[array_rand($destinations)];
$status = $statuses[array_rand($statuses)];
$internal_sended->create(
$id_user,
$id_phone,
$day->format('Y-m-d H:i:s'),
"TEST N°$i:$j",
$destination,
uniqid(),
'adapters\TestAdapter',
false,
false,
null,
[],
null,
$status,
);
}
}
}
}

View File

@ -15,6 +15,23 @@ namespace controllers\internals;
{
protected $model;
/**
* @param int $id_user : User id
* @param ?int $limit : Number of entry to return
* @param ?int $offset : Number of entry to avoid
* @param ?string $search : String to search for
* @param ?array $search_columns : List of columns to search on
* @param ?string $order_column : Name of the column to order by
* @param bool $order_desc : Should result be ordered DESC, if false order ASC
* @param bool $count : Should the query only count results
*
* @return array : Entries list
*/
public function datatable_list_for_user(int $id_user, ?int $limit = null, ?int $offset = null, ?string $search = null, ?array $search_columns = [], ?string $order_column = null, bool $order_desc = false, $count = false)
{
return $this->get_model()->datatable_list_for_user($id_user, $limit, $offset, $search, $search_columns, $order_column, $order_desc, $count);
}
/**
* Return a contact for a user by a number.
*
@ -124,8 +141,11 @@ namespace controllers\internals;
$nb_insert = 0;
$head = null;
$line_nb = 0;
while ($line = fgetcsv($file_handler))
{
$line_nb ++;
if (null === $head)
{
$head = $line;
@ -143,7 +163,7 @@ namespace controllers\internals;
continue;
}
if (!isset($line[array_keys($line)[0]], $line[array_keys($line)[1]]))
if (!isset(array_keys($line)[0], array_keys($line)[1]))
{
continue;
}
@ -168,9 +188,16 @@ namespace controllers\internals;
}
$data = json_encode($data);
$contact_name = $line[array_keys($line)[0]];
$phone_number = \controllers\internals\Tool::parse_phone($line[array_keys($line)[1]]);
if (!$phone_number)
{
throw new \Exception('Erreur à la ligne ' . $line_nb . ' colonne 1, numéro de téléphone invalide.');
}
try
{
$success = $this->create($id_user, $line[array_keys($line)[1]], $line[array_keys($line)[0]], $data);
$success = $this->create($id_user, $line[array_keys($line)[1]], $contact_name, $data);
if ($success)
{
++$nb_insert;
@ -351,7 +378,7 @@ namespace controllers\internals;
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Contact
{
$this->model = $this->model ?? new \models\Contact($this->bdd);

View File

@ -15,6 +15,23 @@ namespace controllers\internals;
{
protected $model;
/**
* @param int $id_user : User id
* @param ?int $limit : Number of entry to return
* @param ?int $offset : Number of entry to avoid
* @param ?string $search : String to search for
* @param ?array $search_columns : List of columns to search on
* @param ?string $order_column : Name of the column to order by
* @param bool $order_desc : Should result be ordered DESC, if false order ASC
* @param bool $count : Should the query only count results
*
* @return array : Entries list
*/
public function datatable_list_for_user(int $id_user, ?int $limit = null, ?int $offset = null, ?string $search = null, ?array $search_columns = [], ?string $order_column = null, bool $order_desc = false, $count = false)
{
return $this->get_model()->datatable_list_for_user($id_user, $limit, $offset, $search, $search_columns, $order_column, $order_desc, $count);
}
/**
* Disabled methods.
*/
@ -56,25 +73,10 @@ namespace controllers\internals;
return $this->get_model()->insert($event);
}
/**
* Gets events for a type, since a date and eventually until a date (both included).
*
* @param int $id_user : User id
* @param string $type : Event type we want
* @param \DateTime $since : Date to get events since
* @param ?\DateTime $until (optional) : Date until wich we want events, if not specified no limit
*
* @return array
*/
public function get_events_by_type_and_date_for_user(int $id_user, string $type, \DateTime $since, ?\DateTime $until = null)
{
$this->get_model()->get_events_by_type_and_date_for_user($id_user, $type, $since, $until);
}
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Event
{
$this->model = $this->model ?? new \models\Event($this->bdd);

View File

@ -11,6 +11,7 @@
namespace controllers\internals;
use DateTime;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
@ -37,15 +38,29 @@ class ExpressionProvider implements ExpressionFunctionProviderInterface
return isset($var);
});
//Check if today is birthday given a birthdate as DateTime
$is_birthday = new ExpressionFunction('is_birthday', function ($birthdate)
{
return sprintf('isset(%1$s) && is_a(%1$s, \'DateTime\') && %1$s->format(\'m-d\') == (new \\DateTime())->format(\'m-d\')', $birthdate);
}, function ($arguments, DateTime $birthdate)
{
return $birthdate->format('m-d') == (new DateTime())->format('m-d');
});
return [
$neutralized_constant,
$exists,
$is_birthday,
ExpressionFunction::fromPhp('mb_strtolower', 'lower'),
ExpressionFunction::fromPhp('mb_strtoupper', 'upper'),
ExpressionFunction::fromPhp('mb_substr', 'substr'),
ExpressionFunction::fromPhp('mb_strlen', 'strlen'),
ExpressionFunction::fromPhp('abs', 'abs'),
ExpressionFunction::fromPhp('strtotime', 'date'),
ExpressionFunction::fromPhp('date_create', 'date'),
ExpressionFunction::fromPhp('date_create_from_format', 'date_from_format'),
ExpressionFunction::fromPhp('intval', 'intval'),
ExpressionFunction::fromPhp('boolval', 'boolval'),
ExpressionFunction::fromPhp('count', 'count'),
];
}
}

View File

@ -130,7 +130,7 @@ namespace controllers\internals;
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Group
{
$this->model = $this->model ?? new \models\Group($this->bdd);

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of RaspiSMS.
*
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
*
* This source file is subject to the GPL-3.0 license that is bundled
* with this source code in the file LICENSE.
*/
namespace controllers\internals;
use Exception;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
/**
* Mailing class.
*/
class LinkShortener
{
/**
* Shorten an URL using the configured YOURLS instance
*/
public static function shorten($url)
{
$api_url = URL_SHORTENER['HOST'] . '/yourls-api.php';
$data = [
'action' => 'shorturl',
'format' => 'json',
'username' => URL_SHORTENER['USER'],
'password' => URL_SHORTENER['PASS'],
'url' => $url,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $api_url);
curl_setopt($ch, CURLOPT_HEADER, 0); // No header in the result
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Enable follow location
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return, do not echo result
curl_setopt($ch, CURLOPT_POST, 1); // This is a POST request
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$response = curl_exec($ch);
curl_close($ch);
try
{
$response = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
}
catch (\Exception $e)
{
return false;
}
$shortlink = $response['shorturl'] ?? false;
return $shortlink;
}
}

0
controllers/internals/Mailer.php Executable file → Normal file
View File

View File

@ -48,7 +48,7 @@ class Media extends StandardController
if (!file_put_contents($new_file_path, 'a'))
{
throw new \Exception('pute de merde');
throw new \Exception('Cannot write file ' . $new_file_path);
}
if (!rename($tmpfile_path, $new_file_path))
@ -56,16 +56,6 @@ class Media extends StandardController
throw new \Exception('Cannot create file ' . $new_file_path);
}
if (!chown($new_file_path, fileowner($user_path)))
{
throw new \Exception('Cannot give file ' . $new_file_path . ' to user : ' . fileowner($user_path));
}
if (!chgrp($new_file_path, filegroup($user_path)))
{
throw new \Exception('Cannot give file ' . $new_file_path . ' to group : ' . filegroup($user_path));
}
if (!chmod($new_file_path, self::DEFAULT_CHMOD))
{
throw new \Exception('Cannot give file ' . $new_file_path . ' rights : ' . self::DEFAULT_CHMOD);
@ -313,7 +303,7 @@ class Media extends StandardController
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Media
{
$this->model = $this->model ?? new \models\Media($this->bdd);

View File

@ -19,6 +19,18 @@ namespace controllers\internals;
protected $model;
/**
* Return all phones for active users.
*
* @param int $id_user : user id
*
* @return array
*/
public function get_all_for_active_users()
{
return $this->get_model()->get_all_for_active_users();
}
/**
* Return all phones of a user.
*
@ -32,15 +44,15 @@ namespace controllers\internals;
}
/**
* Return a phone by his name.
* Return a list of phone limits
*
* @param string $name : Phone name
* @param int $id_phone : Phone id
*
* @return array
*/
public function get_by_name(string $name)
public function get_limits(int $id_phone)
{
return $this->get_model()->get_by_name($name);
return $this->get_model()->get_limits($id_phone);
}
/**
@ -125,19 +137,46 @@ namespace controllers\internals;
* @param string $name : The name of the phone
* @param string $adapter : The adapter to use the phone
* @param string json $adapter_data : A JSON string representing adapter's data (for example credentials for an api)
* @param int $priority : Priority with which to use phone to send SMS. Default 0.
* @param array $limits : An array of limits for this phone. Each limit must be an array with a key volume and a key startpoint
*
* @return bool|int : false on error, new id on success
*/
public function create(int $id_user, string $name, string $adapter, string $adapter_data)
public function create(int $id_user, string $name, string $adapter, string $adapter_data, int $priority = 0, array $limits = [])
{
$phone = [
'id_user' => $id_user,
'name' => $name,
'priority' => $priority,
'adapter' => $adapter,
'adapter_data' => $adapter_data,
];
return $this->get_model()->insert($phone);
//Use transaction to garanty atomicity
$this->bdd->beginTransaction();
$new_phone_id = $this->get_model()->insert($phone);
if (!$new_phone_id)
{
$this->bdd->rollBack();
return false;
}
foreach ($limits as $limit)
{
$limit_id = $this->get_model()->insert_phone_limit($new_phone_id, $limit['volume'], $limit['startpoint']);
if (!$limit_id)
{
$this->bdd->rollBack();
return false;
}
}
$success = $this->bdd->commit();
return ($success ? $new_phone_id : false);
}
/**
@ -147,26 +186,73 @@ namespace controllers\internals;
* @param int $id : Phone id
* @param string $name : The name of the phone
* @param string $adapter : The adapter to use the phone
* @param array $adapter_data : An array of the data of the adapter (for example credentials for an api)
* @param string json $adapter_data : A JSON string representing adapter's data (for example credentials for an api)
* @param int $priority : Priority with which to use phone to send SMS. Default 0.
* @param array $limits : An array of limits for this phone. Each limit must be an array with a key volume and a key startpoint
*
* @return bool : false on error, true on success
*/
public function update_for_user(int $id_user, int $id, string $name, string $adapter, array $adapter_data): bool
public function update_for_user(int $id_user, int $id, string $name, string $adapter, string $adapter_data, int $priority = 0, array $limits = []): bool
{
$phone = [
'id_user' => $id_user,
'name' => $name,
'adapter' => $adapter,
'adapter_data' => json_encode($adapter_data),
'adapter_data' => $adapter_data,
'priority' => $priority,
];
return (bool) $this->get_model()->update_for_user($id_user, $id, $phone);
//Use transaction to garanty atomicity
$this->bdd->beginTransaction();
$nb_delete = $this->get_model()->delete_phone_limits($id);
foreach ($limits as $limit)
{
$limit_id = $this->get_model()->insert_phone_limit($id, $limit['volume'], $limit['startpoint']);
if (!$limit_id)
{
$this->bdd->rollBack();
return false;
}
}
$nb_update = $this->get_model()->update_for_user($id_user, $id, $phone);
$success = $this->bdd->commit();
if (!$success)
{
return false;
}
if ($nb_update == 0 && count($limits) == 0)
{
return false;
}
return true;
}
/**
* Update a phone status.
*
* @param int $id : Phone id
* @param string $status : The new status of the phone
*
* @return bool : false on error, true on success
*/
public function update_status(int $id, string $status) : bool
{
return (bool) $this->get_model()->update($id, ['status' => $status]);
}
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Phone
{
$this->model = $this->model ?? new \models\Phone($this->bdd);

View File

@ -0,0 +1,139 @@
<?php
/*
* This file is part of RaspiSMS.
*
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
*
* This source file is subject to the GPL-3.0 license that is bundled
* with this source code in the file LICENSE.
*/
namespace controllers\internals;
/**
* Classe des groups.
*/
class PhoneGroup extends StandardController
{
protected $model;
/**
* Create a new phone group for a user.
*
* @param int $id_user : user id
* @param stirng $name : Group name
* @param array $phones_ids : Ids of the phones of the group
*
* @return mixed bool|int : false on error, new group id
*/
public function create(int $id_user, string $name, array $phones_ids)
{
$group = [
'id_user' => $id_user,
'name' => $name,
];
$id_group = $this->get_model()->insert($group);
if (!$id_group)
{
return false;
}
$internal_phone = new Phone($this->bdd);
foreach ($phones_ids as $phone_id)
{
$phone = $internal_phone->get_for_user($id_user, $phone_id);
if (!$phone)
{
continue;
}
$this->get_model()->insert_phone_group_phone_relation($id_group, $phone_id);
}
$internal_event = new Event($this->bdd);
$internal_event->create($id_user, 'PHONE_GROUP_ADD', 'Ajout phone group : ' . $name);
return $id_group;
}
/**
* Update a phone group for a user.
*
* @param int $id_user : User id
* @param int $id_group : Group id
* @param stirng $name : Group name
* @param array $phones_ids : Ids of the phones of the group
*
* @return bool : False on error, true on success
*/
public function update_for_user(int $id_user, int $id_group, string $name, array $phones_ids)
{
$group = [
'name' => $name,
];
$result = $this->get_model()->update_for_user($id_user, $id_group, $group);
$this->get_model()->delete_phone_group_phone_relations($id_group);
$internal_phone = new Phone($this->bdd);
$nb_phone_insert = 0;
foreach ($phones_ids as $phone_id)
{
$phone = $internal_phone->get_for_user($id_user, $phone_id);
if (!$phone)
{
continue;
}
if ($this->get_model()->insert_phone_group_phone_relation($id_group, $phone_id))
{
++$nb_phone_insert;
}
}
if (!$result && $nb_phone_insert !== \count($phones_ids))
{
return false;
}
return true;
}
/**
* Return a group by his name for a user.
*
* @param int $id_user : User id
* @param string $name : Group name
*
* @return array
*/
public function get_by_name_for_user(int $id_user, string $name)
{
return $this->get_model()->get_by_name_for_user($id_user, $name);
}
/**
* Get groups phones.
*
* @param int $id_group : Group id
*
* @return array : phones of the group
*/
public function get_phones($id_group)
{
return $this->get_model()->get_phones($id_group);
}
/**
* Get the model for the Controller.
*/
protected function get_model(): \models\PhoneGroup
{
$this->model = $this->model ?? new \models\PhoneGroup($this->bdd);
return $this->model;
}
}

View File

@ -108,14 +108,13 @@ class Quota extends StandardController
}
/**
* Compute how many credit a message represent
* this function count 160 chars per SMS if it can be send as GSM 03.38 encoding and 70 chars per SMS if it can only be send as UTF8.
* Check if a message can be encoded as gsm0338 or if it must be UTF8.
*
* @param string $text : Message to send
*
* @return int : Number of credit to send this message
* @return bool : True if gsm0338, false if UTF8
*/
public static function compute_credits_for_message($text)
public static function is_gsm0338($text)
{
//Gsm 03.38 charset to detect if message is compatible or must use utf8
$gsm0338 = [
@ -144,12 +143,26 @@ class Quota extends StandardController
{
if (!in_array(mb_substr($text, $i, 1), $gsm0338))
{
$is_gsm0338 = false;
break;
return false;
}
}
return true;
}
/**
* Compute how many credit a message represent
* this function count 160 chars per SMS if it can be send as GSM 03.38 encoding and 70 chars per SMS if it can only be send as UTF8.
*
* @param string $text : Message to send
*
* @return int : Number of credit to send this message
*/
public static function compute_credits_for_message($text)
{
$len = mb_strlen($text);
$is_gsm0338 = self::is_gsm0338($text);
return $is_gsm0338 ? ceil($len / 160) : ceil($len / 70);
}
@ -239,7 +252,7 @@ class Quota extends StandardController
$renew_interval = $quota['renew_interval'] ?? 'P0D';
$new_start_date = new \DateTime($quota['expiration_date']);
$new_expiration_date = clone $new_start_date;
$new_expiration_date->add(new \DateInterval($quota['renew_interval']));
$new_expiration_date->add(new \DateInterval($renew_interval));
$report = 0;
if ($quota['report_unused'] && $unused_credit > 0)
@ -288,7 +301,7 @@ class Quota extends StandardController
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Quota
{
$this->model = $this->model ?? new \models\Quota($this->bdd);

View File

@ -11,10 +11,32 @@
namespace controllers\internals;
use Exception;
class Received extends StandardController
{
protected $model;
/**
* Return the list of entries for a user.
*
* @param int $id_user : User id
* @param ?int $limit : Number of entry to return
* @param ?int $offset : Number of entry to avoid
* @param ?string $search : String to search for
* @param ?array $search_columns : List of columns to search on
* @param ?string $order_column : Name of the column to order by
* @param bool $order_desc : Should result be ordered DESC, if false order ASC
* @param bool $count : Should the query only count results
* @param bool $unread : Should only unread messages be returned
*
* @return array : Entrys list
*/
public function datatable_list_for_user(int $id_user, ?int $limit = null, ?int $offset = null, ?string $search = null, ?array $search_columns = [], ?string $order_column = null, bool $order_desc = false, bool $count = false, bool $unread = false)
{
return $this->get_model()->datatable_list_for_user($id_user, $limit, $offset, $search, $search_columns, $order_column, $order_desc, $count, $unread);
}
/**
* Return the list of unread messages for a user.
*
@ -88,6 +110,32 @@ namespace controllers\internals;
return false;
}
//Check if the received message is a SMS STOP and we must register it
$internal_smsstop = new SmsStop($this->bdd);
$is_stop = $internal_smsstop->check_for_stop($received['text']);
if ($is_stop)
{
$stop_exists = (bool) $internal_smsstop->get_by_number_for_user($id_user, $origin);
if ($stop_exists)
{
return $id_received;
}
$internal_smsstop->create($id_user, $origin);
//If stop response enabled, respond to user
//(this will happen only for first stop, any further stop will not trigger responses)
$internal_setting = new Setting($this->bdd);
$user_settings = $internal_setting->gets_for_user($id_user);
if ((int) ($user_settings['smsstop_respond'] ?? false))
{
$response = $user_settings['smsstop_response'];
$internal_scheduled = new Scheduled($this->bdd);
$internal_scheduled->create($id_user, (new \DateTime())->format('Y-m-d H:i:s'), $response, $id_phone, null, false, false, \models\SmsStop::SMS_STOP_TAG, [['number' => $origin, 'data' => '[]']]);
}
}
return $id_received;
}
@ -347,7 +395,7 @@ namespace controllers\internals;
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Received
{
$this->model = $this->model ?? new \models\Received($this->bdd);

View File

@ -43,17 +43,14 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
{
try
{
$this->expression_language->parse($condition, array_keys($data));
//Use @ to hide notices on non defined vars
@$this->expression_language->parse($condition, array_keys($data));
return true;
}
catch (\Exception $e)
{
return false;
}
catch (\Throwable $t)
{
//Just ignore non critical php warning and notice
{ //Catch both, exceptions and php error
return false;
}
}
@ -69,12 +66,13 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
{
try
{
$result = $this->expression_language->evaluate($condition, $data);
//Use @ to hide notices on non defined vars
@$result = $this->expression_language->evaluate($condition, $data);
return (bool) $result;
}
catch (\Exception $e)
{
catch (\Throwable $t)
{ //Catch both, exceptions and php error
return null;
}
}

View File

@ -11,6 +11,9 @@
namespace controllers\internals;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
class Scheduled extends StandardController
{
protected $model;
@ -20,27 +23,31 @@ namespace controllers\internals;
*
* @param int $id_user : User to insert scheduled for
* @param $at : Scheduled date to send
* @param string $text : Text of the message
* @param ?int $id_phone : Id of the phone to send message with, null by default
* @param bool $flash : Is the sms a flash sms, by default false
* @param bool $mms : Is the sms a mms, by default false
* @param array $numbers : Numbers to send message to
* @param array $contacts_ids : Contact ids to send message to
* @param array $groups_ids : Group ids to send message to
* @param array $conditional_group_ids : Conditional Groups ids to send message to
* @param array $media_ids : Ids of the medias to link to scheduled message
* @param string $text : Text of the message
* @param ?int $id_phone : Id of the phone to send message with, null by default
* @param ?int $id_phone_group : Id of the phone group to send message with, null by default
* @param bool $flash : Is the sms a flash sms, by default false
* @param bool $mms : Is the sms a mms, by default false
* @param ?string $tag : A string tag to associate to sended SMS
* @param array $numbers : Array of numbers to send message to, a number is an array ['number' => '+33XXX', 'data' => '{"key":"value", ...}']
* @param array $contacts_ids : Contact ids to send message to
* @param array $groups_ids : Group ids to send message to
* @param array $conditional_group_ids : Conditional Groups ids to send message to
* @param array $media_ids : Ids of the medias to link to scheduled message
*
* @return bool : false on error, new id on success
*/
public function create(int $id_user, $at, string $text, ?int $id_phone = null, bool $flash = false, bool $mms = false, array $numbers = [], array $contacts_ids = [], array $groups_ids = [], array $conditional_group_ids = [], array $media_ids = [])
public function create(int $id_user, $at, string $text, ?int $id_phone = null, ?int $id_phone_group = null, bool $flash = false, bool $mms = false, ?string $tag = null, array $numbers = [], array $contacts_ids = [], array $groups_ids = [], array $conditional_group_ids = [], array $media_ids = [])
{
$scheduled = [
'id_user' => $id_user,
'at' => $at,
'text' => $text,
'id_phone' => $id_phone,
'id_phone_group' => $id_phone_group,
'flash' => $flash,
'mms' => $mms,
'tag' => $tag,
];
if ('' === $text)
@ -59,6 +66,17 @@ namespace controllers\internals;
}
}
if (null !== $id_phone_group)
{
$internal_phone_group = new PhoneGroup($this->bdd);
$find_phone_group = $internal_phone_group->get_for_user($id_user, $id_phone_group);
if (!$find_phone_group)
{
return false;
}
}
//Use transaction to garanty atomicity
$this->bdd->beginTransaction();
@ -84,7 +102,7 @@ namespace controllers\internals;
foreach ($numbers as $number)
{
$this->get_model()->insert_scheduled_number($id_scheduled, $number);
$this->get_model()->insert_scheduled_number($id_scheduled, $number['number'], $number['data']);
}
$internal_contact = new Contact($this->bdd);
@ -144,9 +162,11 @@ namespace controllers\internals;
* @param $at : Scheduled date to send
* @param string $text : Text of the message
* @param ?int $id_phone : Id of the phone to send message with, null by default
* @param ?int $id_phone_group : Id of the phone group to send message with, null by default
* @param bool $flash : Is the sms a flash sms, by default false
* @param bool $mms : Is the sms a mms, by default false
* @param array $numbers : Numbers to send message to
* @param ?string $tag : A string tag to associate to sended SMS
* @param array $numbers : Array of numbers to send message to, a number is an array ['number' => '+33XXX', 'data' => '{"key":"value", ...}']
* @param array $contacts_ids : Contact ids to send message to
* @param array $groups_ids : Group ids to send message to
* @param array $conditional_group_ids : Conditional Groups ids to send message to
@ -154,15 +174,17 @@ namespace controllers\internals;
*
* @return bool : false on error, true on success
*/
public function update_for_user(int $id_user, int $id_scheduled, $at, string $text, ?string $id_phone = null, bool $flash = false, bool $mms = false, array $numbers = [], array $contacts_ids = [], array $groups_ids = [], array $conditional_group_ids = [], array $media_ids = [])
public function update_for_user(int $id_user, int $id_scheduled, $at, string $text, ?int $id_phone = null, ?int $id_phone_group = null, bool $flash = false, bool $mms = false, ?string $tag = null, array $numbers = [], array $contacts_ids = [], array $groups_ids = [], array $conditional_group_ids = [], array $media_ids = [])
{
$scheduled = [
'id_user' => $id_user,
'at' => $at,
'text' => $text,
'id_phone' => $id_phone,
'id_phone_group' => $id_phone_group,
'mms' => $mms,
'flash' => $flash,
'tag' => $tag,
];
if (null !== $id_phone)
@ -176,6 +198,17 @@ namespace controllers\internals;
}
}
if (null !== $id_phone_group)
{
$internal_phone_group = new PhoneGroup($this->bdd);
$find_phone_group = $internal_phone_group->get_for_user($id_user, $id_phone_group);
if (!$find_phone_group)
{
return false;
}
}
//Ensure atomicity
$this->bdd->beginTransaction();
@ -201,7 +234,7 @@ namespace controllers\internals;
foreach ($numbers as $number)
{
$this->get_model()->insert_scheduled_number($id_scheduled, $number);
$this->get_model()->insert_scheduled_number($id_scheduled, $number['number'], $number['data']);
}
$internal_contact = new Contact($this->bdd);
@ -272,206 +305,69 @@ namespace controllers\internals;
}
/**
* Get all messages to send and the number to use to send theme.
* Parse a CSV file of numbers, potentially associated with datas.
*
* @return array : [['id_scheduled', 'text', 'id_phone', 'destination', 'flash', 'mms', 'medias'], ...]
* @param resource $file_handler : File handler pointing to the file
*
* @throws Exception : raise exception if file is not valid
*
* @return mixed : array of numbers ['number' => '+XXXX...', 'data' => ['key' => 'value', ...]]
*/
public function get_smss_to_send()
public function parse_csv_numbers_file($file_handler)
{
$smss_to_send = [];
$numbers = [];
$internal_templating = new \controllers\internals\Templating();
$internal_setting = new \controllers\internals\Setting($this->bdd);
$internal_group = new \controllers\internals\Group($this->bdd);
$internal_conditional_group = new \controllers\internals\ConditionalGroup($this->bdd);
$internal_phone = new \controllers\internals\Phone($this->bdd);
$users_settings = [];
$users_phones = [];
$users_mms_phones = [];
$now = new \DateTime();
$now = $now->format('Y-m-d H:i:s');
$scheduleds = $this->get_model()->gets_before_date($now);
foreach ($scheduleds as $scheduled)
$head = null;
$line_nb = 0;
while ($line = fgetcsv($file_handler))
{
if (!isset($users_settings[$scheduled['id_user']]))
++$line_nb;
if (null === $head)
{
$users_settings[$scheduled['id_user']] = [];
$head = $line;
$settings = $internal_setting->gets_for_user($scheduled['id_user']);
foreach ($settings as $name => $value)
{
$users_settings[$scheduled['id_user']][$name] = $value;
continue;
}
//Padding line with '' entries to make sure its same length as head
//this allow to mix users with data with users without data
$line = array_pad($line, \count($head), '');
$line = array_combine($head, $line);
if (false === $line)
{
continue;
}
$phone_number = \controllers\internals\Tool::parse_phone($line[array_keys($line)[0]] ?? '');
if (!$phone_number)
{
throw new \Exception('Erreur à la ligne ' . $line_nb . ' colonne 1, numéro de téléphone invalide.');
}
$data = [];
$i = 0;
foreach ($line as $key => $value)
{
++$i;
if ($i < 2)
{ // Ignore first column
continue;
}
}
if (!isset($users_phones[$scheduled['id_user']]))
{
$phones = $internal_phone->gets_for_user($scheduled['id_user']);
$mms_phones = $internal_phone->gets_phone_supporting_mms_for_user($scheduled['id_user'], $internal_phone::MMS_SENDING);
$users_phones[$scheduled['id_user']] = $phones ?: [];
$users_mms_phones[$scheduled['id_user']] = $mms_phones ?: [];
}
//Add medias to mms
if ($scheduled['mms'])
{
$internal_media = new Media($this->bdd);
$scheduled['medias'] = $internal_media->gets_for_scheduled($scheduled['id']);
}
$phone_to_use = null;
foreach ($users_phones[$scheduled['id_user']] as $phone)
{
if ($phone['id'] !== $scheduled['id_phone'])
if ('' === $value)
{
continue;
}
$phone_to_use = $phone;
$key = mb_ereg_replace('[\W]', '', $key);
$data[$key] = $value;
}
$messages = [];
//Add messages for numbers
$numbers = $this->get_numbers($scheduled['id']);
foreach ($numbers as $number)
{
if (null === $phone_to_use)
{
if ($scheduled['mms'] && count($users_mms_phones))
{
$rnd_key = array_rand($users_mms_phones[$scheduled['id_user']]);
$random_phone = $users_mms_phones[$scheduled['id_user']][$rnd_key];
}
else
{
$rnd_key = array_rand($users_phones[$scheduled['id_user']]);
$random_phone = $users_phones[$scheduled['id_user']][$rnd_key];
}
}
$message = [
'id_user' => $scheduled['id_user'],
'id_scheduled' => $scheduled['id'],
'id_phone' => $phone_to_use['id'] ?? $random_phone['id'],
'destination' => $number['number'],
'flash' => $scheduled['flash'],
'mms' => $scheduled['mms'],
'medias' => $scheduled['medias'],
];
if ((int) ($users_settings[$scheduled['id_user']]['templating'] ?? false))
{
$render = $internal_templating->render($scheduled['text']);
if (!$render['success'])
{
continue;
}
$message['text'] = $render['result'];
}
else
{
$message['text'] = $scheduled['text'];
}
$messages[] = $message;
}
//Add messages for contacts
$contacts = $this->get_contacts($scheduled['id']);
$groups = $this->get_groups($scheduled['id']);
foreach ($groups as $group)
{
$contacts_to_add = $internal_group->get_contacts($group['id']);
$contacts = array_merge($contacts, $contacts_to_add);
}
$conditional_groups = $this->get_conditional_groups($scheduled['id']);
foreach ($conditional_groups as $conditional_group)
{
$contacts_to_add = $internal_conditional_group->get_contacts_for_condition_and_user($scheduled['id_user'], $conditional_group['condition']);
$contacts = array_merge($contacts, $contacts_to_add);
}
$added_contacts = [];
foreach ($contacts as $contact)
{
if ($added_contacts[$contact['id']] ?? false)
{
continue;
}
$added_contacts[$contact['id']] = true;
if (null === $phone_to_use)
{
if ($scheduled['mms'] && count($users_mms_phones))
{
$rnd_key = array_rand($users_mms_phones[$scheduled['id_user']]);
$random_phone = $users_mms_phones[$scheduled['id_user']][$rnd_key];
}
else
{
$rnd_key = array_rand($users_phones[$scheduled['id_user']]);
$random_phone = $users_phones[$scheduled['id_user']][$rnd_key];
}
}
$message = [
'id_user' => $scheduled['id_user'],
'id_scheduled' => $scheduled['id'],
'id_phone' => $phone_to_use['id'] ?? $random_phone['id'],
'destination' => $contact['number'],
'flash' => $scheduled['flash'],
'mms' => $scheduled['mms'],
'medias' => $scheduled['medias'],
];
if ((int) ($users_settings[$scheduled['id_user']]['templating'] ?? false))
{
$contact['data'] = json_decode($contact['data'], true);
//Add metas of contact by adding contact without data
$metas = $contact;
unset($metas['data'], $metas['id_user']);
$data = ['contact' => $contact['data'], 'contact_metas' => $metas];
$render = $internal_templating->render($scheduled['text'], $data);
if (!$render['success'])
{
continue;
}
$message['text'] = $render['result'];
}
else
{
$message['text'] = $scheduled['text'];
}
$messages[] = $message;
}
foreach ($messages as $message)
{
//Remove empty messages
if ('' === trim($message['text']) && !$message['medias'])
{
continue;
}
$smss_to_send[] = $message;
}
$numbers[] = ['number' => $phone_number, 'data' => $data];
}
return $smss_to_send;
return $numbers;
}
/**
@ -525,10 +421,357 @@ namespace controllers\internals;
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Scheduled
{
$this->model = $this->model ?? new \models\Scheduled($this->bdd);
return $this->model;
}
/**
* Get all messages to send and the number to use to send theme.
*
* @return array : List of smss to send at this time per scheduled id ['1' => [['id_scheduled', 'text', 'id_phone', 'destination', 'flash', 'mms', 'medias'], ...], ...]
*/
public function get_smss_to_send()
{
$sms_per_scheduled = [];
$internal_templating = new \controllers\internals\Templating();
$internal_setting = new \controllers\internals\Setting($this->bdd);
$internal_group = new \controllers\internals\Group($this->bdd);
$internal_conditional_group = new \controllers\internals\ConditionalGroup($this->bdd);
$internal_phone = new \controllers\internals\Phone($this->bdd);
$internal_phone_group = new \controllers\internals\PhoneGroup($this->bdd);
$internal_smsstop = new \controllers\internals\SmsStop($this->bdd);
$internal_sended = new \controllers\internals\Sended($this->bdd);
$users_smsstops = [];
$users_settings = [];
$users_phones = [];
$users_phone_groups = [];
$shortlink_cache = [];
$now = new \DateTime();
$now = $now->format('Y-m-d H:i:s');
$scheduleds = $this->get_model()->gets_before_date($now);
foreach ($scheduleds as $scheduled)
{
$id_scheduled = $scheduled['id'];
$id_user = $scheduled['id_user'];
$sms_per_scheduled[$id_scheduled] = [];
// Forge cache of data about users, sms stops, phones, etc.
if (!isset($users_settings[$id_user]))
{
$users_settings[$id_user] = [];
$settings = $internal_setting->gets_for_user($id_user);
foreach ($settings as $name => $value)
{
$users_settings[$id_user][$name] = $value;
}
}
if (!isset($users_smsstops[$id_user]) && $users_settings[$id_user]['smsstop'])
{
$users_smsstops[$id_user] = [];
$smsstops = $internal_smsstop->gets_for_user($id_user);
foreach ($smsstops as $smsstop)
{
$users_smsstops[$id_user][] = $smsstop['number'];
}
}
if (!isset($users_phones[$id_user]))
{
$users_phones[$id_user] = [];
$phones = $internal_phone->gets_for_user($id_user);
foreach ($phones as &$phone)
{
$limits = $internal_phone->get_limits($phone['id']);
$remaining_volume = PHP_INT_MAX;
foreach ($limits as $limit)
{
$startpoint = new \DateTime($limit['startpoint']);
$consumed = $internal_sended->count_since_for_phone_and_user($id_user, $phone['id'], $startpoint);
$remaining_volume = min(($limit['volume'] - $consumed), $remaining_volume);
}
$phone['remaining_volume'] = $remaining_volume;
$users_phones[$id_user][$phone['id']] = $phone;
}
}
if (!isset($users_phone_groups[$id_user]))
{
$users_phone_groups[$id_user] = [];
$phone_groups = $internal_phone_group->gets_for_user($id_user);
foreach ($phone_groups as $phone_group)
{
$phones = $internal_phone_group->get_phones($phone_group['id']);
$phone_group['phones'] = [];
foreach ($phones as $phone)
{
$phone_group['phones'][] = $phone['id'];
}
$users_phone_groups[$id_user][$phone_group['id']] = $phone_group;
}
}
//Add medias to mms
$scheduled['medias'] = [];
if ($scheduled['mms'])
{
$internal_media = new Media($this->bdd);
$scheduled['medias'] = $internal_media->gets_for_scheduled($id_scheduled);
}
$phone_to_use = null;
if ($scheduled['id_phone'])
{
$phone_to_use = $users_phones[$id_user][$scheduled['id_phone']] ?? null;
}
$phone_group_to_use = null;
if ($scheduled['id_phone_group'])
{
$phone_group_to_use = $users_phone_groups[$id_user][$scheduled['id_phone_group']] ?? null;
}
// We turn all contacts, groups and conditional groups into just contacts
$contacts = $this->get_contacts($id_scheduled);
$groups = $this->get_groups($id_scheduled);
foreach ($groups as $group)
{
$contacts_to_add = $internal_group->get_contacts($group['id']);
$contacts = array_merge($contacts, $contacts_to_add);
}
$conditional_groups = $this->get_conditional_groups($id_scheduled);
foreach ($conditional_groups as $conditional_group)
{
$contacts_to_add = $internal_conditional_group->get_contacts_for_condition_and_user($id_user, $conditional_group['condition']);
$contacts = array_merge($contacts, $contacts_to_add);
}
// We turn all numbers and contacts into simple targets with number, data and meta so we can forge all messages from onlye one data source
$targets = [];
$numbers = $this->get_numbers($id_scheduled);
foreach ($numbers as $number)
{
$metas = ['number' => $number['number']];
$targets[] = [
'number' => $number['number'],
'data' => $number['data'],
'metas' => $metas,
];
}
foreach ($contacts as $contact)
{
$metas = $contact;
unset($metas['data'], $metas['id_user']);
$targets[] = [
'number' => $contact['number'],
'data' => $contact['data'],
'metas' => $metas,
];
}
// Pass on all targets to deduplicate destinations, remove number in sms stops, etc.
$used_destinations = [];
foreach ($targets as $key => $target)
{
if (in_array($target['number'], $used_destinations))
{
unset($targets[$key]);
continue;
}
//Remove messages to smsstops numbers if not with tag SMS_STOP
if ($scheduled['tag'] != \models\SmsStop::SMS_STOP_TAG && ($users_smsstops[$id_user] ?? false) && in_array($target['number'], $users_smsstops[$id_user]))
{
unset($targets[$key]);
continue;
}
$used_destinations[] = $target['number'];
}
// Finally, we forge all messages and select phone to use
foreach ($targets as $target)
{
// Forge message if templating enable
$text = $scheduled['text'];
if ((int) ($users_settings[$id_user]['templating'] ?? false)) // Cast to int because it is more reliable than bool on strings
{
$target['data'] = json_decode($target['data'], true);
$data = ['contact' => $target['data'], 'contact_metas' => $target['metas']];
$render = $internal_templating->render($scheduled['text'], $data);
if (!$render['success'])
{
continue;
}
$text = $render['result'];
}
// Ignore empty messages
if ('' === trim($text) && !$scheduled['medias'])
{
continue;
}
// If we must force GSM 7 alphabet
if ((int) ($users_settings[$id_user]['force_gsm_alphabet'] ?? false))
{
$text = Tool::convert_to_gsm0338($text);
}
// If the text contain http links we must replace them
if (ENABLE_URL_SHORTENER && ((int) ($users_settings[$id_user]['shorten_url'] ?? false)))
{
$http_links = Tool::search_http_links($text);
if ($http_links !== false)
{
foreach ($http_links as $http_link)
{
if (!array_key_exists($http_link, $shortlink_cache))
{
$shortlkink = LinkShortener::shorten($http_link);
// If link shortening failed, keep original one
if ($shortlkink === false)
{
continue;
}
$shortlink_cache[$http_link] = $shortlkink;
}
$shortlink = $shortlink_cache[$http_link];
$text = str_replace($http_link, $shortlink, $text);
}
}
}
/*
Choose phone if no phone defined for message
Phones are choosen using type, priority and remaining volume :
1 - If sms is a mms, try to use mms phone if any available. If mms phone available use mms phone, else use default.
2 - In group of phones, keep only phones with remaining volume. If no phones with remaining volume, use all phones instead.
3 - Groupe keeped phones by priority get group with biggest priority.
4 - Get a random phone in this group.
5 - If their is no phone matching, keep phone at null so sender will directly mark it as failed
*/
$random_phone = null;
if (null === $phone_to_use)
{
$phones_subset = $users_phones[$id_user];
if ($phone_group_to_use)
{
$phones_subset = array_filter($phones_subset, function ($phone) use ($phone_group_to_use) {
return in_array($phone['id'], $phone_group_to_use['phones']);
});
}
if ($scheduled['mms'])
{
$mms_only = array_filter($phones_subset, function ($phone) {
return $phone['adapter']::meta_support_mms_sending();
});
$phones_subset = $mms_only ?: $phones_subset;
}
// Keep only available phones
$remaining_volume_phones = array_filter($phones_subset, function ($phone) {
return $phone['status'] == \models\Phone::STATUS_AVAILABLE;
});
$phones_subset = $remaining_volume_phones ?: $phones_subset;
// Keep only phones with remaining volume
if ((int) ($users_settings[$id_user]['phone_limit'] ?? false))
{
$remaining_volume_phones = array_filter($phones_subset, function ($phone) {
return $phone['remaining_volume'] > 0;
});
$phones_subset = $remaining_volume_phones ?: $phones_subset;
}
if ((int) ($users_settings[$id_user]['phone_priority'] ?? false))
{
$max_priority_phones = [];
$max_priority = PHP_INT_MIN;
foreach ($phones_subset as $phone)
{
if ($phone['priority'] < $max_priority)
{
continue;
}
elseif ($phone['priority'] == $max_priority)
{
$max_priority_phones[] = $phone;
}
elseif ($phone['priority'] > $max_priority)
{
$max_priority_phones = [$phone];
$max_priority = $phone['priority'];
}
}
$phones_subset = $max_priority_phones;
}
if ($phones_subset)
{
$random_phone = $phones_subset[array_rand($phones_subset)];
}
}
// This should only happen if the user try to send a message without any phone in his account, then we simply ignore.
if (!$random_phone && !$phone_to_use)
{
continue;
}
$id_phone = $phone_to_use['id'] ?? $random_phone['id'];
$sms_per_scheduled[$id_scheduled][] = [
'id_user' => $id_user,
'id_scheduled' => $id_scheduled,
'id_phone' => $id_phone,
'destination' => $target['number'],
'flash' => $scheduled['flash'],
'mms' => $scheduled['mms'],
'tag' => $scheduled['tag'],
'medias' => $scheduled['medias'],
'text' => $text,
];
// Consume one sms from remaining volume of phone
$users_phones[$id_user][$id_phone]['remaining_volume'] --;
}
}
return $sms_per_scheduled;
}
}

View File

@ -11,10 +11,29 @@
namespace controllers\internals;
use Exception;
class Sended extends StandardController
{
protected $model;
/**
* @param int $id_user : User id
* @param ?int $limit : Number of entry to return
* @param ?int $offset : Number of entry to avoid
* @param ?string $search : String to search for
* @param ?array $search_columns : List of columns to search on
* @param ?string $order_column : Name of the column to order by
* @param bool $order_desc : Should result be ordered DESC, if false order ASC
* @param bool $count : Should the query only count results
*
* @return array : Entries list
*/
public function datatable_list_for_user(int $id_user, ?int $limit = null, ?int $offset = null, ?string $search = null, ?array $search_columns = [], ?string $order_column = null, bool $order_desc = false, $count = false)
{
return $this->get_model()->datatable_list_for_user($id_user, $limit, $offset, $search, $search_columns, $order_column, $order_desc, $count);
}
/**
* Create a sended.
*
@ -22,17 +41,19 @@ namespace controllers\internals;
* @param int $id_phone : Id of the number the message was send with
* @param $at : Reception date
* @param $text : Text of the message
* @param string $destination : Number of the receiver
* @param string $uid : Uid of the sms on the adapter service used
* @param string $adapter : Name of the adapter service used to send the message
* @param bool $flash : Is the sms a flash
* @param bool $mms : Is the sms a MMS. By default false.
* @param array $medias : Array of medias to link to the MMS
* @param string $status : Status of a the sms. By default \models\Sended::STATUS_UNKNOWN
* @param string $destination : Number of the receiver
* @param string $uid : Uid of the sms on the adapter service used
* @param string $adapter : Name of the adapter service used to send the message
* @param bool $flash : Is the sms a flash
* @param bool $mms : Is the sms a MMS. By default false.
* @param ?string $tag : A string tag to associate to sended SMS
* @param array $medias : Array of medias to link to the MMS
* @param ?int $originating_scheduled : Id of the scheduled message that was responsible for sending this message. By default null.
* @param string $status : Status of a the sms. By default \models\Sended::STATUS_UNKNOWN
*
* @return mixed : false on error, new sended id else
*/
public function create(int $id_user, int $id_phone, $at, string $text, string $destination, string $uid, string $adapter, bool $flash = false, bool $mms = false, array $medias = [], ?string $status = \models\Sended::STATUS_UNKNOWN)
public function create(int $id_user, int $id_phone, $at, string $text, string $destination, string $uid, string $adapter, bool $flash = false, bool $mms = false, ?string $tag = null, array $medias = [], ?int $originating_scheduled = null, ?string $status = \models\Sended::STATUS_UNKNOWN)
{
$sended = [
'id_user' => $id_user,
@ -44,7 +65,9 @@ namespace controllers\internals;
'adapter' => $adapter,
'flash' => $flash,
'mms' => $mms,
'tag' => $tag,
'status' => $status,
'originating_scheduled' => $originating_scheduled,
];
//Ensure atomicity
@ -88,24 +111,22 @@ namespace controllers\internals;
'status' => $status,
];
return (bool) $this->get_model()->update_for_user($id_user, $id_sended, $sended);
}
$success = (bool) $this->get_model()->update_for_user($id_user, $id_sended, $sended);
/**
* Update a sended status for a sended.
*
* @param int $id_sended : Sended id
* @param string $status : Status of a the sms (unknown, delivered, failed)
*
* @return bool : false on error, true on success
*/
public function update_status(int $id_sended, string $status): bool
{
$sended = [
if (!$success)
{
return $success;
}
$webhook = [
'id' => $id_sended,
'status' => $status,
];
return (bool) $this->get_model()->update($id_sended, $sended);
$internal_webhook = new Webhook($this->bdd);
$internal_webhook->trigger($id_user, \models\Webhook::TYPE_SEND_SMS_STATUS_CHANGE, $webhook);
return $success;
}
/**
@ -162,6 +183,22 @@ namespace controllers\internals;
return $this->get_model()->get_by_uid_and_adapter_for_user($id_user, $uid, $adapter);
}
/**
* Get number of sended SMS since a date for a phone
*
* @param int $id_user : User id
* @param int $id_phone : Phone id we want the number of sended message for
* @param ?\DateTime $since : Date since which we want sended number. Default to null.
* @param ?\DateTime $before : Date up to which we want sended number. Default to null.
* @param ?string $tag_like : Tag to filter sms by, this is not a = but a LIKE operator
*
* @return int
*/
public function count_since_for_phone_and_user(int $id_user, int $id_phone, ?\DateTime $since, ?\DateTime $before = null, ?string $tag_like = null): int
{
return $this->get_model()->count_since_for_phone_and_user($id_user, $id_phone, $since, $before, $tag_like);
}
/**
* Get number of sended SMS for every date since a date for a specific user.
*
@ -170,17 +207,9 @@ namespace controllers\internals;
*
* @return array
*/
public function count_by_day_since_for_user(int $id_user, $date)
public function count_by_day_and_status_since_for_user(int $id_user, $date)
{
$counts_by_day = $this->get_model()->count_by_day_since_for_user($id_user, $date);
$return = [];
foreach ($counts_by_day as $count_by_day)
{
$return[$count_by_day['at_ymd']] = $count_by_day['nb'];
}
return $return;
return $this->get_model()->count_by_day_and_status_since_for_user($id_user, $date);
}
/**
@ -203,34 +232,28 @@ namespace controllers\internals;
* @param int $id_user : Id of user to create sended message for
* @param int $id_phone : Id of the phone the message was send with
* @param $text : Text of the message
* @param string $destination : Number of the receiver
* @param bool $flash : Is the sms a flash. By default false.
* @param bool $mms : Is the sms a MMS. By default false.
* @param array $medias : Array of medias to link to the MMS
* @param string $status : Status of a the sms. By default \models\Sended::STATUS_UNKNOWN
* @param string $destination : Number of the receiver
* @param bool $flash : Is the sms a flash. By default false.
* @param ?string $tag : A string tag to associate to sended SMS
* @param bool $mms : Is the sms a MMS. By default false.
* @param array $medias : Array of medias to link to the MMS
* @param string $status : Status of a the sms. By default \models\Sended::STATUS_UNKNOWN
* @param null|mixed $originating_scheduled
*
* @return array : [
* bool 'error' => false if success, true else
* ?string 'error_message' => null if success, error message else
* ]
*/
public function send(\adapters\AdapterInterface $adapter, int $id_user, int $id_phone, string $text, string $destination, bool $flash = false, bool $mms = false, array $medias = [], string $status = \models\Sended::STATUS_UNKNOWN): array
public function send(\adapters\AdapterInterface $adapter, int $id_user, int $id_phone, string $text, string $destination, bool $flash = false, bool $mms = false, ?string $tag = null, array $medias = [], $originating_scheduled = null, string $status = \models\Sended::STATUS_UNKNOWN): array
{
$return = [
'error' => false,
'error_message' => null,
];
//If we reached our max quota, do not send the message
$internal_quota = new Quota($this->bdd);
$nb_credits = $internal_quota::compute_credits_for_message($text); //Calculate how much credit the message require
if (!$internal_quota->has_enough_credit($id_user, $nb_credits))
{
$return['error'] = false;
$return['error_message'] = 'Not enough credit to send message.';
return $return;
}
$internal_setting = new Setting();
$user_settings = $internal_setting->gets_for_user($id_user);
$at = (new \DateTime())->format('Y-m-d H:i:s');
$media_uris = [];
@ -254,42 +277,93 @@ namespace controllers\internals;
$text .= "\n" . join(' - ', $media_urls);
}
$response = $adapter->send($destination, $text, $flash, $mms, $media_uris);
try
{
//If we reached our max quota, do not send the message
$internal_quota = new Quota($this->bdd);
$nb_credits = $internal_quota::compute_credits_for_message($text); //Calculate how much credit the message require
if (!$internal_quota->has_enough_credit($id_user, $nb_credits))
{
throw new Exception('Not enough credit to send message.');
}
if ($response['error'])
// If this phone status indicate it is not available
$internal_phone = new Phone($this->bdd);
$phone = $internal_phone->get_for_user($id_user, $id_phone);
if (!$phone || $phone['status'] != \models\Phone::STATUS_AVAILABLE)
{
throw new Exception('Invalid phone status : ' . $phone['status']);
}
//If we reached limit for this phone and phone limits are enabled, do not send the message
if ((int) ($user_settings['phone_limit'] ?? false))
{
$limits = $internal_phone->get_limits($id_phone);
$remaining_volume = PHP_INT_MAX;
foreach ($limits as $limit)
{
$startpoint = new \DateTime($limit['startpoint']);
$consumed = $this->count_since_for_phone_and_user($id_user, $id_phone, $startpoint);
$remaining_volume = min(($limit['volume'] - $consumed), $remaining_volume);
}
if ($remaining_volume < 1)
{
throw new Exception('Phone send limit have been reached.');
}
}
$response = $adapter->send($destination, $text, $flash, $mms, $media_uris);
if ($response['error'])
{
throw new Exception($response['error_message']);
}
$uid = $response['uid'];
$status = \models\Sended::STATUS_UNKNOWN;
// If send with success, consume credit
$internal_quota->consume_credit($id_user, $nb_credits);
}
catch (Exception $e)
{
$return['error'] = true;
$return['error_message'] = $response['error_message'];
$return['error_message'] = $e->getMessage();
$status = \models\Sended::STATUS_FAILED;
$this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $mms, $medias, $status);
return $return;
}
finally
{
$uid = $uid ?? uniqid();
$sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $uid, $adapter->meta_classname(), $flash, $mms, $tag, $medias, $originating_scheduled, $status);
$webhook_body = [
'id' => $sended_id,
'at' => $at,
'status' => $status,
'text' => $text,
'destination' => $destination,
'origin' => $id_phone,
'mms' => $mms,
'medias' => $medias,
'originating_scheduled' => $originating_scheduled,
];
$internal_webhook = new Webhook($this->bdd);
$internal_webhook->trigger($id_user, \models\Webhook::TYPE_SEND_SMS, $webhook_body);
return $return;
}
$internal_quota->consume_credit($id_user, $nb_credits);
$sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $mms, $medias, $status);
$sended = [
'id' => $sended_id,
'at' => $at,
'text' => $text,
'destination' => $destination,
'origin' => $id_phone,
'mms' => $mms,
'medias' => $medias,
];
$internal_webhook = new Webhook($this->bdd);
$internal_webhook->trigger($id_user, \models\Webhook::TYPE_SEND_SMS, $sended);
return $return;
}
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Sended
{
$this->model = $this->model ?? new \models\Sended($this->bdd);

View File

@ -35,6 +35,18 @@ namespace controllers\internals;
return $settings_array;
}
/**
* Get a user setting by his name for a user.
*
* @param int $id_user : user id
*
* @return array
*/
public function get_by_name_for_user(int $id_user, string $name)
{
return $this->get_model()->get_by_name_for_user($id_user, $name);
}
/**
* Update a setting by his name and user id.
*
@ -89,7 +101,7 @@ namespace controllers\internals;
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Setting
{
$this->model = $this->model ?? new \models\Setting($this->bdd);

View File

@ -64,10 +64,23 @@ namespace controllers\internals;
return $this->get_model()->get_by_number_for_user($id_user, $number);
}
/**
* Parse a string to check if its a SMS stop.
*
* @param string $str : The string to check
*
* @return bool : true if sms stop, false else
*/
public function check_for_stop(string $str)
{
$str = trim(mb_strtolower($str));
return 'stop' == $str || 'stop sms' == $str;
}
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\SmsStop
{
$this->model = $this->model ?? new \models\SmsStop($this->bdd);

View File

@ -137,6 +137,8 @@ namespace controllers\internals;
/**
* Get the model for the Controller.
*
* @return \models\StandardModel
*/
abstract protected function get_model(): \descartes\Model;
abstract protected function get_model();
}

View File

@ -11,6 +11,8 @@
namespace controllers\internals;
use BenMorel\GsmCharsetConverter\Converter;
/**
* Some tools frequently used.
* Not a standard controller as it's not linked to a model in any way.
@ -83,6 +85,22 @@ namespace controllers\internals;
return '<a href="' . self::s($url, false, true, false) . '">' . self::s($number_format, false, true, false) . '</a>';
}
/**
* Check for http link in a text
*
* @param string $text : Text to search a link in
*
* @return bool|array : False if no link in the text, or an array of all http links
*/
public static function search_http_links($text)
{
$regex = "#http(s?)://\S+#i";
$matches = [];
$nb_matches = preg_match_all($regex, $text, $matches);
return $nb_matches > 0 ? $matches[0] : false;
}
/**
* Cette fonction fait la correspondance entre un type d'evenement et une icone font awesome.
*
@ -165,6 +183,24 @@ namespace controllers\internals;
return $objectDate && $objectDate->format($format) === $date;
}
/**
* Check if a relative format date (see https://www.php.net/manual/en/datetime.formats.relative.php) is valid.
*
* @param string $date : Relative date
*
* @return bool : True if valid, false else
*/
public static function validate_relative_date($date)
{
try {
$d = new \DateTime($date);
} catch (\Throwable $th) {
return false;
}
return true;
}
/**
* Check if a sting represent a valid PHP period for creating an interval.
*
@ -372,4 +408,39 @@ namespace controllers\internals;
return $new_dir;
}
/**
* Forge back an url parsed with PHP parse_url function.
*
* @param array $parsed_url : Parsed url returned by parse_url function
*
* @return string : The url as a string
*/
public static function unparse_url(array $parsed_url)
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "{$pass}@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "{$scheme}{$user}{$pass}{$host}{$port}{$path}{$query}{$fragment}";
}
/**
* Transform an UTF-8 string into a valid GSM 7 string (GSM 03.38), remplacing invalid chars with best equivalent whenever possible
*
* @param string $text : Input text to convert into gsm string
* @return string : An UTF-8 string with GSM alphabet only
*/
public static function convert_to_gsm0338(string $text): string
{
$converter = new Converter();
return $converter->cleanUpUtf8String($text, true, '?');
}
}

View File

@ -137,16 +137,16 @@ class Webhook extends StandardController
$error_code = null;
$queue = msg_get_queue(QUEUE_ID_WEBHOOK);
$success = msg_send($queue, QUEUE_TYPE_WEBHOOK, $message, true, true, $error_code);
return (bool) $success;
msg_send($queue, QUEUE_TYPE_WEBHOOK, $message, true, true, $error_code);
}
return true;
}
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
protected function get_model(): \models\Webhook
{
$this->model = $this->model ?? new \models\Webhook($this->bdd);

View File

@ -204,10 +204,11 @@ namespace controllers\publics;
return $this->redirect(\descartes\Router::url('Connect', 'login'));
}
/**
* Allow to stop impersonating a user
* @param mixed $csrf
* Allow to stop impersonating a user.
*
* @param mixed $csrf
*/
public function stop_impersonate()
{
@ -223,6 +224,7 @@ namespace controllers\publics;
$_SESSION = $old_session;
\FlashMessage\FlashMessage::push('success', 'Vous n\'incarnez plus l\'utilisateur ' . $user_email . '.');
return $this->redirect(\descartes\Router::url('Dashboard', 'show'));
}
}

View File

@ -33,6 +33,7 @@ namespace controllers\publics;
'SUSPENDED_USER' => 16,
'CANNOT_DELETE' => 32,
'CANNOT_UPLOAD_FILE' => 64,
'CANNOT_UPDATE' => 128,
];
const ERROR_MESSAGES = [
@ -43,10 +44,12 @@ namespace controllers\publics;
'SUSPENDED_USER' => 'This user account is currently suspended.',
'CANNOT_DELETE' => 'Cannot delete this entry.',
'CANNOT_UPLOAD_FILE' => 'Failed to upload or save an uploaded file : ',
'CANNOT_UPDATE' => 'Cannot update this entry : ',
];
private $internal_user;
private $internal_phone;
private $internal_phone_group;
private $internal_received;
private $internal_sended;
private $internal_scheduled;
@ -70,6 +73,7 @@ namespace controllers\publics;
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$this->internal_user = new \controllers\internals\User($bdd);
$this->internal_phone = new \controllers\internals\Phone($bdd);
$this->internal_phone_group = new \controllers\internals\PhoneGroup($bdd);
$this->internal_received = new \controllers\internals\Received($bdd);
$this->internal_sended = new \controllers\internals\Sended($bdd);
$this->internal_scheduled = new \controllers\internals\Scheduled($bdd);
@ -116,14 +120,14 @@ namespace controllers\publics;
/**
* List all entries of a certain type for the current user, sorted by id.
*
* @param string $entry_type : Type of entries we want to list ['sended', 'received', 'scheduled', 'contact', 'group', 'conditional_group', 'phone', 'media']
* @param string $entry_type : Type of entries we want to list ['sended', 'received', 'scheduled', 'contact', 'group', 'conditional_group', 'phone', 'phone_group', 'media']
* @param int $page : Pagination number, Default = 0. Group of 25 results.
*
* @return : List of entries
*/
public function get_entries(string $entry_type, int $page = 0)
{
$entry_types = ['sended', 'received', 'scheduled', 'contact', 'group', 'conditional_group', 'phone', 'media'];
$entry_types = ['sended', 'received', 'scheduled', 'contact', 'group', 'conditional_group', 'phone', 'phone_group', 'media'];
if (!\in_array($entry_type, $entry_types, true))
{
@ -176,6 +180,39 @@ namespace controllers\publics;
$entries[$key]['contacts'] = $this->internal_group->get_contacts($entry['id']);
}
}
// Special case for phone as we might need to remove adapter_data for security reason
elseif ('phone' == $entry_type)
{
foreach ($entries as $key => $entry)
{
if (!$entry['adapter']::meta_hide_data())
{
continue;
}
unset($entries[$key]['adapter_data']);
}
}
// Special case for phone group we must add phones because its a join
elseif ('phone_group' === $entry_type)
{
foreach ($entries as $key => $entry)
{
$phones = $this->internal_phone_group->get_phones($entry['id']);
// Hide meta data of phones if needed
foreach ($phones as &$phone)
{
if (!$phone['adapter']::meta_hide_data())
{
continue;
}
unset($phone['adapter_data']);
}
$entries[$key]['phones'] = $phones;
}
}
$return = self::DEFAULT_RETURN;
$return['response'] = $entries;
@ -195,18 +232,89 @@ namespace controllers\publics;
return $this->json($return);
}
/**
* Return info about volume of sms sended for a period
*
* @param ?string $_POST['start'] : Date from which to get sms volume, format Y-m-d H:i:s. Default to null.
* @param ?string $_POST['end'] : Date up to which to get sms volume, format Y-m-d H:i:s. Default to null.
* @param ?string $_POST['tag'] : Tag to filter SMS by. If set, only sended sms with a matching tag will be counted. Default to null.
*
* @return : List of entries
*/
public function get_usage()
{
$start = $_GET['start'] ?? null;
$end = $_GET['end'] ?? null;
$tag = $_GET['tag'] ?? null;
$return = self::DEFAULT_RETURN;
if ($start)
{
if (!\controllers\internals\Tool::validate_date($start, 'Y-m-d H:i:s'))
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'start must be a date of format "Y-m-d H:i:s".';
$this->auto_http_code(false);
return $this->json($return);
}
$start = new \DateTime($start);
}
if ($end)
{
if (!\controllers\internals\Tool::validate_date($end, 'Y-m-d H:i:s'))
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'end must be a date of format "Y-m-d H:i:s".';
$this->auto_http_code(false);
return $this->json($return);
}
$end = new \DateTime($end);
}
$total_sended = 0;
$phones_volumes = [];
$phones = $this->internal_phone->gets_for_user($this->user['id']);
foreach ($phones as $phone)
{
$nb_sended = $this->internal_sended->count_since_for_phone_and_user($this->user['id'], $phone['id'], $start, $end, $tag);
$total_sended += $nb_sended;
$phones_volumes[$phone['id']] = $nb_sended;
}
$return['response'] = [
'total' => $total_sended,
'phones_volumes' => $phones_volumes,
];
$this->auto_http_code(true);
return $this->json($return);
}
/**
* Schedule a message to be send.
*
* @param string $_POST['at'] : Date to send message at format Y-m-d H:i:s
* @param string $_POST['text'] : Text of the message to send
* @param string $_POST['id_phone'] : Default null. Id of phone to send the message from. If null use a random phone
* @param string $_POST['id_phone'] : Default null. Id of phone to send the message from. If null and id_phone_group null, use a random phone
* @param string $_POST['id_phone_group'] : Default null. Id of phone group to send the message from. If null abd id_phone null, use a random phone
* @param string $_POST['flash'] : Default false. Is the sms a flash sms.
* @param string $_POST['mms'] : Default false. Is the sms a mms.
* @param string $_POST['tag'] : Default null. Tag to associate to every sms of the campaign.
* @param string $_POST['numbers'] : Array of numbers to send message to
* @param string $_POST['contacts'] : Array of ids of contacts to send message to
* @param string $_POST['groups'] : Array of ids of groups to send message to
* @param string $_POST['conditional_groups'] : Array of ids of conditional groups to send message to
* @param string $_POST['numbers_csv'] : CSV file with numbers and potentially data associated with numbers for templating to send the sms to
*
* @return : Id of scheduled created
*/
@ -215,13 +323,16 @@ namespace controllers\publics;
$at = $_POST['at'] ?? false;
$text = $_POST['text'] ?? false;
$id_phone = empty($_POST['id_phone']) ? null : $_POST['id_phone'];
$id_phone_group = empty($_POST['id_phone_group']) ? null : $_POST['id_phone_group'];
$flash = (bool) ($_POST['flash'] ?? false);
$mms = (bool) ($_POST['mms'] ?? false);
$tag = $_POST['tag'] ?? null;
$numbers = $_POST['numbers'] ?? [];
$contacts = $_POST['contacts'] ?? [];
$groups = $_POST['groups'] ?? [];
$conditional_groups = $_POST['conditional_groups'] ?? [];
$files = $_FILES['medias'] ?? false;
$csv_file = $_FILES['numbers_csv'] ?? false;
$numbers = \is_array($numbers) ? $numbers : [$numbers];
$contacts = \is_array($contacts) ? $contacts : [$contacts];
@ -272,6 +383,36 @@ namespace controllers\publics;
return $this->json($return);
}
if (!is_string($at))
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . ' : at must be a string.';
$this->auto_http_code(false);
return $this->json($return);
}
if (!is_string($text))
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . ' : text must be a string.';
$this->auto_http_code(false);
return $this->json($return);
}
if (mb_strlen($text) > \models\Scheduled::SMS_LENGTH_LIMIT)
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . ' : text must be less than ' . \models\Scheduled::SMS_LENGTH_LIMIT . ' char.';
$this->auto_http_code(false);
return $this->json($return);
}
if (!\controllers\internals\Tool::validate_date($at, 'Y-m-d H:i:s'))
{
$return = self::DEFAULT_RETURN;
@ -282,7 +423,10 @@ namespace controllers\publics;
return $this->json($return);
}
if (($this->user['settings']['mms'] ?? false) && $mms)
$at = (string) $at;
$text = (string) $text;
if ($mms && !(int)($this->user['settings']['mms'] ?? false))
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
@ -292,17 +436,68 @@ namespace controllers\publics;
return $this->json($return);
}
if ($csv_file)
{
$uploaded_file = \controllers\internals\Tool::read_uploaded_file($csv_file);
if (!$uploaded_file['success'])
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'csv : ' . $uploaded_file['content'];
$this->auto_http_code(false);
return $this->json($return);
}
try
{
$csv_numbers = $this->internal_scheduled->parse_csv_numbers_file($uploaded_file['content'], true);
if (!$csv_numbers)
{
throw new \Exception('no valid number in csv file.');
}
foreach ($csv_numbers as $csv_number)
{
$csv_number['data'] = json_encode($csv_number['data']);
$numbers[] = $csv_number;
}
}
catch (\Exception $e)
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'csv : ' . $e->getMessage();
$this->auto_http_code(false);
return $this->json($return);
}
}
foreach ($numbers as $key => $number)
{
$number = \controllers\internals\Tool::parse_phone($number);
// If number is not an array turn it into an array
$number = is_array($number) ? $number : ['number' => $number, 'data' => '[]'];
$number['data'] = $number['data'] ?? '[]';
$number['number'] = \controllers\internals\Tool::parse_phone($number['number'] ?? '');
if (!$number)
if (!$number['number'])
{
unset($numbers[$key]);
continue;
}
if (null === json_decode($number['data']))
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'number data must be a valid json or leave not set.';
$this->auto_http_code(false);
return $this->json($return);
}
$numbers[$key] = $number;
}
@ -316,6 +511,16 @@ namespace controllers\publics;
return $this->json($return);
}
if ($id_phone && $id_phone_group)
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'id_phone, id_phone_group : You must specify at most one of id_phone or id_phone_group, not both.';
$this->auto_http_code(false);
return $this->json($return);
}
$phone = null;
if ($id_phone)
{
@ -332,13 +537,29 @@ namespace controllers\publics;
return $this->json($return);
}
$phone_group = null;
if ($id_phone_group)
{
$phone_group = $this->internal_phone_group->get_for_user($this->user['id'], $id_phone_group);
}
if ($id_phone_group && !$phone_group)
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'id_phone_group : You must specify an id_phone_group number among thoses of user phone groups.';
$this->auto_http_code(false);
return $this->json($return);
}
if ($mms)
{
foreach ($files_arrays as $file)
{
try
{
$new_media_id = $this->internal_media->upload_and_create_for_user($this->user['id'], $file);
$new_media_id = $this->internal_media->create_from_uploaded_file_for_user($this->user['id'], $file);
}
catch (\Exception $e)
{
@ -354,7 +575,7 @@ namespace controllers\publics;
}
}
$scheduled_id = $this->internal_scheduled->create($this->user['id'], $at, $text, $id_phone, $flash, $mms, $numbers, $contacts, $groups, $conditional_groups, $media_ids);
$scheduled_id = $this->internal_scheduled->create($this->user['id'], $at, $text, $id_phone, $id_phone_group, $flash, $mms, $tag, $numbers, $contacts, $groups, $conditional_groups, $media_ids);
if (!$scheduled_id)
{
$return = self::DEFAULT_RETURN;
@ -405,6 +626,8 @@ namespace controllers\publics;
* @param string $_POST['name'] : Phone name
* @param string $_POST['adapter'] : Phone adapter
* @param array $_POST['adapter_data'] : Phone adapter data
* @param int $priority : Priority with which to use phone to send SMS. Default 0.
* @param ?array $_POST['limits'] : Array of limits in number of SMS for a period to be applied to this phone.
*
* @return int : id phone the new phone on success
*/
@ -415,6 +638,10 @@ namespace controllers\publics;
$name = $_POST['name'] ?? false;
$adapter = $_POST['adapter'] ?? false;
$adapter_data = !empty($_POST['adapter_data']) ? $_POST['adapter_data'] : [];
$priority = $_POST['priority'] ?? 0;
$priority = max(((int) $priority), 0);
$limits = $_POST['limits'] ?? [];
$limits = is_array($limits) ? $limits : [$limits];
if (!$name)
{
@ -434,7 +661,7 @@ namespace controllers\publics;
return $this->json($return);
}
$name_exist = $this->internal_phone->get_by_name($name);
$name_exist = $this->internal_phone->get_by_name_and_user($this->user['id'], $name);
if ($name_exist)
{
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
@ -444,6 +671,36 @@ namespace controllers\publics;
return $this->json($return);
}
if ($limits)
{
foreach ($limits as $key => $limit)
{
if (!is_array($limit))
{
unset($limits[$key]);
continue;
}
$startpoint = $limit['startpoint'] ?? false;
$volume = $limit['volume'] ?? false;
if (!$startpoint || !$volume)
{
unset($limits[$key]);
continue;
}
$volume = (int) $volume;
$limits[$key]['volume'] = max($volume, 1);
if (!\controllers\internals\Tool::validate_relative_date($startpoint))
{
unset($limits[$key]);
continue;
}
}
}
$adapters = $this->internal_adapter->list_adapters();
$find_adapter = false;
foreach ($adapters as $metas)
@ -526,7 +783,7 @@ namespace controllers\publics;
return $this->json($return);
}
$phone_id = $this->internal_phone->create($this->user['id'], $name, $adapter, $adapter_data);
$phone_id = $this->internal_phone->create($this->user['id'], $name, $adapter, $adapter_data, $priority, $limits);
if (false === $phone_id)
{
$return['error'] = self::ERROR_CODES['CANNOT_CREATE'];
@ -542,6 +799,191 @@ namespace controllers\publics;
return $this->json($return);
}
/**
* Update an existing phone.
*
* @param int $id : Id of phone to update
* @param string (optionnal) $_POST['name'] : New phone name
* @param string (optionnal) $_POST['adapter'] : New phone adapter
* @param array (optionnal) $_POST['adapter_data'] : New phone adapter data
* @param int $priority : Priority with which to use phone to send SMS. Default 0.
*
* @return int : id phone the new phone on success
*/
public function post_update_phone(int $id)
{
$return = self::DEFAULT_RETURN;
$phone = $this->internal_phone->get_for_user($this->user['id'], $id);
if (!$phone)
{
$return['error'] = self::ERROR_CODES['CANNOT_UPDATE'];
$return['message'] = self::ERROR_MESSAGES['CANNOT_UPDATE'] . ' No phone with this id.';
$this->auto_http_code(false);
return $this->json($return);
}
$limits = $this->internal_phone->get_limits(($phone['id']));
$name = $_POST['name'] ?? $phone['name'];
$priority = $_POST['priority'] ?? $phone['priority'];
$priority = max(((int) $priority), 0);
$adapter = $_POST['adapter'] ?? $phone['adapter'];
$adapter_data = !empty($_POST['adapter_data']) ? $_POST['adapter_data'] : json_decode($phone['adapter_data'], true);
$adapter_data = is_array($adapter_data) ? $adapter_data : [$adapter_data];
$limits = $_POST['limits'] ?? $limits;
$limits = is_array($limits) ? $limits : [$limits];
if (!$name && !$adapter && !$adapter_data)
{
$return['error'] = self::ERROR_CODES['MISSING_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['MISSING_PARAMETER'] . ' You must specify at least one name, adapter or adapter_data.';
$this->auto_http_code(false);
return $this->json($return);
}
$phone_with_same_name = $this->internal_phone->get_by_name_and_user($this->user['id'], $name);
if ($phone_with_same_name && $phone_with_same_name['id'] != $phone['id'])
{
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . ' This name is already used for another phone.';
$this->auto_http_code(false);
return $this->json($return);
}
if ($limits)
{
foreach ($limits as $key => $limit)
{
if (!is_array($limit))
{
unset($limits[$key]);
continue;
}
$startpoint = $limit['startpoint'] ?? false;
$volume = $limit['volume'] ?? false;
if (!$startpoint || !$volume)
{
unset($limits[$key]);
continue;
}
$volume = (int) $volume;
$limits[$key]['volume'] = max($volume, 1);
if (!\controllers\internals\Tool::validate_relative_date($startpoint))
{
unset($limits[$key]);
continue;
}
}
}
$adapters = $this->internal_adapter->list_adapters();
$find_adapter = false;
foreach ($adapters as $metas)
{
if ($metas['meta_classname'] === $adapter)
{
$find_adapter = $metas;
break;
}
}
if (!$find_adapter)
{
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . ' adapter. Adapter "' . $adapter . '" does not exists.';
$this->auto_http_code(false);
return $this->json($return);
}
//If missing required data fields, error
foreach ($find_adapter['meta_data_fields'] as $field)
{
if (false === $field['required'])
{
continue;
}
if (!empty($adapter_data[$field['name']]))
{
continue;
}
$return['error'] = self::ERROR_CODES['MISSING_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['MISSING_PARAMETER'] . ' You must speicify param ' . $field['name'] . ' (' . $field['description'] . ') for this phone.';
$this->auto_http_code(false);
return $this->json($return);
}
//If field phone number is invalid
foreach ($find_adapter['meta_data_fields'] as $field)
{
if (false === ($field['number'] ?? false))
{
continue;
}
if (!empty($adapter_data[$field['name']]))
{
$adapter_data[$field['name']] = \controllers\internals\Tool::parse_phone($adapter_data[$field['name']]);
if ($adapter_data[$field['name']])
{
continue;
}
}
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . ' field ' . $field['name'] . ' is not a valid phone number.';
$this->auto_http_code(false);
return $this->json($return);
}
$adapter_data_json = json_encode($adapter_data);
//Check adapter is working correctly with thoses names and data
$adapter_classname = $find_adapter['meta_classname'];
$adapter_instance = new $adapter_classname($adapter_data_json);
$adapter_working = $adapter_instance->test();
if (!$adapter_working)
{
$return['error'] = self::ERROR_CODES['CANNOT_UPDATE'];
$return['message'] = self::ERROR_MESSAGES['CANNOT_UPDATE'] . ' : Impossible to validate this phone, verify adapters parameters.';
$this->auto_http_code(false);
return $this->json($return);
}
$success = $this->internal_phone->update_for_user($this->user['id'], $phone['id'], $name, $adapter, $adapter_data_json, $priority, $limits);
if (!$success)
{
$return['error'] = self::ERROR_CODES['CANNOT_UPDATE'];
$return['message'] = self::ERROR_MESSAGES['CANNOT_UPDATE'];
$this->auto_http_code(false);
return $this->json($return);
}
$return['response'] = $success;
$this->auto_http_code(true);
return $this->json($return);
}
/**
* Delete a phone.
*
@ -568,4 +1010,62 @@ namespace controllers\publics;
return $this->json($return);
}
/**
* Trigger re-checking of a phone status
*
* @param int $id : Id of phone to re-check status
*/
public function post_update_phone_status ($id)
{
$return = self::DEFAULT_RETURN;
$phone = $this->internal_phone->get_for_user($this->user['id'], $id);
if (!$phone)
{
$return['error'] = self::ERROR_CODES['CANNOT_UPDATE'];
$return['message'] = self::ERROR_MESSAGES['CANNOT_UPDATE'];
$this->auto_http_code(false);
return $this->json($return);
}
// If user have activated phone limits, check if RaspiSMS phone limit have already been reached
$limit_reached = false;
if ((int) ($this->user['settings']['phone_limit'] ?? false))
{
$limits = $this->internal_phone->get_limits($id);
$remaining_volume = PHP_INT_MAX;
foreach ($limits as $limit)
{
$startpoint = new \DateTime($limit['startpoint']);
$consumed = $this->internal_sended->count_since_for_phone_and_user($this->user['id'], $id, $startpoint);
$remaining_volume = min(($limit['volume'] - $consumed), $remaining_volume);
}
if ($remaining_volume < 1)
{
$limit_reached = true;
}
}
if ($limit_reached)
{
$new_status = \models\Phone::STATUS_LIMIT_REACHED;
}
else
{
//Check status on provider side
$adapter_classname = $phone['adapter'];
$adapter_instance = new $adapter_classname($phone['adapter_data']);
$new_status = $adapter_instance->check_phone_status();
}
$status_update = $this->internal_phone->update_status($id, $new_status);
$return['response'] = $new_status;
$this->auto_http_code(true);
return $this->json($return);
}
}

View File

@ -155,6 +155,14 @@ use Monolog\Logger;
{
$this->logger->info('Callback reception call with adapter uid : ' . $adapter_uid);
$phone = $this->internal_phone->get_for_user($this->user['id'], $id_phone);
if (!$phone)
{
$this->logger->error('Callback reception use non existing phone : ' . $id_phone);
return false;
}
//Search for an adapter
$find_adapter = false;
$adapters = $this->internal_adapter->list_adapters();

View File

@ -185,6 +185,45 @@ namespace controllers\publics;
return $this->redirect(\descartes\Router::url('ConditionalGroup', 'list'));
}
/**
* Return contacts of a group as json array
* @param int $id_group = Group id
*
* @return json
*/
public function preview (int $id_group)
{
$return = [
'success' => false,
'result' => 'Une erreur inconnue est survenue.',
];
$group = $this->internal_conditional_group->get_for_user($_SESSION['user']['id'], $id_group);
if (!$group)
{
$return['result'] = 'Ce groupe n\'existe pas.';
echo json_encode($return);
return false;
}
$contacts = $this->internal_conditional_group->get_contacts_for_condition_and_user($_SESSION['user']['id'], $group['condition']);
if (!$contacts)
{
$return['result'] = 'Aucun contact dans le groupe.';
echo json_encode($return);
return false;
}
$return['success'] = true;
$return['result'] = array_values($contacts);
echo json_encode($return);
return true;
}
/**
* Try to get the preview of contacts for a conditionnal group.
*

View File

@ -164,5 +164,4 @@ namespace controllers\publics;
return $this->redirect(\descartes\Router::url('Connect', 'login'));
}
}

View File

@ -11,6 +11,8 @@
namespace controllers\publics;
use Exception;
/**
* Page des contacts.
*/
@ -50,14 +52,37 @@ namespace controllers\publics;
*/
public function list_json()
{
$entities = $this->internal_contact->list_for_user($_SESSION['user']['id']);
$draw = (int) ($_GET['draw'] ?? false);
$columns = [
0 => 'name',
1 => 'number',
2 => 'created_at',
3 => 'updated_at',
];
$search = $_GET['search']['value'] ?? null;
$order_column = $columns[$_GET['order'][0]['column']] ?? null;
$order_desc = ($_GET['order'][0]['dir'] ?? 'asc') == 'desc' ? true : false;
$offset = (int) ($_GET['start'] ?? 0);
$limit = (int) ($_GET['length'] ?? 25);
$entities = $this->internal_contact->datatable_list_for_user($_SESSION['user']['id'], $limit, $offset, $search, $columns, $order_column, $order_desc);
$count_entities = $this->internal_contact->datatable_list_for_user($_SESSION['user']['id'], $limit, $offset, $search, $columns, $order_column, $order_desc, true);
foreach ($entities as &$entity)
{
$entity['number_formatted'] = \controllers\internals\Tool::phone_link($entity['number']);
}
$records_total = $this->internal_contact->count_for_user($_SESSION['user']['id']);
header('Content-Type: application/json');
echo json_encode(['data' => $entities]);
echo json_encode([
'draw' => $draw,
'recordsTotal' => $records_total,
'recordsFiltered' => $count_entities,
'data' => $entities,
]);
}
/**
@ -322,39 +347,28 @@ namespace controllers\publics;
return $this->redirect(\descartes\Router::url('Contact', 'list'));
}
//Try to import file
$invalid_type = false;
switch ($read_file['mime_type'])
try
{
case 'text/csv':
$result = $this->internal_contact->import_csv($id_user, $read_file['content']);
break;
case 'application/json':
$result = $this->internal_contact->import_json($id_user, $read_file['content']);
break;
default:
if ('csv' === $read_file['extension'])
{
$result = false;
switch (true)
{
case ($read_file['mime_type'] === 'text/csv' || 'csv' === $read_file['extension']) :
$result = $this->internal_contact->import_csv($id_user, $read_file['content']);
}
elseif ('json' === $read_file['extension'])
{
$result = $this->internal_contact->import_json($id_user, $read_file['content']);
}
else
{
$invalid_type = true;
}
}
if ($invalid_type)
break;
case ($read_file['mime_type'] === 'text/json' || 'json' === $read_file['extension']) :
$result = $this->internal_contact->import_json($id_user, $read_file['content']);
break;
default:
throw new Exception('Le type de fichier n\'est pas valide.');
}
}
catch (\Exception $e)
{
\FlashMessage\FlashMessage::push('danger', 'Le type de fichier n\'est pas valide.');
\FlashMessage\FlashMessage::push('danger', 'Erreur lors de l\'import: ' . $e->getMessage());
return $this->redirect(\descartes\Router::url('Contact', 'list'));
}

View File

@ -87,11 +87,12 @@ namespace controllers\publics;
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
}
$nb_sendeds_by_day = $this->internal_sended->count_by_day_since_for_user($id_user, $stats_start_date_formated);
$nb_sendeds_by_day = $this->internal_sended->count_by_day_and_status_since_for_user($id_user, $stats_start_date_formated);
$nb_receiveds_by_day = $this->internal_received->count_by_day_since_for_user($id_user, $stats_start_date_formated);
//On va traduire ces données pour les afficher en graphique
$array_area_chart = [];
$array_bar_chart_sended = [];
$array_bar_chart_received = [];
$date = clone $stats_start_date;
$one_day = new \DateInterval('P1D');
@ -101,12 +102,15 @@ namespace controllers\publics;
while ($date <= $now)
{
$date_f = $date->format('Y-m-d');
$array_area_chart[$date_f] = [
$array_bar_chart_sended[$date_f] = [
'period' => $date_f,
'sendeds' => 0,
'receiveds' => 0,
'sendeds_failed' => 0,
'sendeds_unknown' => 0,
'sendeds_delivered' => 0,
];
$array_bar_chart_received[$date_f] = ['period' => $date_f, 'receiveds' => 0];
$date->add($one_day);
}
@ -114,15 +118,16 @@ namespace controllers\publics;
$total_receiveds = 0;
//0n remplie le tableau avec les données adaptées
foreach ($nb_sendeds_by_day as $date => $nb_sended)
foreach ($nb_sendeds_by_day as $nb_sended)
{
$array_area_chart[$date]['sendeds'] = $nb_sended;
$total_sendeds += $nb_sended;
$array_bar_chart_sended[$nb_sended['at_ymd']]['sendeds_' . $nb_sended['status']] = $nb_sended['nb'];
$array_bar_chart_sended[$nb_sended['at_ymd']]['sendeds_total'] = ($array_bar_chart_sended[$nb_sended['at_ymd']]['sendeds_total'] ?? 0) + $nb_sended['nb'];
$total_sendeds += $nb_sended['nb'];
}
foreach ($nb_receiveds_by_day as $date => $nb_received)
{
$array_area_chart[$date]['receiveds'] = $nb_received;
$array_bar_chart_received[$date]['receiveds'] = $nb_received;
$total_receiveds += $nb_received;
}
@ -130,7 +135,8 @@ namespace controllers\publics;
$avg_sendeds = round($total_sendeds / $nb_days, 2);
$avg_receiveds = round($total_receiveds / $nb_days, 2);
$array_area_chart = array_values($array_area_chart);
$array_bar_chart_sended = array_values($array_bar_chart_sended);
$array_bar_chart_received = array_values($array_bar_chart_received);
$this->render('dashboard/show', [
'nb_contacts' => $nb_contacts,
@ -145,7 +151,9 @@ namespace controllers\publics;
'sendeds' => $sendeds,
'receiveds' => $receiveds,
'events' => $events,
'data_area_chart' => json_encode($array_area_chart),
'data_bar_chart_sended' => json_encode($array_bar_chart_sended),
'data_bar_chart_received' => json_encode($array_bar_chart_received),
'stats_start_date_formated' => $stats_start_date_formated,
]);
}
}

View File

@ -94,7 +94,7 @@ namespace controllers\publics;
}
/**
* Cette fonction récupère l'ensemble des messages pour un numéro, recçus, envoyés, en cours.
* Cette fonction récupère l'ensemble des messages pour un numéro, reçus, envoyés, en cours.
*
* @param string $number : Le numéro cible
* @param string $transaction_id : Le numéro unique de la transaction ajax (sert à vérifier si la requete doit être prise en compte)
@ -234,6 +234,7 @@ namespace controllers\publics;
$at = $now;
$text = $_POST['text'] ?? '';
$destination = $_POST['destination'] ?? false;
$tag = $_POST['tag'] ?? null;
$id_phone = $_POST['id_phone'] ?? false;
$files = $_FILES['medias'] ?? false;
@ -313,9 +314,9 @@ namespace controllers\publics;
$mms = (bool) count($media_ids);
//Destinations must be an array of number
$destinations = [$destination];
$destinations = [['number' => $destination, 'data' => '[]']];
if (!$this->internal_scheduled->create($id_user, $at, $text, $id_phone, false, $mms, $destinations, [], [], [], $media_ids))
if (!$this->internal_scheduled->create($id_user, $at, $text, $id_phone, null, false, $mms, $tag, $destinations, [], [], [], $media_ids))
{
$return['success'] = false;
$return['message'] = 'Impossible de créer le Sms';
@ -328,79 +329,4 @@ namespace controllers\publics;
return true;
}
/**
* Cette fonction retourne les id des sms qui sont envoyés.
*
* @return string : json string Tableau des ids des sms qui sont envoyés
*/
public function checksendeds()
{
$_SESSION['discussion_wait_progress'] = isset($_SESSION['discussion_wait_progress']) ? $_SESSION['discussion_wait_progress'] : [];
$scheduleds = $this->internal_scheduled->gets_in_for_user($_SESSION['user']['id'], $_SESSION['discussion_wait_progress']);
//On va chercher à chaque fois si on a trouvé le sms. Si ce n'est pas le cas c'est qu'il a été envoyé
$sendeds = [];
foreach ($_SESSION['discussion_wait_progress'] as $key => $id_scheduled)
{
$found = false;
foreach ($scheduleds as $scheduled)
{
if ($id_scheduled === $scheduled['id'])
{
$found = true;
}
}
if (!$found)
{
unset($_SESSION['discussion_wait_progress'][$key]);
$sendeds[] = $id_scheduled;
}
}
echo json_encode($sendeds);
return true;
}
/**
* Cette fonction retourne les messages reçus pour un numéro après la date $_SESSION['discussion_last_checkreceiveds'].
*
* @param string $number : Le numéro de téléphone pour lequel on veux les messages
*
* @return string : json string Un tableau avec les messages
*/
public function checkreceiveds($number)
{
$now = new \DateTime();
$now = $now->format('Y-m-d H:i');
$_SESSION['discussion_last_checkreceiveds'] = isset($_SESSION['discussion_last_checkreceiveds']) ? $_SESSION['discussion_last_checkreceiveds'] : $now;
$receiveds = $this->internal_received->get_since_for_number_by_date($_SESSION['discussion_last_checkreceiveds'], $number);
//On va gérer le cas des messages en double en stockant ceux déjà reçus et en eliminant les autres
$_SESSION['discussion_already_receiveds'] = isset($_SESSION['discussion_already_receiveds']) ? $_SESSION['discussion_already_receiveds'] : [];
foreach ($receiveds as $key => $received)
{
//Sms jamais recu
if (false === array_search($received['id'], $_SESSION['discussion_already_receiveds'], true))
{
$_SESSION['discussion_already_receiveds'][] = $received['id'];
continue;
}
//Sms déjà reçu => on le supprime des resultats
unset($receiveds[$key]);
}
//On met à jour la date de dernière verif
$_SESSION['discussion_last_checkreceiveds'] = $now;
echo json_encode($receiveds);
}
}

View File

@ -48,14 +48,37 @@ namespace controllers\publics;
*/
public function list_json()
{
$entities = $this->internal_event->list_for_user($_SESSION['user']['id']);
$draw = (int) ($_GET['draw'] ?? false);
$columns = [
0 => 'type',
1 => 'at',
2 => 'text',
3 => 'updated_at',
];
$search = $_GET['search']['value'] ?? null;
$order_column = $columns[$_GET['order'][0]['column']] ?? null;
$order_desc = ($_GET['order'][0]['dir'] ?? 'asc') == 'desc' ? true : false;
$offset = (int) ($_GET['start'] ?? 0);
$limit = (int) ($_GET['length'] ?? 25);
$entities = $this->internal_event->datatable_list_for_user($_SESSION['user']['id'], $limit, $offset, $search, $columns, $order_column, $order_desc);
$count_entities = $this->internal_event->datatable_list_for_user($_SESSION['user']['id'], $limit, $offset, $search, $columns, $order_column, $order_desc, true);
foreach ($entities as &$entity)
{
$entity['icon'] = \controllers\internals\Tool::event_type_to_icon($entity['type']);
}
$records_total = $this->internal_event->count_for_user($_SESSION['user']['id']);
header('Content-Type: application/json');
echo json_encode(['data' => $entities]);
echo json_encode([
'draw' => $draw,
'recordsTotal' => $records_total,
'recordsFiltered' => $count_entities,
'data' => $entities,
]);
}
/**

View File

@ -191,6 +191,45 @@ namespace controllers\publics;
return $this->redirect(\descartes\Router::url('Group', 'list'));
}
/**
* Return contacts of a group as json array
* @param int $id_group = Group id
*
* @return json
*/
public function preview (int $id_group)
{
$return = [
'success' => false,
'result' => 'Une erreur inconnue est survenue.',
];
$group = $this->internal_group->get_for_user($_SESSION['user']['id'], $id_group);
if (!$group)
{
$return['result'] = 'Ce groupe n\'existe pas.';
echo json_encode($return);
return false;
}
$contacts = $this->internal_group->get_contacts($id_group);
if (!$contacts)
{
$return['result'] = 'Aucun contact dans le groupe.';
echo json_encode($return);
return false;
}
$return['success'] = true;
$return['result'] = $contacts;
echo json_encode($return);
return true;
}
/**
* Cette fonction retourne la liste des groups sous forme JSON.
*/

View File

@ -18,11 +18,13 @@ class Phone extends \descartes\Controller
{
private $internal_phone;
private $internal_adapter;
private $internal_sended;
public function __construct()
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$this->internal_phone = new \controllers\internals\Phone($bdd);
$this->internal_sended = new \controllers\internals\Sended($bdd);
$this->internal_adapter = new \controllers\internals\Adapter();
\controllers\internals\Tool::verifyconnect();
@ -55,6 +57,9 @@ class Phone extends \descartes\Controller
foreach ($phones as &$phone)
{
$limits = $this->internal_phone->get_limits($phone['id']);
$phone['limits'] = $limits;
$adapter = $adapters[$phone['adapter']] ?? false;
if (!$adapter)
@ -85,6 +90,11 @@ class Phone extends \descartes\Controller
{
$phone['callback_end_call'] = \descartes\Router::url('Callback', 'end_call', ['id_phone' => $phone['id']], ['api_key' => $api_key]);
}
if ($adapter['meta_support_phone_status'])
{
$phone['support_phone_status'] = true;
}
}
header('Content-Type: application/json');
@ -131,9 +141,11 @@ class Phone extends \descartes\Controller
* Create a new phone.
*
* @param $csrf : CSRF token
* @param string $_POST['name'] : Phone name
* @param string $_POST['adapter'] : Phone adapter
* @param array $_POST['adapter_data'] : Phone adapter data
* @param string $_POST['name'] : Phone name
* @param string $_POST['adapter'] : Phone adapter
* @param ?array $_POST['adapter_data'] : Phone adapter data
* @param ?array $_POST['limits'] : Array of limits in number of SMS for a period to be applied to this phone.
* @param int $_POST['priority'] : Priority with which to use phone to send SMS. Default 0.
*/
public function create($csrf)
{
@ -146,8 +158,12 @@ class Phone extends \descartes\Controller
$id_user = $_SESSION['user']['id'];
$name = $_POST['name'] ?? false;
$priority = $_POST['priority'] ?? 0;
$priority = max(((int) $priority), 0);
$adapter = $_POST['adapter'] ?? false;
$adapter_data = !empty($_POST['adapter_data']) ? $_POST['adapter_data'] : [];
$limits = $_POST['limits'] ?? [];
$limits = is_array($limits) ? $limits : [$limits];
if (!$name || !$adapter)
{
@ -156,7 +172,7 @@ class Phone extends \descartes\Controller
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$name_exist = $this->internal_phone->get_by_name($name);
$name_exist = $this->internal_phone->get_by_name_and_user($id_user, $name);
if ($name_exist)
{
\FlashMessage\FlashMessage::push('danger', 'Ce nom est déjà utilisé pour un autre téléphone.');
@ -164,6 +180,36 @@ class Phone extends \descartes\Controller
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
if ($limits)
{
foreach ($limits as $key => $limit)
{
if (!is_array($limit))
{
unset($limits[$key]);
continue;
}
$startpoint = $limit['startpoint'] ?? false;
$volume = $limit['volume'] ?? false;
if (!$startpoint || !$volume)
{
unset($limits[$key]);
continue;
}
$volume = (int) $volume;
$limits[$key]['volume'] = max($volume, 1);
if (!\controllers\internals\Tool::validate_relative_date($startpoint))
{
unset($limits[$key]);
continue;
}
}
}
$adapters = $this->internal_adapter->list_adapters();
$find_adapter = false;
foreach ($adapters as $metas)
@ -211,7 +257,7 @@ class Phone extends \descartes\Controller
//If field phone number is invalid
foreach ($find_adapter['meta_data_fields'] as $field)
{
if (false === ($field['number'] ?? false))
if ('phone_number' !== ($field['type'] ?? false))
{
continue;
}
@ -245,7 +291,7 @@ class Phone extends \descartes\Controller
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$success = $this->internal_phone->create($id_user, $name, $adapter, $adapter_data);
$success = $this->internal_phone->create($id_user, $name, $adapter, $adapter_data, $priority, $limits);
if (!$success)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible de créer ce téléphone.');
@ -257,4 +303,290 @@ class Phone extends \descartes\Controller
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
/**
* Return the edit page for phones
*
* @param int... $ids : Phones ids
*/
public function edit()
{
$ids = $_GET['ids'] ?? [];
$id_user = $_SESSION['user']['id'];
$phones = $this->internal_phone->gets_in_for_user($id_user, $ids);
if (!$phones)
{
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
$adapters = $this->internal_adapter->list_adapters();
foreach ($phones as &$phone)
{
$limits = $this->internal_phone->get_limits($phone['id']);
$phone['limits'] = $limits;
}
$this->render('phone/edit', [
'phones' => $phones,
'adapters' => $adapters,
]);
}
/**
* Update multiple phones.
*
* @param $csrf : CSRF token
* @param string $_POST['phones']['id']['name'] : Phone name
* @param string $_POST['phones']['id']['adapter'] : Phone adapter
* @param ?array $_POST['phones']['id']['adapter_data'] : Phone adapter data
* @param ?array $_POST['phones']['id']['limits'] : Array of limits in number of SMS for a period to be applied to this phone.
* @param int $_POST['phones']['id']['priority'] : Priority with which to use phone to send SMS. Default 0.
*/
public function update($csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
if (!$_POST['phones'])
{
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
$id_user = $_SESSION['user']['id'];
$nb_update = 0;
foreach ($_POST['phones'] as $id_phone => $phone)
{
$name = $phone['name'] ?? false;
$priority = $phone['priority'] ?? 0;
$priority = max(((int) $priority), 0);
$adapter = $phone['adapter'] ?? false;
$adapter_data = !empty($phone['adapter_data']) ? $phone['adapter_data'] : [];
$limits = $phone['limits'] ?? [];
$limits = is_array($limits) ? $limits : [$limits];
if (!$name || !$adapter)
{
continue;
}
$phone_with_same_name = $this->internal_phone->get_by_name_and_user($id_user, $name);
if ($phone_with_same_name && $phone_with_same_name['id'] != $id_phone)
{
continue;
}
if ($limits)
{
foreach ($limits as $key => $limit)
{
if (!is_array($limit))
{
unset($limits[$key]);
continue;
}
$startpoint = $limit['startpoint'] ?? false;
$volume = $limit['volume'] ?? false;
if (!$startpoint || !$volume)
{
unset($limits[$key]);
continue;
}
$volume = (int) $volume;
$limits[$key]['volume'] = max($volume, 1);
if (!\controllers\internals\Tool::validate_relative_date($startpoint))
{
unset($limits[$key]);
continue;
}
}
}
$adapters = $this->internal_adapter->list_adapters();
$find_adapter = false;
foreach ($adapters as $metas)
{
if ($metas['meta_classname'] === $adapter)
{
$find_adapter = $metas;
break;
}
}
if (!$find_adapter)
{
continue;
}
$current_phone = $this->internal_phone->get_for_user($id_user, $id_phone);
if (!$current_phone)
{
continue;
}
// We can only use an hidden adapter if it was already the adapter we was using
if ($find_adapter['meta_hidden'] && $adapter != $current_phone['adapter'])
{
continue;
}
//If missing required data fields, error
foreach ($find_adapter['meta_data_fields'] as $field)
{
if (false === $field['required'])
{
continue;
}
if (!empty($adapter_data[$field['name']]))
{
continue;
}
continue 2;
}
//If field phone number is invalid
foreach ($find_adapter['meta_data_fields'] as $field)
{
if ('phone_number' !== ($field['type'] ?? false))
{
continue;
}
if (!empty($adapter_data[$field['name']]))
{
$adapter_data[$field['name']] = \controllers\internals\Tool::parse_phone($adapter_data[$field['name']]);
if ($adapter_data[$field['name']])
{
continue;
}
}
continue 2;
}
$adapter_data = json_encode($adapter_data);
//Check adapter is working correctly with thoses names and data
$adapter_classname = $find_adapter['meta_classname'];
$adapter_instance = new $adapter_classname($adapter_data);
$adapter_working = $adapter_instance->test();
if (!$adapter_working)
{
continue;
}
$success = $this->internal_phone->update_for_user($id_user, $id_phone, $name, $adapter, $adapter_data, $priority, $limits);
if (!$success)
{
continue;
}
$nb_update ++;
}
if ($nb_update !== \count($_POST['phones']))
{
\FlashMessage\FlashMessage::push('danger', 'Certains téléphones n\'ont pas pu êtres mis à jour.');
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
\FlashMessage\FlashMessage::push('success', 'Tous les téléphones ont été modifiés avec succès.');
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
/**
* Re-check phone status
* @param array int $_GET['ids'] : ids of phones we want to update status
* @param $csrf : CSRF token
*/
public function update_status ($csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$ids = $_GET['ids'] ?? [];
$id_user = $_SESSION['user']['id'];
foreach ($ids as $id)
{
$phone = $this->internal_phone->get_for_user($id_user, $id);
// If user have activated phone limits, check if RaspiSMS phone limit have already been reached
$limit_reached = false;
if ((int) ($_SESSION['user']['settings']['phone_limit'] ?? false))
{
$limits = $this->internal_phone->get_limits($id);
$remaining_volume = PHP_INT_MAX;
foreach ($limits as $limit)
{
$startpoint = new \DateTime($limit['startpoint']);
$consumed = $this->internal_sended->count_since_for_phone_and_user($_SESSION['user']['id'], $id, $startpoint);
$remaining_volume = min(($limit['volume'] - $consumed), $remaining_volume);
}
if ($remaining_volume < 1)
{
$limit_reached = true;
}
}
if ($limit_reached)
{
$new_status = \models\Phone::STATUS_LIMIT_REACHED;
}
else
{
//Check status on provider side
$adapter_classname = $phone['adapter'];
if (!call_user_func([$adapter_classname, 'meta_support_phone_status']))
{
continue;
}
$adapter_instance = new $adapter_classname($phone['adapter_data']);
$new_status = $adapter_instance->check_phone_status();
}
$status_update = $this->internal_phone->update_status($id, $new_status);
}
\FlashMessage\FlashMessage::push('success', 'Les status des téléphones ont bien été mis à jour.');
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
/**
* Return a list of phones as a JSON array
*/
public function json_list()
{
header('Content-Type: application/json');
echo json_encode($this->internal_phone->list_for_user($_SESSION['user']['id']));
}
}

View File

@ -0,0 +1,245 @@
<?php
/*
* This file is part of RaspiSMS.
*
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
*
* This source file is subject to the GPL-3.0 license that is bundled
* with this source code in the file LICENSE.
*/
namespace controllers\publics;
/**
* Page of phone groups.
*/
class PhoneGroup extends \descartes\Controller
{
private $internal_phone_group;
private $internal_phone;
private $internal_event;
/**
* Call before any other func to check user is connected
*
* @return void;
*/
public function __construct()
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$this->internal_phone_group = new \controllers\internals\PhoneGroup($bdd);
$this->internal_phone = new \controllers\internals\Phone($bdd);
$this->internal_event = new \controllers\internals\Event($bdd);
\controllers\internals\Tool::verifyconnect();
}
/**
* Return all groups as an array for administration.
*/
public function list()
{
$this->render('phone_group/list');
}
/**
* Return groups as json.
*/
public function list_json()
{
$entities = $this->internal_phone_group->list_for_user($_SESSION['user']['id']);
header('Content-Type: application/json');
echo json_encode(['data' => $entities]);
}
/**
* Delete a list of phone groups
*
* @param array int $_GET['ids'] : Ids of phone groups to delete
* @param mixed $csrf
*
* @return boolean;
*/
public function delete($csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('PhoneGroup', 'list'));
}
$ids = $_GET['ids'] ?? [];
foreach ($ids as $id)
{
$this->internal_phone_group->delete_for_user($_SESSION['user']['id'], $id);
}
return $this->redirect(\descartes\Router::url('PhoneGroup', 'list'));
}
/**
* Return the creation page of a group
*/
public function add()
{
$this->render('phone_group/add');
}
/**
* Return the edition page for phone groups
*
* @param array $_GET['ids'] : Ids of phone groups to edit
*/
public function edit()
{
$ids = $_GET['ids'] ?? [];
$groups = $this->internal_phone_group->gets_in_for_user($_SESSION['user']['id'], $ids);
foreach ($groups as $key => $group)
{
$groups[$key]['phones'] = $this->internal_phone_group->get_phones($group['id']);
}
$this->render('phone_group/edit', [
'phone_groups' => $groups,
]);
}
/**
* Create a new phone group
*
* @param $csrf : CSRF token
* @param string $_POST['name'] : Name of phone group
* @param array $_POST['phones'] : Ids of phones to put in the group
*/
public function create($csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('PhoneGroup', 'add'));
}
$name = $_POST['name'] ?? false;
$phones_ids = $_POST['phones'] ?? false;
if (!$name || !$phones_ids)
{
\FlashMessage\FlashMessage::push('danger', 'Des champs sont manquants !');
return $this->redirect(\descartes\Router::url('PhoneGroup', 'add'));
}
$id_group = $this->internal_phone_group->create($_SESSION['user']['id'], $name, $phones_ids);
if (!$id_group)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible de créer ce groupe.');
return $this->redirect(\descartes\Router::url('PhoneGroup', 'add'));
}
\FlashMessage\FlashMessage::push('success', 'Le groupe a bien été créé.');
return $this->redirect(\descartes\Router::url('PhoneGroup', 'list'));
}
/**
* Update a list of phone groups
*
* @param $csrf : CSRF token
* @param array $_POST['phone_groups'] : An array of phone groups with group id as keys
*
* @return boolean;
*/
public function update($csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('PhoneGroup', 'list'));
}
$groups = $_POST['phone_groups'] ?? [];
$nb_groups_update = 0;
foreach ($groups as $id => $group)
{
foreach ($group['phones_ids'] as $key => $value)
{
$group['phones_ids'][$key] = (int) $value;
}
$nb_groups_update += (int) $this->internal_phone_group->update_for_user($_SESSION['user']['id'], $id, $group['name'], $group['phones_ids']);
}
if ($nb_groups_update !== \count($groups))
{
\FlashMessage\FlashMessage::push('danger', 'Certains groupes n\'ont pas pu êtres mis à jour.');
return $this->redirect(\descartes\Router::url('PhoneGroup', 'list'));
}
\FlashMessage\FlashMessage::push('success', 'Tous les groupes ont été modifiés avec succès.');
return $this->redirect(\descartes\Router::url('PhoneGroup', 'list'));
}
/**
* Return phones of a group as json array
* @param int $id_group = PhoneGroup id
*
* @return json
*/
public function preview (int $id_group)
{
$return = [
'success' => false,
'result' => 'Une erreur inconnue est survenue.',
];
$group = $this->internal_phone_group->get_for_user($_SESSION['user']['id'], $id_group);
if (!$group)
{
$return['result'] = 'Ce groupe n\'existe pas.';
echo json_encode($return);
return false;
}
$phones = $this->internal_phone_group->get_phones($id_group);
if (!$phones)
{
$return['result'] = 'Aucun téléphone dans le groupe.';
echo json_encode($return);
return false;
}
foreach ($phones as &$phone)
{
$phone['adapter_name'] = call_user_func([$phone['adapter'], 'meta_name']);
}
$return['success'] = true;
$return['result'] = $phones;
echo json_encode($return);
return true;
}
/**
* Cette fonction retourne la liste des groups sous forme JSON.
*/
public function json_list()
{
header('Content-Type: application/json');
echo json_encode($this->internal_phone_group->list_for_user($_SESSION['user']['id']));
}
}

View File

@ -47,11 +47,31 @@ namespace controllers\publics;
}
/**
* Return received as json.
* Return receiveds as json.
*
* @param bool $unread : Should we only search for unread messages
*/
public function list_json()
public function list_json(bool $unread = false)
{
$entities = $this->internal_received->list_for_user($_SESSION['user']['id']);
$draw = (int) ($_GET['draw'] ?? false);
$columns = [
0 => 'searchable_origin',
1 => 'phone_name',
2 => 'text',
3 => 'at',
4 => 'status',
5 => 'command',
];
$search = $_GET['search']['value'] ?? null;
$order_column = $columns[$_GET['order'][0]['column']] ?? null;
$order_desc = ($_GET['order'][0]['dir'] ?? 'asc') == 'desc' ? true : false;
$offset = (int) ($_GET['start'] ?? 0);
$limit = (int) ($_GET['length'] ?? 25);
$entities = $this->internal_received->datatable_list_for_user($_SESSION['user']['id'], $limit, $offset, $search, $columns, $order_column, $order_desc, false, $unread);
$count_entities = $this->internal_received->datatable_list_for_user($_SESSION['user']['id'], $limit, $offset, $search, $columns, $order_column, $order_desc, true, $unread);
foreach ($entities as &$entity)
{
$entity['origin_formatted'] = \controllers\internals\Tool::phone_link($entity['origin']);
@ -61,8 +81,15 @@ namespace controllers\publics;
}
}
$records_total = $this->internal_received->count_for_user($_SESSION['user']['id']);
header('Content-Type: application/json');
echo json_encode(['data' => $entities]);
echo json_encode([
'draw' => $draw,
'recordsTotal' => $records_total,
'recordsFiltered' => $count_entities,
'data' => $entities,
]);
}
/**
@ -73,25 +100,6 @@ namespace controllers\publics;
$this->render('received/list', ['is_unread' => true]);
}
/**
* Return unred received as json.
*/
public function list_unread_json()
{
$entities = $this->internal_received->list_unread_for_user($_SESSION['user']['id']);
foreach ($entities as &$entity)
{
$entity['origin_formatted'] = \controllers\internals\Tool::phone_link($entity['origin']);
if ($entity['mms'])
{
$entity['medias'] = $this->internal_media->gets_for_received($entity['id']);
}
}
header('Content-Type: application/json');
echo json_encode(['data' => $entities]);
}
/**
* Mark messages as.
*
@ -164,6 +172,8 @@ namespace controllers\publics;
foreach ($receiveds as $key => $received)
{
$receiveds[$key]['text'] = $this->s($received['text'], false, true, false);
if (!$contact = $this->internal_contact->get_by_number_and_user($_SESSION['user']['id'], $received['origin']))
{
continue;

View File

@ -18,6 +18,7 @@ namespace controllers\publics;
{
private $internal_scheduled;
private $internal_phone;
private $internal_phone_group;
private $internal_contact;
private $internal_group;
private $internal_conditional_group;
@ -34,6 +35,7 @@ namespace controllers\publics;
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$this->internal_scheduled = new \controllers\internals\Scheduled($bdd);
$this->internal_phone = new \controllers\internals\Phone($bdd);
$this->internal_phone_group = new \controllers\internals\PhoneGroup($bdd);
$this->internal_contact = new \controllers\internals\Contact($bdd);
$this->internal_group = new \controllers\internals\Group($bdd);
$this->internal_conditional_group = new \controllers\internals\ConditionalGroup($bdd);
@ -118,6 +120,7 @@ namespace controllers\publics;
$contacts = $this->internal_contact->gets_for_user($id_user);
$phones = $this->internal_phone->gets_for_user($id_user);
$phone_groups = $this->internal_phone_group->gets_for_user($id_user);
$contact_ids = (isset($_GET['contact_ids']) && \is_array($_GET['contact_ids'])) ? $_GET['contact_ids'] : [];
$group_ids = (isset($_GET['group_ids']) && \is_array($_GET['group_ids'])) ? $_GET['group_ids'] : [];
@ -153,6 +156,7 @@ namespace controllers\publics;
'now' => $now->format('Y-m-d H:i'),
'contacts' => $contacts,
'phones' => $phones,
'phone_groups' => $phone_groups,
'prefilled_contacts' => $prefilled_contacts,
'prefilled_groups' => $prefilled_groups,
'prefilled_conditional_groups' => $prefilled_conditional_groups,
@ -179,6 +183,7 @@ namespace controllers\publics;
$all_contacts = $this->internal_contact->gets_for_user($_SESSION['user']['id']);
$phones = $this->internal_phone->gets_for_user($_SESSION['user']['id']);
$phone_groups = $this->internal_phone_group->gets_for_user($id_user);
$scheduleds = $this->internal_scheduled->gets_in_for_user($_SESSION['user']['id'], $ids);
//Pour chaque message on ajoute les numéros, les contacts & les groups
@ -197,7 +202,8 @@ namespace controllers\publics;
$numbers = $this->internal_scheduled->get_numbers($scheduled['id']);
foreach ($numbers as $number)
{
$scheduleds[$key]['numbers'][] = $number['number'];
$number['data'] = json_decode($number['data'] ?? '[]');
$scheduleds[$key]['numbers'][] = $number;
}
$contacts = $this->internal_scheduled->get_contacts($scheduled['id']);
@ -225,6 +231,7 @@ namespace controllers\publics;
$this->render('scheduled/edit', [
'scheduleds' => $scheduleds,
'phones' => $phones,
'phone_groups' => $phone_groups,
'contacts' => $all_contacts,
]);
}
@ -237,7 +244,7 @@ namespace controllers\publics;
* @param string $_POST['at'] : Date to send message for
* @param string $_POST['text'] : Text of the message
* @param ?bool $_POST['flash'] : Is the message a flash message (by default false)
* @param ?int $_POST['id_phone'] : Id of the phone to send message from, if null use random phone
* @param ?int $_POST['id_phone'] : Id of the phone or phone group to send message from. id will be preceed by phone_ of phonegroup_ depending on type of ressource to use, if null use random phone
* @param ?array $_POST['numbers'] : Numbers to send the message to
* @param ?array $_POST['contacts'] : Numbers to send the message to
* @param ?array $_POST['groups'] : Numbers to send the message to
@ -257,12 +264,15 @@ namespace controllers\publics;
$at = $_POST['at'] ?? false;
$text = $_POST['text'] ?? false;
$flash = (bool) ($_POST['flash'] ?? false);
$tag = ($_POST['tag'] ?? null) ?: null;
$id_phone = empty($_POST['id_phone']) ? null : $_POST['id_phone'];
$numbers = $_POST['numbers'] ?? [];
$numbers = is_array($numbers) ? $numbers : [$numbers];
$contacts = $_POST['contacts'] ?? [];
$groups = $_POST['groups'] ?? [];
$conditional_groups = $_POST['conditional_groups'] ?? [];
$files = $_FILES['medias'] ?? false;
$csv_file = $_FILES['csv'] ?? false;
//Iterate over files to re-create individual $_FILES array
$files_arrays = [];
@ -291,6 +301,12 @@ namespace controllers\publics;
}
}
//Remove empty csv file input
if ($csv_file && UPLOAD_ERR_NO_FILE === $csv_file['error'])
{
$csv_file = false;
}
if (empty($text))
{
\FlashMessage\FlashMessage::push('danger', 'Vous ne pouvez pas créer un Sms sans message.');
@ -298,6 +314,34 @@ namespace controllers\publics;
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if (empty($at))
{
\FlashMessage\FlashMessage::push('danger', 'Vous ne pouvez pas créer un Sms sans date.');
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if (!is_string($at))
{
\FlashMessage\FlashMessage::push('danger', 'La date doit être valide.');
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if (!is_string($text))
{
\FlashMessage\FlashMessage::push('danger', 'Votre message doit être un texte.');
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if (mb_strlen($text) > \models\Scheduled::SMS_LENGTH_LIMIT)
{
\FlashMessage\FlashMessage::push('danger', 'Votre message doit faire moins de ' . \models\Scheduled::SMS_LENGTH_LIMIT . ' caractères.');
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if (!\controllers\internals\Tool::validate_date($at, 'Y-m-d H:i:s') && !\controllers\internals\Tool::validate_date($at, 'Y-m-d H:i'))
{
\FlashMessage\FlashMessage::push('danger', 'Vous devez fournir une date valide.');
@ -305,17 +349,64 @@ namespace controllers\publics;
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if ($csv_file)
{
$uploaded_file = \controllers\internals\Tool::read_uploaded_file($csv_file);
if (!$uploaded_file['success'])
{
\FlashMessage\FlashMessage::push('danger', 'Impossible de traiter ce fichier CSV : ' . $uploaded_file['content']);
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
try
{
$csv_numbers = $this->internal_scheduled->parse_csv_numbers_file($uploaded_file['content']);
if (!$csv_numbers)
{
\FlashMessage\FlashMessage::push('danger', 'Aucun destinataire valide dans le fichier CSV, assurez-vous de fournir un fichier CSV valide.');
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
$numbers = array_merge($csv_numbers, $numbers);
}
catch (\Exception $e)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible de traiter ce fichier CSV : ' . $e->getMessage());
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
}
foreach ($numbers as $key => $number)
{
$number = \controllers\internals\Tool::parse_phone($number);
// If number is not an array turn it into an array
$number = is_array($number) ? $number : ['number' => $number, 'data' => []];
$number['data'] = $number['data'] ?? [];
$number['number'] = \controllers\internals\Tool::parse_phone($number['number'] ?? '');
if (!$number)
if (!$number['number'])
{
unset($numbers[$key]);
continue;
}
$clean_data = [];
foreach ($number['data'] as $data_key => $value)
{
if ('' === $value)
{
continue;
}
$data_key = mb_ereg_replace('[\W]', '', $data_key);
$clean_data[$data_key] = (string) $value;
}
$clean_data = json_encode($clean_data);
$number['data'] = $clean_data;
$numbers[$key] = $number;
}
@ -349,7 +440,15 @@ namespace controllers\publics;
$mms = (bool) count($media_ids);
$scheduled_id = $this->internal_scheduled->create($id_user, $at, $text, $id_phone, $flash, $mms, $numbers, $contacts, $groups, $conditional_groups, $media_ids);
// Check if we must send message to a phone or a phone_group based on if id_phone start with 'phone_' or 'phonegroup_'
$id_phone_group = null;
if ($id_phone)
{
$id_phone_group = str_starts_with($id_phone, 'phonegroup_') ? mb_substr($id_phone, mb_strlen('phonegroup_')) : null;
$id_phone = str_starts_with($id_phone, 'phone_') ? mb_substr($id_phone, mb_strlen('phone_')) : null;
}
$scheduled_id = $this->internal_scheduled->create($id_user, $at, $text, $id_phone, $id_phone_group, $flash, $mms, $tag, $numbers, $contacts, $groups, $conditional_groups, $media_ids);
if (!$scheduled_id)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible de créer le Sms.');
@ -389,11 +488,13 @@ namespace controllers\publics;
$text = $scheduled['text'] ?? false;
$id_phone = empty($scheduled['id_phone']) ? null : $scheduled['id_phone'];
$flash = (bool) ($scheduled['flash'] ?? false);
$tag = ($scheduled['tag'] ?? null) ?: null;
$numbers = $scheduled['numbers'] ?? [];
$contacts = $scheduled['contacts'] ?? [];
$groups = $scheduled['groups'] ?? [];
$conditional_groups = $scheduled['conditional_groups'] ?? [];
$files = $_FILES['scheduleds_' . $id_scheduled . '_medias'] ?? false;
$csv_file = $_FILES['scheduleds_' . $id_scheduled . '_csv'] ?? false;
$media_ids = $scheduled['media_ids'] ?? [];
//Check scheduled exists and belong to user
@ -430,26 +531,103 @@ namespace controllers\publics;
}
}
//Remove empty csv file input
if ($csv_file && UPLOAD_ERR_NO_FILE === $csv_file['error'])
{
$csv_file = false;
}
if (empty($text))
{
continue;
}
if (empty($at))
{
\FlashMessage\FlashMessage::push('danger', 'Vous ne pouvez pas créer un Sms sans date.');
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if (!is_string($at))
{
\FlashMessage\FlashMessage::push('danger', 'La date doit être valide.');
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if (!is_string($text))
{
\FlashMessage\FlashMessage::push('danger', 'Votre message doit être un texte.');
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if (mb_strlen($text) > \models\Scheduled::SMS_LENGTH_LIMIT)
{
\FlashMessage\FlashMessage::push('danger', 'Votre message doit faire moins de ' . \models\Scheduled::SMS_LENGTH_LIMIT . ' caractères.');
return $this->redirect(\descartes\Router::url('Scheduled', 'add'));
}
if (!\controllers\internals\Tool::validate_date($at, 'Y-m-d H:i:s') && !\controllers\internals\Tool::validate_date($at, 'Y-m-d H:i'))
{
continue;
}
if ($csv_file)
{
$uploaded_file = \controllers\internals\Tool::read_uploaded_file($csv_file);
if (!$uploaded_file['success'])
{
continue;
}
try
{
$csv_numbers = $this->internal_scheduled->parse_csv_numbers_file($uploaded_file['content']);
if (!$csv_numbers)
{
continue;
}
$numbers = array_merge($csv_numbers, $numbers);
}
catch (\Exception $e)
{
continue;
}
}
$numbers = is_array($numbers) ? $numbers : [$numbers];
foreach ($numbers as $key => $number)
{
$number = \controllers\internals\Tool::parse_phone($number);
if (!$number)
// If number is not an array turn it into an array
$number = is_array($number) ? $number : ['number' => $number, 'data' => []];
$number['data'] = $number['data'] ?? [];
$number['number'] = \controllers\internals\Tool::parse_phone($number['number'] ?? '');
if (!$number['number'])
{
unset($numbers[$key]);
continue;
}
$clean_data = [];
foreach ($number['data'] as $data_key => $value)
{
if ('' === $value)
{
continue;
}
$data_key = mb_ereg_replace('[\W]', '', $data_key);
$clean_data[$data_key] = (string) $value;
}
$clean_data = json_encode($clean_data);
$number['data'] = $clean_data;
$numbers[$key] = $number;
}
@ -488,7 +666,14 @@ namespace controllers\publics;
$mms = (bool) count($media_ids);
$this->internal_scheduled->update_for_user($id_user, $id_scheduled, $at, $text, $id_phone, $flash, $mms, $numbers, $contacts, $groups, $conditional_groups, $media_ids);
$id_phone_group = null;
if ($id_phone)
{
$id_phone_group = str_starts_with($id_phone, 'phonegroup_') ? mb_substr($id_phone, mb_strlen('phonegroup_')) : null;
$id_phone = str_starts_with($id_phone, 'phone_') ? mb_substr($id_phone, mb_strlen('phone_')) : null;
}
$this->internal_scheduled->update_for_user($id_user, $id_scheduled, $at, $text, $id_phone, $id_phone_group, $flash, $mms, $tag, $numbers, $contacts, $groups, $conditional_groups, $media_ids);
++$nb_update;
}

View File

@ -53,7 +53,25 @@ namespace controllers\publics;
*/
public function list_json()
{
$entities = $this->internal_sended->list_for_user($_SESSION['user']['id']);
$draw = (int) ($_GET['draw'] ?? false);
$columns = [
0 => 'phone_name',
1 => 'searchable_destination',
2 => 'text',
3 => 'tag',
4 => 'at',
5 => 'status',
];
$search = $_GET['search']['value'] ?? null;
$order_column = $columns[$_GET['order'][0]['column']] ?? null;
$order_desc = ($_GET['order'][0]['dir'] ?? 'asc') == 'desc' ? true : false;
$offset = (int) ($_GET['start'] ?? 0);
$limit = (int) ($_GET['length'] ?? 25);
$entities = $this->internal_sended->datatable_list_for_user($_SESSION['user']['id'], $limit, $offset, $search, $columns, $order_column, $order_desc);
$count_entities = $this->internal_sended->datatable_list_for_user($_SESSION['user']['id'], $limit, $offset, $search, $columns, $order_column, $order_desc, true);
foreach ($entities as &$entity)
{
$entity['destination_formatted'] = \controllers\internals\Tool::phone_link($entity['destination']);
@ -63,8 +81,15 @@ namespace controllers\publics;
}
}
$records_total = $this->internal_sended->count_for_user($_SESSION['user']['id']);
header('Content-Type: application/json');
echo json_encode(['data' => $entities]);
echo json_encode([
'draw' => $draw,
'recordsTotal' => $records_total,
'recordsFiltered' => $count_entities,
'data' => $entities,
]);
}
/**

View File

@ -75,12 +75,27 @@ namespace controllers\publics;
$setting_value = json_encode($setting_value);
}
$update_setting_result = $this->internal_setting->update_for_user($_SESSION['user']['id'], $setting_name, $setting_value);
if (false === $update_setting_result)
// If setting dont exists yet, create it, else update
$setting = $this->internal_setting->get_by_name_for_user($_SESSION['user']['id'], $setting_name);
if (!$setting)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible de mettre à jour ce réglage.');
$success = $this->internal_setting->create($_SESSION['user']['id'], $setting_name, $setting_value);
if (false === $success)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible de mettre à jour ce réglage.');
return $this->redirect(\descartes\Router::url('Setting', 'show'));
return $this->redirect(\descartes\Router::url('Setting', 'show'));
}
}
else
{
$update_setting_result = $this->internal_setting->update_for_user($_SESSION['user']['id'], $setting_name, $setting_value);
if (false === $update_setting_result)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible de mettre à jour ce réglage.');
return $this->redirect(\descartes\Router::url('Setting', 'show'));
}
}
$settings = $this->internal_setting->gets_for_user($_SESSION['user']['id']);

View File

@ -74,13 +74,6 @@ namespace controllers\publics;
return $this->redirect(\descartes\Router::url('SmsStop', 'list'));
}
if (!\controllers\internals\Tool::is_admin())
{
\FlashMessage\FlashMessage::push('danger', 'Vous devez être administrateur pour pouvoir supprimer un "STOP Sms" !');
return $this->redirect(\descartes\Router::url('SmsStop', 'list'));
}
$ids = $_GET['ids'] ?? [];
foreach ($ids as $id)
{

View File

@ -83,6 +83,12 @@ namespace controllers\publics;
$result = $this->internal_templating->render($template, $data);
$return = $result;
// If we must force GSM 7 alphabet
if ((int) ($_SESSION['user']['settings']['force_gsm_alphabet'] ?? false))
{
$return['result'] = \controllers\internals\Tool::convert_to_gsm0338($return['result']);
}
if (!trim($result['result']))
{
$return['result'] = 'Message vide, il ne sera pas envoyé.';

View File

@ -410,13 +410,13 @@ class User extends \descartes\Controller
return $this->redirect(\descartes\Router::url('User', 'list'));
}
/**
* Allow an admin to impersonate a user
* Allow an admin to impersonate a user.
*
* @param mixed $csrf
* @param array int $_GET['user_ids'] : Ids of users to impersonate, the array should actually contain one id only, we keep use of array for simpler compatibility in UI
*/
public function impersonate ($csrf)
public function impersonate($csrf)
{
if (!$this->verify_csrf($csrf))
{
@ -425,7 +425,7 @@ class User extends \descartes\Controller
return $this->redirect(\descartes\Router::url('User', 'list'));
}
if (count($_GET['user_ids']) != 1)
if (1 != count($_GET['user_ids']))
{
\FlashMessage\FlashMessage::push('danger', 'Vous devez séléctionner un et un seul utilisateur à incarner !');
@ -460,17 +460,17 @@ class User extends \descartes\Controller
$user['settings'] = $settings;
//Save old session to get it back later
//Save old session to get it back later
$old_session = $_SESSION;
$_SESSION = [
'old_session' => $old_session,
'old_session' => $old_session,
'impersonate' => true,
'connect' => true,
'user' => $user,
];
\FlashMessage\FlashMessage::push('success', 'Vous incarnez désormais l\'utilisateur ' . $user['email'] . '.');
return $this->redirect(\descartes\Router::url('Dashboard', 'show'));
}
}

View File

@ -44,7 +44,7 @@ class Launcher extends AbstractDaemon
public function run()
{
//Create the internal controllers
$this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$this->internal_phone = new \controllers\internals\Phone($this->bdd);
$this->start_sender_daemon();
@ -53,7 +53,7 @@ class Launcher extends AbstractDaemon
$this->start_mailer_daemon();
$phones = $this->internal_phone->get_all();
$phones = $this->internal_phone->get_all_for_active_users();
$this->start_phones_daemons($phones);
sleep(1);

View File

@ -20,8 +20,9 @@ use Monolog\Logger;
class Phone extends AbstractDaemon
{
private $max_inactivity = 5 * 60;
private $read_delay = 20 / 0.5;
private $read_tick = 0;
private $msg_queue;
private $msg_queue_id;
private $webhook_queue;
private $last_message_at;
private $phone;
@ -36,7 +37,6 @@ class Phone extends AbstractDaemon
public function __construct(array $phone)
{
$this->phone = $phone;
$this->msg_queue_id = (int) (QUEUE_ID_PHONE_PREFIX . $this->phone['id']);
$name = 'RaspiSMS Daemon Phone ' . $this->phone['id'];
$logger = new Logger($name);
@ -56,13 +56,20 @@ class Phone extends AbstractDaemon
{
usleep(0.5 * 1000000); //Micro sleep for perfs
$this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$this->read_tick += 1;
$this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
//Send smss in queue
$this->send_smss();
//Read received smss
$this->read_smss();
//Read only every x ticks (x/2 seconds) to prevent too many call
if ($this->read_tick >= $this->read_delay)
{
//Read received smss
$this->read_smss();
$this->read_tick = 0;
}
//Stop after 5 minutes of inactivity to avoid useless daemon
if ((microtime(true) - $this->last_message_at) > $this->max_inactivity)
@ -78,7 +85,7 @@ class Phone extends AbstractDaemon
//Set last message at to construct time
$this->last_message_at = microtime(true);
$this->msg_queue = msg_get_queue($this->msg_queue_id);
$this->msg_queue = msg_get_queue(QUEUE_ID_PHONE);
//Instanciate adapter
$adapter_class = $this->phone['adapter'];
@ -89,11 +96,7 @@ class Phone extends AbstractDaemon
public function on_stop()
{
//Delete queue on daemon close
$this->logger->info('Closing queue : ' . $this->msg_queue_id);
msg_remove_queue($this->msg_queue);
$this->logger->info('Stopping Phone daemon with pid ' . getmypid());
$this->logger->info('Stopping Phone daemon with pid ' . getmypid());
}
public function handle_other_signals($signal)
@ -116,8 +119,10 @@ class Phone extends AbstractDaemon
$maxsize = 409600;
$message = null;
// Message type is forged from a prefix concat with the phone ID
$message_type = (int) QUEUE_TYPE_SEND_MSG_PREFIX . $this->phone['id'];
$error_code = null;
$success = msg_receive($this->msg_queue, QUEUE_TYPE_SEND_MSG, $msgtype, $maxsize, $message, true, MSG_IPC_NOWAIT, $error_code); //MSG_IPC_NOWAIT == dont wait if no message found
$success = msg_receive($this->msg_queue, $message_type, $msgtype, $maxsize, $message, true, MSG_IPC_NOWAIT, $error_code); //MSG_IPC_NOWAIT == dont wait if no message found
if (!$success && MSG_ENOMSG !== $error_code)
{
@ -139,7 +144,7 @@ class Phone extends AbstractDaemon
//Do message sending
$this->logger->info('Try send message : ' . json_encode($message));
$response = $internal_sended->send($this->adapter, $this->phone['id_user'], $this->phone['id'], $message['text'], $message['destination'], $message['flash'], $message['mms'], $message['medias']);
$response = $internal_sended->send($this->adapter, $this->phone['id_user'], $this->phone['id'], $message['text'], $message['destination'], $message['flash'], $message['mms'], $message['tag'], $message['medias'], $message['id_scheduled']);
if ($response['error'])
{
$this->logger->error('Failed send message : ' . json_encode($message) . ' with error : ' . $response['error_message']);

View File

@ -22,8 +22,9 @@ class Sender extends AbstractDaemon
private $internal_phone;
private $internal_scheduled;
private $internal_received;
private $internal_sended;
private $bdd;
private $queues = [];
private $msg_queue;
public function __construct()
{
@ -45,10 +46,11 @@ class Sender extends AbstractDaemon
{
//Create the internal controllers
$this->internal_scheduled = new \controllers\internals\Scheduled($this->bdd);
$this->internal_sended = new \controllers\internals\Sended($this->bdd);
//Get smss and transmit order to send to appropriate phone daemon
$smss = $this->internal_scheduled->get_smss_to_send();
$this->transmit_smss($smss); //Add new queue to array of queues
$smss_per_scheduled = $this->internal_scheduled->get_smss_to_send();
$this->transmit_smss($smss_per_scheduled); //Add new queue to array of queues
usleep(0.5 * 1000000);
}
@ -56,45 +58,55 @@ class Sender extends AbstractDaemon
/**
* Function to transfer smss to send to phones daemons.
*
* @param array $smss : Smss to send
* @param array $smss_per_scheduled : Smss to send per scheduled id
*/
public function transmit_smss(array $smss): void
public function transmit_smss(array $smss_per_scheduled): void
{
foreach ($smss as $sms)
foreach ($smss_per_scheduled as $id_scheduled => $smss)
{
//If queue not already exists
$queue_id = (int) (QUEUE_ID_PHONE_PREFIX . $sms['id_phone']);
if (!msg_queue_exists($queue_id) || !isset($queues[$queue_id]))
if (!msg_queue_exists(QUEUE_ID_PHONE) || !isset($this->msg_queue))
{
$this->queues[$queue_id] = msg_get_queue($queue_id);
$this->msg_queue = msg_get_queue(QUEUE_ID_PHONE);
}
$msg = [
'id_user' => $sms['id_user'],
'id_scheduled' => $sms['id_scheduled'],
'text' => $sms['text'],
'id_phone' => $sms['id_phone'],
'destination' => $sms['destination'],
'flash' => $sms['flash'],
'mms' => $sms['mms'],
'medias' => $sms['medias'] ?? [],
];
foreach ($smss as $sms)
{
$msg = [
'id_user' => $sms['id_user'],
'id_scheduled' => $sms['id_scheduled'],
'text' => $sms['text'],
'id_phone' => $sms['id_phone'],
'destination' => $sms['destination'],
'flash' => $sms['flash'],
'mms' => $sms['mms'],
'tag' => $sms['tag'],
'medias' => $sms['medias'] ?? [],
];
msg_send($this->queues[$queue_id], QUEUE_TYPE_SEND_MSG, $msg);
// Message type is forged from a prefix concat with the phone ID
$message_type = (int) QUEUE_TYPE_SEND_MSG_PREFIX . $sms['id_phone'];
msg_send($this->msg_queue, $message_type, $msg);
$this->logger->info('Transmit sms send signal to phone ' . $sms['id_phone'] . ' on queue ' . QUEUE_ID_PHONE . ' with message type ' . $message_type . '.');
}
$this->logger->info('Transmit sms send signal to phone ' . $sms['id_phone'] . ' on queue ' . $queue_id . '.');
$this->internal_scheduled->delete($sms['id_scheduled']);
$this->logger->info('Scheduled ' . $id_scheduled . ' treated and deleted.');
$this->internal_scheduled->delete($id_scheduled);
}
}
public function on_start()
{
$this->logger->info('Starting Sender with pid ' . getmypid());
$this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
}
public function on_stop()
{
//Delete queue on daemon close
$this->logger->info('Closing queue : ' . $this->msg_queue);
msg_remove_queue($this->msg_queue);
$this->logger->info('Stopping Sender with pid ' . getmypid());
}

View File

@ -11,6 +11,7 @@
namespace daemons;
use GuzzleHttp\Promise\Utils;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
@ -84,7 +85,7 @@ class Webhook extends AbstractDaemon
try
{
$responses = \GuzzleHttp\Promise\unwrap($promises);
$responses = Utils::unwrap($promises);
}
catch (\Exception $e)
{

View File

@ -0,0 +1,42 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddScheduledIdToSended extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* addCustomColumn
* renameColumn
* addIndex
* addForeignKey
*
* Any other destructive changes will result in an error when trying to
* rollback the migration.
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('sended');
$table->addColumn('originating_scheduled', 'integer', [
'null' => true,
'comment' => 'Id of the scheduled that was responsible for sending this message.',
'after' => 'mms'
])
->update();
}
}

View File

@ -0,0 +1,16 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddWebhookTypeSendSmsStatusChange extends AbstractMigration
{
public function up()
{
$this->execute('ALTER TABLE `webhook` MODIFY `type` ENUM(\'send_sms\', \'send_sms_status_change\', \'receive_sms\', \'inbound_call\')');
}
public function down()
{
$this->execute('ALTER TABLE `webhook` MODIFY `type` ENUM(\'send_sms\', \'receive_sms\', \'inbound_call\')');
}
}

View File

@ -0,0 +1,38 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddDataToScheduledNumber extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* addCustomColumn
* renameColumn
* addIndex
* addForeignKey
*
* Any other destructive changes will result in an error when trying to
* rollback the migration.
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('scheduled_number');
$table->addColumn('data', 'text', ['default' => null, 'null' => true, 'after' => 'number'])
->update();
}
}

View File

@ -0,0 +1,43 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddPhoneLimits extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* addCustomColumn
* renameColumn
* addIndex
* addForeignKey
*
* Any other destructive changes will result in an error when trying to
* rollback the migration.
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('phone_limit');
$table->addColumn('id_phone', 'integer', ['null' => false])
->addColumn('volume', 'integer', ['null' => false])
->addColumn('startpoint', 'string', ['null' => false, 'limit' => 254]) # A relative time to use as startpoint for counting volume. See https://www.php.net/manual/en/datetime.formats.relative.php
->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated_at', 'timestamp', ['null' => true, 'update' => 'CURRENT_TIMESTAMP'])
->addForeignKey('id_phone', 'phone', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
->create();
}
}

View File

@ -0,0 +1,43 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddPhonePriority extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* addCustomColumn
* renameColumn
* addIndex
* addForeignKey
*
* Any other destructive changes will result in an error when trying to
* rollback the migration.
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('phone');
$table->addColumn('priority', 'integer', [
'null' => false,
'default' => 0,
'comment' => 'Priority with which the phone will be used. The higher the more prioritary.',
'after' => 'name'
])
->update();
}
}

View File

@ -0,0 +1,38 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddStatusPhone extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* addCustomColumn
* renameColumn
* addIndex
* addForeignKey
*
* Any other destructive changes will result in an error when trying to
* rollback the migration.
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('phone');
$table->addColumn('status', 'enum', ['values' => ['available', 'unavailable', 'no_credit'], 'default' => 'available']);
$table->update();
}
}

View File

@ -0,0 +1,52 @@
<?php
use Phinx\Db\Adapter\MysqlAdapter;
use Phinx\Migration\AbstractMigration;
class CreatePhoneGroup extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* addCustomColumn
* renameColumn
* addIndex
* addForeignKey
*
* Any other destructive changes will result in an error when trying to
* rollback the migration.
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$this->table('phone_group')
->addColumn('id_user', 'integer', ['null' => false])
->addColumn('name', 'string', ['null' => false, 'limit' => 100])
->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated_at', 'timestamp', ['null' => true, 'update' => 'CURRENT_TIMESTAMP'])
->addForeignKey('id_user', 'user', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
->create();
$this->table('phone_group_phone')
->addColumn('id_phone_group', 'integer', ['null' => false])
->addColumn('id_phone', 'integer', ['null' => false])
->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated_at', 'timestamp', ['null' => true, 'update' => 'CURRENT_TIMESTAMP'])
->addForeignKey('id_phone_group', 'phone_group', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
->addForeignKey('id_phone', 'phone', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
->create();
}
}

View File

@ -0,0 +1,39 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddIdPhoneGroupToScheduled extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* addCustomColumn
* renameColumn
* addIndex
* addForeignKey
*
* Any other destructive changes will result in an error when trying to
* rollback the migration.
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$this->table('scheduled')
->addColumn('id_phone_group', 'integer', ['null' => true, 'default' => null, 'after' => 'id_phone'])
->addForeignKey('id_phone_group', 'phone_group', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
->update();
}
}

View File

@ -0,0 +1,42 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddScheduledTag extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* addCustomColumn
* renameColumn
* addIndex
* addForeignKey
*
* Any other destructive changes will result in an error when trying to
* rollback the migration.
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('scheduled');
$table->addColumn('tag', 'string', ['default' => NULL, 'null' => true, 'limit' => 1000])
->update();
$table = $this->table('sended');
$table->addColumn('tag', 'string', ['default' => NULL, 'null' => true, 'limit' => 255])
->update();
}
}

Some files were not shown because too many files have changed in this diff Show More