private static function _initStaticProperties() { self::$_hasIntl = extension_loaded('intl'); PFXUtils::validateSettings(self::$_SETTINGS, self::$_SETTING_TESTS); self::$_stringComparisonCallback = function (URL $url, $compURL) { return (string) $url === (string) $compURL; }; self::$_hostComparisonCallback = function (URL $url, $compURL, $requireSubdomainMatch = false) { $schemeDelimiterPos = strpos($compURL, '://'); if ($schemeDelimiterPos === false) { return false; } $schemeDelimiterPos += 3; $slashPos = strpos($compURL, '/', $schemeDelimiterPos); if ($slashPos === false) { $compHost = substr($compURL, $schemeDelimiterPos); } else { $compHost = substr($compURL, $schemeDelimiterPos, $slashPos - $schemeDelimiterPos); } if ($requireSubdomainMatch) { return $url->getHost() === $compHost; } try { return $url->compareRootDomain(new URL($compHost)); } catch (URLException $e) { return false; } }; self::$_staticPropsReady = true; }
protected static function _initStaticProperties() { if (self::$_dbConn) { return; } \PFXUtils::validateSettings(self::$_SETTINGS, self::$_SETTING_TESTS); /* Since we use the OAuth back end for authentication, and it (optionally) uses the database, we can piggyback off its credentials if provided. This isn't enforced because some subclasses may be built to work correctly with no database connection. */ if (OAUTH_DB_DSN) { self::$_dbConn = \PFXUtils::getDBConn(OAUTH_DB_DSN, OAUTH_DB_USER, OAUTH_DB_PASSWORD); /* This table used to pertain only to the Google Analytics back end, but then it became obvious that it was more generally useful. I retained the old name simply because it created more hassle than necessary to change it. */ self::$_DB_STATEMENTS['google_analytics_api_fetch_log'] = array(); $q = <<<EOF INSERT INTO google_analytics_api_fetch_log (entity, etag, result_count, fetch_date) VALUES (:entity, :etag, :result_count, UNIX_TIMESTAMP()) EOF; self::$_DB_STATEMENTS['google_analytics_api_fetch_log']['insert'] = self::$_dbConn->prepare($q); $q = <<<EOF SELECT * FROM google_analytics_api_fetch_log WHERE entity = :entity ORDER BY id DESC LIMIT 1 EOF; self::$_DB_STATEMENTS['google_analytics_api_fetch_log']['select'] = self::$_dbConn->prepare($q); } }
/** * Ensures required settings are set and valid. */ private static function _validateSettings() { try { \PFXUtils::validateSettings(self::$_SETTINGS, self::$_SETTING_TESTS); } catch (\UnexpectedValueException $e) { throw new UnexpectedValueException('Caught error while validating settings.', null, $e); } }
public static function setUpBeforeClass() { \PFXUtils::validateSettings(array('TEST_DB_DSN' => null, 'TEST_DB_USER' => null, 'TEST_DB_PASSWORD' => null, 'TEST_DB_DBNAME' => null, 'TEST_DB_SCHEMA_HISTORY' => null, 'TEST_DB_SCHEMA_DIRECTORY' => null, 'TEST_DB_BINARY' => null, 'TEST_IGNORE_DB' => false), array('TEST_DB_DSN' => 'str', 'TEST_DB_USER' => 'str', 'TEST_DB_PASSWORD' => 'str', 'TEST_DB_DBNAME' => 'str', 'TEST_DB_SCHEMA_HISTORY' => '?file', 'TEST_DB_SCHEMA_DIRECTORY' => '?string', 'TEST_DB_BINARY' => 'executable')); self::$_dbConn = \PFXUtils::getDBConn(TEST_DB_DSN, TEST_DB_USER, TEST_DB_PASSWORD); /* If we're ignoring the database, we can bail out now that we have satisfied PHPUnit's requirements about what this class has to set up. */ if (TEST_IGNORE_DB) { return; } // Make sure the database is empty first $stmt = self::$_dbConn->query('SHOW TABLES'); if ($stmt->fetch()) { throw new \RuntimeException('Cannot execute a test case against a non-empty database.'); } printf('Running scripts against test database %s...%s', TEST_DB_DBNAME, PHP_EOL); /* This opens a security hole if the test database user's password is sensitive, which it really shouldn't be. */ $cmd = sprintf('%s %s --user=%s --password=%s', TEST_DB_BINARY, TEST_DB_DBNAME, TEST_DB_USER, TEST_DB_PASSWORD); $scripts = array(); if (TEST_DB_SCHEMA_HISTORY) { $fh = fopen(TEST_DB_SCHEMA_HISTORY, 'r'); $baseDir = dirname(TEST_DB_SCHEMA_HISTORY); while ($line = fgets($fh)) { $scripts[] = $baseDir . DIRECTORY_SEPARATOR . trim($line); } } if (TEST_DB_SCHEMA_DIRECTORY) { // Treat this as a comma-delimited list of directories $directories = explode(',', TEST_DB_SCHEMA_DIRECTORY); foreach ($directories as $dir) { $dir = trim($dir); if ($dir[0] != '/') { $dir = realpath(__DIR__ . '/../../schemata') . '/' . $dir; } $dirH = opendir($dir); if ($dirH === false) { throw new RuntimeException('Failed to open the directory ' . $dir . ' for reading.'); } while (false !== ($entry = readdir($dirH))) { $fullPath = $dir . '/' . $entry; if (!is_dir($fullPath)) { $scripts[] = $fullPath; } } closedir($dirH); } } foreach ($scripts as $script) { system($cmd . ' < ' . $script, $returnVal); if ($returnVal) { throw new \RuntimeException('Execution of database script ' . $script . ' failed ' . 'with exit code ' . $returnVal . '.'); } } printf('Done.%s', PHP_EOL); }
protected static function _initStaticProperties() { parent::_initStaticProperties(); \PFXUtils::validateSettings(self::$_SETTINGS, self::$_SETTING_TESTS); if (PFX_CA_BUNDLE) { self::_registerSSLCertificate(PFX_CA_BUNDLE); } self::_prepareDBStatements(); self::$_apiMutex = new \Mutex(__CLASS__); self::$_validator = new \Validator(__NAMESPACE__); self::$_staticPropsReady = true; }
/** * Determine the nature of the environment and how we will perform the * mutex. * * @param mixed $lockParam */ public function __construct($lockParam) { if (self::$_useSysV === null) { PFXUtils::validateSettings(self::$_SETTINGS, self::$_SETTING_TESTS); if (MUTEX_DEBUG_LOG_FILE) { self::$_log = fopen(MUTEX_DEBUG_LOG_FILE, 'w'); } $osType = strtolower(substr(PHP_OS, 0, 3)); // Cygwin has the SysV functions but they don't actually work if (!function_exists('sem_acquire') || $osType == 'cyg') { self::$_useSysV = false; self::$_lockFileBase = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'mutex.{LOCK_ID}.tmp'; } else { self::$_useSysV = true; } self::$_activeResources = new SplQueue(); } if (self::$_log) { $this->_mutexID = uniqid(); $trace = debug_backtrace(); self::_log(sprintf('Mutex %s constructed in %s (%s)', $this->_mutexID, $trace[0]['file'], $trace[0]['line'])); } // If we were passed an object instance, get its class name if (is_object($lockParam)) { $lockParam = get_class($lockParam); } if (filter_var($lockParam, FILTER_VALIDATE_INT) !== false) { $lockMethod = self::LOCK_METHOD_MANUAL; $lockID = (int) $lockParam; } else { $lockMethod = self::LOCK_METHOD_AUTO; $lockID = crc32($lockParam); } if (isset(self::$_REGISTERED_KEY_LOCK_METHODS[$lockID]) && self::$_REGISTERED_KEY_LOCK_METHODS[$lockID] != $lockMethod) { throw new MutexException('The mutex lock parameter "' . $lockParam . '" conflicts ' . 'with an existing mutex.'); } self::$_REGISTERED_KEY_LOCK_METHODS[$lockID] = $lockMethod; $this->_lockID = $lockID; if (self::$_useSysV) { $this->_lockResource = sem_get($this->_lockID, 1, 0666, 1); } else { $lockFile = str_replace('{LOCK_ID}', $this->_lockID, self::$_lockFileBase); $this->_lockResource = fopen($lockFile, 'a+b'); // I'm not supporting the cleanup operation for real sempahores self::$_activeResources->enqueue($this->_lockResource); self::$_activeResources->enqueue($lockFile); } if (!$this->_lockResource) { throw new MutexException('Failed to obtain lock resource for mutex.'); } }
private static function _initStaticProperties() { \PFXUtils::validateSettings(self::$_SETTINGS, self::$_SETTING_TESTS); $reflector = new \ReflectionClass(__CLASS__); self::$_constantsByName = $reflector->getConstants(); self::$_constantsByVal = array_flip(self::$_constantsByName); }
/** * Tests PFXUtils::collapseArgs(). */ public function testCollapseArgs() { /* This is a pretty unorthodox test due to the fact that we can't maniuplate the argument vector from within this process. To solve that problem we have a helper script that will be responsible for making the actual calls to PFXUtils::collapseArgs for us. */ PFXUtils::validateSettings(array('PHP_BINARY' => null), array('PHP_BINARY' => 'executable')); $this->assertTrue(file_exists(__DIR__ . DIRECTORY_SEPARATOR . 'test_collapse_args.php')); $expected = array('foo' => true); $this->assertEquals($expected, $this->runCollapseArgsCommand('--foo', array('f'), array('foo'))); // Should get the same thing with the short form $this->assertEquals($expected, $this->runCollapseArgsCommand('-f', array('f'), array('foo'))); // With an argument $expected = array('foo' => 'bar'); $this->assertEquals($expected, $this->runCollapseArgsCommand('--foo=bar', array('f:'), array('foo:'))); $this->assertEquals($expected, $this->runCollapseArgsCommand('-f=bar', array('f:'), array('foo:'))); // Omitting a mandatory argument should spell death $this->assertThrows('InvalidArgumentException', array($this, 'runCollapseArgsCommand'), array('-f=bar', array('f:', '!b:'), array('foo:', 'bar:'))); // Put the mandatory flag on the long form only $this->assertThrows('InvalidArgumentException', array($this, 'runCollapseArgsCommand'), array('-f=bar', array('f:', 'b:'), array('foo:', '!bar:'))); // How about on both $this->assertThrows('InvalidArgumentException', array($this, 'runCollapseArgsCommand'), array('-f=bar', array('f:', '!b:'), array('foo:', '!bar:'))); // But omitting a non-mandatory argument should be OK $expected = array('bar' => 'foo'); $this->assertEquals($expected, $this->runCollapseArgsCommand('--bar=foo --baz', array('f:', '!b:'), array('foo:', 'bar:'))); /* We should be able to mix arguments that come in both forms, arguments that only have a short form, and arguments that only have a long form, provided they line up in the arrays we pass. */ $expected = array('a' => true, 'bee' => '2', 'cee' => 'three'); $this->assertEquals($expected, $this->runCollapseArgsCommand('-a -b=2 --cee=three', array('a', 'b:'), array(null, 'bee:', 'cee:'))); /* If an available boolean argument is not supplied, it should automatically be resolved as false. */ $expected = array('foo' => true, 'bar' => false); $this->assertEquals($expected, $this->runCollapseArgsCommand('-f', array('f', 'b'), array('foo', 'bar'))); $expected = array('a' => false, 'b' => true, 'c' => false); $this->assertEquals($expected, $this->runCollapseArgsCommand('-b', array('a', 'b', 'c'))); // Multiple invocations should end up in an array $expected = array('f' => array('bar', 'baz'), 'a' => 'b'); $this->assertEquals($expected, $this->runCollapseArgsCommand('-f=bar -f=baz -a=b', array('f:', 'a:'))); $expected = array('foo' => array('bar', 'baz'), 'a' => 'b'); $this->assertEquals($expected, $this->runCollapseArgsCommand('-f=bar -f=baz -a=b', array('f:', 'a:'), array('foo:'))); /* Even if a short form is used for one invocation, and the long form for another. */ $this->assertEquals($expected, $this->runCollapseArgsCommand('-f=bar -a=b --foo=baz', array('f:', 'a:'), array('foo:'))); /* If PFX_USAGE_MESSAGE is defined, and the command includes --help (or something that maps to it), PFXUtils::collapseArgs() will normally print it and exit. */ $usageMessage = <<<EOF Some kind of helpful message. It will probably be spread across several lines. EOF; $this->assertEquals($expected, $this->runCollapseArgsCommand('-f=bar -a=b --foo=baz', array('f:', 'a:'), array('foo:'))); $this->assertEquals($usageMessage, $this->runCollapseArgsCommand('-f=bar -a=b --foo=baz --help', array('f:', 'a:', 'h'), array('foo:', null, 'help'), null, $usageMessage)); // This happens even if required arguments are missing $this->assertEquals($usageMessage, $this->runCollapseArgsCommand('-h', array('!f:', 'a:', 'h'), array('foo:', null, 'help'), null, $usageMessage)); // But if we turn this off, we get the normal behavior $expected['help'] = true; $this->assertEquals($expected, $this->runCollapseArgsCommand('-f=bar -a=b --foo=baz --help', array('f:', 'a:', 'h'), array('foo:', null, 'help'), false, $usageMessage)); $this->assertThrows('InvalidArgumentException', array($this, 'runCollapseArgsCommand'), array('-h', array('!f:', 'a:', 'h'), array('foo:', null, 'help'), false, $usageMessage)); $shortUsageMessage = <<<EOF A shorter message. Also probably multiple lines. EOF; /* If both a usage message and a short usage message are passed, the short one is used if there is an error in the command line arguments, while the long one is used if we ask for help. */ $this->assertEquals($usageMessage, $this->runCollapseArgsCommand('--help', array('!f:', 'h'), array('!foo:', 'help'), true, $usageMessage, $shortUsageMessage, false)); $this->assertEquals($shortUsageMessage, $this->runCollapseArgsCommand('--baz', array('!f:', 'h'), array('!foo:', 'help'), true, $usageMessage, $shortUsageMessage, false)); }
/** * Handles certain setup tasks. This is the kind of thing that a * constructor would handle, but since this is an abstract class and I want * child classes to be free to accept any arguments they want, I'm not * using a constructor here. */ private function _setupInstance() { if (!self::$_testedGenericSettings) { \PFXUtils::validateSettings(self::$_SETTINGS, self::$_SETTING_TESTS); self::_buildDefaultHTTPActionMap(); self::$_validator = new \Validator(); } /* If there is an exception type configured, validate that it really exists. Otherwise default to RuntimeException. */ if ($this->_EXCEPTION_TYPE) { if (!class_exists($this->_EXCEPTION_TYPE) || !is_a($this->_EXCEPTION_TYPE, 'Exception', true)) { throw new \Exception('Exception types registered in descendants of this ' . 'class must be valid Exception subclasses.'); } } else { $this->_EXCEPTION_TYPE = 'RuntimeException'; } if ($this->_responseFormat && array_key_exists($this->_responseFormat, self::$_STANDARD_PARSE_CALLBACKS)) { $callbackData = self::$_STANDARD_PARSE_CALLBACKS[$this->_responseFormat]; $callback = array_shift($callbackData); $this->_registerParseCallback($callback, $callbackData); } self::$_validator->number($this->_responseTries, 'The number of attempts to use when connecting to API URLs ' . 'must be a non-zero integer.', \Validator::ASSERT_INT_DEFAULT); self::$_validator->number($this->_repeatPauseInterval, 'The number of seconds to wait before repeating an API ' . 'request must be a positive integer.', \Validator::ASSERT_INT | \Validator::ASSERT_POSITIVE); if ($this->_requestDelayInterval !== null) { self::$_validator->number($this->_requestDelayInterval, 'Delay intervals must be positive integers.', \Validator::ASSERT_INT | \Validator::ASSERT_POSITIVE); /* In order to observe this delay across processes, we need to have a mutex and use shared memory. */ try { if (!$this->_mutex) { $this->_mutex = new \Mutex($this); } /* We will be keeping track of a millisecond timestamp, which means 13 digits. */ $allocBytes = \SharedMemory::getRequiredBytes(array(1000000000000)); $this->_shmSegment = new \SharedMemory($this->_mutex, $allocBytes); } catch (\Exception $e) { throw new $this->_EXCEPTION_TYPE('Caught error while configuring shared memory segment.', null, $e); } /* Not only do we need to keep track of the delay interval, but we also need a variable name that reflects the specific subclass for which we are tracking this interval. */ $this->_lastRequestTimeVarName = 'req' . get_class($this); } foreach (self::$_HTTP_RESPONSE_DEFAULT_MAP as $code => $action) { /* If the child has already registered certain actions in any of these positions, skip them. */ if (!array_key_exists($code, $this->_httpResponseActionMap)) { $this->_httpResponseActionMap[$code] = $action; } } $this->_instanceSetupComplete = true; }
/** * Establishes default values for identifying the source of emails sent by * this class. */ private static function _setDefaults() { PFXUtils::validateSettings(self::$_SETTINGS, self::$_SETTING_TESTS); if (EMAIL_FROM_HOSTNAME) { self::$_DEFAULT_FROM_HOSTNAME = EMAIL_FROM_HOSTNAME; } else { // Look for a HOSTNAME environment variable self::$_DEFAULT_FROM_HOSTNAME = getenv('HOSTNAME'); if (!self::$_DEFAULT_FROM_HOSTNAME) { /* Not too creative, but a reasonably sensible default. The dot is necessary because 'localhost' does not pass PHP's email validation filter when used as the domain part. */ self::$_DEFAULT_FROM_HOSTNAME = 'local.host'; } } if (EMAIL_FROM_USER) { self::$_DEFAULT_FROM_USER = EMAIL_FROM_USER; } else { // There really should be a USER environment variable self::$_DEFAULT_FROM_USER = getenv('USER'); if (!self::$_DEFAULT_FROM_USER) { // Again, not too creative, but it probably won't come up self::$_DEFAULT_FROM_USER = '******'; } } if (EMAIL_FROM_NAME) { self::$_DEFAULT_FROM_NAME = EMAIL_FROM_NAME; } else { // Unlike above, this isn't a standard environment variable name self::$_DEFAULT_FROM_NAME = getenv('EMAIL_FROM'); if (!self::$_DEFAULT_FROM_NAME) { self::$_DEFAULT_FROM_NAME = 'Nobody'; } } if (EMAIL_REPLY_TO) { self::$_DEFAULT_REPLY_TO = EMAIL_REPLY_TO; } else { self::$_DEFAULT_REPLY_TO = self::$_DEFAULT_FROM_USER . '@' . self::$_DEFAULT_FROM_HOSTNAME; } self::$_validator = new Validator(); self::$_validator->setExceptionType('EmailException'); self::$_defaultCharset = ini_get('default_charset'); }