Implemented post uploads (closed #11)
This commit is contained in:
parent
bb7b1f3321
commit
bc8e1b05a6
37 changed files with 1159 additions and 89 deletions
1
data/.gitignore
vendored
1
data/.gitignore
vendored
|
@ -2,3 +2,4 @@ db.sqlite
|
|||
executed_upgrades.txt
|
||||
local.ini
|
||||
thumbnails
|
||||
posts
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
|
|||
listSketchyPosts: 'listSketchyPosts',
|
||||
listUnsafePosts: 'listUnsafePosts',
|
||||
uploadPosts: 'uploadPosts',
|
||||
uploadPostsAnonymously: 'uploadPostsAnonymously',
|
||||
|
||||
listTags: 'listTags',
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,15 +112,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label class="form-label" for="post-anonymous">Anonymity:</label>
|
||||
<div class="form-input">
|
||||
<label for="post-anonymous">
|
||||
<input type="checkbox" id="post-anonymous" name="anonymous"/>
|
||||
Don't show my name in this post
|
||||
</label>
|
||||
<% if (canUploadPostsAnonymously) { %>
|
||||
<div class="form-row">
|
||||
<label class="form-label" for="post-anonymous">Anonymity:</label>
|
||||
<div class="form-input">
|
||||
<label for="post-anonymous">
|
||||
<input type="checkbox" id="post-anonymous" name="anonymous"/>
|
||||
Don't show my name in this post
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
|
41
src/Controllers/PostController.php
Normal file
41
src/Controllers/PostController.php
Normal 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);
|
||||
}
|
||||
}
|
27
src/Controllers/ViewProxies/PostViewProxy.php
Normal file
27
src/Controllers/ViewProxies/PostViewProxy.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
20
src/Dao/EntityConverters/TagEntityConverter.php
Normal file
20
src/Dao/EntityConverters/TagEntityConverter.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
13
src/Dao/TagDao.php
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
39
src/FormData/UploadFormData.php
Normal file
39
src/FormData/UploadFormData.php
Normal 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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
|
|
37
src/Helpers/MimeHelper.php
Normal file
37
src/Helpers/MimeHelper.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
176
src/Services/PostService.php
Normal file
176
src/Services/PostService.php
Normal 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';
|
||||
}
|
||||
}
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
42
src/Upgrades/Upgrade03.php
Normal file
42
src/Upgrades/Upgrade03.php
Normal 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)
|
||||
)');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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),
|
||||
];
|
||||
}),
|
||||
];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
13
tests/Helpers/InputReaderTest.php
Normal file
13
tests/Helpers/InputReaderTest.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
157
tests/Services/PostServiceTest.php
Normal file
157
tests/Services/PostServiceTest.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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
BIN
tests/test_files/flash.swf
Normal file
Binary file not shown.
BIN
tests/test_files/image.jpg
Normal file
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
BIN
tests/test_files/video.mp4
Normal file
Binary file not shown.
Loading…
Reference in a new issue