diff --git a/data/.gitignore b/data/.gitignore
index a64f8e06..7d3317ca 100644
--- a/data/.gitignore
+++ b/data/.gitignore
@@ -2,3 +2,4 @@ db.sqlite
executed_upgrades.txt
local.ini
thumbnails
+posts
diff --git a/data/config.ini b/data/config.ini
index 3cca980d..827f5c52 100644
--- a/data/config.ini
+++ b/data/config.ini
@@ -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
diff --git a/public_html/css/forms.css b/public_html/css/forms.css
index 17f5ad80..f96040ef 100644
--- a/public_html/css/forms.css
+++ b/public_html/css/forms.css
@@ -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;
}
diff --git a/public_html/js/Auth.js b/public_html/js/Auth.js
index fdbe202e..2ffcf07d 100644
--- a/public_html/js/Auth.js
+++ b/public_html/js/Auth.js
@@ -22,6 +22,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
listSketchyPosts: 'listSketchyPosts',
listUnsafePosts: 'listUnsafePosts',
uploadPosts: 'uploadPosts',
+ uploadPostsAnonymously: 'uploadPostsAnonymously',
listTags: 'listTags',
};
diff --git a/public_html/js/Controls/TagInput.js b/public_html/js/Controls/TagInput.js
index b794d687..3ad09284 100644
--- a/public_html/js/Controls/TagInput.js
+++ b/public_html/js/Controls/TagInput.js
@@ -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;
};
diff --git a/public_html/js/Presenters/PostUploadPresenter.js b/public_html/js/Presenters/PostUploadPresenter.js
index 8a12f272..deb4074d 100644
--- a/public_html/js/Presenters/PostUploadPresenter.js
+++ b/public_html/js/Presenters/PostUploadPresenter.js
@@ -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);
diff --git a/public_html/templates/post-upload.tpl b/public_html/templates/post-upload.tpl
index 5d5fbf4c..e338043a 100644
--- a/public_html/templates/post-upload.tpl
+++ b/public_html/templates/post-upload.tpl
@@ -47,13 +47,13 @@
-
-
+
-
-
+
-
-
+
-
@@ -112,15 +112,17 @@
-
diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php
new file mode 100644
index 00000000..91f9eb52
--- /dev/null
+++ b/src/Controllers/PostController.php
@@ -0,0 +1,41 @@
+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);
+ }
+}
diff --git a/src/Controllers/ViewProxies/PostViewProxy.php b/src/Controllers/ViewProxies/PostViewProxy.php
new file mode 100644
index 00000000..237f4bbc
--- /dev/null
+++ b/src/Controllers/ViewProxies/PostViewProxy.php
@@ -0,0 +1,27 @@
+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;
+ }
+}
diff --git a/src/Dao/AbstractDao.php b/src/Dao/AbstractDao.php
index 75d53e7b..b8e3dbed 100644
--- a/src/Dao/AbstractDao.php
+++ b/src/Dao/AbstractDao.php
@@ -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)
+ {
+ }
}
diff --git a/src/Dao/EntityConverters/PostEntityConverter.php b/src/Dao/EntityConverters/PostEntityConverter.php
index 63c7f4a9..0a9353bf 100644
--- a/src/Dao/EntityConverters/PostEntityConverter.php
+++ b/src/Dao/EntityConverters/PostEntityConverter.php
@@ -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;
}
}
diff --git a/src/Dao/EntityConverters/TagEntityConverter.php b/src/Dao/EntityConverters/TagEntityConverter.php
new file mode 100644
index 00000000..b5121240
--- /dev/null
+++ b/src/Dao/EntityConverters/TagEntityConverter.php
@@ -0,0 +1,20 @@
+ $entity->getName(),
+ ];
+ }
+
+ public function toEntity(array $array)
+ {
+ $entity = new \Szurubooru\Entities\Tag($array['name']);
+ $entity->setName($array['name']);
+ return $entity;
+ }
+}
diff --git a/src/Dao/PostDao.php b/src/Dao/PostDao.php
index 41d72490..f2597213 100644
--- a/src/Dao/PostDao.php
+++ b/src/Dao/PostDao.php
@@ -1,14 +1,82 @@
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();
+ }
+ }
}
diff --git a/src/Dao/TagDao.php b/src/Dao/TagDao.php
new file mode 100644
index 00000000..0be82ed0
--- /dev/null
+++ b/src/Dao/TagDao.php
@@ -0,0 +1,13 @@
+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;
+ }
}
diff --git a/src/Entities/Post.php b/src/Entities/Post.php
index 47efd227..eda44e71 100644
--- a/src/Entities/Post.php
+++ b/src/Entities/Post.php
@@ -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);
+ }
}
diff --git a/src/Entities/Tag.php b/src/Entities/Tag.php
index 075f6b86..f2d381bb 100644
--- a/src/Entities/Tag.php
+++ b/src/Entities/Tag.php
@@ -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;
diff --git a/src/FormData/UploadFormData.php b/src/FormData/UploadFormData.php
new file mode 100644
index 00000000..3b1b5a48
--- /dev/null
+++ b/src/FormData/UploadFormData.php
@@ -0,0 +1,39 @@
+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');
+ }
+}
+
diff --git a/src/Helpers/EnumHelper.php b/src/Helpers/EnumHelper.php
index d749f2a2..b0838655 100644
--- a/src/Helpers/EnumHelper.php
+++ b/src/Helpers/EnumHelper.php
@@ -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];
}
diff --git a/src/Helpers/MimeHelper.php b/src/Helpers/MimeHelper.php
new file mode 100644
index 00000000..ed0ccd62
--- /dev/null
+++ b/src/Helpers/MimeHelper.php
@@ -0,0 +1,37 @@
+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);
+ }
+}
diff --git a/src/Privilege.php b/src/Privilege.php
index c2b107fd..8032bc75 100644
--- a/src/Privilege.php
+++ b/src/Privilege.php
@@ -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';
}
diff --git a/src/Services/FileService.php b/src/Services/FileService.php
index 71d1be48..d0c3dbbc 100644
--- a/src/Services/FileService.php
+++ b/src/Services/FileService.php
@@ -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;
+ }
}
diff --git a/src/Services/PostService.php b/src/Services/PostService.php
new file mode 100644
index 00000000..05053424
--- /dev/null
+++ b/src/Services/PostService.php
@@ -0,0 +1,176 @@
+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';
+ }
+}
diff --git a/src/Services/ThumbnailGenerators/SmartThumbnailGenerator.php b/src/Services/ThumbnailGenerators/SmartThumbnailGenerator.php
index 5dcef4b2..429b1d95 100644
--- a/src/Services/ThumbnailGenerators/SmartThumbnailGenerator.php
+++ b/src/Services/ThumbnailGenerators/SmartThumbnailGenerator.php
@@ -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']);
- }
}
diff --git a/src/Services/ThumbnailService.php b/src/Services/ThumbnailService.php
index 2dd340dc..09d0b234 100644
--- a/src/Services/ThumbnailService.php
+++ b/src/Services/ThumbnailService.php
@@ -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;
diff --git a/src/Upgrades/Upgrade03.php b/src/Upgrades/Upgrade03.php
new file mode 100644
index 00000000..0650e346
--- /dev/null
+++ b/src/Upgrades/Upgrade03.php
@@ -0,0 +1,42 @@
+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)
+ )');
+ }
+}
diff --git a/src/Validator.php b/src/Validator.php
index 079a1f7c..92ad86dc 100644
--- a/src/Validator.php
+++ b/src/Validator.php
@@ -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');
diff --git a/src/di.php b/src/di.php
index efdbc012..9f706e41 100644
--- a/src/di.php
+++ b/src/di.php
@@ -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),
];
}),
];
diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php
index 6eb4b73f..0d28df13 100644
--- a/tests/AbstractTestCase.php
+++ b/tests/AbstractTestCase.php
@@ -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();
diff --git a/tests/Dao/PostDaoTest.php b/tests/Dao/PostDaoTest.php
index 8bf65a94..8b176817 100644
--- a/tests/Dao/PostDaoTest.php
+++ b/tests/Dao/PostDaoTest.php
@@ -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;
+ }
}
diff --git a/tests/Helpers/InputReaderTest.php b/tests/Helpers/InputReaderTest.php
new file mode 100644
index 00000000..f423b2b9
--- /dev/null
+++ b/tests/Helpers/InputReaderTest.php
@@ -0,0 +1,13 @@
+decodeBase64('data:text/plain,YXdlc29tZSBkb2c=');
+ $expected = 'awesome dog';
+ $this->assertEquals($expected, $actual);
+ }
+}
diff --git a/tests/Services/FileServiceTest.php b/tests/Services/FileServiceTest.php
index de74469a..fc22f09a 100644
--- a/tests/Services/FileServiceTest.php
+++ b/tests/Services/FileServiceTest.php
@@ -7,12 +7,20 @@ class FileServiceTest extends \Szurubooru\Tests\AbstractTestCase
{
$testDirectory = $this->createTestDirectory();
$configMock = $this->mockConfig($testDirectory);
- $httpHelper = $this->mock( \Szurubooru\Helpers\HttpHelper::class);
+ $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));
+ }
}
diff --git a/tests/Services/PostServiceTest.php b/tests/Services/PostServiceTest.php
new file mode 100644
index 00000000..04718239
--- /dev/null
+++ b/tests/Services/PostServiceTest.php
@@ -0,0 +1,157 @@
+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);
+ }
+}
diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php
index d9ced6bd..7353ae19 100644
--- a/tests/ValidatorTest.php
+++ b/tests/ValidatorTest.php
@@ -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);
diff --git a/tests/test_files/flash.swf b/tests/test_files/flash.swf
new file mode 100644
index 00000000..5d3447d2
Binary files /dev/null and b/tests/test_files/flash.swf differ
diff --git a/tests/test_files/image.jpg b/tests/test_files/image.jpg
new file mode 100644
index 00000000..feedf932
Binary files /dev/null and b/tests/test_files/image.jpg differ
diff --git a/tests/test_files/video.mp4 b/tests/test_files/video.mp4
new file mode 100644
index 00000000..1567d80b
Binary files /dev/null and b/tests/test_files/video.mp4 differ