Implemented post uploads (closed #11)

This commit is contained in:
Marcin Kurczewski 2014-09-15 11:38:24 +02:00
parent bb7b1f3321
commit bc8e1b05a6
37 changed files with 1159 additions and 89 deletions

1
data/.gitignore vendored
View file

@ -2,3 +2,4 @@ db.sqlite
executed_upgrades.txt
local.ini
thumbnails
posts

View file

@ -38,6 +38,7 @@ listSafePosts = anonymous, regularUser, powerUser, moderator, administ
listSketchyPosts = anonymous, regularUser, powerUser, moderator, administrator
listUnsafePosts = anonymous, regularUser, powerUser, moderator, administrator
uploadPosts = regularUser, powerUser, moderator, administrator
uploadPostsAnonymously = regularUser, powerUser, moderator, administrator
listTags = anonymous, regularUser, powerUser, moderator, administrator

View file

@ -51,19 +51,25 @@ input[type=button] {
background: #eee;
font-family: 'Droid Sans', sans-serif;
font-size: 17px;
}
button:not(:disabled),
input[type=button]:not(:disabled) {
cursor: pointer;
}
button:hover,
input[type=button]:hover {
button:not(:disabled):hover,
input[type=button]:not(:disabled):hover {
background: #f5f5f5;
}
button:disabled {
color: gray;
}
button.highlight,
input[type=button].highlight {
background: #ad5;
}
button:hover.highlight,
input[type=button]:hover.highlight {
button:not(:disabled):hover.highlight,
input[type=button]:not(:disabled):hover.highlight {
background: #dfa;
}

View file

@ -22,6 +22,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
listSketchyPosts: 'listSketchyPosts',
listUnsafePosts: 'listUnsafePosts',
uploadPosts: 'uploadPosts',
uploadPostsAnonymously: 'uploadPostsAnonymously',
listTags: 'listTags',
};

View file

@ -11,6 +11,7 @@ App.Controls.TagInput = function(
var KEY_RETURN = 13;
var KEY_SPACE = 32;
var KEY_BACKSPACE = 8;
var tagConfirmKeys = [KEY_RETURN, KEY_SPACE];
var tags = [];
var options = {
@ -54,7 +55,7 @@ App.Controls.TagInput = function(
});
$input.unbind('keydown').bind('keydown', function(e) {
if (e.which === KEY_RETURN || e.which === KEY_SPACE) {
if (_.contains(tagConfirmKeys, e.which)) {
e.preventDefault();
var tag = $input.val();
addTag(tag);
@ -128,11 +129,16 @@ App.Controls.TagInput = function(
return tags;
}
function focus() {
$input.focus();
}
_.extend(options, {
setTags: setTags,
getTags: getTags,
removeTag: removeTag,
addTag: addTag,
focus: focus,
});
return options;
};

View file

@ -7,6 +7,7 @@ App.Presenters.PostUploadPresenter = function(
mousetrap,
promise,
util,
auth,
api,
router,
topNavigationPresenter,
@ -30,7 +31,9 @@ App.Presenters.PostUploadPresenter = function(
}
function render() {
$el.html(template());
$el.html(template({
canUploadPostsAnonymously: auth.hasPrivilege(auth.privileges.uploadPostsAnonymously)
}));
$messages = $el.find('.messages');
tagInput = new App.Controls.TagInput($el.find('form [name=tags]'), _, jQuery);
@ -61,10 +64,10 @@ App.Presenters.PostUploadPresenter = function(
return {
safety: 'safe',
source: null,
fileName: null,
anonymous: false,
tags: [],
fileName: null,
content: null,
url: null,
thumbnail: null,
@ -273,7 +276,9 @@ App.Presenters.PostUploadPresenter = function(
hidePostEditForm();
} else {
showPostEditForm(selectedPosts);
tagInput.focus();
}
$el.find('.post-table-op').prop('disabled', selectedPosts.length === 0);
}
function hidePostEditForm() {
@ -292,7 +297,7 @@ App.Presenters.PostUploadPresenter = function(
} else {
var post = selectedPosts[0];
$postEditForm.parent('.form-slider').find('.thumbnail').slideDown();
$postEditForm.find('.file-name strong').text(post.fileName);
$postEditForm.find('.file-name strong').text(post.fileName || post.url);
updatePostThumbnailInForm(post);
}
@ -488,6 +493,7 @@ App.Presenters.PostUploadPresenter = function(
function uploadNextPost() {
messagePresenter.hideMessages($messages);
messagePresenter.showInfo($messages, 'Uploading in progress…');
var posts = getAllPosts();
if (posts.length === 0) {
@ -503,14 +509,16 @@ App.Presenters.PostUploadPresenter = function(
if (post.url) {
formData.url = post.url;
} else {
formData.file = post.content;
formData.content = post.content;
formData.contentFileName = post.fileName;
}
formData.source = post.source;
formData.safety = post.safety;
formData.anonymous = post.anonymous;
formData.tags = post.tags.join(', ');
formData.anonymous = (post.anonymous | 0);
formData.tags = post.tags.join(' ');
if (post.tags.length === 0) {
messagePresenter.hideMessages($messages);
messagePresenter.showError($messages, 'No tags set.');
interactionEnabled = true;
return;
@ -519,9 +527,12 @@ App.Presenters.PostUploadPresenter = function(
promise.wait(api.post('/posts', formData)).then(function(response) {
$row.slideUp(function(response) {
$row.remove();
posts.shift();
setAllPosts(posts);
uploadNextPost();
});
}).fail(function(response) {
messagePresenter.hideMessages($messages);
messagePresenter.showError($messages, response.json && response.json.error || response);
interactionEnabled = true;
});
@ -536,7 +547,6 @@ App.Presenters.PostUploadPresenter = function(
$el.find('tbody input[type=checkbox]').prop('checked', false);
postTableCheckboxesChanged();
messagePresenter.showInfo($messages, 'Uploading in progress…');
interactionEnabled = false;
uploadNextPost();
@ -549,4 +559,4 @@ App.Presenters.PostUploadPresenter = function(
};
App.DI.register('postUploadPresenter', ['_', 'jQuery', 'mousetrap', 'promise', 'util', 'api', 'router', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.PostUploadPresenter);
App.DI.register('postUploadPresenter', ['_', 'jQuery', 'mousetrap', 'promise', 'util', 'auth', 'api', 'router', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.PostUploadPresenter);

View file

@ -47,13 +47,13 @@
<ul class="operations"><!--
--><li>
<button class="remove"><i class="fa fa-remove"></i> Remove</button>
<button class="post-table-op remove"><i class="fa fa-remove"></i> Remove</button>
</li><!--
--><li>
<button class="move-up"><i class="fa fa-chevron-up"></i> Move up</button>
<button class="post-table-op move-up"><i class="fa fa-chevron-up"></i> Move up</button>
</li><!--
--><li>
<button class="move-down"><i class="fa fa-chevron-down"></i> Move down</button>
<button class="post-table-op move-down"><i class="fa fa-chevron-down"></i> Move down</button>
</li><!--
--><li class="right">
<button class="submit highlight" type="submit"><i class="fa fa-upload"></i> Submit</button>
@ -112,6 +112,7 @@
</div>
</div>
<% if (canUploadPostsAnonymously) { %>
<div class="form-row">
<label class="form-label" for="post-anonymous">Anonymity:</label>
<div class="form-input">
@ -121,6 +122,7 @@
</label>
</div>
</div>
<% } %>
</form>
</div>

View file

@ -0,0 +1,41 @@
<?php
namespace Szurubooru\Controllers;
final class PostController extends AbstractController
{
private $privilegeService;
private $postService;
private $inputReader;
private $postViewProxy;
public function __construct(
\Szurubooru\Services\PrivilegeService $privilegeService,
\Szurubooru\Services\PostService $postService,
\Szurubooru\Helpers\InputReader $inputReader,
\Szurubooru\Controllers\ViewProxies\PostViewProxy $postViewProxy)
{
$this->privilegeService = $privilegeService;
$this->postService = $postService;
$this->inputReader = $inputReader;
$this->postViewProxy = $postViewProxy;
}
public function registerRoutes(\Szurubooru\Router $router)
{
$router->post('/api/posts', [$this, 'createPost']);
}
public function createPost()
{
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::UPLOAD_POSTS);
$formData = new \Szurubooru\FormData\UploadFormData($this->inputReader);
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::UPLOAD_POSTS);
if ($formData->anonymous)
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::UPLOAD_POSTS_ANONYMOUSLY);
$post = $this->postService->createPost($formData);
return $this->postViewProxy->fromEntity($post);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Szurubooru\Controllers\ViewProxies;
class PostViewProxy extends AbstractViewProxy
{
public function fromEntity($post)
{
$result = new \StdClass;
if ($post)
{
$result->id = $post->getId();
$result->name = $post->getName();
$result->userId = $post->getUserId();
$result->uploadTime = $post->getUploadTime();
$result->lastEditTime = $post->getLastEditTime();
$result->safety = \Szurubooru\Helpers\EnumHelper::postSafetyToString($post->getSafety());
$result->contentType = \Szurubooru\Helpers\EnumHelper::postTypeToString($post->getContentType());
$result->contentChecksum = $post->getContentChecksum();
$result->source = $post->getSource();
$result->imageWidth = $post->getImageWidth();
$result->imageHeight = $post->getImageHeight();
$result->tags = $post->getTags();
$result->originalFileSize = $post->getOriginalFileSize();
}
return $result;
}
}

View file

@ -32,17 +32,15 @@ abstract class AbstractDao implements ICrudDao
public function save(&$entity)
{
$arrayEntity = $this->entityConverter->toArray($entity);
if ($entity->getId())
{
$this->fpdo->update($this->tableName)->set($arrayEntity)->where('id', $entity->getId())->execute();
$entity = $this->update($entity);
}
else
{
$this->fpdo->insertInto($this->tableName)->values($arrayEntity)->execute();
$arrayEntity['id'] = $this->pdo->lastInsertId();
$entity = $this->create($entity);
}
$entity = $this->entityConverter->toEntity($arrayEntity);
$this->afterSave($entity);
return $entity;
}
@ -53,6 +51,7 @@ abstract class AbstractDao implements ICrudDao
foreach ($query as $arrayEntity)
{
$entity = $this->entityConverter->toEntity($arrayEntity);
$this->afterLoad($entity);
$entities[$entity->getId()] = $entity;
}
return $entities;
@ -73,6 +72,21 @@ abstract class AbstractDao implements ICrudDao
return $this->deleteBy('id', $entityId);
}
protected function update(\Szurubooru\Entities\Entity $entity)
{
$arrayEntity = $this->entityConverter->toArray($entity);
$this->fpdo->update($this->tableName)->set($arrayEntity)->where('id', $entity->getId())->execute();
return $entity;
}
protected function create(\Szurubooru\Entities\Entity $entity)
{
$arrayEntity = $this->entityConverter->toArray($entity);
$this->fpdo->insertInto($this->tableName)->values($arrayEntity)->execute();
$entity->setId(intval($this->pdo->lastInsertId()));
return $entity;
}
protected function hasAnyRecords()
{
return count(iterator_to_array($this->fpdo->from($this->tableName)->limit(1))) > 0;
@ -81,11 +95,24 @@ abstract class AbstractDao implements ICrudDao
protected function findOneBy($columnName, $value)
{
$arrayEntity = iterator_to_array($this->fpdo->from($this->tableName)->where($columnName, $value));
return $arrayEntity ? $this->entityConverter->toEntity($arrayEntity[0]) : null;
if (!$arrayEntity)
return null;
$entity = $this->entityConverter->toEntity($arrayEntity[0]);
$this->afterLoad($entity);
return $entity;
}
protected function deleteBy($columnName, $value)
{
$this->fpdo->deleteFrom($this->tableName)->where($columnName, $value)->execute();
}
protected function afterLoad(\Szurubooru\Entities\Entity $entity)
{
}
protected function afterSave(\Szurubooru\Entities\Entity $entity)
{
}
}

View file

@ -9,13 +9,35 @@ class PostEntityConverter implements IEntityConverter
[
'id' => $entity->getId(),
'name' => $entity->getName(),
'userId' => $entity->getUserId(),
'uploadTime' => $entity->getUploadTime(),
'lastEditTime' => $entity->getLastEditTime(),
'safety' => $entity->getSafety(),
'contentType' => $entity->getContentType(),
'contentChecksum' => $entity->getContentChecksum(),
'source' => $entity->getSource(),
'imageWidth' => $entity->getImageWidth(),
'imageHeight' => $entity->getImageHeight(),
'originalFileSize' => $entity->getOriginalFileSize(),
'originalFileName' => $entity->getOriginalFileName(),
];
}
public function toEntity(array $array)
{
$entity = new \Szurubooru\Entities\Post($array['id']);
$entity = new \Szurubooru\Entities\Post(intval($array['id']));
$entity->setName($array['name']);
$entity->setUserId($array['userId']);
$entity->setUploadTime($array['uploadTime']);
$entity->setLastEditTime($array['lastEditTime']);
$entity->setSafety(intval($array['safety']));
$entity->setContentType(intval($array['contentType']));
$entity->setContentChecksum($array['contentChecksum']);
$entity->setSource($array['source']);
$entity->setImageWidth($array['imageWidth']);
$entity->setImageHeight($array['imageHeight']);
$entity->setOriginalFileSize($array['originalFileSize']);
$entity->setOriginalFileName($array['originalFileName']);
return $entity;
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Szurubooru\Dao\EntityConverters;
class TagEntityConverter implements IEntityConverter
{
public function toArray(\Szurubooru\Entities\Entity $entity)
{
return
[
'name' => $entity->getName(),
];
}
public function toEntity(array $array)
{
$entity = new \Szurubooru\Entities\Tag($array['name']);
$entity->setName($array['name']);
return $entity;
}
}

View file

@ -1,14 +1,82 @@
<?php
namespace Szurubooru\Dao;
final class PostDao extends AbstractDao implements ICrudDao
class PostDao extends AbstractDao implements ICrudDao
{
public function __construct(
\Szurubooru\DatabaseConnection $databaseConnection)
public function __construct(\Szurubooru\DatabaseConnection $databaseConnection)
{
parent::__construct(
$databaseConnection,
'posts',
new \Szurubooru\Dao\EntityConverters\PostEntityConverter());
}
public function findByName($name)
{
return $this->findOneBy('name', $name);
}
public function findByContentChecksum($checksum)
{
return $this->findOneBy('contentChecksum', $checksum);
}
protected function afterLoad(\Szurubooru\Entities\Entity $entity)
{
$entity->setLazyLoader('tags', function(\Szurubooru\Entities\Post $post)
{
return $this->getTags($post);
});
}
protected function afterSave(\Szurubooru\Entities\Entity $entity)
{
$this->syncTags($entity->getId(), $entity->getTags());
}
private function getTags(\Szurubooru\Entities\Post $post)
{
$postId = $post->getId();
$result = [];
$query = $this->fpdo->from('postTags')->where('postId', $postId)->select('tagName');
foreach ($query as $arrayEntity)
$result[] = $arrayEntity['tagName'];
return $result;
}
private function syncTags($postId, array $tags)
{
$existingTags = iterator_to_array($this->fpdo->from('postTags')->where('postId', $postId));
$tagRelationsToInsert = array_diff($tags, $existingTags);
$tagRelationsToDelete = array_diff($existingTags, $tags);
$this->createMissingTags($tags);
foreach ($tagRelationsToInsert as $tag)
{
$this->fpdo->insertInto('postTags')->values(['postId' => $postId, 'tagName' => $tag])->execute();
}
foreach ($tagRelationsToDelete as $tag)
{
$this->fpdo->deleteFrom('postTags')->where('postId', $postId)->and('tagName', $tag)->execute();
}
}
private function createMissingTags(array $tags)
{
if (empty($tags))
return;
$tagsNotToCreate = array_map(
function($arrayEntity)
{
return $arrayEntity['name'];
},
iterator_to_array($this->fpdo->from('tags')->where('name', $tags)));
$tagsToCreate = array_diff($tags, $tagsNotToCreate);
foreach ($tagsToCreate as $tag)
{
$this->fpdo->insertInto('tags')->values(['name' => $tag])->execute();
}
}
}

13
src/Dao/TagDao.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace Szurubooru\Dao;
class TagDao extends AbstractDao implements ICrudDao
{
public function __construct(\Szurubooru\DatabaseConnection $databaseConnection)
{
parent::__construct(
$databaseConnection,
'tags',
new \Szurubooru\Dao\EntityConverters\TagEntityConverter());
}
}

View file

@ -4,6 +4,8 @@ namespace Szurubooru\Entities;
abstract class Entity
{
protected $id = null;
private $lazyLoaders = [];
private $lazyContainers = [];
public function __construct($id = null)
{
@ -14,4 +16,42 @@ abstract class Entity
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function resetLazyLoaders()
{
$this->lazyLoaders = [];
}
public function setLazyLoader($lazyContainerName, $getter)
{
$this->lazyLoaders[$lazyContainerName] = $getter;
}
protected function lazyLoad($lazyContainerName, $defaultValue)
{
if (!isset($this->lazyContainers[$lazyContainerName]))
{
if (!isset($this->lazyLoaders[$lazyContainerName]))
{
return $defaultValue;
}
$result = $this->lazyLoaders[$lazyContainerName]($this);
$this->lazySave($lazyContainerName, $result);
}
else
{
$result = $this->lazyContainers[$lazyContainerName];
}
return $result;
}
protected function lazySave($lazyContainerName, $value)
{
$this->lazyContainers[$lazyContainerName] = $value;
}
}

View file

@ -3,7 +3,32 @@ namespace Szurubooru\Entities;
final class Post extends Entity
{
const POST_SAFETY_SAFE = 1;
const POST_SAFETY_SKETCHY = 2;
const POST_SAFETY_UNSAFE = 3;
const POST_TYPE_IMAGE = 1;
const POST_TYPE_FLASH = 2;
const POST_TYPE_VIDEO = 3;
const POST_TYPE_YOUTUBE = 4;
protected $name;
protected $userId;
protected $uploadTime;
protected $lastEditTime;
protected $safety;
protected $contentType;
protected $contentChecksum;
protected $source;
protected $imageWidth;
protected $imageHeight;
protected $originalFileSize;
protected $originalFileName;
public function getIdMarkdown()
{
return '@' . $this->id;
}
public function getName()
{
@ -14,4 +39,136 @@ final class Post extends Entity
{
$this->name = $name;
}
public function getUserId()
{
return $this->userId;
}
public function setUserId($userId)
{
$this->userId = $userId;
}
public function setUser(\Szurubooru\Entities\User $user = null)
{
if ($user)
{
$this->userId = $user->getId();
}
else
{
$this->userId = null;
}
}
public function getSafety()
{
return $this->safety;
}
public function setSafety($safety)
{
$this->safety = $safety;
}
public function getUploadTime()
{
return $this->uploadTime;
}
public function setUploadTime($uploadTime)
{
$this->uploadTime = $uploadTime;
}
public function getLastEditTime()
{
return $this->lastEditTime;
}
public function setLastEditTime($lastEditTime)
{
$this->lastEditTime = $lastEditTime;
}
public function getContentType()
{
return $this->contentType;
}
public function setContentType($contentType)
{
$this->contentType = $contentType;
}
public function getContentChecksum()
{
return $this->contentChecksum;
}
public function setContentChecksum($contentChecksum)
{
$this->contentChecksum = $contentChecksum;
}
public function getSource()
{
return $this->source;
}
public function setSource($source)
{
$this->source = $source;
}
public function getImageWidth()
{
return $this->imageWidth;
}
public function setImageWidth($imageWidth)
{
$this->imageWidth = $imageWidth;
}
public function getImageHeight()
{
return $this->imageHeight;
}
public function setImageHeight($imageHeight)
{
$this->imageHeight = $imageHeight;
}
public function getOriginalFileSize()
{
return $this->originalFileSize;
}
public function setOriginalFileSize($originalFileSize)
{
$this->originalFileSize = $originalFileSize;
}
public function getOriginalFileName()
{
return $this->originalFileName;
}
public function setOriginalFileName($originalFileName)
{
$this->originalFileName = $originalFileName;
}
public function getTags()
{
return $this->lazyLoad('tags', []);
}
public function setTags(array $tags)
{
$this->lazySave('tags', $tags);
}
}

View file

@ -6,6 +6,16 @@ final class Tag extends Entity
protected $name;
protected $usages;
public function getId()
{
return $this->name;
}
public function setId($id)
{
$this->name = $id;
}
public function getName()
{
return $this->name;

View file

@ -0,0 +1,39 @@
<?php
namespace Szurubooru\FormData;
class UploadFormData implements \Szurubooru\IValidatable
{
public $contentFileName;
public $content;
public $url;
public $anonymous;
public $safety;
public $source;
public $tags;
public function __construct($inputReader = null)
{
if ($inputReader !== null)
{
$this->contentFileName = $inputReader->contentFileName;
$this->content = $inputReader->decodeBase64($inputReader->content);
$this->url = $inputReader->url;
$this->anonymous = $inputReader->anonymous;
$this->safety = \Szurubooru\Helpers\EnumHelper::postSafetyFromString($inputReader->safety);
$this->source = $inputReader->source;
$this->tags = preg_split('/[\s+]/', $inputReader->tags);
}
}
public function validate(\Szurubooru\Validator $validator)
{
if ($this->content === null and $this->url === null)
throw new \DomainException('Neither data or URL provided.');
$validator->validatePostTags($this->tags);
if ($this->source !== null)
$validator->validateMaxLength($this->source, 200, 'Source');
}
}

View file

@ -20,6 +20,21 @@ class EnumHelper
'blank' => \Szurubooru\Entities\User::AVATAR_STYLE_BLANK,
];
private static $postSafetyMap =
[
'safe' => \Szurubooru\Entities\Post::POST_SAFETY_SAFE,
'sketchy' => \Szurubooru\Entities\Post::POST_SAFETY_SKETCHY,
'unsafe' => \Szurubooru\Entities\Post::POST_SAFETY_UNSAFE,
];
private static $postTypeMap =
[
'image' => \Szurubooru\Entities\Post::POST_TYPE_IMAGE,
'video' => \Szurubooru\Entities\Post::POST_TYPE_VIDEO,
'flash' => \Szurubooru\Entities\Post::POST_TYPE_FLASH,
'youtube' => \Szurubooru\Entities\Post::POST_TYPE_YOUTUBE,
];
public static function accessRankToString($accessRank)
{
return self::enumToString(self::$accessRankMap, $accessRank);
@ -40,6 +55,21 @@ class EnumHelper
return self::stringToEnum(self::$avatarStyleMap, $avatarStyleString);
}
public static function postSafetyToString($postSafety)
{
return self::enumToString(self::$postSafetyMap, $postSafety);
}
public static function postSafetyFromString($postSafetyString)
{
return self::stringToEnum(self::$postSafetyMap, $postSafetyString);
}
public static function postTypeToString($postType)
{
return self::enumToString(self::$postTypeMap, $postType);
}
private static function enumToString($enumMap, $enumValue)
{
$reverseMap = array_flip($enumMap);
@ -53,7 +83,7 @@ class EnumHelper
{
$key = trim(strtolower($enumString));
if (!isset($enumMap[$key]))
throw new \DomainException('Unrecognized avatar style: ' . $enumString);
throw new \DomainException('Unrecognized value: ' . $enumString);
return $enumMap[$key];
}

View file

@ -0,0 +1,37 @@
<?php
namespace Szurubooru\Helpers;
class MimeHelper
{
public static function getMimeTypeFromFile($path)
{
$finfo = new \finfo(FILEINFO_MIME);
return self::stripCharset($finfo->load($path));
}
public static function getMimeTypeFromBuffer($buffer)
{
$finfo = new \finfo(FILEINFO_MIME);
return self::stripCharset($finfo->buffer($buffer));
}
public static function isFlash($mime)
{
return $mime === 'application/x-shockwave-flash';
}
public static function isVideo($mime)
{
return $mime === 'application/ogg' or preg_match('/video\//', $mime);
}
public static function isImage($mime)
{
return in_array($mime, ['image/jpeg', 'image/png', 'image/gif']);
}
private static function stripCharset($mime)
{
return preg_replace('/;\s*charset.*$/', '', $mime);
}
}

View file

@ -22,6 +22,7 @@ class Privilege
const LIST_SKETCHY_POSTS = 'listSketchyPosts';
const LIST_UNSAFE_POSTS = 'listUnsafePosts';
const UPLOAD_POSTS = 'uploadPosts';
const UPLOAD_POSTS_ANONYMOUSLY = 'uploadPostsAnonymously';
const LIST_TAGS = 'listTags';
}

View file

@ -61,7 +61,7 @@ class FileService
public function createFolders($target)
{
$fullPath = $this->getFullPath($target);
$fullPath = $this->getFullPath(dirname($target));
if (!file_exists($fullPath))
mkdir($fullPath, 0777, true);
}
@ -81,6 +81,7 @@ class FileService
public function save($destination, $data)
{
$this->createFolders($destination);
$finalDestination = $this->getFullPath($destination);
file_put_contents($finalDestination, $data);
}
@ -89,4 +90,42 @@ class FileService
{
return $this->dataDirectory . DIRECTORY_SEPARATOR . $destination;
}
public function download($url, $maxBytes = null)
{
set_time_limit(60);
try
{
$srcHandle = fopen($url, 'rb');
}
catch (Exception $e)
{
throw new \Exception('Cannot open URL for reading: ' . $e->getMessage());
}
if (!$srcHandle)
throw new \Exception('Cannot open URL for reading');
$result = '';
try
{
while (!feof($srcHandle))
{
$buffer = fread($srcHandle, 4 * 1024);
if ($maxBytes !== null and ftell($dstHandle) > $maxBytes)
{
throw new \Exception(
'File is too big (maximum size: %s)',
TextHelper::useBytesUnits($maxBytes));
}
$result .= $buffer;
}
}
finally
{
fclose($srcHandle);
}
return $result;
}
}

View file

@ -0,0 +1,176 @@
<?php
namespace Szurubooru\Services;
class PostService
{
private $config;
private $validator;
private $transactionManager;
private $postDao;
private $fileService;
private $timeService;
private $authService;
public function __construct(
\Szurubooru\Config $config,
\Szurubooru\Validator $validator,
\Szurubooru\Dao\TransactionManager $transactionManager,
\Szurubooru\Dao\PostDao $postDao,
\Szurubooru\Services\AuthService $authService,
\Szurubooru\Services\TimeService $timeService,
\Szurubooru\Services\FileService $fileService)
{
$this->config = $config;
$this->validator = $validator;
$this->transactionManager = $transactionManager;
$this->postDao = $postDao;
$this->fileService = $fileService;
$this->timeService = $timeService;
$this->authService = $authService;
}
public function createPost(\Szurubooru\FormData\UploadFormData $formData)
{
return $this->transactionManager->commit(function() use ($formData)
{
$formData->validate($this->validator);
$post = new \Szurubooru\Entities\Post();
$post->setUploadTime($this->timeService->getCurrentTime());
$post->setLastEditTime($this->timeService->getCurrentTime());
$post->setUser($formData->anonymous ? null : $this->authService->getLoggedInUser());
$post->setOriginalFileName($formData->contentFileName);
$post->setName($this->getUniqueRandomPostName());
$this->updatePostSafety($post, $formData->safety);
$this->updatePostSource($post, $formData->source);
$this->updatePostTags($post, $formData->tags);
$this->updatePostContentFromStringOrUrl($post, $formData->content, $formData->url);
return $this->postDao->save($post);
});
}
private function updatePostSafety(\Szurubooru\Entities\Post $post, $newSafety)
{
$post->setSafety($newSafety);
}
private function updatePostSource(\Szurubooru\Entities\Post $post, $newSource)
{
$post->setSource($newSource);
}
private function updatePostContentFromStringOrUrl(\Szurubooru\Entities\Post $post, $content, $url)
{
if ($url)
$this->updatePostContentFromUrl($post, $url);
else if ($content)
$this->updatePostContentFromString($post, $content);
else
throw new \DomainException('No content specified');
}
private function updatePostContentFromString(\Szurubooru\Entities\Post $post, $content)
{
if (!$content)
throw new \DomainException('File cannot be empty.');
$mime = \Szurubooru\Helpers\MimeHelper::getMimeTypeFromBuffer($content);
if (\Szurubooru\Helpers\MimeHelper::isFlash($mime))
$post->setContentType(\Szurubooru\Entities\Post::POST_TYPE_FLASH);
elseif (\Szurubooru\Helpers\MimeHelper::isImage($mime))
$post->setContentType(\Szurubooru\Entities\Post::POST_TYPE_IMAGE);
elseif (\Szurubooru\Helpers\MimeHelper::isVideo($mime))
$post->setContentType(\Szurubooru\Entities\Post::POST_TYPE_VIDEO);
else
throw new \DomainException('Unhandled file type: "' . $mime . '"');
$post->setContentChecksum(sha1($content));
$this->assertNoPostWithThisContentChecksum($post);
$target = $this->getPostContentPath($post);
$this->fileService->save($target, $content);
$fullPath = $this->fileService->getFullPath($target);
list ($imageWidth, $imageHeight) = getimagesize($fullPath);
$post->setImageWidth($imageWidth);
$post->setImageHeight($imageHeight);
$post->setOriginalFileSize(filesize($fullPath));
}
private function updatePostContentFromUrl(\Szurubooru\Entities\Post $post, $url)
{
if (!preg_match('/^https?:\/\//', $url))
throw new \InvalidArgumentException('Invalid URL "' . $url . '"');
$youtubeId = null;
if (preg_match('/youtube.com\/watch.*?=([a-zA-Z0-9_-]+)/', $url, $matches))
$youtubeId = $matches[1];
if ($youtubeId)
{
$post->setContentType(\Szurubooru\Entities\Post::POST_TYPE_YOUTUBE);
$post->setImageWidth(null);
$post->setImageHeight(null);
$post->setContentChecksum($url);
$post->setOriginalFileName($url);
$post->setOriginalFileSize(null);
$post->setContentChecksum($youtubeId);
$this->assertNoPostWithThisContentChecksum($post);
$this->removeThumbnail($post);
}
else
{
$contents = $this->fileService->download($url);
$this->updatePostContentFromString($post, $contents);
}
}
private function updatePostTags(\Szurubooru\Entities\Post $post, array $newTags)
{
$post->setTags($newTags);
}
private function removeThumbnail(\Szurubooru\Entities\Post $post)
{
//...
//todo: remove thumbnail on upload
}
private function assertNoPostWithThisContentChecksum(\Szurubooru\Entities\Post $parent)
{
$checksumToCheck = $parent->getContentChecksum();
$postWithThisChecksum = $this->postDao->findByContentChecksum($checksumToCheck);
if ($postWithThisChecksum and $postWithThisChecksum->getId() !== $parent->getId())
throw new \DomainException('Duplicate post: ' . $postWithThisChecksum->getIdMarkdown());
}
private function getRandomPostName()
{
return sha1(microtime(true) . mt_rand() . uniqid());
}
private function getUniqueRandomPostName()
{
while (true)
{
$name = $this->getRandomPostName();
if (!$this->postDao->findByName($name))
return $name;
}
}
private function getPostContentPath(\Szurubooru\Entities\Post $post)
{
return 'posts' . DIRECTORY_SEPARATOR . $post->getName();
}
private function getPostThumbnailSourcePath(\Szuruboor\Entities\Post $post)
{
return 'posts' . DIRECTORY_SEPARATOR . $post->getName() . '-custom-thumb';
}
}

View file

@ -22,32 +22,17 @@ class SmartThumbnailGenerator implements IThumbnailGenerator
if (!file_exists($srcPath))
throw new \InvalidArgumentException($srcPath . ' does not exist');
$mime = mime_content_type($srcPath);
$mime = \Szurubooru\Helpers\MimeHelper::getMimeTypeFromFile($srcPath);
if ($this->isFlash($mime))
if (\Szurubooru\Helpers\MimeHelper::isFlash($mime))
return $this->flashThumbnailGenerator->generate($srcPath, $dstPath, $width, $height);
if ($this->isVideo($mime))
if (\Szurubooru\Helpers\MimeHelper::isVideo($mime))
return $this->videoThumbnailGenerator->generate($srcPath, $dstPath, $width, $height);
if ($this->isImage($mime))
if (\Szurubooru\Helpers\MimeHelper::isImage($mime))
return $this->imageThumbnailGenerator->generate($srcPath, $dstPath, $width, $height);
throw new \InvalidArgumentException('Invalid thumbnail file type: ' . $mime);
}
private function isFlash($mime)
{
return $mime === 'application/x-shockwave-flash';
}
private function isVideo($mime)
{
return $mime === 'application/ogg' or preg_match('/video\//', $mime);
}
private function isImage($mime)
{
return in_array($mime, ['image/jpeg', 'image/png', 'image/gif']);
}
}

View file

@ -41,7 +41,7 @@ class ThumbnailService
$fullSource = $this->fileService->getFullPath($source);
$fullTarget = $this->fileService->getFullPath($target);
$this->fileService->createFolders(dirname($target));
$this->fileService->createFolders($target);
$this->thumbnailGenerator->generate($fullSource, $fullTarget, $width, $height);
return $target;

View file

@ -0,0 +1,42 @@
<?php
namespace Szurubooru\Upgrades;
class Upgrade03 implements IUpgrade
{
public function run(\Szurubooru\DatabaseConnection $databaseConnection)
{
$databaseConnection->getPDO()->exec('DROP TABLE "posts"');
$databaseConnection->getPDO()->exec('
CREATE TABLE "posts"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
userId INTEGER,
uploadTime TIMESTAMP NOT NULL,
lastEditTime TIMESTAMP,
safety INTEGER NOT NULL,
contentType INTEGER NOT NULL,
contentChecksum TEXT NOT NULL,
source TEXT,
imageWidth INTEGER,
imageHeight INTEGER,
originalFileSize INTEGER,
originalFileName TEXT
)');
$databaseConnection->getPDO()->exec('
CREATE TABLE "tags"
(
name TEXT PRIMARY KEY NOT NULL
)');
$databaseConnection->getPDO()->exec('
CREATE TABLE "postTags"
(
postId INTEGER NOT NULL,
tagName TEXT NOT NULL,
PRIMARY KEY (postId, tagName)
)');
}
}

View file

@ -80,6 +80,30 @@ class Validator
}
}
public function validatePostTags($tags)
{
if (empty($tags))
throw new \DomainException('Tags cannot be empty.');
$illegalCharacters = str_split("\r\n\t " . chr(160));
foreach ($tags as $tag)
{
if (empty($tag))
{
throw new \DomainException('Tags cannot be empty.');
}
foreach ($illegalCharacters as $char)
{
if (strpos($tag, $char) !== false)
{
throw new \DomainException(
'Tags cannot contain any of following characters: ' . implode(', ', $illegalCharacters));
}
}
}
}
public function validateToken($token)
{
$this->validateNonEmpty($token, 'Token');

View file

@ -11,6 +11,7 @@ return [
return [
$container->get(\Szurubooru\Upgrades\Upgrade01::class),
$container->get(\Szurubooru\Upgrades\Upgrade02::class),
$container->get(\Szurubooru\Upgrades\Upgrade03::class),
];
}),
@ -19,6 +20,7 @@ return [
$container->get(\Szurubooru\Controllers\AuthController::class),
$container->get(\Szurubooru\Controllers\UserController::class),
$container->get(\Szurubooru\Controllers\UserAvatarController::class),
$container->get(\Szurubooru\Controllers\PostController::class),
];
}),
];

View file

@ -26,6 +26,16 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase
return $path;
}
public function getTestFile($fileName)
{
return file_get_contents($this->getTestFilePath($fileName));
}
public function getTestFilePath($fileName)
{
return __DIR__ . DIRECTORY_SEPARATOR . 'test_files' . DIRECTORY_SEPARATOR . $fileName;
}
protected function tearDown()
{
$this->cleanTestDirectory();

View file

@ -5,10 +5,9 @@ final class PostDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
{
public function testCreating()
{
$postDao = new \Szurubooru\Dao\PostDao($this->databaseConnection);
$postDao = $this->getPostDao();
$post = new \Szurubooru\Entities\Post();
$post->setName('test');
$post = $this->getPost();
$savedPost = $postDao->save($post);
$this->assertEquals('test', $post->getName());
$this->assertNotNull($savedPost->getId());
@ -16,9 +15,8 @@ final class PostDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
public function testUpdating()
{
$postDao = new \Szurubooru\Dao\PostDao($this->databaseConnection);
$post = new \Szurubooru\Entities\Post();
$post->setName('test');
$postDao = $this->getPostDao();
$post = $this->getPost();
$post = $postDao->save($post);
$this->assertEquals('test', $post->getName());
$id = $post->getId();
@ -30,17 +28,17 @@ final class PostDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
public function testGettingAll()
{
$postDao = new \Szurubooru\Dao\PostDao($this->databaseConnection);
$post1 = new \Szurubooru\Entities\Post();
$post1->setName('test2');
$post2 = new \Szurubooru\Entities\Post();
$post2->setName('test2');
$postDao = $this->getPostDao();
$post1 = $this->getPost();
$post2 = $this->getPost();
$postDao->save($post1);
$postDao->save($post2);
$actual = $postDao->findAll();
foreach ($actual as $post)
$post->resetLazyLoaders();
$expected = [
$post1->getId() => $post1,
$post2->getId() => $post2,
@ -51,31 +49,27 @@ final class PostDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
public function testGettingById()
{
$postDao = new \Szurubooru\Dao\PostDao($this->databaseConnection);
$post1 = new \Szurubooru\Entities\Post();
$post1->setName('test2');
$post2 = new \Szurubooru\Entities\Post();
$post2->setName('test2');
$postDao = $this->getPostDao();
$post1 = $this->getPost();
$post2 = $this->getPost();
$postDao->save($post1);
$postDao->save($post2);
$actualPost1 = $postDao->findById($post1->getId());
$actualPost2 = $postDao->findById($post2->getId());
$actualPost1->resetLazyLoaders();
$actualPost2->resetLazyLoaders();
$this->assertEquals($post1, $actualPost1);
$this->assertEquals($post2, $actualPost2);
}
public function testDeletingAll()
{
$postDao = new \Szurubooru\Dao\PostDao($this->databaseConnection);
$post1 = new \Szurubooru\Entities\Post();
$post1->setName('test2');
$post2 = new \Szurubooru\Entities\Post();
$post2->setName('test2');
$postDao = $this->getPostDao();
$post1 = $this->getPost();
$post2 = $this->getPost();
$postDao->save($post1);
$postDao->save($post2);
@ -90,13 +84,10 @@ final class PostDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
public function testDeletingById()
{
$postDao = new \Szurubooru\Dao\PostDao($this->databaseConnection);
$post1 = new \Szurubooru\Entities\Post();
$post1->setName('test2');
$post2 = new \Szurubooru\Entities\Post();
$post2->setName('test2');
$postDao = $this->getPostDao();
$post1 = $this->getPost();
$post2 = $this->getPost();
$postDao->save($post1);
$postDao->save($post2);
@ -108,4 +99,41 @@ final class PostDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
$this->assertEquals($actualPost2, $actualPost2);
$this->assertEquals(1, count($postDao->findAll()));
}
public function testSavingTags()
{
$testTags = ['tag1', 'tag2'];
$postDao = $this->getPostDao();
$post = $this->getPost();
$post->setTags($testTags);
$postDao->save($post);
$savedPost = $postDao->findById($post->getId());
$this->assertEquals($testTags, $post->getTags());
$this->assertEquals($post->getTags(), $savedPost->getTags());
$tagDao = $this->getTagDao();
$this->assertEquals(2, count($tagDao->findAll()));
}
private function getPostDao()
{
return new \Szurubooru\Dao\PostDao($this->databaseConnection);
}
private function getTagDao()
{
return new \Szurubooru\Dao\TagDao($this->databaseConnection);
}
private function getPost()
{
$post = new \Szurubooru\Entities\Post();
$post->setName('test');
$post->setUploadTime('whatever');
$post->setSafety(\Szurubooru\Entities\Post::POST_SAFETY_SAFE);
$post->setContentType(\Szurubooru\Entities\Post::POST_TYPE_YOUTUBE);
$post->setContentChecksum('whatever');
return $post;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Szurubooru\Tests\Helpers;
class InputReaderTest extends \Szurubooru\Tests\AbstractTestCase
{
public function testDecodingBase64()
{
$inputReader = new \Szurubooru\Helpers\InputReader();
$actual = $inputReader->decodeBase64('data:text/plain,YXdlc29tZSBkb2c=');
$expected = 'awesome dog';
$this->assertEquals($expected, $actual);
}
}

View file

@ -9,10 +9,18 @@ class FileServiceTest extends \Szurubooru\Tests\AbstractTestCase
$configMock = $this->mockConfig($testDirectory);
$httpHelper = $this->mock(\Szurubooru\Helpers\HttpHelper::class);
$fileService = new \Szurubooru\Services\FileService($configMock, $httpHelper);
$input = 'data:text/plain,YXdlc29tZSBkb2c=';
$fileService->saveFromBase64($input, 'dog.txt');
$fileService->save('dog.txt', 'awesome dog');
$expected = 'awesome dog';
$actual = file_get_contents($testDirectory . DIRECTORY_SEPARATOR . 'dog.txt');
$this->assertEquals($expected, $actual);
}
public function testDownload()
{
$configMock = $this->mockConfig();
$httpHelper = $this->mock(\Szurubooru\Helpers\HttpHelper::class);
$fileService = new \Szurubooru\Services\FileService($configMock, $httpHelper);
$content = $fileService->download('http://modernseoul.files.wordpress.com/2012/04/korean-alphabet-chart-modern-seoul.jpg');
$this->assertGreaterThan(0, strlen($content));
}
}

View file

@ -0,0 +1,157 @@
<?php
namespace Szurubooru\Tests\Services;
class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
{
private $configMock;
private $validatorMock;
private $transactionManagerMock;
private $postDaoMock;
private $authServiceMock;
private $timeServiceMock;
private $fileServiceMock;
public function setUp()
{
$this->configMock = $this->mockConfig();
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
$this->transactionManagerMock = $this->mockTransactionManager();
$this->postDaoMock = $this->mock(\Szurubooru\Dao\PostDao::class);
$this->authServiceMock = $this->mock(\Szurubooru\Services\AuthService::class);
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
$this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class);
}
public function testCreatingYoutubePost()
{
$formData = new \Szurubooru\FormData\UploadFormData;
$formData->safety = \Szurubooru\Entities\Post::POST_SAFETY_SAFE;
$formData->source = 'source';
$formData->tags = ['test', 'test2'];
$formData->url = 'https://www.youtube.com/watch?v=QYK2c4OVG6s';
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
$this->authServiceMock->expects($this->once())->method('getLoggedInUser')->willReturn(new \Szurubooru\Entities\User(5));
$this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData);
$this->assertEquals(\Szurubooru\Entities\Post::POST_SAFETY_SAFE, $savedPost->getSafety());
$this->assertEquals(5, $savedPost->getUserId());
$this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_YOUTUBE, $savedPost->getContentType());
$this->assertEquals('QYK2c4OVG6s', $savedPost->getContentChecksum());
$this->assertEquals('source', $savedPost->getSource());
$this->assertNull($savedPost->getImageWidth());
$this->assertNull($savedPost->getImageHeight());
$this->assertEquals($formData->url, $savedPost->getOriginalFileName());
$this->assertNull($savedPost->getOriginalFileSize());
$this->assertEquals(['test', 'test2'], $savedPost->getTags());
}
public function testCreatingPosts()
{
$formData = new \Szurubooru\FormData\UploadFormData;
$formData->safety = \Szurubooru\Entities\Post::POST_SAFETY_SAFE;
$formData->tags = ['test'];
$formData->content = $this->getTestFile('image.jpg');
$formData->contentFileName = 'blah';
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
$this->fileServiceMock->expects($this->once())->method('save');
$this->fileServiceMock->expects($this->once())->method('getFullPath')->willReturn($this->getTestFilePath('image.jpg'));
$this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData);
$this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_IMAGE, $savedPost->getContentType());
$this->assertEquals('24216edd12328de3a3c55e2f98220ee7613e3be1', $savedPost->getContentChecksum());
$this->assertEquals(640, $savedPost->getImageWidth());
$this->assertEquals(480, $savedPost->getImageHeight());
$this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName());
$this->assertEquals(687645, $savedPost->getOriginalFileSize());
}
public function testCreatingVideos()
{
$formData = new \Szurubooru\FormData\UploadFormData;
$formData->safety = \Szurubooru\Entities\Post::POST_SAFETY_SAFE;
$formData->tags = ['test'];
$formData->content = $this->getTestFile('video.mp4');
$formData->contentFileName = 'blah';
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
$this->fileServiceMock->expects($this->once())->method('save');
$this->fileServiceMock->expects($this->once())->method('getFullPath')->willReturn($this->getTestFilePath('video.mp4'));
$this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData);
$this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_VIDEO, $savedPost->getContentType());
$this->assertEquals('16dafaa07cda194d03d590529c06c6ec1a5b80b0', $savedPost->getContentChecksum());
$this->assertNull($savedPost->getImageWidth());
$this->assertNull($savedPost->getImageHeight());
$this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName());
$this->assertEquals(14667, $savedPost->getOriginalFileSize());
}
public function testCreatingFlashes()
{
$formData = new \Szurubooru\FormData\UploadFormData;
$formData->safety = \Szurubooru\Entities\Post::POST_SAFETY_SAFE;
$formData->tags = ['test'];
$formData->content = $this->getTestFile('flash.swf');
$formData->contentFileName = 'blah';
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
$this->fileServiceMock->expects($this->once())->method('save');
$this->fileServiceMock->expects($this->once())->method('getFullPath')->willReturn($this->getTestFilePath('flash.swf'));
$this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData);
$this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_FLASH, $savedPost->getContentType());
$this->assertEquals('d897e044b801d892291b440534c3be3739034f68', $savedPost->getContentChecksum());
$this->assertEquals(320, $savedPost->getImageWidth());
$this->assertEquals(240, $savedPost->getImageHeight());
$this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName());
$this->assertEquals(226172, $savedPost->getOriginalFileSize());
}
public function testFileDuplicates()
{
$formData = new \Szurubooru\FormData\UploadFormData;
$formData->safety = \Szurubooru\Entities\Post::POST_SAFETY_SAFE;
$formData->tags = ['test'];
$formData->content = $this->getTestFile('flash.swf');
$formData->contentFileName = 'blah';
$this->postDaoMock->expects($this->once())->method('findByContentChecksum')->willReturn(new \Szurubooru\Entities\Post(5));
$this->setExpectedException(\Exception::class, 'Duplicate post: @5');
$this->postService = $this->getPostService();
$this->postService->createPost($formData);
}
public function testYoutubeDuplicates()
{
$formData = new \Szurubooru\FormData\UploadFormData;
$formData->safety = \Szurubooru\Entities\Post::POST_SAFETY_SAFE;
$formData->tags = ['test'];
$formData->url = 'https://www.youtube.com/watch?v=QYK2c4OVG6s';
$this->postDaoMock->expects($this->once())->method('findByContentChecksum')->with('QYK2c4OVG6s')->willReturn(new \Szurubooru\Entities\Post(5));
$this->setExpectedException(\Exception::class, 'Duplicate post: @5');
$this->postService = $this->getPostService();
$this->postService->createPost($formData);
}
private function getPostService()
{
return new \Szurubooru\Services\PostService(
$this->configMock,
$this->validatorMock,
$this->transactionManagerMock,
$this->postDaoMock,
$this->authServiceMock,
$this->timeServiceMock,
$this->fileServiceMock);
}
}

View file

@ -124,6 +124,33 @@ final class ValidatorTest extends \Szurubooru\Tests\AbstractTestCase
$this->assertNull($validator->validatePassword('password'));
}
public function testNoTags()
{
$this->setExpectedException(\Exception::class, 'Tags cannot be empty');
$validator = $this->getValidator();
$validator->validatePostTags([]);
}
public function testEmptyTags()
{
$this->setExpectedException(\Exception::class, 'Tags cannot be empty');
$validator = $this->getValidator();
$validator->validatePostTags(['good_tag', '']);
}
public function testTagsWithInvalidCharacters()
{
$this->setExpectedException(\Exception::class, 'Tags cannot contain any of following');
$validator = $this->getValidator();
$validator->validatePostTags(['good_tag', 'bad' . chr(160) . 'tag']);
}
public function testValidTags()
{
$validator = $this->getValidator();
$this->assertNull($validator->validatePostTags(['good_tag', 'good_tag2', 'góód_as_well', ':3']));
}
private function getValidator()
{
return new \Szurubooru\Validator($this->configMock);

BIN
tests/test_files/flash.swf Normal file

Binary file not shown.

BIN
tests/test_files/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

BIN
tests/test_files/video.mp4 Normal file

Binary file not shown.