浏览代码

Merge remote-tracking branch 'origin/master' into cake5

# Conflicts:
#	docs/Helper/Format.md
#	src/Model/Entity/Token.php
#	src/View/Helper/FormatHelper.php
mscherer 2 年之前
父节点
当前提交
a8abcfc7d9

+ 4 - 1
docs/Behavior/AfterSave.md

@@ -1,11 +1,14 @@
 # AfterSave Behavior
 # AfterSave Behavior
 
 
-A CakePHP behavior to allow the entity to be available inside afterSave() callback.
+A CakePHP behavior to allow the entity to be available inside afterSave() callback in the state
+it was before the save.
 
 
 ## Introduction
 ## Introduction
 It takes a clone of the entity from beforeSave(). This allows all the
 It takes a clone of the entity from beforeSave(). This allows all the
 info on it to be available in the afterSave() callback or from the outside without resetting (dirty, ...).
 info on it to be available in the afterSave() callback or from the outside without resetting (dirty, ...).
 
 
+This can be useful if one wants to compare what fields got changed, or e.g. for logging the diff.
+
 ### Technical limitation
 ### Technical limitation
 Make sure you do not further modify the entity in the table's beforeSave() then. As this would
 Make sure you do not further modify the entity in the table's beforeSave() then. As this would
 not be part of the cloned and stored entity here.
 not be part of the cloned and stored entity here.

+ 2 - 3
docs/Behavior/Jsonable.md

@@ -12,8 +12,7 @@ Using 3.5+ you might not even need this anymore, as you can use type classes dir
      *
      *
      * @return \Cake\Database\Schema\TableSchema
      * @return \Cake\Database\Schema\TableSchema
      */
      */
-    protected function _initializeSchema(TableSchema $schema)
-    {
+    protected function _initializeSchema(TableSchema $schema) {
         $schema->columnType('my_field', 'json');
         $schema->columnType('my_field', 'json');
 
 
         return $schema;
         return $schema;
@@ -22,7 +21,7 @@ Using 3.5+ you might not even need this anymore, as you can use type classes dir
 This is best combined with the Shim.Json type, as it properly handles `null` values:
 This is best combined with the Shim.Json type, as it properly handles `null` values:
 ```php
 ```php
 // in bootstrap
 // in bootstrap
-Type::map('json', 'Shim\Database\Type\JsonType');
+TypeFactory::map('json', 'Shim\Database\Type\JsonType');
 ```
 ```
 
 
 But if you still need/want more flexible approaches, continue reading.
 But if you still need/want more flexible approaches, continue reading.

+ 9 - 0
docs/Helper/Icon.md

@@ -141,6 +141,15 @@ foreach ($icons as $iconSet => $list) {
 }
 }
 ```
 ```
 
 
+## Configuration
+
+You can enable `checkExistence` to ensure each icon exists or otherwise throws a warning in logs:
+```php
+'Icon' => [
+    'checkExistence' => true,
+    ...
+],
+```
 
 
 ## Tips
 ## Tips
 
 

+ 1 - 1
src/Model/Entity/Token.php

@@ -4,7 +4,7 @@ namespace Tools\Model\Entity;
 
 
 /**
 /**
  * @property int $id
  * @property int $id
- * @property int $user_id
+ * @property int|null $user_id
  * @property string $type
  * @property string $type
  * @property string $token_key
  * @property string $token_key
  * @property string $content
  * @property string $content

+ 19 - 5
src/Model/Table/Table.php

@@ -63,17 +63,31 @@ class Table extends ShimTable {
 	 * the same site_id. Scoping will only be used if the scoping field is present in
 	 * the same site_id. Scoping will only be used if the scoping field is present in
 	 * the data to be validated.
 	 * the data to be validated.
 	 *
 	 *
-	 * @override To allow multiple scoped values
+	 * @override To allow multiple scoped fields with NULL values.
 	 *
 	 *
 	 * @param mixed $value The value of column to be checked for uniqueness
 	 * @param mixed $value The value of column to be checked for uniqueness
 	 * @param array<string, mixed> $options The options array, optionally containing the 'scope' key
 	 * @param array<string, mixed> $options The options array, optionally containing the 'scope' key
-	 * @param array $context The validation context as provided by the validation routine
+	 * @param array|null $context The validation context as provided by the validation routine
 	 * @return bool true if the value is unique
 	 * @return bool true if the value is unique
 	 */
 	 */
-	public function validateUniqueExt($value, array $options, array $context = []) {
-		$context += $options;
+	public function validateUniqueExt($value, array $options, ?array $context = null) {
+		$data = $context['data'] ?? null;
+		if ($data) {
+			foreach ($data as $field => $value) {
+				if (empty($options['scope']) || !in_array($field, $options['scope'], true)) {
+					continue;
+				}
+
+				if ($value !== '') {
+					continue;
+				}
+
+				$data[$field] = null;
+			}
+			$context['data'] = $data;
+		}
 
 
-		return parent::validateUnique($value, $context);
+		return parent::validateUnique($value, $options, $context);
 	}
 	}
 
 
 	/**
 	/**

+ 9 - 3
src/Model/Table/TokensTable.php

@@ -13,12 +13,18 @@ use RuntimeException;
  * @author Mark Scherer
  * @author Mark Scherer
  * @license http://opensource.org/licenses/mit-license.php MIT
  * @license http://opensource.org/licenses/mit-license.php MIT
  * @method \Tools\Model\Entity\Token get($primaryKey, $options = [])
  * @method \Tools\Model\Entity\Token get($primaryKey, $options = [])
- * @method \Tools\Model\Entity\Token newEntity($data = null, array $options = [])
+ * @method \Tools\Model\Entity\Token newEntity(array $data, array $options = [])
  * @method array<\Tools\Model\Entity\Token> newEntities(array $data, array $options = [])
  * @method array<\Tools\Model\Entity\Token> newEntities(array $data, array $options = [])
  * @method \Tools\Model\Entity\Token|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
  * @method \Tools\Model\Entity\Token|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
  * @method \Tools\Model\Entity\Token patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
  * @method \Tools\Model\Entity\Token patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
- * @method array<\Tools\Model\Entity\Token> patchEntities($entities, array $data, array $options = [])
- * @method \Tools\Model\Entity\Token findOrCreate($search, callable $callback = null, $options = [])
+ * @method array<\Tools\Model\Entity\Token> patchEntities(iterable $entities, array $data, array $options = [])
+ * @method \Tools\Model\Entity\Token findOrCreate($search, ?callable $callback = null, $options = [])
+ * @method \Tools\Model\Entity\Token newEmptyEntity()
+ * @method \Tools\Model\Entity\Token saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
+ * @method \Cake\Datasource\ResultSetInterface<\Tools\Model\Entity\Token>|false saveMany(iterable $entities, $options = [])
+ * @method \Cake\Datasource\ResultSetInterface<\Tools\Model\Entity\Token> saveManyOrFail(iterable $entities, $options = [])
+ * @method \Cake\Datasource\ResultSetInterface<\Tools\Model\Entity\Token>|false deleteMany(iterable $entities, $options = [])
+ * @method \Cake\Datasource\ResultSetInterface<\Tools\Model\Entity\Token> deleteManyOrFail(iterable $entities, $options = [])
  */
  */
 class TokensTable extends Table {
 class TokensTable extends Table {
 
 

+ 0 - 68
src/Utility/GitterLog.php

@@ -1,68 +0,0 @@
-<?php
-
-namespace Tools\Utility;
-
-use Cake\Core\Configure;
-use Cake\Http\Client;
-use InvalidArgumentException;
-use Psr\Log\LogLevel;
-
-/**
- * Wrapper class to log data into Gitter API.
- *
- * e.g simple post: curl -d message=hello your_url
- * e.g error levels: curl -d message=oops -d level=error your_url
- * e.g markdown: curl --data-urlencode "message=_markdown_ is fun" your_url
- *
- * Uses {@link \Cake\Http\Client} to make the API call.
- */
-class GitterLog {
-
-	/**
-	 * @var string
-	 */
-	protected const URL = 'https://webhooks.gitter.im/e/%s';
-
-	/**
-	 * @param string $message
-	 * @param string|null $level
-	 *
-	 * @return void
-	 */
-	public function write(string $message, ?string $level = null): void {
-		$url = sprintf(static::URL, Configure::readOrFail('Gitter.key'));
-
-		$data = [
-			'message' => $message,
-		];
-		if ($level !== null) {
-			$levelString = $this->levelString($level);
-			$data['level'] = $levelString;
-		}
-
-		$options = [];
-		$client = $this->getClient();
-		$client->post($url, $data, $options);
-	}
-
-	/**
-	 * @return \Cake\Http\Client
-	 */
-	protected function getClient(): Client {
-		return new Client();
-	}
-
-	/**
-	 * @param string $level
-	 *
-	 * @return string
-	 */
-	protected function levelString(string $level): string {
-		if (!in_array($level, [LogLevel::ERROR, LogLevel::INFO], true)) {
-			throw new InvalidArgumentException('Only levels `info` and `error`are allowed.');
-		}
-
-		return $level;
-	}
-
-}

+ 36 - 9
src/Utility/Utility.php

@@ -2,6 +2,7 @@
 
 
 namespace Tools\Utility;
 namespace Tools\Utility;
 
 
+use Cake\Core\Configure;
 use Cake\Log\Log;
 use Cake\Log\Log;
 use Cake\Routing\Router;
 use Cake\Routing\Router;
 use Cake\Utility\Hash;
 use Cake\Utility\Hash;
@@ -193,18 +194,18 @@ class Utility {
 	}
 	}
 
 
 	/**
 	/**
-	 * Remove unnessary stuff + add http:// for external urls
-	 * TODO: protocol to lower!
+	 * Remove unnecessary stuff + add http:// for external urls
 	 *
 	 *
 	 * @param string $url
 	 * @param string $url
 	 * @param bool $headerRedirect
 	 * @param bool $headerRedirect
+	 * @param bool|null $detectHttps
 	 * @return string Cleaned Url
 	 * @return string Cleaned Url
 	 */
 	 */
-	public static function cleanUrl($url, $headerRedirect = false) {
+	public static function cleanUrl($url, $headerRedirect = false, $detectHttps = null) {
 		if ($url === '' || $url === 'http://' || $url === 'http://www' || $url === 'http://www.') {
 		if ($url === '' || $url === 'http://' || $url === 'http://www' || $url === 'http://www.') {
 			$url = '';
 			$url = '';
 		} else {
 		} else {
-			$url = static::autoPrefixUrl($url, 'http://');
+			$url = static::autoPrefixUrl($url, 'http://', $detectHttps);
 		}
 		}
 
 
 		if ($headerRedirect && !empty($url)) {
 		if ($headerRedirect && !empty($url)) {
@@ -274,10 +275,27 @@ class Utility {
 	 * So if you check on strpos(http) === 0 you can use this
 	 * So if you check on strpos(http) === 0 you can use this
 	 * to check for URLs instead.
 	 * to check for URLs instead.
 	 *
 	 *
-	 * @param string $url Absolute URL
+	 * @param string $url Absolute URL.
+	 * @param array $statusCodes List of accepted status codes. Defaults to 200 OK.
 	 * @return bool Success
 	 * @return bool Success
 	 */
 	 */
-	public static function urlExists($url) {
+	public static function urlExists($url, array $statusCodes = []) {
+		if (function_exists('curl_init')) {
+			$curl = curl_init($url);
+			curl_setopt($curl, CURLOPT_NOBODY, true);
+			$result = curl_exec($curl);
+			if ($result === false) {
+				return false;
+			}
+
+			$statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+			if ($statusCodes === []) {
+				$statusCodes = [200];
+			}
+
+			return in_array($statusCode, $statusCodes, true);
+		}
+
 		// @codingStandardsIgnoreStart
 		// @codingStandardsIgnoreStart
 		$headers = @get_headers($url);
 		$headers = @get_headers($url);
 		// @codingStandardsIgnoreEnd
 		// @codingStandardsIgnoreEnd
@@ -344,21 +362,30 @@ class Utility {
 	 *
 	 *
 	 * @param string $url
 	 * @param string $url
 	 * @param string|null $prefix
 	 * @param string|null $prefix
+	 * @param bool|null $detectHttps
 	 * @return string
 	 * @return string
 	 */
 	 */
-	public static function autoPrefixUrl($url, $prefix = null) {
+	public static function autoPrefixUrl($url, $prefix = null, $detectHttps = null) {
 		if ($prefix === null) {
 		if ($prefix === null) {
 			$prefix = 'http://';
 			$prefix = 'http://';
 		}
 		}
 
 
+		$modifiedUrl = $url;
 		$pos = strpos($url, '.');
 		$pos = strpos($url, '.');
 		if ($pos !== false) {
 		if ($pos !== false) {
 			if (strpos(substr($url, 0, $pos), '//') === false) {
 			if (strpos(substr($url, 0, $pos), '//') === false) {
-				$url = $prefix . $url;
+				$modifiedUrl = $prefix . $url;
+			}
+
+			if ($detectHttps === null) {
+				$detectHttps = !Configure::read('debug') || PHP_SAPI !== 'cli';
+			}
+			if ($prefix === 'http://' && $detectHttps && static::urlExists('https://' . $url)) {
+				$modifiedUrl = 'https://' . $url;
 			}
 			}
 		}
 		}
 
 
-		return $url;
+		return $modifiedUrl;
 	}
 	}
 
 
 	/**
 	/**

+ 1 - 0
src/View/Helper/FormatHelper.php

@@ -65,6 +65,7 @@ class FormatHelper extends Helper {
 			'ok' => '<span class="ok-{{type}}" style="color:{{color}}"{{attributes}}>{{content}}</span>',
 			'ok' => '<span class="ok-{{type}}" style="color:{{color}}"{{attributes}}>{{content}}</span>',
 		],
 		],
 		'slugger' => null,
 		'slugger' => null,
+		'iconHelper' => false, // FC with new Icon helper
 	];
 	];
 
 
 	/**
 	/**

+ 25 - 0
src/View/Icon/IconCollection.php

@@ -26,6 +26,11 @@ class IconCollection {
 	protected array $iconSets = [];
 	protected array $iconSets = [];
 
 
 	/**
 	/**
+	 * @var array|null
+	 */
+	protected $names;
+
+	/**
 	 * @param array<string, mixed> $config
 	 * @param array<string, mixed> $config
 	 */
 	 */
 	public function __construct(array $config = []) {
 	public function __construct(array $config = []) {
@@ -67,6 +72,10 @@ class IconCollection {
 	 * @return array<string, array<string>>
 	 * @return array<string, array<string>>
 	 */
 	 */
 	public function names(): array {
 	public function names(): array {
+		if ($this->names !== null) {
+			return $this->names;
+		}
+
 		$names = [];
 		$names = [];
 		foreach ($this->iconSets as $name => $set) {
 		foreach ($this->iconSets as $name => $set) {
 			$iconNames = $set->names();
 			$iconNames = $set->names();
@@ -74,6 +83,7 @@ class IconCollection {
 		}
 		}
 
 
 		ksort($names);
 		ksort($names);
+		$this->names = $names;
 
 
 		return $names;
 		return $names;
 	}
 	}
@@ -119,8 +129,23 @@ class IconCollection {
 		}
 		}
 
 
 		unset($options['attributes']);
 		unset($options['attributes']);
+		if ($this->getConfig('checkExistence') && !$this->exists($icon, $set)) {
+			trigger_error(sprintf('Icon `%s` does not exist', $set . ':' . $icon), E_USER_WARNING);
+		}
 
 
 		return $this->iconSets[$set]->render($icon, $options, $attributes);
 		return $this->iconSets[$set]->render($icon, $options, $attributes);
 	}
 	}
 
 
+	/**
+	 * @param string $icon
+	 * @param string $set
+	 *
+	 * @return bool
+	 */
+	protected function exists(string $icon, string $set): bool {
+		$names = $this->names();
+
+		return !empty($names[$set]) && in_array($icon, $names[$set], true);
+	}
+
 }
 }

+ 2 - 1
tests/TestCase/Controller/Component/MobileComponentTest.php

@@ -79,7 +79,8 @@ class MobileComponentTest extends TestCase {
 
 
 		$this->Controller->setRequest($this->Controller->getRequest()->withEnv('HTTP_ACCEPT', 'text/vnd.wap.wml,text/html,text/plain,image/png,*/*'));
 		$this->Controller->setRequest($this->Controller->getRequest()->withEnv('HTTP_ACCEPT', 'text/vnd.wap.wml,text/html,text/plain,image/png,*/*'));
 		$is = $this->Controller->Mobile->detect();
 		$is = $this->Controller->Mobile->detect();
-		$this->assertTrue($is);
+		//FIXME
+		//$this->assertTrue($is);
 	}
 	}
 
 
 	/**
 	/**

+ 1 - 1
tests/TestCase/Utility/DateTimeTest.php

@@ -215,7 +215,7 @@ class DateTimeTest extends TestCase {
 	public function testLocalDate() {
 	public function testLocalDate() {
 		$values = [
 		$values = [
 			['2009-12-01 00:00:00', 'd.m.Y', '01.12.2009'],
 			['2009-12-01 00:00:00', 'd.m.Y', '01.12.2009'],
-			['2009-12-01 00:00:00', 'M', 'Dez.'],
+			//['2009-12-01 00:00:00', 'M', 'Dez.'],
 		];
 		];
 		foreach ($values as $v) {
 		foreach ($values as $v) {
 			$ret = $this->Time->localDate($v[0], $v[1], ['language' => 'de']);
 			$ret = $this->Time->localDate($v[0], $v[1], ['language' => 'de']);

+ 0 - 55
tests/TestCase/Utility/GitterLogTest.php

@@ -1,55 +0,0 @@
-<?php
-
-namespace Tools\Test\TestCase\Utility;
-
-use Cake\Core\Configure;
-use Cake\Http\Client;
-use Cake\Http\Client\Request;
-use Psr\Log\LogLevel;
-use Shim\TestSuite\TestCase;
-use Tools\Utility\GitterLog;
-
-/**
- * GitterLogTest class
- */
-class GitterLogTest extends TestCase {
-
-	/**
-	 * @return void
-	 */
-	public function setUp(): void {
-		parent::setUp();
-
-		$key = env('GITTER_KEY') ?: '123';
-		Configure::write('Gitter.key', $key);
-	}
-
-	/**
-	 * @return void
-	 */
-	public function tearDown(): void {
-		parent::tearDown();
-
-		Configure::delete('Gitter.key');
-	}
-
-	/**
-	 * testLogsIntoDefaultFile method
-	 *
-	 * @return void
-	 */
-	public function testLogsIntoDefaultFile(): void {
-		$mockClient = $this->getMockBuilder(Client::class)->onlyMethods(['send'])->getMock();
-
-		$callback = function(Request $value) {
-			return (string)$value->getBody() === 'message=Test%3A+It+%2Aworks%2A+with+some+error+%5Bmarkup%5D%28https%3A%2F%2Fmy-url.com%29%21&level=error';
-		};
-		$mockClient->expects($this->once())->method('send')->with($this->callback($callback));
-
-		$gitterLog = $this->getMockBuilder(GitterLog::class)->onlyMethods(['getClient'])->getMock();
-		$gitterLog->expects($this->once())->method('getClient')->willReturn($mockClient);
-
-		$gitterLog->write('Test: It *works* with some error [markup](https://my-url.com)!', LogLevel::ERROR);
-	}
-
-}

+ 9 - 0
tests/TestCase/Utility/UtilityTest.php

@@ -264,6 +264,15 @@ class UtilityTest extends TestCase {
 	}
 	}
 
 
 	/**
 	/**
+	 * @covers ::autoPrefixUrl
+	 * @return void
+	 */
+	public function testAutoPrefixUrlWithDetection() {
+		$res = Utility::autoPrefixUrl('www.spiegel.de', null, true);
+		$this->assertSame('https://www.spiegel.de', $res);
+	}
+
+	/**
 	 * @covers ::cleanUrl
 	 * @covers ::cleanUrl
 	 * @return void
 	 * @return void
 	 */
 	 */