<?php
/**
* cpanel - /usr/local/cpanel/php/cpanel.php Copyright(c) 2020 cPanel, L.L.C.
* All rights reserved.
* [email protected] http://cpanel.net
*/
if (version_compare(PHP_VERSION, '5.2.0', '<')) {
trigger_error(
"cPanel's Live PHP class must be executed with PHP >= 5.2",
E_USER_ERROR
);
exit;
}
/** cPanel LiveAPI PHP Class
*
* This class allows for cPanel frontend pages to be developed in PHP using an
* object for accessing the APIs.
* For the full documentation, see https://go.cpanel.net/livephp.
*
* You are free to include this module in your program as long as it is for use
* with cPanel. This module is only licensed for use with the version of cPanel
* it is distributed with.
*
* The backend APIs are subject to change. If you ignore this message you will
* find that this module will not work in future versions. This class will be
* updated if the backend APIs change. We will make all efforts to provide
* backwards compatibility, but if you do use this class with any version
* of cPanel other than the one it is distributed with the results could be
* disasterous.
*
* FOR THE AVOIDANCE OF DOUBT: MAKE SURE YOU ONLY USE THIS MODULE WITH THE
* VERSION OF CPANEL THAT IT CAME WITH
*
* For debugging purposes you can set the following two constants to enable
* debug mode:
* - LIVEPHP_DEBUG_LEVEL - 0 or 1 - enable or disable debugging
* - LIVEPHP_DEBUG_LOG - path - The absolute path and filename for logging.
*
* This class also provides a set_debug() method for enabling/disabling debug
* mode.
*
* Changes:
* Version 2.1
* - Corrected various documentation
* - Altered code and documentation for better adherence to PEAR PHP coding
* standards without breaking BC
* - Fixed bug (constructor should explicitly return "$this")
* - Altered methods cpanelif() and cpanelfeature() to enforce a boolean
* return.
* - Implemented the use pre-defined SPL Exception classes instead of generic
* Exception base class
* - Use Exceptions wherever possible instead of simple log via error_log()
* - Use trigger_error() instead of error_log()
* - Use error_log() only when logging cPanel related information (i.e., what
* normally might be E_NOTICE or E_DEPRECATED, but specific only to cPanel
* technicians and developers)
* - Added 'deprecated' PHP DocBlock to cpanellangprint(). (future versions
* will likely throw E_USER_DEPRECATED; instead use
* API1's Locale::maketext() (## no extract maketext)
*
* Version 2.0
* - Changed the backend serialization format to JSON
* - Added debug logger
* - Added Exceptions
*
* @category Cpanel
* @package CPANEL
* @author cPanel, Inc. <[email protected]>
* @copyright 1997-2020 cPanel, L.L.C.
* @license http://cpanel.net
* @version Release: 2.1
* @link https://go.cpanel.net/livephp
*/
class CPANEL
{
/**
* Socket resource for communicating with cPanel LiveAPI parser
* @var resource Local socket
*/
private $_cpanelfh;
/**
* State tracker for socket resource
* @var boolean State of private resource
*/
public $connected = 0;
/**
* Absolute path and filename of debug log
*
* NOTE: If LIVEPHP_DEBUG_LOG environment variable is not set, this variable
* will be populated with a random log file (if debugging is enabled):
* ~/.cpanel/livephp.log.$randomstring.
*
* @var string Log file
*/
private $_debug_log;
/**
* File handle for debug log
* @var resource File handle for debug log
*/
private $_debug_fh;
/**
* Debug logging level
*
* Value values are:
* 0 - Debugging disabled
* 1 - Log all socket communication to debug log file
*
* @var integer Debug logging level
*/
private $_debug_level = 0;
/**
* Storage location for last server response
* @var array Array data structure of the last server response
*/
private $_result;
/**
* Storage location for a stringified DOM as used by the header() and footer() methods
* note: modern themes only
*/
private $_dom = 0;
/**
* Instantiate the LiveAPI PHP Object
*
* This will create the "CPANEL" object; open the communication socket.
*
* @return CPANEL A LiveAPI object
* @throws RuntimeException if CPANEL_PHPCONNECT_SOCKET environment variable
* is not set
* @throws RuntimeException if file socket cannot be established
* @throws RuntimeException if stream blocking cannot be set for file socket
*/
public function __construct()
{
$this->connected = 1;
// Attempt to set debugging based on defined PHP constants
if (defined('LIVEPHP_DEBUG_LOG')) {
$this->_debug_log = LIVEPHP_DEBUG_LOG;
}
if (defined('LIVEPHP_DEBUG_LEVEL')) {
$this->set_debug(LIVEPHP_DEBUG_LEVEL);
}
// prepare socket to communicate with cPanel API parser
$socketfile = getenv('CPANEL_PHPCONNECT_SOCKET');
if (!$socketfile) {
throw new RuntimeException(
'There was a problem fetching the env variable'
. 'containing the path to the socket'
);
}
$this->_cpanelfh = fsockopen("unix://" . $socketfile);
if (!$this->_cpanelfh) {
$this->connected = 0;
throw new RuntimeException(
'There was a problem connecting back to the cPanel engine.'
.' Make sure your script ends with .live.php or .livephp'
);
}
stream_set_blocking($this->_cpanelfh, 1) || $this->connected = 0;
if (!$this->connected) {
throw new RuntimeException(
'There was a problem connecting back to the cPanel engine.'
.' Make sure your script ends with .live.php or .livephp'
);
}
// enable enbedded json in the protocol
$this->exec('<cpaneljson enable="1">');
return $this;
}
/**
* Enable debugging mode
*
* Passing this a non-zero value will enable socket logging.
*
* NOTE: This should only be used when attempting to debug the transactions
* that happen over the socket. ALL data will be log!
*
* The valid logging level are as follows:
* 0 - Disable logging (default)
* 1 - Write socket transactions to the log.
*
* @param int $debug_level The debug level
*
* @return void
* @throws UnexpectedValueException if $debug_level is not numeric
*/
public function set_debug($debug_level)
{
if (is_numeric($debug_level)) {
// Open the debug log if it isn't already
if ($debug_level > 0 && !is_resource($this->_debug_fh)) {
// Set the debug log
if (!isset($this->_debug_log)) {
$user_pwnam = posix_getpwuid(posix_getuid());
$this->_debug_log = $user_pwnam['dir']
. '/.cpanel/livephp.log.' . mt_rand(10000000, 99999999);
}
$this->_debug_fh = fopen($this->_debug_log, 'a');
} elseif (is_resource($this->_debug_fh) && $debug_level == 0) {
// Close debug_log if debug logging is being disabled
fclose($this->_debug_fh);
}
$this->_debug_level = $debug_level;
} else {
$this->set_debug(0);
throw new UnexpectedValueException(
'CPANEL::set_debug given non-integer value.'
);
}
}
/**
* Write to the debug log
*
* Write a message to the debug log
*
* @param int $level The desired logging level for $log_msg to appear.
* @param string $log_msg The message you wish to have logged
*
* @return void
* @throws RuntimeException if log filehandle does not exist
*/
private function debug_log($level, $log_msg)
{
if ($level > 0 && $level <= $this->_debug_level) {
if (is_resource($this->_debug_fh)) {
fwrite($this->_debug_fh, date("[d-M-Y H:i:s] ") . $log_msg . "\n");
} else {
throw new RuntimeException(
'Attempted to execute debugging statement on closed filehandle'
);
}
}
}
/**
* Parse and log a JSON formatted string.
*
* @param string $str JSON formatted string to decode and log
*
* @return void
*/
private function debug_log_json($str)
{
$parsed = json_decode($str, true);
if ($parsed !== null) {
ob_start();
var_dump($parsed);
$log_msg = ob_get_clean();
} elseif (function_exists('json_last_error') && json_last_error()) {
// json_last_error is only PHP>=5.3
switch (json_last_error()) {
case JSON_ERROR_DEPTH:
$log_msg = 'Maximum stack depth exceeded';
break;
case JSON_ERROR_CTRL_CHAR:
$log_msg = 'Unexpected control character found';
break;
case JSON_ERROR_SYNTAX:
$log_msg = 'Syntax error, malformed JSON';
break;
case JSON_ERROR_NONE:
//do nothing;
break;
}
} else {
$log_msg = "Error decoding JSON string";
}
$this->debug_log(1, 'JSON_decode: ' . $log_msg);
}
/**
* Get the filename of the debug log currently in use.
*
* @return string Current log file
*/
public function get_debug_log()
{
return $this->_debug_log;
}
/**
* Return the currently set debug level.
*
* @return int Current debug level
*/
public function get_debug_level()
{
return $this->_debug_level;
}
/**
* Return the value of a cPvar
*
* @param string $var The cPvar to fetch (e.g. $CPDATA{'DNS'} )
*
* @return array Response containing cPvar value
*/
public function fetch($var)
{
if (!$this->connected) {
return;
}
return $this->exec('<cpanel print="' . $var . '">');
}
/**
* Execute an API1 call
*
* @param string $module An API1 module name.
* @param string $func An API1 function.
* @param array $args An ordinal array containing arguments for the API1 function
*
* @return array Returned response from the API1 function
* @throws RuntimeException if LiveAPI socket is not available
*/
public function api1($module, $func, $args = array())
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
return $this->api('exec', "1", $module, $func, $args);
}
/**
* Execute an API2 call
*
* @param string $module An API2 module name.
* @param string $func An API2 function.
* @param array $args An associative array containing arguments for the API2 function
*
* @return array Returned response from the API2 function
* @throws RuntimeException if LiveAPI socket is not available
*/
public function api2($module, $func, $args = array())
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
return $this->api('exec', "2", $module, $func, $args);
}
/**
* Execute an API3 call, an alias for a UAPI call
*
* @param string $module A UAPI module name.
* @param string $func A UAPI function.
* @param array $args An associative array containing arguments for the UAPIfunction
*
* @return array Returned response from the UAPI function
* @throws RuntimeException if LiveAPI socket is not available
*/
public function api3($module, $func, $args = array())
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
return $this->api('exec', "3", $module, $func, $args);
}
/**
* Execute an UAPI call
*
* @param string $module A UAPI module name.
* @param string $func A UAPI function.
* @param array $args An associative array containing arguments for the UAPIfunction
*
* @return array Returned response from the UAPI function
* @throws RuntimeException if LiveAPI socket is not available
*/
public function uapi($module, $func, $args = array())
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
return $this->api('exec', "uapi", $module, $func, $args);
}
/**
* Evaluate a cpanelif statement
*
* This method will return a boolean value based on the evaluation of the
* code expression
*
* @param string $code A cPvar or logical test condition
*
* @link https://go.cpanel.net/PluginVars ExpVar Reference Chart
* @return boolean Whether the $code expression evaluates as true or false
* @throws RuntimeException if LiveAPI socket is not available
*/
public function cpanelif($code)
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
$value = (simple_result($this->api('if', '1', 'if', 'if', $code)))? 1 : 0;
return $value;
}
/**
* Determine if the current cPanel account has access to a specific feature
*
* @param string $feature A feature name
*
* @return boolean Whether the current cPanel account has access to queried
* feature.
* @throws RuntimeException if LiveAPI socket is not available
*/
public function cpanelfeature($feature)
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
$value = (simple_result($this->api('feature', '1', 'feature', 'feature', $feature)))? 1 : 0;
return $value;
}
/**
* Return the value of a cPvar
*
* This method will return the value of a cPvar. This differs from fetch()
* which returns the complete response as an array. The method will only
* return the cPvar value as a string.
*
* @param string $var The cPvar to retrieve (e.g. $CPDATA{'DNS'} )
*
* @return string The value of the queried cPvar
* @throws RuntimeException if LiveAPI socket is not available
*/
public function cpanelprint($var)
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
return simple_result($this->api1('print', '', $var));
}
/**
* Process a language key for the cPanel account's current language
*
* @param string $key A language key
*
* @deprecated The cpanellongprint tag is no longer supported. Use API1
* Locale::maketext ## no extract maketext
* @see https://go.cpanel.net/maketext ## no extract maketext
*
* @return string Translated version of the requested language key
* @throws RuntimeException if LiveAPI socket is not available
*/
public function cpanellangprint($key)
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
return simple_result($this->api1('langprint', '', $key));
}
/**
* Execute a cpanel tag
*
* In most cases there is no need to call this method directly. Instead one
* should use the api1(), api2() or cpanel*() methods (which all call this
* method internally).
*
* @param string $code A cPanel tag to execute.
* @param boolean $skip_return (optional) If set to true, this function will
* not return anything.
*
* @return array Returned response in an array data structure
* @throws RuntimeException if LiveAPI socket is not available
*/
public function exec($code, $skip_return = 0)
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
// SEND CODE
$buffer = '';
$result = '';
if ($this->_debug_level) {
$this->debug_log(1, '(exec) SEND:' . $code);
}
fwrite($this->_cpanelfh, strlen($code) . "\n" . $code);
//RECV CODE
while ($buffer = fgets($this->_cpanelfh)) {
$result = $result . $buffer;
if (strstr($buffer, '</cpanelresult>') !== false) {
break;
}
}
if ($this->_debug_level) {
$this->debug_log(1, '(exec) RECV:' . $result);
}
if ($skip_return) {
$this->_result = null;
return;
}
// Parse out return code, build livePHP result
$json_start_pos = strpos($result, "<cpanelresult>{");
if ($json_start_pos !== false) {
$json_start_pos+= 14;
if ($this->_debug_level) {
$this->debug_log_json(
substr(
trim($result),
$json_start_pos,
strpos(
$result,
"</cpanelresult>"
) - $json_start_pos
)
);
}
$parsed = json_decode(
substr(
trim($result),
$json_start_pos,
strpos(
$result,
"</cpanelresult>"
) - $json_start_pos
),
true
);
if (strpos($result, '<cpanelresult>{"cpanelresult"') === false
&& $parsed !== null
) {
/**
* needed for compat: API2 tags will end up with both due to
* the internals
*/
$this->_result = array('cpanelresult' => $parsed);
} else {
$this->_result = $parsed;
}
} elseif (strpos($result, "<cpanelresult></cpanelresult>") !== false) {
/* This is a hybird api1/api2/api3 response to ensure that
the developer using api gets the error field in the position
they are looking for */
$this->_result = array('cpanelresult' => array('error' => 'Error cannot be propagated to liveapi, please check the cPanel error_log.', 'result' => array('errors' => array('Error cannot be propagated to liveapi, please check the cPanel error_log.'))));
} elseif (strpos($result, "<cpanelresult>") !== false) {
/**
* This logic flow is provide for BC in the unlikely event that the
* cPanel engine doesn't not handle JSON.
* - log this directly to the PHP error log in hopes that it gets
* reported
*/
if ($this->_debug_level) {
$this->debug_log(1, 'XML_unserialize:' . $result);
}
error_log(
'cPanel LiveAPI parser returned XML, which is deprecated. '
.'Please file a bug report at https://tickets.cpanel.net/submit/'
);
include_once '/usr/local/cpanel/php/xml.php';
# XML_unserialize takes a reference, and PHP doesn't like it if we
# pass a non-variable by reference.
$temp = trim($result);
$this->_result = XML_unserialize($temp);
}
return $this->_result;
}
/**
* Execute an API call
*
* In most cases there is no need to call this method directly. Instead one
* should use the api1(), api2() or cpanel*() methods (which all call this
* method, or exec(), internally).
*
* @param string $reqtype The type of request used by the cPanel API parser;
* valid values are 'exec', 'feature' or 'if'
* @param int $version The version of the API; valid values are either
* '1' or '2'
* @param string $module An API module name
* @param string $func An API function name
* @param mixed $args Associate array for API2, ordered array for API1,
* string for non exec $reqtypes
*
* @see api1()
* @see api2()
* @return array Returned response in an array data structure
* @throws RuntimeException if LiveAPI socket is not available
*/
public function api($reqtype, $version, $module, $func, $args = array())
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
$input = array(
"module" => $module,
"reqtype" => $reqtype,
"func" => $func,
"apiversion" => $version
);
// Args may actually be a string instead of an array.
// yay for php 4.x-isms which would automagically turn 'string' into array('string') when accessed as array
// As such, just check that it isn't empty instead of doing count()
if (!empty($args)) {
$input['args'] = $args;
}
/**
* cPanel engine can process the JSON much much faster than XML
*/
if (function_exists('json_encode')) {
$code = "<cpanelaction>\n" . json_encode($input) . "\n</cpanelaction>";
} else {
/**
* This logic flow is provide for BC in the unlikely event that the
* cPanel engine doesn't not handle JSON.
* - log this directly to the PHP error log in hopes that it gets
* reported
*/
error_log(
'cPanel LiveAPI parser returned XML, which is deprecated. '
.'Please file a bug report at https://tickets.cpanel.net/submit/'
);
include_once '/usr/local/cpanel/php/xml.php';
$temp = array("cpanelaction" => array($input));
$code = XML_serialize($temp);
}
return $this->exec($code);
}
/**
* Get the data result node of the last call
*
* This method will return the ['cpanelresult']['data']['result'] node from
* the last call that was made.
*
* @return mixed A string if the last call was API1, an array or array of
* associative arrays if the last call was API2
* @throws UnexpectedValueException if no data is available from a previous
* call
* @throws OutOfBoundsException if previous data response does not contain
* proper hierarchy
*
*/
public function get_result()
{
if ( !$this->_result ) {
throw new UnexpectedValueException('No previous result exists');
}
if (!is_array($this->_result)
|| !is_array($this->_result['cpanelresult'])
|| !is_array($this->_result['cpanelresult']['data'])
) {
throw new OutOfBoundsException(
'cpanelresult->data associative array key does not exist or '
.'previous call did not return array'
);
}
if (array_key_exists('result', $this->_result['cpanelresult']['data'])) {
return $this->_result['cpanelresult']['data']['result'];
} else {
return $this->_result['cpanelresult']['data'];
}
}
/**
* Get the string containing all of the output up until the header
*
* This method will return everything up until just past the body-content div
* this intended as a method of writing a liveAPI application that matches cpanel's
* presentation.
*
* @return string A string containing all output before the body-content div
* @throws UnexpectedValueException if no header value is detected
*/
public function header( $title = '', $app_key = '' ) {
if ( !$this->_dom ) {
$result = $this->uapi( 'Chrome', 'get_dom', array( 'page_title' => $title, 'app_key' => $app_key ) );
$this->_dom = $result['cpanelresult']['result']['data'];
}
if ( !array_key_exists( 'header', $this->_dom ) ) {
throw new UnexpectedValueException('No header in DOM response!');
}
return $this->_dom['header'];
}
/**
* Get the string containing all of the output after the body
*
* This method will return everything past the body-content div
* this intended as a method of writing a liveAPI application that matches cpanel's
* presentation.
*
* @return string A string containing all output after the body-content div
* @throws UnexpectedValueException if no footer value is detected
*/
public function footer( $title = '' ) {
if ( !$this->_dom ) {
$result = $this->uapi( 'Chrome', 'get_dom', array( 'title' => $title ) );
$this->_dom = $result['cpanelresult']['result']['data'];
}
if ( !array_key_exists( 'footer', $this->_dom ) ) {
throw new UnexpectedValueException('No footer in DOM response!');
}
return $this->_dom['footer'];
}
/**
* Close the connection and destroy the object
*
* Calling this method should not be required since all logic actually
* resides in the class deconstructor. This is provided for BC.
*
* @return void
* @throws RuntimeException if LiveAPI socket is not available
*/
public function end()
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
$this->__destruct();
}
/**
* Deconstructor is responsible for closing communication with the cPanel
* engine
*
* @return void
* @throws RuntimeException if LiveAPI socket is not available
*/
public function __destruct()
{
if (!$this->connected) {
throw new RuntimeException(
'The LiveAPI PHP socket has closed, unable to continue.'
);
}
if (is_resource($this->_cpanelfh)) {
$this->exec('<cpanelxml shutdown="1" />', 1);
while (!feof($this->_cpanelfh)) {
fgets($this->_cpanelfh);
}
fclose($this->_cpanelfh);
if ($this->_debug_level) {
$this->debug_log(1, 'MAX_MEM: ' . memory_get_peak_usage());
if (is_resource($this->_debug_fh)) {
fclose($this->_debug_fh);
}
}
}
}
}
/**
* Retrieve the contents of the 'result' node within a return response data
* structure
*
* This function is only valid for responses which have only a single response
* in their data structure, i.e., special cpanel tags. In most cases, one
* should consider using CPANEL::get_result() immediately following an API or
* cPanel tag query. This function may be deprecated in future versions of the
* LiveAPI PHP client code.
*
* @param array $result_data Returned response in array format
*
* @return string Contents of the "result" node in the provided data structure
*/
function simple_result($result_data)
{
return $result_data['cpanelresult']['data']['result'];
}
?>
Comments