Merge branch 'develop' into pr/session-class

This commit is contained in:
Kijin Sung 2017-02-07 22:13:08 +09:00
commit 483ac84796
454 changed files with 10659 additions and 30145 deletions

View file

@ -136,7 +136,6 @@ class Config
// Save the main config file.
$buff = '<?php' . "\n" . '// Rhymix System Configuration' . "\n" . 'return ' . self::serialize(self::$_config) . ';' . "\n";
$result = Storage::write(\RX_BASEDIR . self::$config_filename, $buff) ? true : false;
//if (!$result) return false;
// Save XE-compatible config files.
$db_info = \Context::convertDBInfo(self::$_config);
@ -148,7 +147,7 @@ class Config
Storage::write(\RX_BASEDIR . self::$old_db_config_filename, $buff);
$buff = '<?php' . "\n\n" . $warning . "\n\n" . '$ftp_info = ' . self::serialize($ftp_info) . ';' . "\n";
Storage::write(\RX_BASEDIR . self::$old_ftp_config_filename, $buff);
return true;
return $result;
}
/**

View file

@ -236,10 +236,33 @@ class Debug
'file' => $query['called_file'],
'line' => $query['called_line'],
'method' => $query['called_method'],
'backtrace' => $query['backtrace'],
'backtrace' => $query['backtrace'] ?: array(),
);
self::$_queries[] = $query_object;
// Add the entry to the error log if the result wasn't successful.
if ($query['result'] === 'error')
{
$error_object = (object)array(
'type' => 'Query Error',
'time' => $query_object->time,
'message' => $query['errstr'] . ' (code ' . intval($query['errno']) . ')',
'file' => $query_object->file,
'line' => $query_object->line,
'backtrace' => $query_object->backtrace ?: array(),
);
self::$_errors[] = $error_object;
if (config('debug.write_error_log') === 'all')
{
$log_entry = strtr(sprintf('Query Error: %s in %s on line %d', $error_object->message, $error_object->file, intval($error_object->line)), "\0\r\n\t\v\e\f", ' ');
error_log($log_entry . \PHP_EOL . self::formatBacktrace($error_object->backtrace));
}
}
// Add the entry to the slow query log.
if ($query_object->query_time && $query_object->query_time >= config('debug.log_slow_queries'))
{
self::$_slow_queries[] = $query_object;

View file

@ -143,6 +143,10 @@ class Redis implements \Rhymix\Framework\Drivers\CacheInterface
{
return null;
}
if (ctype_digit($value))
{
return $value;
}
$value = unserialize($value);
if ($value === false)
@ -168,7 +172,8 @@ class Redis implements \Rhymix\Framework\Drivers\CacheInterface
{
try
{
return $this->_conn->setex($key, $ttl, serialize($value)) ? true : false;
$value = (is_scalar($value) && ctype_digit($value)) ? $value : serialize($value);
return $this->_conn->setex($key, $ttl, $value) ? true : false;
}
catch (\RedisException $e)
{

View file

@ -61,6 +61,7 @@ class SQLite implements \Rhymix\Framework\Drivers\CacheInterface
protected function _connect($filename)
{
$this->_dbh = new \SQLite3($filename);
$this->_dbh->busyTimeout(250);
$this->_dbh->exec('PRAGMA journal_mode = MEMORY');
$this->_dbh->exec('PRAGMA synchronous = OFF');
}
@ -89,7 +90,7 @@ class SQLite implements \Rhymix\Framework\Drivers\CacheInterface
*/
public static function isSupported()
{
return class_exists('\\SQLite3', false) && config('crypto.authentication_key') !== null;
return class_exists('\\SQLite3', false) && config('crypto.authentication_key') !== null && stripos(\PHP_SAPI, 'win') === false;
}
/**
@ -117,8 +118,18 @@ class SQLite implements \Rhymix\Framework\Drivers\CacheInterface
{
$table = 'cache_' . (crc32($key) % 32);
$stmt = $this->_dbh->prepare('SELECT v, exp FROM ' . $table . ' WHERE k = :key');
if (!$stmt)
{
return null;
}
$stmt->bindValue(':key', $key, \SQLITE3_TEXT);
$result = $stmt->execute();
if (!$result)
{
return null;
}
$row = $result->fetchArray(\SQLITE3_NUM);
if ($row)
{
@ -154,6 +165,11 @@ class SQLite implements \Rhymix\Framework\Drivers\CacheInterface
{
$table = 'cache_' . (crc32($key) % 32);
$stmt = $this->_dbh->prepare('INSERT OR REPLACE INTO ' . $table . ' (k, v, exp) VALUES (:key, :val, :exp)');
if (!$stmt)
{
return false;
}
$stmt->bindValue(':key', $key, \SQLITE3_TEXT);
$stmt->bindValue(':val', serialize($value), \SQLITE3_TEXT);
$stmt->bindValue(':exp', $ttl ? (time() + $ttl) : 0, \SQLITE3_INTEGER);
@ -173,6 +189,11 @@ class SQLite implements \Rhymix\Framework\Drivers\CacheInterface
{
$table = 'cache_' . (crc32($key) % 32);
$stmt = $this->_dbh->prepare('DELETE FROM ' . $table . ' WHERE k = :key');
if (!$stmt)
{
return false;
}
$stmt->bindValue(':key', $key, \SQLITE3_TEXT);
return $stmt->execute() ? true : false;
}
@ -189,9 +210,19 @@ class SQLite implements \Rhymix\Framework\Drivers\CacheInterface
{
$table = 'cache_' . (crc32($key) % 32);
$stmt = $this->_dbh->prepare('SELECT 1 FROM ' . $table . ' WHERE k = :key AND (exp = 0 OR exp >= :exp)');
if (!$stmt)
{
return false;
}
$stmt->bindValue(':key', $key, \SQLITE3_TEXT);
$stmt->bindValue(':exp', time(), \SQLITE3_INTEGER);
$result = $stmt->execute();
if (!$result)
{
return false;
}
$row = $result->fetchArray(\SQLITE3_NUM);
if ($row)
{
@ -215,14 +246,17 @@ class SQLite implements \Rhymix\Framework\Drivers\CacheInterface
*/
public function incr($key, $amount)
{
$this->_dbh->exec('BEGIN');
$current_value = $this->get($key);
$new_value = intval($current_value) + $amount;
if ($this->set($key, $new_value))
{
$this->_dbh->exec('COMMIT');
return $new_value;
}
else
{
$this->_dbh->exec('ROLLBACK');
return false;
}
}

View file

@ -29,7 +29,7 @@ abstract class Base implements \Rhymix\Framework\Drivers\MailInterface
* Create a new instance of the current mail driver, using the given settings.
*
* @param array $config
* @return void
* @return object
*/
public static function getInstance(array $config)
{

View file

@ -160,13 +160,16 @@ class Woorimail extends Base implements \Rhymix\Framework\Drivers\MailInterface
$data['receiver_nickname'] = implode(',', $data['receiver_nickname']);
// Define connection options.
$headers = array(
'Accept' => 'application/json, text/javascript, */*; q=0.1',
);
$options = array(
'timeout' => 5,
'useragent' => 'PHP',
);
// Send the API request.
$request = \Requests::post(self::$_url, array(), $data, $options);
$request = \Requests::post(self::$_url, $headers, $data, $options);
$result = @json_decode($request->body);
// Parse the result.

View file

@ -0,0 +1,120 @@
<?php
namespace Rhymix\Framework\Drivers\SMS;
/**
* The base class for other SMS drivers.
*/
abstract class Base implements \Rhymix\Framework\Drivers\SMSInterface
{
/**
* The configuration is stored here.
*/
protected $_config = null;
/**
* The driver specification is stored here.
*/
protected static $_spec = array();
/**
* Config keys used by this driver are stored here.
*/
protected static $_required_config = array();
protected static $_optional_config = array();
/**
* Direct invocation of the constructor is not permitted.
*/
protected function __construct(array $config)
{
$this->_config = $config;
}
/**
* Create a new instance of the current SMS driver, using the given settings.
*
* @param array $config
* @return object
*/
public static function getInstance(array $config)
{
return new static($config);
}
/**
* Get the human-readable name of this SMS driver.
*
* @return string
*/
public static function getName()
{
return class_basename(get_called_class());
}
/**
* Get the list of configuration fields required by this SMS driver.
*
* @return array
*/
public static function getRequiredConfig()
{
return static::$_required_config;
}
/**
* Get the list of configuration fields optionally used by this SMS driver.
*
* @return array
*/
public static function getOptionalConfig()
{
return static::$_optional_config;
}
/**
* Get the list of API types supported by this SMS driver.
*
* @return array
*/
public static function getAPITypes()
{
return array();
}
/**
* Get the spec for this SMS driver.
*
* @return array
*/
public static function getAPISpec()
{
return static::$_spec;
}
/**
* Check if the current SMS driver is supported on this server.
*
* This method returns true on success and false on failure.
*
* @return bool
*/
public static function isSupported()
{
return false;
}
/**
* Send a message.
*
* This method returns true on success and false on failure.
*
* @param array $messages
* @param object $original
* @return bool
*/
public function send(array $messages, \Rhymix\Framework\SMS $original)
{
return false;
}
}

View file

@ -0,0 +1,123 @@
<?php
namespace Rhymix\Framework\Drivers\SMS;
/**
* The CoolSMS SMS driver.
*/
class CoolSMS extends Base implements \Rhymix\Framework\Drivers\SMSInterface
{
/**
* API specifications.
*/
protected static $_spec = array(
'max_recipients' => 1000,
'sms_max_length' => 90,
'sms_max_length_in_charset' => 'CP949',
'lms_supported' => true,
'lms_supported_country_codes' => array(82),
'lms_max_length' => 2000,
'lms_max_length_in_charset' => 'CP949',
'lms_subject_supported' => true,
'lms_subject_max_length' => 40,
'mms_supported' => true,
'mms_supported_country_codes' => array(82),
'mms_max_length' => 2000,
'mms_max_length_in_charset' => 'CP949',
'mms_subject_supported' => true,
'mms_subject_max_length' => 40,
'image_allowed_types' => array('jpg', 'gif', 'png'),
'image_max_dimensions' => array(2048, 2048),
'image_max_filesize' => 300000,
'delay_supported' => true,
);
/**
* Config keys used by this driver are stored here.
*/
protected static $_required_config = array('api_key', 'api_secret');
protected static $_optional_config = array('sender_key');
/**
* Check if the current SMS driver is supported on this server.
*
* This method returns true on success and false on failure.
*
* @return bool
*/
public static function isSupported()
{
return true;
}
/**
* Send a message.
*
* This method returns true on success and false on failure.
*
* @param array $messages
* @param object $original
* @return bool
*/
public function send(array $messages, \Rhymix\Framework\SMS $original)
{
try
{
$sender = new \Nurigo\Api\Message($this->_config['api_key'], $this->_config['api_secret']);
$status = true;
foreach ($messages as $i => $message)
{
$options = new \stdClass;
if ($this->_config['sender_key'])
{
$options->sender_key = $this->_config['sender_key'];
$options->type = 'CTA';
}
else
{
$options->type = $message->type;
}
$options->from = $message->from;
$options->to = implode(',', $message->to);
$options->text = $message->content ?: $message->type;
$options->charset = 'utf8';
$options->srk = 'K0009334574';
if ($message->delay && $message->delay > time())
{
$options->datetime = gmdate('YmdHis', $message->delay + (3600 * 9));
}
if ($message->country && $message->country != 82)
{
$options->country = $message->country;
}
if ($message->subject)
{
$options->subject = $message->subject;
}
if ($message->image)
{
$options->image = $message->image;
}
foreach ($original->getExtraVars() as $key => $value)
{
$options->$key = $value;
}
$result = $sender->send($options);
if (!$result->success_count)
{
$error_codes = implode(', ', $result->error_list ?: array('Unknown'));
$original->addError('Error (' . $error_codes . ') while sending message ' . ($i + 1) . ' of ' . count($messages) . ' to ' . $options->to);
$status = false;
}
}
return $status;
}
catch (\Nurigo\Exceptions\CoolsmsException $e)
{
$message->errors[] = class_basename($e) . ': ' . $e->getMessage();
return false;
}
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Rhymix\Framework\Drivers\SMS;
/**
* The dummy SMS driver.
*/
class Dummy extends Base implements \Rhymix\Framework\Drivers\SMSInterface
{
/**
* API specifications.
*/
protected static $_spec = array(
'max_recipients' => 100,
'sms_max_length' => 90,
'sms_max_length_in_charset' => 'CP949',
'lms_supported' => true,
'lms_supported_country_codes' => array(82),
'lms_max_length' => 2000,
'lms_max_length_in_charset' => 'CP949',
'lms_subject_supported' => true,
'lms_subject_max_length' => 40,
'mms_supported' => true,
'mms_supported_country_codes' => array(82),
'mms_max_length' => 2000,
'mms_max_length_in_charset' => 'CP949',
'mms_subject_supported' => false,
'mms_subject_max_length' => 40,
'image_allowed_types' => array(),
'image_max_dimensions' => array(1024, 1024),
'image_max_filesize' => 300000,
'delay_supported' => true,
);
/**
* Sent messages are stored here for debugging and testing.
*/
protected $_sent_messages = array();
/**
* Check if the current SMS driver is supported on this server.
*
* This method returns true on success and false on failure.
*
* @return bool
*/
public static function isSupported()
{
return true;
}
/**
* Send a message.
*
* This method returns true on success and false on failure.
*
* @param array $messages
* @param object $original
* @return bool
*/
public function send(array $messages, \Rhymix\Framework\SMS $original)
{
foreach ($messages as $message)
{
$this->_sent_messages[] = $message;
}
return true;
}
/**
* Get sent messages.
*
* @return array
*/
public function getSentMessages()
{
return $this->_sent_messages;
}
/**
* Reset sent messages.
*
* @return void
*/
public function resetSentMessages()
{
$this->_sent_messages = array();
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Rhymix\Framework\Drivers;
/**
* The SMS driver interface.
*/
interface SMSInterface
{
/**
* Create a new instance of the current SMS driver, using the given settings.
*
* @param array $config
* @return void
*/
public static function getInstance(array $config);
/**
* Get the human-readable name of this SMS driver.
*
* @return string
*/
public static function getName();
/**
* Get the list of configuration fields required by this SMS driver.
*
* @return array
*/
public static function getRequiredConfig();
/**
* Get the list of configuration fields optionally used by this SMS driver.
*
* @return array
*/
public static function getOptionalConfig();
/**
* Get the list of API types supported by this SMS driver.
*
* @return array
*/
public static function getAPITypes();
/**
* Get the spec for this SMS driver.
*
* @return array
*/
public static function getAPISpec();
/**
* Check if the current SMS driver is supported on this server.
*
* This method returns true on success and false on failure.
*
* @return bool
*/
public static function isSupported();
/**
* Send a message.
*
* This method returns true on success and false on failure.
*
* @param array $messages
* @param object $original
* @return bool
*/
public function send(array $messages, \Rhymix\Framework\SMS $original);
}

View file

@ -16,8 +16,8 @@ class FilenameFilter
public static function clean($filename)
{
// Replace dangerous characters with safe alternatives, maintaining meaning as much as possible.
$illegal = array('\\', '/', '<', '>', '{', '}', ':', ';', '|', '"', '~', '`', '@', '#', '$', '%', '^', '&', '*', '?');
$replace = array('', '', '(', ')', '(', ')', '_', ',', '_', '', '_', '\'', '_', '_', '_', '_', '_', '_', '', '');
$illegal = array('\\', '/', '<', '>', '{', '}', ':', ';', '|', '"', '~', '`', '$', '%', '^', '*', '?');
$replace = array('', '', '(', ')', '(', ')', '_', ',', '_', '', '_', '\'', '_', '_', '_', '', '');
$filename = str_replace($illegal, $replace, $filename);
// Remove control characters.
@ -85,4 +85,22 @@ class FilenameFilter
// Trim trailing slashes.
return rtrim($path, '/');
}
/**
* Check if a file has an extension that would allow direct download.
*
* @param string $filename
* @return bool
*/
public static function isDirectDownload($filename)
{
if (preg_match('/\.(as[fx]|avi|flac|flv|gif|jpe?g|m4[av]|midi?|mkv|moov|mov|mp[1234]|mpe?g|ogg|png|qt|ram?|rmm?|swf|wav|web[mp]|wm[av])$/i', $filename))
{
return true;
}
else
{
return false;
}
}
}

View file

@ -2,6 +2,7 @@
namespace Rhymix\Framework\Filters;
use Rhymix\Framework\Config;
use Rhymix\Framework\Security;
use Rhymix\Framework\Storage;
@ -11,9 +12,9 @@ use Rhymix\Framework\Storage;
class HTMLFilter
{
/**
* HTMLPurifier instance is cached here.
* HTMLPurifier instances are cached here.
*/
protected static $_htmlpurifier;
protected static $_instances = array();
/**
* Pre-processing and post-processing filters are stored here.
@ -69,18 +70,42 @@ class HTMLFilter
* Filter HTML content to block XSS attacks.
*
* @param string $input
* @param array|bool $allow_classes (optional)
* @param bool $allow_editor_components (optional)
* @param bool $allow_widgets (optional)
* @return string
*/
public static function clean($input)
public static function clean($input, $allow_classes = false, $allow_editor_components = true, $allow_widgets = false)
{
foreach (self::$_preproc as $callback)
{
$input = $callback($input);
}
$input = self::_preprocess($input);
$output = self::getHTMLPurifier()->purify($input);
$output = self::_postprocess($output);
if ($allow_classes === true)
{
$allowed_classes = null;
}
else
{
if (is_array($allow_classes))
{
$allowed_classes = array_values($allow_classes);
}
else
{
$allowed_classes = Config::get('mediafilter.classes') ?: array();
}
if ($allow_widgets)
{
$allowed_classes[] = 'zbxe_widget_output';
}
}
$input = self::_preprocess($input, $allow_editor_components, $allow_widgets);
$output = self::getHTMLPurifier($allowed_classes)->purify($input);
$output = self::_postprocess($output, $allow_editor_components, $allow_widgets);
foreach (self::$_postproc as $callback)
{
@ -93,17 +118,27 @@ class HTMLFilter
/**
* Get an instance of HTMLPurifier.
*
* @param array|null $allowed_classes (optional)
* @return object
*/
public static function getHTMLPurifier()
public static function getHTMLPurifier($allowed_classes = null)
{
// Keep separate instances for different sets of allowed classes.
if ($allowed_classes !== null)
{
$allowed_classes = array_unique($allowed_classes);
sort($allowed_classes);
}
$key = sha1(serialize($allowed_classes));
// Create an instance with reasonable defaults.
if (self::$_htmlpurifier === null)
if (!isset(self::$_instances[$key]))
{
// Get the default configuration.
$config = \HTMLPurifier_Config::createDefault();
// Customize the default configuration.
$config->set('Attr.AllowedClasses', $allowed_classes);
$config->set('Attr.AllowedFrameTargets', array('_blank'));
$config->set('Attr.DefaultImageAlt', '');
$config->set('Attr.EnableID', true);
@ -143,11 +178,11 @@ class HTMLFilter
self::_supportCSS3($config);
// Cache our instance of HTMLPurifier.
self::$_htmlpurifier = new \HTMLPurifier($config);
self::$_instances[$key] = new \HTMLPurifier($config);
}
// Return the cached instance.
return self::$_htmlpurifier;
return self::$_instances[$key];
}
/**
@ -226,6 +261,7 @@ class HTMLFilter
));
// Support additional properties.
$def->addAttribute('i', 'aria-hidden', 'Text');
$def->addAttribute('img', 'srcset', 'Text');
$def->addAttribute('iframe', 'allowfullscreen', 'Bool');
}
@ -378,12 +414,17 @@ class HTMLFilter
* Rhymix-specific preprocessing method.
*
* @param string $content
* @param bool $allow_editor_components (optional)
* @param bool $allow_widgets (optional)
* @return string
*/
protected static function _preprocess($content)
protected static function _preprocess($content, $allow_editor_components = true, $allow_widgets = false)
{
// Encode widget and editor component properties so that they are not removed by HTMLPurifier.
$content = self::_encodeWidgetsAndEditorComponents($content);
if ($allow_editor_components || $allow_widgets)
{
$content = self::_encodeWidgetsAndEditorComponents($content, $allow_editor_components, $allow_widgets);
}
return $content;
}
@ -391,9 +432,11 @@ class HTMLFilter
* Rhymix-specific postprocessing method.
*
* @param string $content
* @param bool $allow_editor_components (optional)
* @param bool $allow_widgets (optional)
* @return string
*/
protected static function _postprocess($content)
protected static function _postprocess($content, $allow_editor_components = true, $allow_widgets = false)
{
// Define acts to allow and deny.
$allow_acts = array('procFileDownload');
@ -435,7 +478,7 @@ class HTMLFilter
}, $content);
// Restore widget and editor component properties.
$content = self::_decodeWidgetsAndEditorComponents($content);
$content = self::_decodeWidgetsAndEditorComponents($content, $allow_editor_components, $allow_widgets);
return $content;
}
@ -443,11 +486,27 @@ class HTMLFilter
* Encode widgets and editor components before processing.
*
* @param string $content
* @param bool $allow_editor_components (optional)
* @param bool $allow_widgets (optional)
* @return string
*/
protected static function _encodeWidgetsAndEditorComponents($content)
protected static function _encodeWidgetsAndEditorComponents($content, $allow_editor_components = true, $allow_widgets = false)
{
return preg_replace_callback('!<(div|img)([^>]*)(editor_component="[^"]+"|class="zbxe_widget_output")([^>]*)>!i', function($match) {
$regexp = array();
if ($allow_editor_components)
{
$regexp[] = 'editor_component="[^"]+"';
}
if ($allow_widgets)
{
$regexp[] = 'class="zbxe_widget_output"';
}
if (!count($regexp))
{
return $content;
}
return preg_replace_callback('!<(div|img)([^>]*)(' . implode('|', $regexp) . ')([^>]*)>!i', function($match) {
$tag = strtolower($match[1]);
$attrs = array();
$html = preg_replace_callback('!([a-zA-Z0-9_-]+)="([^"]+)"!', function($attr) use($tag, &$attrs) {
@ -476,10 +535,25 @@ class HTMLFilter
* Decode widgets and editor components after processing.
*
* @param string $content
* @param bool $allow_editor_components (optional)
* @param bool $allow_widgets (optional)
* @return string
*/
protected static function _decodeWidgetsAndEditorComponents($content)
protected static function _decodeWidgetsAndEditorComponents($content, $allow_editor_components = true, $allow_widgets = false)
{
if (!$allow_editor_components)
{
$content = preg_replace('!(<(?:div|img)[^>]*)\s(editor_component="(?:[^"]+)")!i', '$1', $content);
}
if (!$allow_widgets)
{
$content = preg_replace('!(<(?:div|img)[^>]*)\s(widget="(?:[^"]+)")!i', '$1blocked-$2', $content);
}
if (!$allow_editor_components && !$allow_widgets)
{
return $content;
}
return preg_replace_callback('!<(div|img)([^>]*)(\srx_encoded_properties="([^"]+)")!i', function($match) {
$attrs = array();
$decoded_properties = Security::decrypt($match[4]);

820
common/framework/sms.php Normal file
View file

@ -0,0 +1,820 @@
<?php
namespace Rhymix\Framework;
/**
* The SMS class.
*/
class SMS
{
/**
* Instance properties.
*/
public $driver = null;
protected $caller = '';
protected $from = null;
protected $to = array();
protected $subject = '';
protected $content = '';
protected $attachments = array();
protected $extra_vars = array();
protected $delay_timestamp = 0;
protected $force_sms = false;
protected $allow_split_sms = true;
protected $allow_split_lms = true;
protected $errors = array();
protected $sent = false;
/**
* Static properties.
*/
public static $default_driver = null;
public static $custom_drivers = array();
/**
* Set the default driver.
*
* @param object $driver
* @return void
*/
public static function setDefaultDriver(Drivers\SMSInterface $driver)
{
self::$default_driver = $driver;
}
/**
* Get the default driver.
*
* @return object
*/
public static function getDefaultDriver()
{
if (!self::$default_driver)
{
$default_driver = config('sms.type');
$default_driver_class = '\Rhymix\Framework\Drivers\SMS\\' . $default_driver;
if (class_exists($default_driver_class))
{
$default_driver_config = config('sms.' . $default_driver) ?: array();
self::$default_driver = $default_driver_class::getInstance($default_driver_config);
}
else
{
self::$default_driver = Drivers\SMS\Dummy::getInstance(array());
}
}
return self::$default_driver;
}
/**
* Add a custom mail driver.
*/
public static function addDriver(Drivers\SMSInterface $driver)
{
self::$custom_drivers[] = $driver;
}
/**
* Get the list of supported mail drivers.
*
* @return array
*/
public static function getSupportedDrivers()
{
$result = array();
foreach (Storage::readDirectory(__DIR__ . '/drivers/sms', false) as $filename)
{
$driver_name = substr($filename, 0, -4);
$class_name = '\Rhymix\Framework\Drivers\SMS\\' . $driver_name;
if ($class_name::isSupported())
{
$result[$driver_name] = array(
'name' => $class_name::getName(),
'required' => $class_name::getRequiredConfig(),
'optional' => $class_name::getOptionalConfig(),
'api_types' => $class_name::getAPITypes(),
'api_spec' => $class_name::getAPISpec(),
);
}
}
foreach (self::$custom_drivers as $driver)
{
if ($driver->isSupported())
{
$result[strtolower(class_basename($driver))] = array(
'name' => $driver->getName(),
'required' => $driver->getRequiredConfig(),
'optional' => $driver->getOptionalConfig(),
'api_types' => $driver->getAPITypes(),
'api_spec' => $class_name::getAPISpec(),
);
}
}
ksort($result);
return $result;
}
/**
* The constructor.
*/
public function __construct()
{
$this->driver = self::getDefaultDriver();
$this->from = trim(preg_replace('/[^0-9]/', '', config('sms.default_from'))) ?: null;
$this->allow_split_sms = (config('sms.allow_split.sms') !== false);
$this->allow_split_lms = (config('sms.allow_split.lms') !== false);
}
/**
* Set the sender's phone number.
*
* @param string $number Phone number
* @return bool
*/
public function setFrom($number)
{
$this->from = preg_replace('/[^0-9]/', '', $number);
return true;
}
/**
* Get the sender's phone number.
*
* @return string|null
*/
public function getFrom()
{
return $this->from;
}
/**
* Add a recipient.
*
* @param string $number Phone number
* @param string $country Country code (optional)
* @return bool
*/
public function addTo($number, $country = 0)
{
$this->to[] = (object)array(
'number' => preg_replace('/[^0-9]/', '', $number),
'country' => intval(preg_replace('/[^0-9]/', '', $country)),
);
return true;
}
/**
* Get the list of recipients without country codes.
*
* @return array
*/
public function getRecipients()
{
return array_map(function($recipient) {
return $recipient->number;
}, $this->to);
}
/**
* Get the list of recipients with country codes.
*
* @return array
*/
public function getRecipientsWithCountry()
{
return $this->to;
}
/**
* Get the list of recipients grouped by country code.
*
* @return array
*/
public function getRecipientsGroupedByCountry()
{
$result = array();
foreach ($this->to as $recipient)
{
$result[$recipient->country][] = $recipient->number;
}
return $result;
}
/**
* Set the subject.
*
* @param string $subject
* @return bool
*/
public function setSubject($subject)
{
$this->subject = utf8_trim(utf8_clean($subject));
return true;
}
/**
* Get the subject.
*
* @return string
*/
public function getSubject()
{
return $this->subject;
}
/**
* Set the subject (alias to setSubject).
*
* @param string $subject
* @return bool
*/
public function setTitle($subject)
{
return $this->setSubject($subject);
}
/**
* Get the subject (alias to getSubject).
*
* @return string
*/
public function getTitle()
{
return $this->getSubject();
}
/**
* Set the content.
*
* @param string $content
* @return bool
*/
public function setBody($content)
{
$this->content = utf8_trim(utf8_clean($content));
$this->content = strtr($this->content, array("\r\n" => "\n"));
return true;
}
/**
* Get the content.
*
* @return string
*/
public function getBody()
{
return $this->content;
}
/**
* Set the content (alias to setBody).
*
* @param string $content
* @return void
*/
public function setContent($content)
{
return $this->setBody($content);
}
/**
* Get the content (alias to getBody).
*
* @return string
*/
public function getContent()
{
return $this->getBody();
}
/**
* Attach a file.
*
* @param string $local_filename
* @param string $display_filename (optional)
* @return bool
*/
public function attach($local_filename, $display_filename = null)
{
if ($display_filename === null)
{
$display_filename = basename($local_filename);
}
if (!Storage::exists($local_filename))
{
return false;
}
$this->attachments[] = (object)array(
'type' => 'mms',
'local_filename' => $local_filename,
'display_filename' => $display_filename,
'cid' => null,
);
return true;
}
/**
* Get the list of attachments to this message.
*
* @return array
*/
public function getAttachments()
{
return $this->attachments;
}
/**
* Set an extra variable.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function setExtraVar($key, $value)
{
$this->extra_vars[$key] = $value;
}
/**
* Get an extra variable.
*
* @param string $key
* @return mixed
*/
public function getExtraVar($key)
{
return isset($this->extra_vars[$key]) ? $this->extra_vars[$key] : null;
}
/**
* Get all extra variables.
*
* @param string $key
* @return mixed
*/
public function getExtraVars()
{
return $this->extra_vars;
}
/**
* Set all extra variables.
*
* @param array $vars
* @return void
*/
public function setExtraVars(array $vars)
{
$this->extra_vars = $vars;
}
/**
* Delay sending the message.
*
* Delays (in seconds) less than 1 year will be treated as relative to the
* current time. Greater values will be interpreted as a Unix timestamp.
*
* This feature may not be implemented by all drivers.
*
* @param int $when Unix timestamp
* @return bool
*/
public function setDelay($when)
{
if ($when <= (86400 * 365))
{
$when = time() + $when;
}
if ($when <= time())
{
$when = 0;
}
$this->delay_timestamp = intval($when);
return true;
}
/**
* Get the Unix timestamp of when to send the message.
*
* This method always returns a Unix timestamp, even if the original value
* was given as a relative delay.
*
* This feature may not be implemented by all drivers.
*
* @return int
*/
public function getDelay()
{
return $this->delay_timestamp;
}
/**
* Force this message to use SMS (not LMS or MMS).
*
* @return void
*/
public function forceSMS()
{
$this->force_sms = true;
}
/**
* Unforce this message to use SMS (not LMS or MMS).
*
* @return void
*/
public function unforceSMS()
{
$this->force_sms = false;
}
/**
* Check if this message is forced to use SMS.
*
* @return bool
*/
public function isForceSMS()
{
return $this->force_sms;
}
/**
* Allow this message to be split into multiple SMS.
*
* @return void
*/
public function allowSplitSMS()
{
$this->allow_split_sms = true;
}
/**
* Allow this message to be split into multiple LMS.
*
* @return void
*/
public function allowSplitLMS()
{
$this->allow_split_lms = true;
}
/**
* Disallow this message to be split into multiple SMS.
*
* @return void
*/
public function disallowSplitSMS()
{
$this->allow_split_sms = false;
}
/**
* Disallow this message to be split into multiple LMS.
*
* @return void
*/
public function disallowSplitLMS()
{
$this->allow_split_lms = false;
}
/**
* Check if splitting this message into multiple SMS is allowed.
*
* @return bool
*/
public function isSplitSMSAllowed()
{
return $this->allow_split_sms;
}
/**
* Check if splitting this message into multiple LMS is allowed.
*
* @return bool
*/
public function isSplitLMSAllowed()
{
return $this->allow_split_lms;
}
/**
* Send the message.
*
* @return bool
*/
public function send()
{
// Get caller information.
$backtrace = debug_backtrace(0);
if(count($backtrace) && isset($backtrace[0]['file']))
{
$this->caller = $backtrace[0]['file'] . ($backtrace[0]['line'] ? (' line ' . $backtrace[0]['line']) : '');
}
$output = \ModuleHandler::triggerCall('sms.send', 'before', $this);
if(!$output->toBool())
{
$this->errors[] = $output->getMessage();
return false;
}
if (config('sms.default_force') && config('sms.default_from'))
{
$this->setFrom(config('sms.default_from'));
}
try
{
if ($this->driver)
{
$messages = $this->_formatSpec($this->driver->getAPISpec());
if (count($messages))
{
$this->sent = $this->driver->send($messages, $this) ? true : false;
}
else
{
$this->errors[] = 'No recipients selected';
$this->sent = false;
}
}
else
{
$this->errors[] = 'No SMS driver selected';
$this->sent = false;
}
}
catch(\Exception $e)
{
$this->errors[] = class_basename($e) . ': ' . $e->getMessage();
$this->sent = false;
}
$output = \ModuleHandler::triggerCall('sms.send', 'after', $this);
if(!$output->toBool())
{
$this->errors[] = $output->getMessage();
}
return $this->sent;
}
/**
* Check if the message was sent.
*
* @return bool
*/
public function isSent()
{
return $this->sent;
}
/**
* Get caller information.
*
* @return string
*/
public function getCaller()
{
return $this->caller;
}
/**
* Get errors.
*
* @return array
*/
public function getErrors()
{
return $this->errors;
}
/**
* Add an error message.
*
* @param string $message
* @return void
*/
public function addError($message)
{
$this->errors[] = $message;
}
/**
* Format the current message according to an API spec.
*
* @param array $spec API specifications
* @return array
*/
protected function _formatSpec(array $spec)
{
// Initialize the return array.
$result = array();
// Get the list of recipients.
$recipients = $this->getRecipientsGroupedByCountry();
// Group the recipients by country code.
foreach ($recipients as $country_code => $country_recipients)
{
// Merge recipients into groups.
if ($spec['max_recipients'] > 1)
{
$country_recipients = array_chunk($country_recipients, $spec['max_recipients']);
}
// Send to each set of merged recipients.
foreach ($country_recipients as $recipient_numbers)
{
// Populate the item.
$item = new \stdClass;
$item->type = 'SMS';
$item->from = $this->getFrom();
$item->to = $recipient_numbers;
$item->country = $country_code;
if ($spec['delay_supported'])
{
$item->delay = $this->getDelay() ?: 0;
}
// Get message content.
$subject = $this->getSubject();
$content = $this->getContent();
$attachments = $attachments = $this->getAttachments();
// Determine the message type.
if (!$this->isForceSMS() && ($spec['lms_supported'] || $spec['mms_supported']))
{
// Check attachments, subject, and message length.
if ($spec['mms_supported'] && count($attachments))
{
$item->type = 'MMS';
}
elseif ($spec['lms_supported'] && $subject)
{
$item->subject = $subject;
$item->type = 'LMS';
}
elseif ($spec['lms_supported'] && $this->_getLengthInCharset($content, $spec['sms_max_length_in_charset']) > $spec['sms_max_length'])
{
$item->type = 'LMS';
}
else
{
$item->type = 'SMS';
}
// Check the country code.
if ($item->type === 'MMS' && $country_code && is_array($spec['mms_supported_country_codes']) && !in_array($country_code, $spec['mms_supported_country_codes']))
{
$item->type = 'LMS';
}
if ($item->type === 'LMS' && $country_code && is_array($spec['lms_supported_country_codes']) && !in_array($country_code, $spec['lms_supported_country_codes']))
{
$item->type = 'SMS';
}
}
// Remove subject and attachments if the message type is SMS.
if ($item->type === 'SMS')
{
if ($subject)
{
$content = $subject . "\n" . $content;
unset($item->subject);
}
$attachments = array();
}
// If message subject is not supported, prepend it to the content instead.
if ($item->subject && !$spec[strtolower($item->type) . '_subject_supported'])
{
$content = $item->subject . "\n" . $content;
unset($item->subject);
}
elseif ($item->subject && $this->_getLengthInCharset($item->subject, $spec[strtolower($item->type) . '_max_length_in_charset']) > $spec[strtolower($item->type) . '_subject_max_length'])
{
$subject_parts = $this->_splitString($item->subject, $spec[strtolower($item->type) . '_subject_max_length'], $spec[strtolower($item->type) . '_max_length_in_charset']);
$subject_short = array_shift($subject_parts);
$subject_remainder = utf8_trim(substr($item->subject, strlen($subject_short)));
$item->subject = $subject_short;
$content = $subject_remainder . "\n" . $content;
}
// Split the content if necessary.
if (($item->type === 'SMS' && $this->allow_split_sms) || ($item->type !== 'SMS' && $this->allow_split_lms))
{
if ($this->_getLengthInCharset($content, $spec[strtolower($item->type) . '_max_length_in_charset']) > $spec[strtolower($item->type) . '_max_length'])
{
$content_parts = $this->_splitString($content, $spec[strtolower($item->type) . '_max_length'], $spec[strtolower($item->type) . '_max_length_in_charset']);
}
else
{
$content_parts = array($content);
}
}
else
{
$content_parts = array($content);
}
// Generate a message for each part of the content and attachments.
$message_count = max(count($content_parts), count($attachments));
$last_content = $item->type;
for ($i = 1; $i <= $message_count; $i++)
{
// Get the message content.
if ($content_part = array_shift($content_parts))
{
$item->content = $last_content = $content_part;
}
else
{
$item->content = $last_content ?: $item->type;
}
// Get the attachment.
if ($attachment = array_shift($attachments))
{
$item->image = $attachment->local_filename;
}
else
{
unset($item->image);
}
// Clone the item to make a part.
$cloneitem = clone $item;
// Determine the best message type for this part.
if ($cloneitem->type !== 'SMS' && !$cloneitem->subject)
{
$cloneitem->type = $attachment ? 'MMS' : ($this->_getLengthInCharset($content_part, $spec['sms_max_length_in_charset']) > $spec['sms_max_length'] ? 'LMS' : 'SMS');
}
// Add the cloned part to the result array.
$result[] = $cloneitem;
}
}
}
// Return the message parts.
return $result;
}
/**
* Get the length of a string in another character set.
*
* @param string $str String to measure
* @param string $charset Character set to measure length
* @return
*/
protected function _getLengthInCharset($str, $charset)
{
$str = @iconv('UTF-8', $charset . '//IGNORE', $str);
return strlen($str);
}
/**
* Split a string into several short chunks.
*
* @param string $str String to split
* @param int $max_length Maximum length of a chunk
* @param string $charset Character set to measure length
* @return array
*/
protected function _splitString($str, $max_length, $charset)
{
$str = utf8_trim(utf8_normalize_spaces($str, true));
$chars = preg_split('//u', $str, -1, \PREG_SPLIT_NO_EMPTY);
$result = array();
$current_entry = '';
$current_length = 0;
foreach ($chars as $char)
{
$char_length = strlen(@iconv('UTF-8', $charset . '//IGNORE', $char));
if (($current_length + $char_length > $max_length) || ($current_length + $char_length > $max_length - 7 && ctype_space($char)))
{
$result[] = trim($current_entry);
$current_entry = $char;
$current_length = $char_length;
}
else
{
$current_entry .= $char;
$current_length += $char_length;
}
}
if ($current_entry !== '')
{
$result[] = trim($current_entry);
}
return $result;
}
}

View file

@ -714,6 +714,36 @@ class Storage
}
}
/**
* Delete a directory only if it is empty.
*
* @param string $dirname
* @param bool $delete_empty_parents (optional)
* @return bool
*/
public static function deleteEmptyDirectory($dirname, $delete_empty_parents = false)
{
$dirname = rtrim($dirname, '/\\');
if (!self::isDirectory($dirname) || !self::isEmptyDirectory($dirname))
{
return false;
}
$result = @rmdir($dirname);
if (!$result)
{
return false;
}
else
{
if ($delete_empty_parents)
{
self::deleteEmptyDirectory(dirname($dirname), true);
}
return true;
}
}
/**
* Get the current umask.
*
@ -742,7 +772,7 @@ class Storage
/**
* Determine the best umask for this installation of Rhymix.
*
* @return int
* @return string
*/
public static function recommendUmask()
{
@ -756,27 +786,7 @@ class Storage
$file_uid = fileowner(__FILE__);
// Get the UID of the current PHP process.
if (function_exists('posix_geteuid'))
{
$php_uid = posix_geteuid();
}
else
{
$testfile = \RX_BASEDIR . 'files/cache/uidcheck';
if (self::exists($testfile))
{
self::delete($testfile);
}
if (self::write($testfile, 'TEST'))
{
$php_uid = fileowner($testfile);
self::delete($testfile);
}
else
{
$php_uid = -1;
}
}
$php_uid = self::getServerUID();
// If both UIDs are the same, set the umask to 0022.
if ($file_uid == $php_uid)
@ -790,4 +800,36 @@ class Storage
return '0000';
}
}
/**
* Get the UID of the server process.
*
* @return int|false
*/
public static function getServerUID()
{
if (function_exists('posix_geteuid'))
{
return posix_geteuid();
}
else
{
$testfile = \RX_BASEDIR . 'files/cache/uidcheck_' . time();
if (self::exists($testfile))
{
self::delete($testfile);
}
if (self::write($testfile, 'TEST'))
{
$uid = fileowner($testfile);
self::delete($testfile);
return $uid;
}
else
{
return false;
}
}
}
}