<?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace Akeeba\Engine\Dump; defined('AKEEBAENGINE') || die(); use Akeeba\Engine\Base\Part; use Akeeba\Engine\Core\Domain\Pack; use Akeeba\Engine\Driver\Base as DriverBase; use Akeeba\Engine\Factory; use Akeeba\Engine\Platform; use Akeeba\Engine\Util\FileCloseAware; use Exception; use RuntimeException; abstract class Base extends Part { use FileCloseAware; // ********************************************************************** // Configuration parameters // ********************************************************************** /** @var int Current dump file part number */ public $partNumber = 0; /** @var string Prefix to this database */ protected $prefix = ''; /** @var string MySQL database server host name or IP address */ protected $host = ''; /** @var string MySQL database server port (optional) */ protected $port = ''; /** @var string MySQL socket or named pipe (optional) */ protected $socket = ''; /** @var string MySQL user name, for authentication */ protected $username = ''; /** @var string MySQL password, for authentication */ protected $password = ''; /** @var string MySQL database */ protected $database = ''; /** @var string The database driver to use */ protected $driver = ''; /** @var array|null The MySQL SSL options */ protected $ssl; // ********************************************************************** // File handling fields // ********************************************************************** /** @var boolean Should I post process quoted values */ protected $postProcessValues = false; /** @var string Absolute path to dump file; must be writable (optional; if left blank it is automatically calculated) */ protected $dumpFile = ''; /** @var string Data cache, used to cache data before being written to disk */ protected $data_cache = ''; /** @var int */ protected $largest_query = 0; /** @var int Size of the data cache, default 128Kb */ protected $cache_size = 131072; /** @var bool Should I process empty prefixes when creating abstracted names? */ protected $processEmptyPrefix = true; /** @var string Absolute path to the temp file */ protected $tempFile = ''; /** @var string Relative path of how the file should be saved in the archive */ protected $saveAsName = ''; /** @var array Contains the sorted (by dependencies) list of tables/views to backup */ protected $tables = []; // ********************************************************************** // Protected fields (data handling) // ********************************************************************** /** @var array Contains the configuration data of the tables */ protected $tables_data = []; /** @var array Maps database table names to their abstracted format */ protected $table_name_map = []; /** @var array Contains the dependencies of tables and views (temporary) */ protected $dependencies = []; /** @var string The next table to backup */ protected $nextTable; /** @var integer The next row of the table to start backing up from */ protected $nextRange; /** @var integer Current table's row count */ protected $maxRange; /** @var bool Use extended INSERTs */ protected $extendedInserts = false; /** @var integer Maximum packet size for extended INSERTs, in bytes */ protected $packetSize = 0; /** @var string Extended INSERT query, while it's being constructed */ protected $query = ''; /** @var int Dump part's maximum size */ protected $partSize = 0; /** @var resource Filepointer to the current dump part */ private $fp = null; /** * Should I be using an abstract prefix (#__) for table names? * * @var bool * @since 9.1.0 */ private $useAbstractPrefix; /** * Public constructor. * * @return void * @since 9.1.0 */ public function __construct() { $this->useAbstractPrefix = Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') !== 'output'; parent::__construct(); } /** * This method is called when the factory is being serialized and is used to perform necessary cleanup steps. * * @return void */ public function _onSerialize() { $this->closeFile(); } /** * This method is called when the object is destroyed and is used to perform necessary cleanup steps. */ public function __destruct() { $this->closeFile(); } /** * Close any open SQL dump (output) file. */ public function closeFile() { if (!is_resource($this->fp)) { return; } Factory::getLog()->debug("Closing SQL dump file."); $this->conditionalFileClose($this->fp); $this->fp = null; } /** * Call a specific stage of the dump engine * * @param string $stage * * @throws Exception */ public function callStage($stage) { switch ($stage) { case '_prepare': $this->_prepare(); break; case '_run': $this->_run(); break; case '_finalize': $this->_finalize(); break; } } /** * Find where to store the backup files * * @param $partNumber int The SQL part number, default is 0 (.sql) */ protected function getBackupFilePaths($partNumber = 0) { Factory::getLog()->debug(__CLASS__ . " :: Getting temporary file"); $basename = substr(md5(microtime() . random_bytes(8)), random_int(0, 16), 16); $this->tempFile = Factory::getTempFiles()->registerTempFile($basename . '.sql'); Factory::getLog()->debug(__CLASS__ . " :: Temporary file is {$this->tempFile}"); // Get the base name of the dump file $partNumber = intval($partNumber); $baseName = $this->dumpFile; if ($partNumber > 0) { // The file names are in the format dbname.sql, dbname.s01, dbname.s02, etc if (strtolower(substr($baseName, -4)) == '.sql') { $baseName = substr($baseName, 0, -4) . '.s' . sprintf('%02u', $partNumber); } else { $baseName = $baseName . '.s' . sprintf('%02u', $partNumber); } } if (empty($this->installerSettings)) { // Fetch the installer settings $this->installerSettings = (object) [ 'installerroot' => 'installation', 'sqlroot' => 'installation/sql', 'databasesini' => 1, 'readme' => 1, 'extrainfo' => 1, ]; $config = Factory::getConfiguration(); $installerKey = $config->get('akeeba.advanced.embedded_installer'); $installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList(); if (array_key_exists($installerKey, $installerDescriptors)) { // The selected installer exists, use it $this->installerSettings = (object) $installerDescriptors[$installerKey]; } elseif (array_key_exists('angie', $installerDescriptors)) { // The selected installer doesn't exist, but ANGIE exists; use that instead $this->installerSettings = (object) $installerDescriptors['angie']; } } switch (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal')) { case 'output': // The SQL file will be stored uncompressed in the output directory $statistics = Factory::getStatistics(); $statRecord = $statistics->getRecord(); $this->saveAsName = $statRecord['absolute_path']; break; case 'normal': // The SQL file will be stored in the SQL root of the archive, as // specified by the particular embedded installer's settings $this->saveAsName = $this->installerSettings->sqlroot . '/' . $baseName; break; case 'short': // The SQL file will be stored on archive's root $this->saveAsName = $baseName; break; } if ($partNumber > 0) { Factory::getLog()->debug("AkeebaDomainDBBackup :: Creating new SQL dump part #$partNumber"); } Factory::getLog()->debug("AkeebaDomainDBBackup :: SQL temp file is " . $this->tempFile); Factory::getLog()->debug("AkeebaDomainDBBackup :: SQL file location in archive is " . $this->saveAsName); } /** * Deletes any leftover files from previous backup attempts * */ protected function removeOldFiles() { Factory::getLog()->debug("AkeebaDomainDBBackup :: Deleting leftover files, if any"); if (file_exists($this->tempFile)) { @unlink($this->tempFile); } } /** * Populates the table arrays with the information for the db entities to backup * * @return null */ protected abstract function getTablesToBackup(); /** * Runs a step of the database dump * * @return null */ protected abstract function stepDatabaseDump(); /** * Implements the _prepare abstract method * * @throws Exception */ protected function _prepare() { $this->setStep('Initialization'); $this->setSubstep(''); // Process parameters, passed to us using the setup() public method Factory::getLog()->debug(__CLASS__ . " :: Processing parameters"); if (is_array($this->_parametersArray)) { $this->driver = $this->_parametersArray['driver'] ?? $this->driver; $this->host = $this->_parametersArray['host'] ?? $this->host; $this->port = $this->_parametersArray['port'] ?? $this->port; $this->socket = $this->_parametersArray['socket'] ?? $this->socket; $this->username = $this->_parametersArray['username'] ?? $this->username; $this->username = $this->_parametersArray['user'] ?? $this->username; $this->password = $this->_parametersArray['password'] ?? $this->password; $this->database = $this->_parametersArray['database'] ?? $this->database; $this->prefix = $this->_parametersArray['prefix'] ?? $this->prefix; $this->dumpFile = $this->_parametersArray['dumpFile'] ?? $this->dumpFile; $this->processEmptyPrefix = $this->_parametersArray['process_empty_prefix'] ?? $this->processEmptyPrefix; $this->ssl = $this->_parametersArray['ssl'] ?? $this->ssl; $this->ssl = is_array($this->ssl) ? $this->ssl : []; $this->ssl['enable'] = (bool)(($this->ssl['enable'] ?? $this->_parametersArray['dbencryption'] ?? false) ?: false); $this->ssl['cipher'] = ($this->ssl['cipher'] ?? $this->_parametersArray['dbsslcipher'] ?? null) ?: null; $this->ssl['ca'] = ($this->ssl['ca'] ?? $this->_parametersArray['dbsslca'] ?? null) ?: null; $this->ssl['capath'] = ($this->ssl['capath'] ?? $this->_parametersArray['dbsslcapath'] ?? null) ?: null; $this->ssl['key'] = ($this->ssl['key'] ?? $this->_parametersArray['dbsslkey'] ?? null) ?: null; $this->ssl['cert'] = ($this->ssl['cert'] ?? $this->_parametersArray['dbsslcert'] ?? null) ?: null; $this->ssl['verify_server_cert'] = (bool)(($this->ssl['verify_server_cert'] ?? $this->_parametersArray['dbsslverifyservercert'] ?? false) ?: false); } // Make sure we have self-assigned the first part $this->partNumber = 0; // Get DB backup only mode $configuration = Factory::getConfiguration(); // Find tables to be included and put them in the $_tables variable $this->getTablesToBackup(); // Find where to store the database backup files $this->getBackupFilePaths($this->partNumber); // Remove any leftovers $this->removeOldFiles(); // Initialize the extended INSERTs feature $this->extendedInserts = ($configuration->get('engine.dump.common.extended_inserts', 0) != 0); $this->packetSize = (int) $configuration->get('engine.dump.common.packet_size', 0); if ($this->packetSize == 0) { $this->extendedInserts = false; } // Initialize the split dump feature $this->partSize = $configuration->get('engine.dump.common.splitsize', 1048576); if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output') { $this->partSize = 0; } if (($this->partSize != 0) && ($this->packetSize != 0) && ($this->packetSize > $this->partSize)) { $this->packetSize = floor($this->partSize / 2); } // Initialize the algorithm Factory::getLog()->debug(__CLASS__ . " :: Initializing algorithm for first run"); $this->nextTable = array_shift($this->tables); // If there is no table to back up we are done with the database backup if (empty($this->nextTable)) { $this->setState(self::STATE_POSTRUN); return; } $this->nextRange = 0; $this->query = ''; // FIX 2.2: First table of extra databases was not being written to disk. // This deserved a place in the Bug Fix Hall Of Fame. In subsequent calls to _init, the $fp in // _writeline() was not nullified. Therefore, the first dump chunk (that is, the first table's // definition and first chunk of its data) were not written to disk. This call causes $fp to be // nullified, causing it to be recreated, pointing to the correct file. $null = null; $this->writeline($null); // Finally, mark ourselves "prepared". $this->setState(self::STATE_PREPARED); } /** * Implements the _run() abstract method * * @throws Exception */ protected function _run() { // Check if we are already done if ($this->getState() == self::STATE_POSTRUN) { Factory::getLog()->debug(__CLASS__ . " :: Already finished"); $this->setStep(""); $this->setSubstep(""); return; } // Mark ourselves as still running (we will test if we actually do towards the end ;) ) $this->setState(self::STATE_RUNNING); /** * Resume packing / post-processing part files if necessary. * * @see \Akeeba\Engine\Archiver\BaseArchiver::putDataFromFileIntoArchive * * Sometimes the SQL part file may be bigger than the big file threshold (engine.archiver.common. * big_file_threshold). In this case when we try to add it to the backup archive the archiver engine figures * out it has to be added uncompressed, one chunk (engine.archiver.common.chunk_size) bytes at a time. This * happens in a loop. We read a chunk, push it to the archive, rinse and repeat. * * There are two cases when we might break the loop: * * 1. Not enough free space in the backup archive part and engine.postproc.common.after_part (immediate post- * processing) is enabled. We break the step to let the backup part be post-processed. * * 2. We ran out of time copying data. * * The following if-blocks deal with these two cases. */ if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output') { $archiver = Factory::getArchiverEngine(); $configuration = Factory::getConfiguration(); // Check whether we need to immediately post-processing a done part if (Pack::postProcessDonePartFile($archiver, $configuration)) { return; } // We had already started putting the DB dump file into the archive but it needs more time if ($configuration->get('volatile.engine.archiver.processingfile', false)) { /** * We MUST NOT try to continue adding the file to the backup archive manually. Instead, we have to go * through getNextDumpPart. This method will continue adding the part to the backup archive and when * this is done it will remove the file and create a new one. * * If that method returns false it means that we either hit an error or the archiver didn't have enough * time to add the part to the backup archive. In either case we have to return and let the Engine step. */ if ($this->getNextDumpPart() === false) { return; } } } $this->stepDatabaseDump(); $null = null; $this->writeline($null); } /** * Implements the _finalize() abstract method * * @throws Exception */ protected function _finalize() { Factory::getLog()->debug("Adding any extra SQL statements imposed by the filters"); foreach (Factory::getFilters()->getExtraSQL($this->databaseRoot) as $sqlStatement) { $sqlStatement = trim($sqlStatement) . "\n"; $this->writeDump($sqlStatement, true); } // We need this to write out the cached extra SQL statements before closing the file. $this->writeDump(null, true); // Close the file pointer (otherwise the SQL file is left behind) $this->closeFile(); // If we are not just doing a main db only backup, add the SQL file to the archive $finished = true; if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output') { $archiver = Factory::getArchiverEngine(); $configuration = Factory::getConfiguration(); if ($configuration->get('volatile.engine.archiver.processingfile', false)) { // We had already started archiving the db file, but it needs more time Factory::getLog()->debug("Continuing adding the SQL dump to the archive"); $archiver->addFile(null, null, null); $finished = !$configuration->get('volatile.engine.archiver.processingfile', false); } else { // We have to add the dump file to the archive Factory::getLog()->debug("Adding the final SQL dump to the archive"); $archiver->addFileRenamed($this->tempFile, $this->saveAsName); $finished = !$configuration->get('volatile.engine.archiver.processingfile', false); } } else { // We just have to move the dump file to its final destination Factory::getLog()->debug("Moving the SQL dump to its final location"); $result = Platform::getInstance()->move($this->tempFile, $this->saveAsName); if (!$result) { Factory::getLog()->debug("Removing temporary file of final SQL dump"); Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true); throw new RuntimeException('Could not move the SQL dump to its final location'); } } // Make sure that if the archiver needs more time to process the file we can supply it if ($finished) { Factory::getLog()->debug("Removing temporary file of final SQL dump"); Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true); $this->setState(self::STATE_FINISHED); } } /** * Creates a new dump part * * @return bool * @throws Exception */ protected function getNextDumpPart() { // On database dump only mode we mustn't create part files! if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output') { return false; } // Is the archiver still processing? $configuration = Factory::getConfiguration(); $archiver = Factory::getArchiverEngine(); $stillProcessing = $configuration->get('volatile.engine.archiver.processingfile', false); if ($stillProcessing) { /** * The archiver is still adding the previous dump part. This means that we are called from the top few lines * of the _run method. We must continue adding the previous dump part. */ Factory::getLog()->debug("Continuing adding the SQL dump part to the archive"); $archiver->addFile('', '', ''); } else { /** * There is no other dump part being processed. Therefore the current SQL dump part is still open. We must * close it and ask the archiver to add it to the backup archive. */ $this->closeFile(); Factory::getLog()->debug("Adding the SQL dump part to the archive"); $archiver->addFileRenamed($this->tempFile, $this->saveAsName); } // Return false if the file didn't finish getting added to the archive if ($configuration->get('volatile.engine.archiver.processingfile', false)) { Factory::getLog()->debug("The SQL dump file has not been processed thoroughly by the archiver. Resuming in the next step."); return false; } /** * If you are here the SQL dump part file is completely added to the backup archive. All we have to do now is * remove it and create a new dump part file. */ // Remove the old file Factory::getLog()->debug("Removing dump part's temporary file"); Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true); // Create the new dump part $this->partNumber++; $this->getBackupFilePaths($this->partNumber); $null = null; $this->writeline($null); return true; } /** * Creates a new dump part, but only if required to do so * * @return bool * @throws Exception */ protected function createNewPartIfRequired() { if ($this->partSize == 0) { return true; } $filesize = 0; if (@file_exists($this->tempFile)) { $filesize = @filesize($this->tempFile); } $projectedSize = $filesize + strlen($this->query); if ($this->extendedInserts) { $projectedSize = $filesize + $this->packetSize; } if ($projectedSize > $this->partSize) { return $this->getNextDumpPart(); } return true; } /** * Returns a table's abstract name (replacing the prefix with the magic #__ string) * * @param string $tableName The canonical name, e.g. 'jos_content' * * @return string The abstract name, e.g. '#__content' */ protected function getAbstract($tableName) { // Don't return abstract names for non-CMS tables if (is_null($this->prefix)) { return $tableName; } switch ($this->prefix) { case '': if ($this->processEmptyPrefix) { // This is more of a hack; it assumes all tables are core CMS tables if the prefix is empty. return '#__' . $tableName; } // If $this->processEmptyPrefix (the process_empty_prefix config flag) is false, we don't // assume anything. return $tableName; break; default: // Normal behaviour for 99% of sites. Start by assuming the table has no prefix, therefore is non-core. $tableAbstract = $tableName; // If there's a prefix use the abstract name if (!empty($this->prefix) && (substr($tableName, 0, strlen($this->prefix)) == $this->prefix)) { $tableAbstract = '#__' . substr($tableName, strlen($this->prefix)); } return $tableAbstract; break; } } /** * Writes the SQL dump into the output files. If it fails, it sets the error * * @param string $data Data to write to the dump file. Pass NULL to force flushing to file. * @param bool $addMarker Should I prefix the data with a marker? * * @return boolean TRUE on successful write, FALSE otherwise * @throws Exception */ protected function writeDump($data, $addMarker = false) { if (!empty($data)) { if ($addMarker && $this->useAbstractPrefix) { $this->data_cache .= '/**ABDB**/'; } elseif (!$this->useAbstractPrefix) { // Replace #__ with the prefix when writing plain .sql files $db = $this->getDB(); $data = $db->replacePrefix($data) . "\n"; } $this->data_cache .= $data; if (strlen($data) > $this->largest_query) { $this->largest_query = strlen($data); Factory::getConfiguration()->set('volatile.database.largest_query', $this->largest_query); } } if ((strlen($this->data_cache) >= $this->cache_size) || (is_null($data) && (!empty($this->data_cache)))) { $this->data_cache = rtrim($this->data_cache, "\n"); if ($this->useAbstractPrefix && $addMarker && substr($this->data_cache, -10) !== '/**ABDB**/') { $this->data_cache .= '/**ABDB**/'; } $this->data_cache .= "\n"; Factory::getLog()->debug("Writing " . strlen($this->data_cache) . " bytes to the dump file"); $result = $this->writeline($this->data_cache); if (!$result) { $errorMessage = sprintf('Couldn\'t write to the SQL dump file %s; check the temporary directory permissions and make sure you have enough disk space available.', $this->tempFile); throw new RuntimeException($errorMessage); } $this->data_cache = ''; } return true; } /** * Saves the string in $fileData to the file $backupfile. Returns TRUE. If saving * failed, return value is FALSE. * * @param string $fileData Data to write. Set to null to close the file handle. * * @return boolean TRUE is saving to the file succeeded * @throws Exception */ protected function writeline(&$fileData) { if (!is_resource($this->fp)) { $this->fp = @fopen($this->tempFile, 'a'); if ($this->fp === false) { throw new RuntimeException('Could not open ' . $this->tempFile . ' for append, in DB dump.'); } } if (is_null($fileData)) { $this->conditionalFileClose($this->fp); $this->fp = null; return true; } else { if ($this->fp) { $ret = fwrite($this->fp, $fileData); @clearstatcache(); // Make sure that all data was written to disk return ($ret == strlen($fileData)); } return false; } } /** * Return an instance of DriverBase * * @return DriverBase|bool * * @throws Exception */ protected function &getDB() { $ssl = $this->ssl ?? []; $ssl = is_array($ssl) ? $ssl : []; $options = [ 'driver' => $this->driver, 'host' => $this->host, 'port' => $this->port, 'socket' => $this->socket, 'user' => $this->username, 'password' => $this->password, 'database' => $this->database, 'prefix' => is_null($this->prefix) ? '' : $this->prefix, 'ssl' => $ssl, ]; $db = Factory::getDatabase($options); if ($db->getErrorNum() > 0) { $error = $db->getErrorMsg(); throw new RuntimeException(__CLASS__ . ' :: Database Error: ' . $error); } return $db; } /** * Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL) * * @return string */ abstract protected function getDatabaseNameFromConnection(); /** * Returns the database name. If the name was not declared when the object was created we will go through the * getDatabaseNameFromConnection method to populate it. * * @return string */ protected function getDatabaseName() { if (empty($this->database)) { $this->database = $this->getDatabaseNameFromConnection(); } return $this->database; } /** * Post process a quoted value before it's written to the database dump. * So far it's only required for SQL Server which has a problem escaping * newline characters... * * @param string $value The quoted value to post-process * * @return string */ protected function postProcessQuotedValue($value) { return $value; } /** * Returns a preamble for the data dump portion of the SQL backup. This is * used to output commands before the first INSERT INTO statement for a * table when outputting a plain SQL file. * * Practical use: the SET IDENTITY_INSERT sometable ON required for SQL Server * * @param string $tableAbstract Abstract name of the table, e.g. #__foobar * @param string $tableName Real name of the table, e.g. abc_foobar * @param integer $maxRange Row count on this table * * @return string The SQL commands you want to be written in the dump file */ protected function getDataDumpPreamble($tableAbstract, $tableName, $maxRange) { return ''; } /** * Returns an epilogue for the data dump portion of the SQL backup. This is * used to output commands after the last INSERT INTO statement for a * table when outputting a plain SQL file. * * Practical use: the SET IDENTITY_INSERT sometable OFF required for SQL Server * * @param string $tableAbstract Abstract name of the table, e.g. #__foobar * @param string $tableName Real name of the table, e.g. abc_foobar * @param integer $maxRange Row count on this table * * @return string The SQL commands you want to be written in the dump file */ protected function getDataDumpEpilogue($tableAbstract, $tableName, $maxRange) { return ''; } /** * Return a list of field names for the INSERT INTO statements. This is only * required for Microsoft SQL Server because without it the SET IDENTITY_INSERT * has no effect. * * @param array|string $fieldNames A list of field names in array format or '*' if it's all fields * * @return string * @throws Exception */ protected function getFieldListSQL($fieldNames) { // If we get a literal '*' we dumped all columns so we don't need to add column names in the INSERT. if ($fieldNames === '*') { return ''; } return '(' . implode(', ', array_map([$this->getDB(), 'qn'], $fieldNames)) . ')'; } /** * Return a list of columns to use in the SELECT query for dumping table data. * * This is used to filter out all generated rows. * * @param string $tableAbstract * * @return string|array An array of table columns or the string literal '*' to quickly select all columns. * * @see https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html */ protected function getSelectColumns($tableAbstract) { return '*'; } /** * Converts a human formatted size to integer representation of bytes, * e.g. 1M to 1024768 * * @param string $setting The value in human readable format, e.g. "1M" * * @return integer The value in bytes */ protected function humanToIntegerBytes($setting) { $val = trim($setting); $last = strtolower($val[strlen($val) - 1]); if (is_numeric($last)) { return $setting; } switch ($last) { case 't': $val *= 1024; case 'g': $val *= 1024; case 'm': $val *= 1024; case 'k': $val *= 1024; } return (int) $val; } /** * Get the PHP memory limit in bytes * * @return int|null Memory limit in bytes or null if we can't figure it out. */ protected function getMemoryLimit() { if (!function_exists('ini_get')) { return null; } $memLimit = ini_get("memory_limit"); if ((is_numeric($memLimit) && ($memLimit < 0)) || !is_numeric($memLimit)) { $memLimit = 0; // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb! } $memLimit = $this->humanToIntegerBytes($memLimit); return $memLimit; } }