Compare commits

...

70 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
113 changed files with 9605 additions and 1483 deletions

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.5.2
v3.9.1

View File

@ -79,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.
*/
@ -119,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;
@ -152,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.
*
@ -193,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.
@ -207,5 +221,5 @@ interface AdapterInterface
* ]
* ]
*/
public function end_call_callback(): array;
public static function end_call_callback(): array;
}

View File

@ -111,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.
*/
@ -226,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;
@ -241,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

@ -121,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.
*/
@ -188,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',
@ -271,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);
@ -300,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
@ -316,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 [];
}
@ -337,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']),
@ -348,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']),

View File

@ -209,6 +209,14 @@ class KannelAdapter 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.
*/
@ -354,6 +362,16 @@ class KannelAdapter 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
@ -490,12 +508,12 @@ class KannelAdapter 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

@ -174,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.
*/
@ -325,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
@ -468,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

@ -173,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.
*/
@ -317,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
@ -461,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

@ -162,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.
*/
@ -327,6 +335,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
@ -388,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

@ -166,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.
*/
@ -317,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
@ -378,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

@ -116,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.
*/
@ -287,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;
@ -333,7 +351,7 @@ namespace adapters;
return [];
}
public function inbound_call_callback(): array
public static function inbound_call_callback(): array
{
$response = [
'error' => false,
@ -362,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

@ -160,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.
*/
@ -295,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
@ -356,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,7 +375,8 @@ footer img
text-align: right;
}
.scheduleds-number-groupe
.scheduleds-number-groupe,
.phone-limits-group
{
padding-top: 15px;
padding-bottom: 15px;
@ -383,7 +386,8 @@ footer img
position: relative;
}
.scheduleds-number-groupe-remove
.scheduleds-number-groupe-remove,
.phone-limits-group-remove
{
position: absolute;
top: 15px;
@ -391,7 +395,8 @@ footer img
color: #888;
}
.scheduleds-number-groupe-remove:hover
.scheduleds-number-groupe-remove:hover,
.phone-limits-group-remove:hover
{
color: #555;
}
@ -466,6 +471,16 @@ footer img
color: #9b2420;
}
/* PREVIEW CONTACT */
.preview-contact-name
{
font-weight: bold;
}
.preview-contact-number
{
font-style: italic;
}
/* PHONE */
#adapter-data-container

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";

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

@ -11,6 +11,8 @@
namespace controllers\internals;
use DateInterval;
/**
* Class to call the console scripts.
*/
@ -212,4 +214,46 @@ namespace controllers\internals;
$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

@ -141,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;
@ -160,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;
}
@ -185,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;

View File

@ -73,21 +73,6 @@ 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.
*/

View File

@ -44,11 +44,6 @@ class ExpressionProvider implements ExpressionFunctionProviderInterface
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)
{
if (!($birthdate ?? false))
{
return false;
}
return $birthdate->format('m-d') == (new DateTime())->format('m-d');
});

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);

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,20 +186,67 @@ 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]);
}
/**

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

@ -11,6 +11,8 @@
namespace controllers\internals;
use Exception;
class Received extends StandardController
{
protected $model;
@ -88,14 +90,6 @@ 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)
{
$internal_smsstop->create($id_user, $origin);
}
//Link medias
$internal_media = new Media($this->bdd);
foreach ($media_ids as $media_id)
@ -116,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;
}

View File

@ -23,27 +23,31 @@ use Monolog\Logger;
*
* @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 : 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
* @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)
@ -62,6 +66,17 @@ use Monolog\Logger;
}
}
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();
@ -147,8 +162,10 @@ use Monolog\Logger;
* @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 ?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
@ -157,15 +174,17 @@ use Monolog\Logger;
*
* @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)
@ -179,6 +198,17 @@ use Monolog\Logger;
}
}
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();
@ -274,236 +304,6 @@ use Monolog\Logger;
return $this->get_model()->gets_after_date_for_number_and_user($id_user, $date, $number);
}
/**
* 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()
{
$smss_to_send_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_smsstop = new \controllers\internals\SmsStop($this->bdd);
$users_smsstops = [];
$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)
{
$smss_to_send_per_scheduled[$scheduled['id']] = [];
if (!isset($users_settings[$scheduled['id_user']]))
{
$users_settings[$scheduled['id_user']] = [];
$settings = $internal_setting->gets_for_user($scheduled['id_user']);
foreach ($settings as $name => $value)
{
$users_settings[$scheduled['id_user']][$name] = $value;
}
}
if (!isset($users_smsstops[$scheduled['id_user']]) && $users_settings[$scheduled['id_user']]['smsstop'])
{
$users_smsstops[$scheduled['id_user']] = [];
$smsstops = $internal_smsstop->gets_for_user($scheduled['id_user']);
foreach ($smsstops as $smsstop)
{
$users_smsstops[$scheduled['id_user']][] = $smsstop['number'];
}
}
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
$scheduled['medias'] = [];
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'])
{
continue;
}
$phone_to_use = $phone;
}
$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))
{
$number['data'] = json_decode($number['data'] ?? '[]', true);
$metas = ['number' => $number['number']];
$data = ['contact' => $number['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;
}
//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;
}
//Remove messages to smsstops numbers
if (($users_smsstops[$scheduled['id_user']] ?? false) && in_array($message['destination'], $users_smsstops[$scheduled['id_user']]))
{
continue;
}
$smss_to_send_per_scheduled[$scheduled['id']][] = $message;
}
}
return $smss_to_send_per_scheduled;
}
/**
* Parse a CSV file of numbers, potentially associated with datas.
*
@ -627,4 +427,351 @@ use Monolog\Logger;
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,6 +11,8 @@
namespace controllers\internals;
use Exception;
class Sended extends StandardController
{
protected $model;
@ -44,13 +46,14 @@ namespace controllers\internals;
* @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 = [], ?int $originating_scheduled = null, ?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,
@ -62,6 +65,7 @@ namespace controllers\internals;
'adapter' => $adapter,
'flash' => $flash,
'mms' => $mms,
'tag' => $tag,
'status' => $status,
'originating_scheduled' => $originating_scheduled,
];
@ -179,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.
*
@ -187,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);
}
/**
@ -222,6 +234,7 @@ namespace controllers\internals;
* @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 ?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
@ -232,23 +245,15 @@ namespace controllers\internals;
* ?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 = [], $originating_scheduled = null, 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 = [];
@ -272,16 +277,71 @@ 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'];
$status = \models\Sended::STATUS_FAILED;
$sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $mms, $medias, $originating_scheduled, $status);
$return['error_message'] = $e->getMessage();
$sended = [
$status = \models\Sended::STATUS_FAILED;
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,
@ -294,31 +354,10 @@ namespace controllers\internals;
];
$internal_webhook = new Webhook($this->bdd);
$internal_webhook->trigger($id_user, \models\Webhook::TYPE_SEND_SMS, $sended);
$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, $originating_scheduled, $status);
$sended = [
'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, $sended);
return $return;
}
/**

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.
*

View File

@ -73,7 +73,8 @@ namespace controllers\internals;
*/
public function check_for_stop(string $str)
{
return 'stop' == trim(mb_strtolower($str));
$str = trim(mb_strtolower($str));
return 'stop' == $str || 'stop sms' == $str;
}
/**

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.
*
@ -394,4 +430,17 @@ namespace controllers\internals;
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,10 +137,10 @@ 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;
}
/**

View File

@ -49,6 +49,7 @@ namespace controllers\publics;
private $internal_user;
private $internal_phone;
private $internal_phone_group;
private $internal_received;
private $internal_sended;
private $internal_scheduled;
@ -72,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);
@ -118,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))
{
@ -191,6 +193,26 @@ namespace controllers\publics;
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;
@ -210,14 +232,84 @@ 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
@ -231,8 +323,10 @@ 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'] ?? [];
@ -332,7 +426,7 @@ namespace controllers\publics;
$at = (string) $at;
$text = (string) $text;
if (($this->user['settings']['mms'] ?? false) && $mms)
if ($mms && !(int)($this->user['settings']['mms'] ?? false))
{
$return = self::DEFAULT_RETURN;
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
@ -417,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)
{
@ -433,6 +537,22 @@ 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)
@ -455,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;
@ -506,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
*/
@ -516,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)
{
@ -535,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'];
@ -545,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)
@ -627,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'];
@ -650,6 +806,7 @@ namespace controllers\publics;
* @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
*/
@ -667,10 +824,16 @@ namespace controllers\publics;
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']);
$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)
@ -683,7 +846,7 @@ namespace controllers\publics;
}
$phone_with_same_name = $this->internal_phone->get_by_name($name);
$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'];
@ -693,6 +856,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)
@ -775,7 +968,7 @@ namespace controllers\publics;
return $this->json($return);
}
$success = $this->internal_phone->update_for_user($this->user['id'], $phone['id'], $name, $adapter, $adapter_data);
$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'];
@ -817,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

@ -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

@ -11,6 +11,8 @@
namespace controllers\publics;
use Exception;
/**
* Page des contacts.
*/
@ -345,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

@ -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;
@ -315,7 +316,7 @@ namespace controllers\publics;
//Destinations must be an array of number
$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';

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)
@ -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

@ -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
@ -226,6 +231,7 @@ namespace controllers\publics;
$this->render('scheduled/edit', [
'scheduleds' => $scheduleds,
'phones' => $phones,
'phone_groups' => $phone_groups,
'contacts' => $all_contacts,
]);
}
@ -238,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
@ -258,6 +264,7 @@ 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];
@ -433,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.');
@ -473,6 +488,7 @@ 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'] ?? [];
@ -650,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

@ -59,8 +59,9 @@ namespace controllers\publics;
0 => 'phone_name',
1 => 'searchable_destination',
2 => 'text',
3 => 'at',
4 => 'status',
3 => 'tag',
4 => 'at',
5 => 'status',
];
$search = $_GET['search']['value'] ?? null;

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

@ -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

@ -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

@ -23,7 +23,6 @@ class Phone extends AbstractDaemon
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;
@ -38,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);
@ -87,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'];
@ -98,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)
@ -125,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)
{
@ -148,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'], $message['id_scheduled']);
$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,6 +46,7 @@ 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_per_scheduled = $this->internal_scheduled->get_smss_to_send();
@ -62,15 +64,14 @@ class Sender extends AbstractDaemon
{
foreach ($smss_per_scheduled as $id_scheduled => $smss)
{
//If queue not already exists
if (!msg_queue_exists(QUEUE_ID_PHONE) || !isset($this->msg_queue))
{
$this->msg_queue = msg_get_queue(QUEUE_ID_PHONE);
}
foreach ($smss as $sms)
{
//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]))
{
$this->queues[$queue_id] = msg_get_queue($queue_id);
}
$msg = [
'id_user' => $sms['id_user'],
'id_scheduled' => $sms['id_scheduled'],
@ -79,11 +80,14 @@ class Sender extends AbstractDaemon
'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);
$this->logger->info('Transmit sms send signal to phone ' . $sms['id_phone'] . ' on queue ' . $queue_id . '.');
// 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('Scheduled ' . $id_scheduled . ' treated and deleted.');
@ -99,6 +103,10 @@ class Sender extends AbstractDaemon
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

@ -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();
}
}

View File

@ -0,0 +1,38 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddReachedLimitPhoneStatus 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->changeColumn('status', 'enum', ['values' => ['available', 'unavailable', 'no_credit', 'limit_reached'], 'default' => 'available']);
$table->save();
}
}

View File

@ -0,0 +1,24 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddIndexToSmsTimestamps extends AbstractMigration
{
/**
* Add indexes on most SMS table timestamp (and possibly other fields) to improve perfs on query using date, like stats, sending limits, etc.
*/
public function change()
{
$table = $this->table('sended');
$table->addIndex('at');
$table->update();
$table = $this->table('received');
$table->addIndex('at');
$table->update();
$table = $this->table('scheduled');
$table->addIndex('at');
$table->update();
}
}

View File

@ -0,0 +1,24 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddIndexOnSendedUid extends AbstractMigration
{
/**
* Modify sended uid and call to be 100 char long, we dont need a 500 char uid and too long a char ss hurting perfs
* Add index on sended uid to make status update more efficient
*/
public function change()
{
$table = $this->table('sended');
$table->changeColumn('uid', 'string', ['limit' => 100]);
$table->addIndex('uid');
$table->update();
$table = $this->table('call');
$table->changeColumn('uid', 'string', ['limit' => 100]);
$table->addIndex('uid');
$table->update();
}
}

View File

@ -91,21 +91,21 @@
/**
* Cette fonction permet de faire un retour sous forme de json
* @param array $data : Les données à retourner sous forme de json
* @param array $datas : Les données à retourner sous forme de json
* @param boolean $secure : Défini si l'affichage doit être sécurisé contre les XSS, par défaut true
* @return ApiController : On retourne l'API controlleur lui meme pour pouvoir chainer
*/
public function json ($data, $secure = true)
public function json ($datas, $secure = true)
{
header('Content-Type: application/json');
if ($secure)
{
echo htmlspecialchars(json_encode($data), ENT_NOQUOTES);
echo htmlspecialchars(json_encode($datas), ENT_NOQUOTES);
}
else
{
echo json_encode($data);
echo json_encode($datas);
}
return $this;

View File

@ -24,7 +24,7 @@
if (!is_readable($template_path))
{
throw new DescartesTemplateNotReadableException('Template ' . $template_path . ' is not readable.');
throw new exceptions\DescartesExceptionTemplateNotReadable('Template ' . $template_path . ' is not readable.');
}
require $template_path;

View File

@ -144,7 +144,7 @@
/**
* Generate IN query params and values
* @param string $values : Values to generate in array from
* @param array $values : Values to generate in array from
* @return array : Array ['QUERY' => string 'IN(...)', 'PARAMS' => [parameters to pass to execute]]
*/
protected function _generate_in_from_array ($values)
@ -214,6 +214,11 @@
$operator = '>';
break;
case ('%' == $first_char) :
$true_fieldname = mb_substr($fieldname, 1);
$operator = 'LIKE';
break;
case ('=' == $first_char) :
$true_fieldname = mb_substr($fieldname, 1);
$operator = '=';
@ -227,8 +232,11 @@
//Protect against injection in fieldname
$true_fieldname = preg_replace('#[^a-zA-Z0-9_]#', '', $true_fieldname);
$query = '`' . $true_fieldname . '` ' . $operator . ' :where_' . $true_fieldname;
$param = ['where_' . $true_fieldname => $value];
// Add a uid to fieldname so we can combine multiple rules on same field
$uid = uniqid();
$query = '`' . $true_fieldname . '` ' . $operator . ' :where_' . $true_fieldname . '_' . $uid;
$param = ['where_' . $true_fieldname . '_' . $uid => $value];
return ['QUERY' => $query, 'PARAM' => $param];
}
@ -358,7 +366,6 @@
}
$query = "SELECT COUNT(*) as `count` FROM `" . $table . "` WHERE 1 " . (count($wheres) ? 'AND ' : '') . implode(' AND ', $wheres);
$query = $this->pdo->prepare($query);
foreach ($params as $label => &$param)

View File

@ -72,8 +72,7 @@
protected static function clean_url (string $url)
{
$to_remove = parse_url(HTTP_PWD, PHP_URL_PATH);
$url = mb_strcut($url, mb_strlen($to_remove));
$url = mb_strcut($url, $to_remove ? mb_strlen($to_remove) : 0);
$url = parse_url($url, PHP_URL_PATH);
return $url;
@ -252,6 +251,7 @@
$type = $parameter->getType();
$type = $type ?? false;
$type = ($type instanceof \ReflectionNamedType) ? $type->getName() : $type;
if ($type)
{

View File

@ -4,25 +4,32 @@
/*
* Define Descartes env
*/
$http_dir_path = '/raspisms'; //Path we need to put after servername in url to access app
$http_protocol = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://';
$http_server_name = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost';
$http_server_port = isset($_SERVER['SERVER_PORT']) ? ($_SERVER['SERVER_PORT'] == 80) ? '' : ':' . $_SERVER['SERVER_PORT'] : '';
$https = $_SERVER['HTTPS'] ?? false;
$http_dir_path = ''; //Path we need to put after servername in url to access app
$https = $_SERVER['HTTPS'] ?? 0;
if ( !isset($_SERVER['SERVER_PORT']) || ($_SERVER['SERVER_PORT'] == 80 && !$https) || ($_SERVER['SERVER_PORT'] == 443 && $https) )
// Check for proxy forward
$forwarded_https = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? $_SERVER['HTTP_FORWARDED_PROTO'] ?? NULL) == 'https';
$forwarded_ssl = ($_SERVER['HTTP_X_FORWARDED_SSL'] ?? NULL) == 'on';
$proxy = $forwarded_https || $forwarded_ssl;
$http_protocol = 'http://';
if ($https)
{
$http_server_port = '';
}
else
{
$http_server_port = ':' . $_SERVER['SERVER_PORT'];
$http_protocol = 'https://';
}
$http_server_name = $_SERVER['SERVER_NAME'] ?? 'localhost';
// Check port to only set it if not default port
$port = $_SERVER['SERVER_PORT'] ?? '';
$port = ($port == 80 && !$https) ? '' : $port;
$port = ($port == 443 && $https) ? '' : $port;
$port = $proxy ? '' : $port;
$http_server_port = $port ? ':' . $port : '';
$pwd = substr(__DIR__, 0, strrpos(__DIR__, '/'));
$http_pwd = $http_protocol . $http_server_name . $http_server_port . $http_dir_path;
$env = [
//Global http and file path
@ -31,31 +38,25 @@
'HTTP_SERVER_NAME' => $http_server_name,
'HTTP_SERVER_PORT' => $http_server_port,
'PWD' => $pwd,
'HTTP_PWD' => $http_pwd,
//path of back resources
'PWD_CONTROLLER' => $pwd . '/controllers', //Controllers dir
'PWD_MODEL' => $pwd . '/models', //Models dir
'PWD_TEMPLATES' => $pwd . '/templates', //Templates dir
//path of front resources
'PWD_ASSETS' => $pwd . '/assets', //Assets dir
'HTTP_PWD_ASSETS' => $http_pwd . '/assets', //HTTP path of asset dir
'PWD_ASSETS' => $pwd . '/assets', //Assets dir
//images
'PWD_IMG' => $pwd . '/assets' . '/img',
'HTTP_PWD_IMG' => $http_pwd . '/assets' . '/img',
//css
'PWD_CSS' => $pwd . '/assets' . '/css',
'HTTP_PWD_CSS' => $http_pwd . '/assets' . '/css',
//javascript
'PWD_JS' => $pwd . '/assets' . '/js',
'HTTP_PWD_JS' => $http_pwd . '/assets' . '/js',
//fonts
'PWD_FONT' => $pwd . '/assets' . '/font',
'HTTP_PWD_FONT' => $http_pwd . '/assets' . '/font',
];

View File

@ -21,6 +21,7 @@
$environment = [];
$env = [];
// Load descartes base env
require_once(__DIR__ . '/env.php');
$environment = array_merge($environment, $env);
@ -31,11 +32,8 @@
$environment = array_merge($environment, $env);
}
//Define all Descartes constants
define_array($environment);
### GLOBAL ENV ###
$environment = [];
//Load global app env
$env = [];
if (file_exists(__DIR__ . '/../env.php'))
{
@ -43,19 +41,30 @@
$environment = array_merge($environment, $env);
}
define_array($environment);
### SPECIFIC ENV ###
$environment = [];
// Load specific environment env
$env = [];
if (defined('ENV') && file_exists(__DIR__ . '/../env.' . ENV . '.php'))
if (isset($environment['ENV']) && file_exists(__DIR__ . '/../env.' . $environment['ENV'] . '.php'))
{
require_once(__DIR__ . '/../env.' . ENV . '.php');
require_once(__DIR__ . '/../env.' . $environment['ENV'] . '.php');
$environment = array_merge($environment, $env);
}
### BUILD HTTP PWD CONSTS ###
// We compute http pwd at last minute to allow for simple overriding by user
// by simply defining custom HTTP_* (PROTOCOL, SERVER_NAME, SERVER_PORT, DIR_PATH)
$http_pwd = $environment['HTTP_PROTOCOL'] . $environment['HTTP_SERVER_NAME'] . $environment['HTTP_SERVER_PORT'] . $environment['HTTP_DIR_PATH'];
$env = [
"HTTP_PWD" => $http_pwd,
'HTTP_PWD_ASSETS' => $http_pwd . '/assets', //HTTP path of asset dir
'HTTP_PWD_IMG' => $http_pwd . '/assets' . '/img',
'HTTP_PWD_CSS' => $http_pwd . '/assets' . '/css',
'HTTP_PWD_JS' => $http_pwd . '/assets' . '/js',
'HTTP_PWD_FONT' => $http_pwd . '/assets' . '/font',
];
$environment = array_merge($environment, $env);
define_array($environment);
}

View File

@ -2,28 +2,31 @@
/*
This file define constants and options for the app
*/
$dir_path = '/raspisms';
$http_pwd = $environment['HTTP_PROTOCOL'] . $dir_path . $environment['HTTP_SERVER_PORT'] . $environment['HTTP_DIR_PATH'];
$env = [
'ENV' => '%APP_ENV%', #env name (probably 'dev' or 'prod'), this value is used to get the env.XXX.php.dist matching env file
'SESSION_NAME' => 'raspisms',
'HTTP_DIR_PATH' => $dir_path, // Override default dir path
//RaspiSMS settings
'WEBSITE_TITLE' => 'RaspiSMS',
'WEBSITE_DESCRIPTION' => '',
'WEBSITE_AUTHOR' => 'Raspberry Pi FR',
'PWD_SCRIPTS' => PWD . '/scripts',
'PWD_RECEIVEDS' => PWD . '/receiveds',
'HTTP_PWD_SOUND' => HTTP_PWD_ASSETS . '/sounds',
'PWD_ADAPTERS' => PWD . '/adapters',
'PWD_DATA' => PWD . '/data',
'HTTP_PWD_DATA' => HTTP_PWD . '/data',
'PWD_DATA_PUBLIC' => PWD . '/data/public',
'HTTP_PWD_DATA_PUBLIC' => HTTP_PWD . '/data/public',
'PWD_SCRIPTS' => $environment['PWD'] . '/scripts',
'PWD_RECEIVEDS' => $environment['PWD'] . '/receiveds',
'HTTP_PWD_SOUND' => $http_pwd . '/assets' . '/sounds',
'PWD_ADAPTERS' => $environment['PWD'] . '/adapters',
'PWD_DATA' => $environment['PWD'] . '/data',
'HTTP_PWD_DATA' => $http_pwd . '/data',
'PWD_DATA_PUBLIC' => $environment['PWD'] . '/data/public',
'HTTP_PWD_DATA_PUBLIC' => $http_pwd . '/data/public',
'PWD_LOGS' => '/var/log/raspisms',
'PWD_PID' => '/var/run/raspisms',
'APP_SECRET' => '%APP_SECRET%',
'ENABLE_COMMAND' => false,
'ENABLE_ACCOUNT_DELETION' => true,
'ENABLE_URL_SHORTENER' => %APP_URL_SHORTENER%,
//E-mail types
'EMAIL_RESET_PASSWORD' => [
@ -53,17 +56,16 @@
],
//Phone messages types
'QUEUE_ID_PHONE_PREFIX' => ftok(__FILE__, 'p'),
'QUEUE_TYPE_SEND_MSG' => 1,
'QUEUE_TYPE_RECEIVE_MSG' => 2,
'QUEUE_ID_PHONE' => ftok(__FILE__, 'p'),
'QUEUE_TYPE_SEND_MSG_PREFIX' => 100,
//Queues ids
'QUEUE_ID_WEBHOOK' => ftok(__FILE__, 'w'),
'QUEUE_TYPE_WEBHOOK' => 3,
'QUEUE_TYPE_WEBHOOK' => 100,
//Queue email
'QUEUE_ID_EMAIL' => ftok(__FILE__, 'e'),
'QUEUE_TYPE_EMAIL' => 3,
'QUEUE_TYPE_EMAIL' => 100,
//User default settings
'USER_DEFAULT_SETTINGS' => [
@ -83,6 +85,12 @@
'alert_quota_limit_reached' => 1,
'alert_quota_limit_close' => 0.9,
'hide_menus' => '',
'force_gsm_alphabet' => 0,
'phone_limit' => 0,
'phone_priority' => 0,
'shorten_url' => 0,
'smsstop_respond' => 1,
'smsstop_response' => 'Demande prise en compte, vous ne recevrez plus de messages.',
],
];

View File

@ -21,4 +21,11 @@
'FROM' => '%APP_MAIL_FROM%',
],
//YOURLS url shortener settings
'URL_SHORTENER' => [
'HOST' => '%APP_URL_SHORTENER_HOST%',
'USER' => '%APP_URL_SHORTENER_USER%',
'PASS' => '%APP_URL_SHORTENER_PASS%',
]
];

View File

@ -84,32 +84,6 @@ namespace models;
return $this->_select('event', ['id_user' => $id_user], 'at', true, $nb_entry);
}
/**
* 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)
{
$where = [
'id_user' => $id_user,
'type' => $type,
'>=at' => $since->format('Y-m-d H:i:s'),
];
if (null !== $until)
{
$where['<=at'] = $until->format('Y-m-d H:i:s');
}
return $this->_select('event', $where, 'at');
}
/**
* Return table name.
*/

View File

@ -13,6 +13,36 @@ namespace models;
class Phone extends StandardModel
{
const STATUS_AVAILABLE = 'available';
const STATUS_UNAVAILABLE = 'unavailable';
const STATUS_NO_CREDIT = 'no_credit';
const STATUS_LIMIT_REACHED = 'limit_reached';
/**
* Return all phones that belongs to active users
*
* @return array
*/
public function get_all_for_active_users()
{
$query = '
SELECT phone.*
FROM phone
LEFT JOIN user
ON phone.id_user = user.id
WHERE user.status = :status
';
$params = [
'status' => \models\User::STATUS_ACTIVE,
];
$result = $this->_run_query($query, $params);
return $result;
}
/**
* Return a phone by his name and user.
*
@ -38,6 +68,48 @@ namespace models;
return $this->_select_one('phone', ['name' => $name]);
}
/**
* Return a list of phone limits
*
* @param int $id_phone : Phone id
*
* @return array
*/
public function get_limits(int $id_phone)
{
return $this->_select('phone_limit', ['id_phone' => $id_phone]);
}
/**
* Add a limit for a phone.
*
* @param int $id_phone : Phone id
* @param int $volume : Limit in volume of SMS
* @param string $startpoint : A relative time to use as startpoint for counting volume. See https://www.php.net/manual/en/datetime.formats.relative.php
*
* @return mixed (bool|int) : False on error, new row id else
*/
public function insert_phone_limit(int $id_phone, int $volume, string $startpoint)
{
$success = $this->_insert('phone_limit', ['id_phone' => $id_phone, 'volume' => $volume, 'startpoint' => $startpoint]);
return $success ? $this->_last_id() : false;
}
/**
* Delete limits for a phone
*
* @param array $id_phone : Phone id
*
* @return array
*/
public function delete_phone_limits(int $id_phone)
{
return $this->_delete('phone_limit', ['id_phone' => $id_phone]);
}
/**
* Return table name.
*/

123
models/PhoneGroup.php Normal file
View File

@ -0,0 +1,123 @@
<?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 models;
class PhoneGroup extends StandardModel
{
/**
* Return a list of phone groups for a user.
* Add a column nb_phone.
*
* @param int $id_user : user id
* @param ?int $limit : Number of entry to return or null
* @param ?int $offset : Number of entry to ignore or null
*
* @return array
*/
public function list_for_user(int $id_user, $limit, $offset)
{
$query = '
SELECT pg.*, COUNT(pgp.id) as nb_phone
FROM `phone_group` as pg
LEFT JOIN phone_group_phone as pgp
ON pg.id = pgp.id_phone_group
WHERE pg.id_user = :id_user
GROUP BY pg.id
';
if (null !== $limit)
{
$limit = (int) $limit;
$query .= ' LIMIT ' . $limit;
if (null !== $offset)
{
$offset = (int) $offset;
$query .= ' OFFSET ' . $offset;
}
}
$params = [
'id_user' => $id_user,
];
return $this->_run_query($query, $params);
}
/**
* Return a phone 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->_select_one($this->get_table_name(), ['id_user' => $id_user, 'name' => $name]);
}
/**
* Delete relations between phone group and phone for a group.
*
* @param int $id_phone_group : Group id
*
* @return int : Number of deleted rows
*/
public function delete_phone_group_phone_relations(int $id_phone_group)
{
return $this->_delete('phone_group_phone', ['id_phone_group' => $id_phone_group]);
}
/**
* Insert a relation between a phone group and a phone.
*
* @param int $id_phone_group : Phone Group id
* @param int $id_phone : Phone id
*
* @return mixed (bool|int) : False on error, new row id else
*/
public function insert_phone_group_phone_relation(int $id_phone_group, int $id_phone)
{
$success = (bool) $this->_insert('phone_group_phone', ['id_phone_group' => $id_phone_group, 'id_phone' => $id_phone]);
return $success ? $this->_last_id() : false;
}
/**
* Get phone groups phones.
*
* @param int $id_phone_group : Phone Group id
*
* @return array : Phones of the group
*/
public function get_phones(int $id_phone_group)
{
$query = '
SELECT *
FROM `phone`
WHERE id IN (SELECT id_phone FROM `phone_group_phone` WHERE id_phone_group = :id_phone_group)
';
$params = ['id_phone_group' => $id_phone_group];
return $this->_run_query($query, $params);
}
/**
* Return table name.
*/
protected function get_table_name(): string
{
return 'phone_group';
}
}

View File

@ -178,6 +178,42 @@ namespace models;
return $this->_select_one('sended', ['id_user' => $id_user, 'uid' => $uid, 'adapter' => $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 = null, ?\DateTime $before = null, ?string $tag_like = null) : int
{
$data = [
'id_user' => $id_user,
'id_phone' => $id_phone,
];
if ($since)
{
$data['>=at'] = $since->format('c');
}
if ($before)
{
$data['<=at'] = $before->format('c');
}
if ($tag_like)
{
$data['%tag'] = $tag_like;
}
return $this->_count('sended', $data);
}
/**
* Get number of sended SMS for every date since a date for a specific user.
*
@ -186,14 +222,14 @@ namespace models;
*
* @return array
*/
public function count_by_day_since_for_user($id_user, $date)
public function count_by_day_and_status_since_for_user($id_user, $date)
{
$query = "
SELECT COUNT(id) as nb, DATE_FORMAT(at, '%Y-%m-%d') as at_ymd
SELECT COUNT(id) as nb, status, DATE_FORMAT(at, '%Y-%m-%d') as at_ymd
FROM sended
WHERE at > :date
AND id_user = :id_user
GROUP BY at_ymd
GROUP BY at_ymd, status
";
$params = [

View File

@ -27,6 +27,18 @@ namespace models;
return $this->_update($this->get_table_name(), ['value' => $value], ['id_user' => $id_user, 'name' => $name]);
}
/**
* 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->_select_one($this->get_table_name(), ['name' => $name, 'id_user' => $id_user]);
}
/**
* Return table name.
*/

View File

@ -13,6 +13,8 @@ namespace models;
class SmsStop extends StandardModel
{
const SMS_STOP_TAG = 'SMS_STOP';
/**
* Return a smsstop by his number and user.
*

View File

@ -76,6 +76,7 @@
'delete' => '/group/delete/{csrf}/',
'edit' => '/group/edit/',
'update' => '/group/update/{csrf}/',
'preview' => '/group/preview/{id_group}/',
'json_list' => '/groups.json/',
],
@ -88,6 +89,7 @@
'edit' => '/conditional_group/edit/',
'update' => '/conditional_group/update/{csrf}/',
'contacts_preview' => '/conditional_group/preview/',
'preview' => '/conditional_group/preview/{id_group}/',
'json_list' => '/conditional_groups.json/',
],
@ -160,6 +162,22 @@
'add' => '/phone/add/',
'create' => '/phone/create/{csrf}/',
'delete' => '/phone/delete/{csrf}/',
'edit' => '/phone/edit/',
'update' => '/phone/update/{csrf}/',
'update_status' => '/phone/update_status/{csrf}/',
'json_list' => '/phones.json/',
],
'PhoneGroup' => [
'list' => '/phone_group/',
'list_json' => '/phone_group/json/',
'add' => '/phone_group/add/',
'create' => '/phone_group/create/{csrf}/',
'delete' => '/phone_group/delete/{csrf}/',
'edit' => '/phone_group/edit/',
'update' => '/phone_group/update/{csrf}/',
'preview' => '/phone_group/preview/{id_group}/',
'json_list' => '/phone_group.json/',
],
'Call' => [
@ -190,6 +208,7 @@
'/api/list/{entry_type}/',
'/api/list/{entry_type}/{page}/',
],
'get_usage' => '/api/usage/',
'post_scheduled' => [
'/api/scheduled/',
],
@ -202,6 +221,9 @@
'post_update_phone' => [
'/api/phone/{id}/',
],
'post_update_phone_status' => [
'/api/phone/{id}/status/',
],
'delete_phone' => [
'/api/phone/{id}/',
],

View File

@ -43,6 +43,7 @@
<th>Condition</th>
<th>Date de création</th>
<th>Dernière modification</th>
<th>Preview</th>
<th class="checkcolumn"><input type="checkbox" id="check-all"/></th>
</tr>
</thead>
@ -69,9 +70,56 @@
</div>
</div>
</div>
<div class="modal fade" tabindex="-1" id="preview-text-modal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Prévisualisation des contacts</h4>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
jQuery(document).ready(function ()
{
jQuery('body').on('click', '.preview-button', function (e)
{
e.preventDefault();
var group_id = jQuery(this).attr('data-id-group');
jQuery.ajax({
type: "GET",
url: HTTP_PWD + '/conditional_group/preview/' + group_id + '/',
success: function (data) {
if (!data.success) {
jQuery('#preview-text-modal').find('.modal-body').text(data.result);
} else {
html = '';
for (contact of data.result)
{
html += '<div class="preview-contact well">';
html += ' <div class="preview-contact-name">' + jQuery.fn.dataTable.render.text().display(contact.name) + '</div>'
html += ' <div class="preview-contact-number">' + jQuery.fn.dataTable.render.text().display(contact.number) + '</div>'
html += ' <code class="preview-contact-data">' + jQuery.fn.dataTable.render.text().display(contact.data) + '</code>'
html += '</div>';
console.log(contact);
}
jQuery('#preview-text-modal').find('.modal-body').html(html);
}
jQuery('#preview-text-modal').modal({'keyboard': true});
},
dataType: 'json'
});
});
jQuery('.datatable').DataTable({
"pageLength": 25,
"lengthMenu": [[25, 50, 100, 1000, 10000, -1], [25, 50, 100, 1000, 10000, "All"]],
@ -97,6 +145,12 @@ jQuery(document).ready(function ()
},
{data: 'created_at'},
{data: 'updated_at'},
{
data: '_',
render: function (data, type, row, meta) {
return '<a class="btn btn-info preview-button" href="#" data-id-group="' + jQuery.fn.dataTable.render.text().display(row.id) + '"><span class="fa fa-eye"></span></a>';
},
},
{
data: 'id',
render: function (data, type, row, meta) {
@ -106,7 +160,6 @@ jQuery(document).ready(function ()
],
"deferRender": true
});
});
</script>
<?php

View File

@ -1,147 +1,160 @@
<?php
//Template dashboard
$this->render('incs/head')
//Template dashboard
$this->render('incs/head')
?>
<div id="wrapper">
<?php
$this->render('incs/nav', ['page' => 'dashboard'])
$this->render('incs/nav', ['page' => 'dashboard'])
?>
<div id="page-wrapper">
<div class="container-fluid">
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">
Dashboard <small>Statistiques d'utilisation</small>
</h1>
<ol class="breadcrumb">
<li class="active">
<i class="fa fa-dashboard"></i> Dashboard
</li>
</ol>
</div>
</div>
<!-- /.row -->
<div id="page-wrapper">
<div class="container-fluid">
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">
Dashboard <small>Statistiques d'utilisation</small>
</h1>
<ol class="breadcrumb">
<li class="active">
<i class="fa fa-dashboard"></i> Dashboard
</li>
</ol>
</div>
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-3 col-md-6">
<div class="panel panel-primary">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-user fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge"><?php echo $nb_contacts; ?></div>
<div>Contacts</div>
</div>
</div>
</div>
<a href="<?php echo \descartes\Router::url('Contact', 'list') ?>">
<div class="panel-footer">
<span class="pull-left">Voir vos contacts</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="panel panel-green">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-group fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge"><?php echo $nb_groups; ?></div>
<div>Groupes</div>
</div>
</div>
</div>
<a href="<?php echo \descartes\Router::url('Group', 'list') ?>">
<div class="panel-footer">
<span class="pull-left">Voir les groupes</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="panel panel-yellow">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-calendar fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge"><?php echo $nb_scheduleds; ?></div>
<div>SMS programmés</div>
</div>
</div>
</div>
<a href="<?php echo \descartes\Router::url('Scheduled', 'list') ?>">
<div class="panel-footer">
<span class="pull-left">Voir les SMS programmés</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="panel panel-red">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-eye-slash fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge"><?php echo $nb_unreads; ?></div>
<div>SMS non lus</div>
</div>
</div>
</div>
<a href="<?php echo \descartes\Router::url('Received', 'list_unread') ?>">
<div class="panel-footer">
<span class="pull-left">Voir les SMS non lus</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-3 col-md-6">
<div class="panel panel-primary">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-user fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge"><?php echo $nb_contacts; ?></div>
<div>Contacts</div>
</div>
</div>
</div>
<a href="<?php echo \descartes\Router::url('Contact', 'list') ?>">
<div class="panel-footer">
<span class="pull-left">Voir vos contacts</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="panel panel-green">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-group fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge"><?php echo $nb_groups; ?></div>
<div>Groupes</div>
</div>
</div>
</div>
<a href="<?php echo \descartes\Router::url('Group', 'list') ?>">
<div class="panel-footer">
<span class="pull-left">Voir les groupes</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="panel panel-yellow">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-calendar fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge"><?php echo $nb_scheduleds; ?></div>
<div>SMS programmés</div>
</div>
</div>
</div>
<a href="<?php echo \descartes\Router::url('Scheduled', 'list') ?>">
<div class="panel-footer">
<span class="pull-left">Voir les SMS programmés</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="panel panel-red">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<i class="fa fa-eye-slash fa-5x"></i>
</div>
<div class="col-xs-9 text-right">
<div class="huge"><?php echo $nb_unreads; ?></div>
<div>SMS non lus</div>
</div>
</div>
</div>
<a href="<?php echo \descartes\Router::url('Received', 'list_unread') ?>">
<div class="panel-footer">
<span class="pull-left">Voir les SMS non lus</span>
<span class="pull-right"><i class="fa fa-arrow-circle-right"></i></span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default dashboard-panel-chart">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> Activité de la semaine : </h3>
<span style="color: #5CB85C;">SMS envoyés (moyenne = <?php echo $avg_sendeds; ?> par jour).</span><br/>
<span style="color: #EDAB4D">SMS reçus (moyenne = <?php echo $avg_receiveds; ?> par jour).</span>
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default dashboard-panel-chart">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> SMS envoyés depuis le <?= $stats_start_date_formated; ?> : </h3>
<span style="color: #5CB85C;">SMS envoyés (moyenne = <?php echo $avg_sendeds; ?> par jour).</span><br/>
<?php if ($quota_unused) { ?>
<br/>
<span style="color: #d9534f">Crédits restants : <?= $quota_unused; ?>.</span>
<?php } ?>
</div>
<div class="panel-body">
<div id="morris-area-chart"></div>
</div>
</div>
</div>
</div>
<!-- /.row -->
</div>
<div class="panel-body">
<div id="morris-bar-chart-sended"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-upload fa-fw"></i> SMS Envoyés</h3>
</div>
<div class="col-lg-12">
<div class="panel panel-default dashboard-panel-chart">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> SMS reçus depuis le <?= $stats_start_date_formated; ?> : </h3>
<span style="color: #EDAB4D">SMS reçus (moyenne = <?php echo $avg_receiveds; ?> par jour).</span>
</div>
<div class="panel-body">
<div id="morris-bar-chart-received"></div>
</div>
</div>
</div>
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-upload fa-fw"></i> SMS Envoyés</h3>
</div>
<div class="panel-body">
<?php if (!$sendeds) { ?>
Vous n'avez envoyé aucun SMS pour l'instant.
@ -168,14 +181,14 @@
<a href="<?php echo \descartes\Router::url('Sended', 'list'); ?>">Voir tous les SMS envoyés <i class="fa fa-arrow-circle-right"></i></a>
</div>
<?php } ?>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-download fa-fw"></i> SMS Reçus</h3>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-download fa-fw"></i> SMS Reçus</h3>
</div>
<div class="panel-body">
<?php if (!$receiveds) { ?>
Vous n'avez reçu aucun SMS pour l'instant.
@ -204,14 +217,14 @@
<a href="<?php echo \descartes\Router::url('Received', 'list'); ?>">Voir tous les SMS reçus <i class="fa fa-arrow-circle-right"></i></a>
</div>
<?php } ?>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-clock-o fa-fw"></i> Évènements survenus</h3>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-clock-o fa-fw"></i> Évènements survenus</h3>
</div>
<div class="panel-body">
<?php if (!$events) { ?>
Aucun évènement n'est encore survenu.
@ -228,41 +241,73 @@
<a href="<?php echo \descartes\Router::url('Event', 'list'); ?>">Voirs tous les évènements survenus <i class="fa fa-arrow-circle-right"></i></a>
</div>
<?php } ?>
</div>
</div>
</div>
</div>
<!-- /.row -->
</div>
</div>
</div>
</div>
<!-- /.row -->
</div>
<!-- /.container-fluid -->
</div>
<!-- /.container-fluid -->
</div>
<!-- /#page-wrapper -->
</div>
<!-- /#page-wrapper -->
</div>
<script>
jQuery(document).ready(function()
{
Morris.Area({
element: 'morris-area-chart',
behaveLikeLine: true,
fillOpacity: 0.4,
data: <?php echo $data_area_chart;?>,
jQuery(document).ready(function()
{
Morris.Bar({
element: 'morris-bar-chart-sended',
fillOpacity: 0.4,
data: <?php echo $data_bar_chart_sended;?>,
xkey: 'period',
parseTime: false,
ykeys: ['sendeds', 'receiveds'],
labels: ['SMS envoyés', 'SMS reçus'],
lineColors: ['#5CB85C', '#EDAB4D'],
goals: [<?php echo $avg_sendeds; ?>, <?php echo $avg_receiveds; ?>],
goalLineColors: ['#5CB85C', '#EDAB4D'],
goalStrokeWidth: 2,
pointSize: 4,
hideHover: 'auto',
resize: true
});
});
ykeys: ['sendeds_failed', 'sendeds_unknown', 'sendeds_delivered'],
labels: ['SMS échoués', 'SMS inconnus', 'SMS délivrés'],
barColors: ['#D9534F', '#337AB7', '#5CB85C'],
goals: [<?php echo $avg_sendeds; ?>,],
goalLineColors: ['#5CB85C'],
goalStrokeWidth: 2,
pointSize: 4,
hideHover: 'auto',
resize: true,
stacked: true,
hoverCallback: function (index, options, content, row) {
ret = '';
for (i = 0; i < options.ykeys.length; i++)
{
ret += options.labels[i];
ret += ' : ';
ret += row[options.ykeys[i]];
ret += ' (';
ret += (row[options.ykeys[i]] / (row.sendeds_total ? row.sendeds_total : 1) * 100).toFixed(2);
ret += '%)';
ret += "<br/>";
}
return ret;
}
});
Morris.Bar({
element: 'morris-bar-chart-received',
fillOpacity: 0.4,
data: <?php echo $data_bar_chart_received;?>,
xkey: 'period',
parseTime: false,
ykeys: ['receiveds'],
labels: ['SMS reçus'],
barColors: ['#EDAB4D'],
goals: [<?php echo $avg_receiveds; ?>],
goalLineColors: ['#EDAB4D'],
goalStrokeWidth: 2,
pointSize: 4,
hideHover: 'auto',
resize: true,
});
});
</script>
<!-- /#wrapper -->
<?php
$this->render('incs/footer');
$this->render('incs/footer');

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