ソースを参照

BitmaskedBehavior enhancements

euromark 13 年 前
コミット
db0a2cc0de

+ 11 - 4
Console/Command/PwdShell.php

@@ -11,11 +11,14 @@ App::uses('ComponentCollection', 'Controller');
  * 2011-11-05 ms
  */
 class PwdShell extends AppShell {
-	public $tasks = array();
-	//public $uses = array('User');
 
 	public $Auth = null;
 
+	/**
+	 * PwdShell::hash()
+	 *
+	 * @return void
+	 */
 	public function hash() {
 		$components = array('Tools.AuthExt', 'Auth');
 
@@ -43,11 +46,15 @@ class PwdShell extends AppShell {
 		echo $pw;
 	}
 
-
-
+	/**
+	 * PwdShell::help()
+	 *
+	 * @return void
+	 */
 	public function help() {
 		$this->out('-- Hash Passwort with Auth(Ext) Component --');
 		$this->out('-- cake Tools.Pwd hash');
 		$this->out('---- using the salt of the core.php (!)');
 	}
+
 }

+ 33 - 11
Lib/GeocodeLib.php

@@ -379,7 +379,31 @@ class GeocodeLib {
 
 			if ($this->options['output'] === 'json') {
 				//TODO? necessary?
-				//$res = json_decode($result);
+				$res = json_decode($result, true);
+				$xmlArray = $res;
+				foreach ($xmlArray['results'] as $key => $val) {
+					if (isset($val['address_components'])) {
+						$xmlArray['results'][$key]['address_component'] = $val['address_components'];
+						unset($xmlArray['results'][$key]['address_components']);
+					}
+					if (isset($val['types'])) {
+						$xmlArray['results'][$key]['type'] = $val['types'];
+						unset($xmlArray['results'][$key]['types']);
+					}
+				}
+
+				if (count($xmlArray['results']) === 1) {
+					$xmlArray['result'] = $xmlArray['results'][0];
+				} elseif (!$xmlArray['result']) {
+					$this->setError('JSON parsing failed');
+					CakeLog::write('geocode', __('Failed with JSON parsing of \'%s\'', $address));
+					return false;
+				} else {
+					$xmlArray['result'] = $xmlArray['results'];
+				}
+				unset($xmlArray['results']);
+				//die(debug($xmlArray));
+
 			} else {
 				try {
 					$res = Xml::build($result);
@@ -387,16 +411,15 @@ class GeocodeLib {
 					CakeLog::write('geocode', $e->getMessage());
 					$res = array();
 				}
+				if (!is_object($res)) {
+					$this->setError('XML parsing failed');
+					CakeLog::write('geocode', __('Failed with XML parsing of \'%s\'', $address));
+					return false;
+				}
+				$xmlArray = Xml::toArray($res);
+				$xmlArray = $xmlArray['GeocodeResponse'];
 			}
 
-			if (!is_object($res)) {
-				$this->setError('XML parsing failed');
-				CakeLog::write('geocode', __('Failed with XML parsing of \'%s\'', $address));
-				return false;
-			}
-
-			$xmlArray = Xml::toArray($res);
-			$xmlArray = $xmlArray['GeocodeResponse'];
 			$status = $xmlArray['status'];
 
 			if ($status == self::CODE_SUCCESS) {
@@ -504,8 +527,7 @@ class GeocodeLib {
 	}
 
 	protected function _transformJson($record) {
-		$res = array();
-		//TODO
+		$res = $this->_transformXml($record);
 		return $res;
 	}
 

+ 70 - 10
Lib/HttpSocketLib.php

@@ -3,7 +3,16 @@ App::uses('HttpSocket', 'Network/Http');
 App::uses('CurlLib', 'Tools.Lib');
 
 /**
- * Wrapper for curl, php or file_get_contents
+ * Wrapper for curl, php or file_get_contents with some fixes or improvements:
+ * - All 2xx OK status codes will return the proper result
+ * - Response will be properly utf8 encoded on WINDOWS, as well
+ * - Timeout is reduced to 5 by default and can easily be adjusted
+ * - file_get_contents wrapper is fixed for HTTP1.1 to default to "Connection: close"
+ *   to avoid leaving the connection open
+ * - Caching possibilities included
+ * - Auto-Fallback if curl is not available
+ *
+ * TODO: throw exceptions instead of error stuff here
  *
  * @author Mark Scherer
  * @license MIT
@@ -14,9 +23,13 @@ class HttpSocketLib {
 
 	// First tries with curl, then cake, then php
 	public $use = array('curl' => true, 'cake'=> true, 'php' => true);
+
 	public $debug = null;
+
 	public $timeout = 5;
+
 	public $cacheUsed = null;
+
 	public $error = array();
 
 	public function __construct($use = array()) {
@@ -36,6 +49,9 @@ class HttpSocketLib {
 		}
 	}
 
+	/**
+	 * @param string $error
+	 */
 	public function setError($error) {
 		if (empty($error)) {
 			return;
@@ -43,6 +59,9 @@ class HttpSocketLib {
 		$this->error[] = $error;
 	}
 
+	/**
+	 * @return string
+	 */
 	public function error($asString = true, $separator = ', ') {
 		return implode(', ', $this->error);
 	}
@@ -52,12 +71,14 @@ class HttpSocketLib {
 		$this->debug = null;
 	}
 
-
 	/**
 	 * fetches url with curl if available
 	 * fallbacks: cake and php
 	 * note: expects url with json encoded content
-	 * @access private
+	 *
+	 * @param string $url
+	 * @param array $options
+	 * @return string Response or false on failure
 	 **/
 	public function fetch($url, $options = array()) {
 		if (!is_array($options)) {
@@ -92,6 +113,11 @@ class HttpSocketLib {
 		return $res;
 	}
 
+	/**
+	 * @param string $url
+	 * @param array $options
+	 * @return string Response or false on failure
+	 */
 	public function _fetch($url, $options) {
 		if ($options['use']['curl'] && function_exists('curl_init')) {
 			$this->debug = 'curl';
@@ -99,11 +125,12 @@ class HttpSocketLib {
 			$Ch->setUserAgent($options['agent']);
 			$data = $Ch->get($url);
 			$response = $data[0];
-			$status = $data[1]['http_code'];
-			if ((int)$status !== 200) {
-				$this->setError('Error '.$status);
+			$statusCode = $data[1]['http_code'];
+			if (!in_array($statusCode, array(200, 201, 202, 203, 204, 205, 206))) {
+				$this->setError('Error '.$statusCode);
 				return false;
 			}
+			$response = $this->_assertEncoding($response);
 			return $response;
 
 		} elseif ($options['use']['cake']) {
@@ -111,19 +138,36 @@ class HttpSocketLib {
 
 			$HttpSocket = new HttpSocket(array('timeout' => $options['timeout']));
 			$response = $HttpSocket->get($url);
-			if ($response->code != 200) { //TODO: status 200?
+			if (!in_array($response->code, array(200, 201, 202, 203, 204, 205, 206))) {
 				return false;
 			}
+			$response = $this->_assertEncoding($response);
 			return $response;
 
 		} elseif ($options['use']['php']) {
 			$this->debug = 'php';
 
-			$response = file_get_contents($url, 'r');
-			//TODO: status 200?
-			if (empty($response)) {
+			$opts = array(
+				'http' => array(
+            'method' => 'GET',
+            'header' => array('Connection: close'),
+            'timeout' => $options['timeout']
+        )
+			);
+			if (isset($options['http'])) {
+				$opts['http'] = array_merge($opts['http'], $options['http']);
+			}
+			if (is_array($opts['http']['header'])) {
+				$opts['http']['header'] = implode(PHP_EOL, $opts['http']['header']);
+			}
+			$context = stream_context_create($opts);
+			$response = file_get_contents($url, false, $context);
+			preg_match('/^HTTP.*\s([0-9]{3})/', $http_response_header[0], $matches);
+			$statusCode = (int)$matches[1];
+			if (!in_array($statusCode, array(200, 201, 202, 203, 204, 205, 206))) {
 				return false;
 			}
+			$response = $this->_assertEncoding($response);
 			return $response;
 
 		} else {
@@ -132,5 +176,21 @@ class HttpSocketLib {
 		return null;
 	}
 
+	/**
+	 * It seems all three methods have encoding issues if not run through this method
+	 *
+	 * @param string $response
+	 * @param string Correctly encoded response
+	 */
+	protected function _assertEncoding($response) {
+		if (!WINDOWS) {
+			return $response;
+		}
+		$x = mb_detect_encoding($response, 'auto', true);
+		if ($x !== 'UTF-8') {
+			$response = iconv(null, "utf-8", $response);
+		}
+		return $response;
+	}
 
 }

+ 6 - 6
Lib/ImapLib.php

@@ -57,6 +57,10 @@ class ImapLib {
 	public $currentSettings = array();
 	public $currentRef = '';
 
+	public function __construct() {
+		$this->dependenciesMatch();
+	}
+
 	public function buildConnector($data = array()) {
 		$data = array_merge($this->settings, $data);
 		$string = '{';
@@ -123,10 +127,6 @@ class ImapLib {
 	 * 2011-10-25 ms
 	 */
 	public function connect($user, $pass, $server, $port = null) {
-		if (!$this->dependenciesMatch()) {
-			return false;
-		}
-
 		$this->settings[self::S_SERVER] = $server;
 		if ($port || !$port && $this->settings[self::S_SERVICE] === 'imap') {
 			$this->settings[self::S_PORT] = $port;
@@ -508,13 +508,13 @@ class ImapLib {
 
 	/**
 	 * makes sure imap_open is available etc
+	 * @throws InternalErrorException
 	 * @return bool $success
 	 * 2011-10-25 ms
 	 */
 	public function dependenciesMatch() {
 		if (!function_exists('imap_open')) {
-			trigger_error('imap_open not available. Please install extension/module.');
-			return false;
+			throw new InternalErrorException('imap_open not available. Please install extension/module.');
 		}
 		return true;
 	}

+ 38 - 11
Model/Behavior/BitmaskedBehavior.php

@@ -25,14 +25,16 @@ App::uses('ModelBehavior', 'Model');
 class BitmaskedBehavior extends ModelBehavior {
 
 	/**
-	 * settings defaults
+	 * Settings defaults
+	 *
+	 * @var array
 	 */
 	protected $_defaults = array(
 		'field' => 'status',
-		'mappedField' => null, # NULL = same as above
-		//'mask' => null,
+		'mappedField' => null, // NULL = same as above
 		'bits' => null,
 		'before' => 'validate', // on: save or validate
+		'defaultValue' => null, // NULL = auto (use empty string to trigger "notEmpty" rule for "default NOT NULL" db fields)
 	);
 
 	/**
@@ -63,6 +65,9 @@ class BitmaskedBehavior extends ModelBehavior {
 		$this->settings[$Model->alias] = $config;
 	}
 
+	/**
+	 * @return array
+	 */
 	public function beforeFind(Model $Model, $query) {
 		$field = $this->settings[$Model->alias]['field'];
 
@@ -73,6 +78,9 @@ class BitmaskedBehavior extends ModelBehavior {
 		return $query;
 	}
 
+	/**
+	 * @return array
+	 */
 	public function afterFind(Model $Model, $results, $primary) {
 		$field = $this->settings[$Model->alias]['field'];
 		if (!($mappedField = $this->settings[$Model->alias]['mappedField'])) {
@@ -88,6 +96,9 @@ class BitmaskedBehavior extends ModelBehavior {
 		return $results;
 	}
 
+	/**
+	 * @return boolean Success
+	 */
 	public function beforeValidate(Model $Model) {
 		if ($this->settings[$Model->alias]['before'] !== 'validate') {
 			return true;
@@ -96,6 +107,9 @@ class BitmaskedBehavior extends ModelBehavior {
 		return true;
 	}
 
+	/**
+	 * @return boolean Success
+	 */
 	public function beforeSave(Model $Model) {
 		if ($this->settings[$Model->alias]['before'] !== 'save') {
 			return true;
@@ -104,7 +118,6 @@ class BitmaskedBehavior extends ModelBehavior {
 		return true;
 	}
 
-
 	/**
 	 * @param int $bitmask
 	 * @return array $bitmaskArray
@@ -127,20 +140,23 @@ class BitmaskedBehavior extends ModelBehavior {
 	 * @return int $bitmask
 	 * from APP to DB
 	 */
-	public function encodeBitmask(Model $Model, $value) {
+	public function encodeBitmask(Model $Model, $value, $defaultValue = null) {
 		$res = 0;
 		if (empty($value)) {
-			return null;
+			return $defaultValue;
 		}
 		foreach ((array)$value as $key => $val) {
 			$res |= (int)$val;
 		}
 		if ($res === 0) {
-			return null; # make sure notEmpty validation rule triggers
+			return $defaultValue; // make sure notEmpty validation rule triggers
 		}
 		return $res;
 	}
 
+	/**
+	 * @return array $conditions
+	 */
 	public function encodeBitmaskConditions(Model $Model, $conditions) {
 		$field = $this->settings[$Model->alias]['field'];
 		if (!($mappedField = $this->settings[$Model->alias]['mappedField'])) {
@@ -150,13 +166,13 @@ class BitmaskedBehavior extends ModelBehavior {
 		foreach ($conditions as $key => $val) {
 			if ($key === $mappedField) {
 				$conditions[$field] = $this->encodeBitmask($Model, $val);
-				if ($field != $mappedField) {
+				if ($field !== $mappedField) {
 					unset($conditions[$mappedField]);
 				}
 				continue;
 			} elseif ($key === $Model->alias . '.' . $mappedField) {
 				$conditions[$Model->alias . '.' .$field] = $this->encodeBitmask($Model, $val);
-				if ($field != $mappedField) {
+				if ($field !== $mappedField) {
 					unset($conditions[$Model->alias . '.' .$mappedField]);
 				}
 				continue;
@@ -169,16 +185,27 @@ class BitmaskedBehavior extends ModelBehavior {
 		return $conditions;
 	}
 
+	/**
+	 * @return void
+	 */
 	public function encodeBitmaskData(Model $Model) {
 		$field = $this->settings[$Model->alias]['field'];
 		if (!($mappedField = $this->settings[$Model->alias]['mappedField'])) {
 			$mappedField = $field;
 		}
+		$default = null;
+		$schema = $Model->schema($field);
+		if ($schema && isset($schema['default'])) {
+			$default = $schema['default'];
+		}
+		if ($this->settings[$Model->alias]['defaultValue'] !== null) {
+			$default = $this->settings[$Model->alias]['defaultValue'];
+		}
 
 		if (isset($Model->data[$Model->alias][$mappedField])) {
-			$Model->data[$Model->alias][$field] = $this->encodeBitmask($Model, $Model->data[$Model->alias][$mappedField]);
+			$Model->data[$Model->alias][$field] = $this->encodeBitmask($Model, $Model->data[$Model->alias][$mappedField], $default);
 		}
-		if ($field != $mappedField) {
+		if ($field !== $mappedField) {
 			unset($Model->data[$Model->alias][$mappedField]);
 		}
 	}

+ 11 - 0
Test/Case/Lib/GeocodeLibTest.php

@@ -100,6 +100,17 @@ class GeocodeLibTest extends MyCakeTestCase {
 
 	}
 
+	public function testWithJson() {
+		$this->Geocode->setOptions(array('output' => 'json'));
+		$address = '74523 Deutschland';
+		echo '<h2>'.$address.'</h2>';
+		$is = $this->Geocode->geocode($address);
+		$this->assertTrue($is);
+
+		$is = $this->Geocode->getResult();
+		debug($is);
+		$this->assertTrue(!empty($is));
+	}
 
 	public function testSetOptions() {
 		# should be the default

+ 27 - 2
Test/Case/Model/Behavior/BitmaskedBehaviorTest.php

@@ -3,7 +3,7 @@
 App::import('Behavior', 'Tools.Bitmasked');
 App::uses('AppModel', 'Model');
 App::uses('MyCakeTestCase', 'Tools.TestSuite');
-App::uses('MyModel', 'Tools.Lib');
+App::uses('MyModel', 'Tools.Model');
 
 class BitmaskedBehaviorTest extends MyCakeTestCase {
 
@@ -49,7 +49,10 @@ class BitmaskedBehaviorTest extends MyCakeTestCase {
 		$this->Comment->create();
 		$this->Comment->set($data);
 		$res = $this->Comment->validates();
-		$this->assertFalse($res);
+		$this->assertTrue($res);
+
+		$is = $this->Comment->data['BitmaskedComment']['status'];
+		$this->assertSame('0', $is);
 
 		$data = array(
 			'comment' => 'test save',
@@ -60,6 +63,9 @@ class BitmaskedBehaviorTest extends MyCakeTestCase {
 		$res = $this->Comment->validates();
 		$this->assertTrue($res);
 
+		$is = $this->Comment->data['BitmaskedComment']['status'];
+		$this->assertSame(BitmaskedComment::STATUS_PUBLISHED | BitmaskedComment::STATUS_APPROVED, $is);
+
 		# save + find
 
 		$this->Comment->create();
@@ -96,6 +102,25 @@ class BitmaskedBehaviorTest extends MyCakeTestCase {
 		$this->assertEquals($expected, $res['BitmaskedComment']['statuses']);
 	}
 
+	/**
+	 * assert that you can manually trigger "notEmpty" rule with null instead of 0 for "not null" db fields
+	 */
+	public function testSaveWithDefaultValue() {
+		$this->Comment->Behaviors->load('Bitmasked', array('mappedField'=>'statuses', 'defaultValue' => ''));
+		$data = array(
+			'comment' => 'test save',
+			'statuses' => array(),
+		);
+		$this->Comment->create();
+		$this->Comment->set($data);
+		$res = $this->Comment->validates();
+		debug($this->Comment->data); ob_flush();
+		$this->assertFalse($res);
+
+		$is = $this->Comment->data['BitmaskedComment']['status'];
+		$this->assertSame('', $is);
+	}
+
 	public function testIs() {
 		$res = $this->Comment->isBit(BitmaskedComment::STATUS_PUBLISHED);
 		$expected = array('BitmaskedComment.status' => 2);