diff --git a/composer.json b/composer.json index d1c4bc5..98e408d 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,10 @@ ] }, "config": { - "preferred-install": "dist" + "preferred-install": "dist", + "allow-plugins": { + "composer/installers": true + } }, "minimum-stability": "dev", "prefer-stable": true diff --git a/plugins/rainlab/blog/classes/PostResource.php b/plugins/rainlab/blog/classes/PostResource.php index 9194012..5f421e2 100644 --- a/plugins/rainlab/blog/classes/PostResource.php +++ b/plugins/rainlab/blog/classes/PostResource.php @@ -1,33 +1,37 @@ -get('locale'); - - return [ - 'id' => $this->id, - 'title' => $this->getAttributeTranslated('title', $locale), - 'slug' => $this->getAttributeTranslated('slug', $locale), - 'excerpt' => $this->getAttributeTranslated('excerpt', $locale), - 'published_at' => $this->published_at->format('d.m.Y'), - 'featured_images' => ImageResource::collection($this->featured_images), - 'content_html' => $this->getAttributeTranslated('content_html', $locale), - 'categories' => CategoryResource::collection($this->categories), - 'powerseo_title' => $this->getAttributeTranslated('powerseo_title', $locale), - 'powerseo_description' => $this->getAttributeTranslated('powerseo_description', $locale), - 'powerseo_keywords' => $this->getAttributeTranslated('powerseo_keywords', $locale), - ]; - } -} +get('locale'); + $path = Config::get('app.cdn').Config::get('cms.storage.media.path'); + + return [ + 'id' => $this->id, + 'title' => $this->getAttributeTranslated('title', $locale), + 'slug' => $this->getAttributeTranslated('slug', $locale), + 'excerpt' => $this->getAttributeTranslated('excerpt', $locale), + 'published_at' => $this->published_at->format('d.m.Y'), + 'type' => $this->type, + 'featured_images' => ImageResource::collection($this->featured_images), + 'video' => $path.$this->video, + 'content_html' => $this->getAttributeTranslated('content_html', $locale), + 'categories' => CategoryResource::collection($this->categories), + 'powerseo_title' => $this->getAttributeTranslated('powerseo_title', $locale), + 'powerseo_description' => $this->getAttributeTranslated('powerseo_description', $locale), + 'powerseo_keywords' => $this->getAttributeTranslated('powerseo_keywords', $locale), + ]; + } +} diff --git a/plugins/rainlab/blog/models/Post.php b/plugins/rainlab/blog/models/Post.php index c472ba0..2d6099a 100644 --- a/plugins/rainlab/blog/models/Post.php +++ b/plugins/rainlab/blog/models/Post.php @@ -1,737 +1,746 @@ - 'required', - 'slug' => ['required', 'regex:/^[a-z0-9\/\:_\-\*\[\]\+\?\|]*$/i', 'unique:rainlab_blog_posts'], - 'content' => 'required', - 'powerseo_title' => 'required', - 'powerseo_description' => 'required', - 'powerseo_keywords' => 'required', - 'excerpt' => 'required' - ]; - - /** - * @var array Attributes that support translation, if available. - */ - public $translatable = [ - 'title', - 'content', - 'content_html', - 'excerpt', - 'metadata', - 'powerseo_title', - 'powerseo_description', - 'powerseo_keywords', - ['slug', 'index' => true] - ]; - - /** - * @var array Attributes to be stored as JSON - */ - protected $jsonable = ['metadata']; - - /** - * The attributes that should be mutated to dates. - * @var array - */ - protected $dates = ['published_at']; - - /** - * The attributes on which the post list can be ordered. - * @var array - */ - public static $allowedSortingOptions = [ - 'title asc' => 'rainlab.blog::lang.sorting.title_asc', - 'title desc' => 'rainlab.blog::lang.sorting.title_desc', - 'created_at asc' => 'rainlab.blog::lang.sorting.created_asc', - 'created_at desc' => 'rainlab.blog::lang.sorting.created_desc', - 'updated_at asc' => 'rainlab.blog::lang.sorting.updated_asc', - 'updated_at desc' => 'rainlab.blog::lang.sorting.updated_desc', - 'published_at asc' => 'rainlab.blog::lang.sorting.published_asc', - 'published_at desc' => 'rainlab.blog::lang.sorting.published_desc', - 'random' => 'rainlab.blog::lang.sorting.random' - ]; - - /* - * Relations - */ - public $belongsTo = [ - 'user' => BackendUser::class - ]; - - public $belongsToMany = [ - 'categories' => [ - Category::class, - 'table' => 'rainlab_blog_posts_categories', - 'order' => 'name' - ] - ]; - - public $attachMany = [ - 'featured_images' => [\System\Models\File::class, 'order' => 'sort_order'], - 'content_images' => \System\Models\File::class - ]; - - /** - * @var array The accessors to append to the model's array form. - */ - protected $appends = ['summary', 'has_summary']; - - public $preview = null; - - /** - * Limit visibility of the published-button - * - * @param $fields - * @param null $context - * @return void - */ - public function filterFields($fields, $context = null) - { - if (!isset($fields->published, $fields->published_at)) { - return; - } - - $user = BackendAuth::getUser(); - - if (!$user->hasAnyAccess(['rainlab.blog.access_publish'])) { - $fields->published->hidden = true; - $fields->published_at->hidden = true; - } - else { - $fields->published->hidden = false; - $fields->published_at->hidden = false; - } - } - - /** - * beforeValidate - */ - public function beforeValidate() - { - if (empty($this->user)) { - $user = BackendAuth::getUser(); - if (!is_null($user)) { - $this->user = $user->id; - } - } - - $this->content_html = self::formatHtml($this->content); - } - - /** - * afterValidate - */ - public function afterValidate() - { - if ($this->published && !$this->published_at) { - throw new ValidationException([ - 'published_at' => Lang::get('rainlab.blog::lang.post.published_validation') - ]); - } - } - - /** - * getUserOptions - */ - public function getUserOptions() - { - $options = []; - - foreach (BackendUser::all() as $user) { - $options[$user->id] = $user->fullname . ' ('.$user->login.')'; - } - - return $options; - } - - /** - * Sets the "url" attribute with a URL to this object. - * @param string $pageName - * @param Controller $controller - * @param array $params Override request URL parameters - * - * @return string - */ - public function setUrl($pageName, $controller, $params = []) - { - $params = array_merge([ - 'id' => $this->id, - 'slug' => $this->slug, - ], $params); - - if (empty($params['category'])) { - $params['category'] = $this->categories->count() ? $this->categories->first()->slug : null; - } - - // Expose published year, month and day as URL parameters. - if ($this->published) { - $params['year'] = $this->published_at->format('Y'); - $params['month'] = $this->published_at->format('m'); - $params['day'] = $this->published_at->format('d'); - } - - return $this->url = $controller->pageUrl($pageName, $params); - } - - /** - * Used to test if a certain user has permission to edit post, - * returns TRUE if the user is the owner or has other posts access. - * @param BackendUser $user - * @return bool - */ - public function canEdit(BackendUser $user) - { - return ($this->user_id == $user->id) || $user->hasAnyAccess(['rainlab.blog.access_other_posts']); - } - - public static function formatHtml($input, $preview = false) - { - $result = Markdown::parse(trim($input)); - - // Check to see if the HTML should be cleaned from potential XSS - $user = BackendAuth::getUser(); - if (!$user || !$user->hasAccess('backend.allow_unsafe_markdown')) { - $result = Html::clean($result); - } - - if ($preview) { - $result = str_replace('
', '', $result);
- }
-
- $result = TagProcessor::instance()->processTags($result, $preview);
-
- return $result;
- }
-
- //
- // Scopes
- //
-
- public function scopeIsPublished($query)
- {
- return $query
- ->whereNotNull('published')
- ->where('published', true)
- ->whereNotNull('published_at')
- ->where('published_at', '<', Carbon::now())
- ;
- }
-
- /**
- * Lists posts for the frontend
- *
- * @param $query
- * @param array $options Display options
- * @return Post
- */
- public function scopeListFrontEnd($query, $options)
- {
- /*
- * Default options
- */
- extract(array_merge([
- 'page' => 1,
- 'perPage' => 30,
- 'sort' => 'created_at',
- 'categories' => null,
- 'exceptCategories' => null,
- 'category' => null,
- 'search' => '',
- 'published' => true,
- 'featured' => null,
- 'date' => '',
- 'exceptPost' => null
- ], $options));
-
- $searchableFields = ['title', 'slug', 'excerpt', 'content'];
-
- if ($published) {
- $query->isPublished();
- }
-
-
- if (isset($featured)) {
- $query->where('featured', $featured);
- }
-
- $date = trim($date);
-
- if (strtotime($date)) {
- $query->whereDate('published_at', '=', $date);
- }
-
- /*
- * Except post(s)
- */
- if ($exceptPost) {
- $exceptPosts = (is_array($exceptPost)) ? $exceptPost : [$exceptPost];
- $exceptPostIds = [];
- $exceptPostSlugs = [];
-
- foreach ($exceptPosts as $exceptPost) {
- $exceptPost = trim($exceptPost);
-
- if (is_numeric($exceptPost)) {
- $exceptPostIds[] = $exceptPost;
- } else {
- $exceptPostSlugs[] = $exceptPost;
- }
- }
-
- if (count($exceptPostIds)) {
- $query->whereNotIn('id', $exceptPostIds);
- }
- if (count($exceptPostSlugs)) {
- $query->whereNotIn('slug', $exceptPostSlugs);
- }
- }
-
- /*
- * Sorting
- */
- if (in_array($sort, array_keys(static::$allowedSortingOptions))) {
- if ($sort == 'random') {
- $query->inRandomOrder();
- } else {
- @list($sortField, $sortDirection) = explode(' ', $sort);
-
- if (is_null($sortDirection)) {
- $sortDirection = "desc";
- }
-
- $query->orderBy($sortField, $sortDirection);
- }
- }
-
- /*
- * Search
- */
- $search = trim($search);
- if (strlen($search)) {
- $query->searchWhere($search, $searchableFields);
- }
-
- /*
- * Categories
- */
- if ($categories !== null) {
- $categories = is_array($categories) ? $categories : [$categories];
- $query->whereHas('categories', function($q) use ($categories) {
- $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
- });
- }
-
- /*
- * Except Categories
- */
- if (!empty($exceptCategories)) {
- $exceptCategories = is_array($exceptCategories) ? $exceptCategories : [$exceptCategories];
- array_walk($exceptCategories, 'trim');
-
- $query->whereDoesntHave('categories', function ($q) use ($exceptCategories) {
- $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('slug', $exceptCategories);
- });
- }
-
- /*
- * Category, including children
- */
- if ($category !== null) {
- $category = Category::find($category);
-
- $categories = $category->getAllChildrenAndSelf()->lists('id');
- $query->whereHas('categories', function($q) use ($categories) {
- $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
- });
- }
-
- return $query->paginate($perPage, $page);
- }
-
- /**
- * Allows filtering for specifc categories.
- * @param Illuminate\Query\Builder $query QueryBuilder
- * @param array $categories List of category ids
- * @return Illuminate\Query\Builder QueryBuilder
- */
- public function scopeFilterCategories($query, $categories)
- {
- return $query->whereHas('categories', function($q) use ($categories) {
- $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
- });
- }
-
- //
- // Summary / Excerpt
- //
-
- /**
- * Used by "has_summary", returns true if this post uses a summary (more tag).
- * @return boolean
- */
- public function getHasSummaryAttribute()
- {
- $more = Config::get('rainlab.blog::summary_separator', '');
- $length = Config::get('rainlab.blog::summary_default_length', 600);
-
- return (
- !!strlen(trim($this->excerpt)) ||
- strpos($this->content_html, $more) !== false ||
- strlen(Html::strip($this->content_html)) > $length
- );
- }
-
- /**
- * Used by "summary", if no excerpt is provided, generate one from the content.
- * Returns the HTML content before the tag or a limited 600
- * character version.
- *
- * @return string
- */
- public function getSummaryAttribute()
- {
- $excerpt = $this->excerpt;
- if (strlen(trim($excerpt))) {
- return $excerpt;
- }
-
- $more = Config::get('rainlab.blog::summary_separator', '');
-
- if (strpos($this->content_html, $more) !== false) {
- $parts = explode($more, $this->content_html);
-
- return array_get($parts, 0);
- }
-
- $length = Config::get('rainlab.blog::summary_default_length', 600);
-
- return Html::limit($this->content_html, $length);
- }
-
- //
- // Next / Previous
- //
-
- /**
- * Apply a constraint to the query to find the nearest sibling
- *
- * // Get the next post
- * Post::applySibling()->first();
- *
- * // Get the previous post
- * Post::applySibling(-1)->first();
- *
- * // Get the previous post, ordered by the ID attribute instead
- * Post::applySibling(['direction' => -1, 'attribute' => 'id'])->first();
- *
- * @param $query
- * @param array $options
- * @return
- */
- public function scopeApplySibling($query, $options = [])
- {
- if (!is_array($options)) {
- $options = ['direction' => $options];
- }
-
- extract(array_merge([
- 'direction' => 'next',
- 'attribute' => 'published_at'
- ], $options));
-
- $isPrevious = in_array($direction, ['previous', -1]);
- $directionOrder = $isPrevious ? 'asc' : 'desc';
- $directionOperator = $isPrevious ? '>' : '<';
-
- $query->where('id', '<>', $this->id);
-
- if (!is_null($this->$attribute)) {
- $query->where($attribute, $directionOperator, $this->$attribute);
- }
-
- return $query->orderBy($attribute, $directionOrder);
- }
-
- /**
- * Returns the next post, if available.
- * @return self
- */
- public function nextPost()
- {
- return self::isPublished()->applySibling()->first();
- }
-
- /**
- * Returns the previous post, if available.
- * @return self
- */
- public function previousPost()
- {
- return self::isPublished()->applySibling(-1)->first();
- }
-
- //
- // Menu helpers
- //
-
- /**
- * Handler for the pages.menuitem.getTypeInfo event.
- * Returns a menu item type information. The type information is returned as array
- * with the following elements:
- * - references - a list of the item type reference options. The options are returned in the
- * ["key"] => "title" format for options that don't have sub-options, and in the format
- * ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional,
- * required only if the menu item type requires references.
- * - nesting - Boolean value indicating whether the item type supports nested items. Optional,
- * false if omitted.
- * - dynamicItems - Boolean value indicating whether the item type could generate new menu items.
- * Optional, false if omitted.
- * - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to
- * resolve the item URL.
- *
- * @param string $type Specifies the menu item type
- * @return array Returns an array
- */
- public static function getMenuTypeInfo($type)
- {
- $result = [];
-
- if ($type == 'blog-post') {
- $references = [];
-
- $posts = self::orderBy('title')->get();
- foreach ($posts as $post) {
- $references[$post->id] = $post->title;
- }
-
- $result = [
- 'references' => $references,
- 'nesting' => false,
- 'dynamicItems' => false
- ];
- }
-
- if ($type == 'all-blog-posts') {
- $result = [
- 'dynamicItems' => true
- ];
- }
-
- if ($type == 'category-blog-posts') {
- $references = [];
-
- $categories = Category::orderBy('name')->get();
- foreach ($categories as $category) {
- $references[$category->id] = $category->name;
- }
-
- $result = [
- 'references' => $references,
- 'dynamicItems' => true
- ];
- }
-
- if ($result) {
- $theme = Theme::getActiveTheme();
-
- $pages = CmsPage::listInTheme($theme, true);
- $cmsPages = [];
-
- foreach ($pages as $page) {
- if (!$page->hasComponent('blogPost')) {
- continue;
- }
-
- /*
- * Component must use a categoryPage filter with a routing parameter and post slug
- * eg: categoryPage = "{{ :somevalue }}", slug = "{{ :somevalue }}"
- */
- $properties = $page->getComponentProperties('blogPost');
- if (!isset($properties['categoryPage']) || !preg_match('/{{\s*:/', $properties['slug'])) {
- continue;
- }
-
- $cmsPages[] = $page;
- }
-
- $result['cmsPages'] = $cmsPages;
- }
-
- return $result;
- }
-
- /**
- * Handler for the pages.menuitem.resolveItem event.
- * Returns information about a menu item. The result is an array
- * with the following keys:
- * - url - the menu item URL. Not required for menu item types that return all available records.
- * The URL should be returned relative to the website root and include the subdirectory, if any.
- * Use the Url::to() helper to generate the URLs.
- * - isActive - determines whether the menu item is active. Not required for menu item types that
- * return all available records.
- * - items - an array of arrays with the same keys (url, isActive, items) + the title key.
- * The items array should be added only if the $item's $nesting property value is TRUE.
- *
- * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item.
- * @param \Cms\Classes\Theme $theme Specifies the current theme.
- * @param string $url Specifies the current page URL, normalized, in lower case
- * The URL is specified relative to the website root, it includes the subdirectory name, if any.
- * @return mixed Returns an array. Returns null if the item cannot be resolved.
- */
- public static function resolveMenuItem($item, $url, $theme)
- {
- $result = null;
-
- if ($item->type == 'blog-post') {
- if (!$item->reference || !$item->cmsPage) {
- return;
- }
-
- $category = self::find($item->reference);
- if (!$category) {
- return;
- }
-
- $pageUrl = self::getPostPageUrl($item->cmsPage, $category, $theme);
- if (!$pageUrl) {
- return;
- }
-
- $pageUrl = Url::to($pageUrl);
-
- $result = [];
- $result['url'] = $pageUrl;
- $result['isActive'] = $pageUrl == $url;
- $result['mtime'] = $category->updated_at;
- }
- elseif ($item->type == 'all-blog-posts') {
- $result = [
- 'items' => []
- ];
-
- $posts = self::isPublished()
- ->orderBy('title')
- ->get()
- ;
-
- foreach ($posts as $post) {
- $postItem = [
- 'title' => $post->title,
- 'url' => self::getPostPageUrl($item->cmsPage, $post, $theme),
- 'mtime' => $post->updated_at
- ];
-
- $postItem['isActive'] = $postItem['url'] == $url;
-
- $result['items'][] = $postItem;
- }
- }
- elseif ($item->type == 'category-blog-posts') {
- if (!$item->reference || !$item->cmsPage) {
- return;
- }
-
- $category = Category::find($item->reference);
- if (!$category) {
- return;
- }
-
- $result = [
- 'items' => []
- ];
-
- $query = self::isPublished()
- ->orderBy('title');
-
- $categories = $category->getAllChildrenAndSelf()->lists('id');
- $query->whereHas('categories', function($q) use ($categories) {
- $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
- });
-
- $posts = $query->get();
-
- foreach ($posts as $post) {
- $postItem = [
- 'title' => $post->title,
- 'url' => self::getPostPageUrl($item->cmsPage, $post, $theme),
- 'mtime' => $post->updated_at
- ];
-
- $postItem['isActive'] = $postItem['url'] == $url;
-
- $result['items'][] = $postItem;
- }
- }
-
- return $result;
- }
-
- /**
- * Returns URL of a post page.
- *
- * @param $pageCode
- * @param $category
- * @param $theme
- */
- protected static function getPostPageUrl($pageCode, $category, $theme)
- {
- $page = CmsPage::loadCached($theme, $pageCode);
- if (!$page) {
- return;
- }
-
- $properties = $page->getComponentProperties('blogPost');
- if (!isset($properties['slug'])) {
- return;
- }
-
- /*
- * Extract the routing parameter name from the category filter
- * eg: {{ :someRouteParam }}
- */
- if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['slug'], $matches)) {
- return;
- }
-
- $paramName = substr(trim($matches[1]), 1);
- $params = [
- $paramName => $category->slug,
- 'year' => $category->published_at->format('Y'),
- 'month' => $category->published_at->format('m'),
- 'day' => $category->published_at->format('d')
- ];
- $url = CmsPage::url($page->getBaseFileName(), $params);
-
- return $url;
- }
-}
+ 'required',
+ 'slug' => ['required', 'regex:/^[a-z0-9\/\:_\-\*\[\]\+\?\|]*$/i', 'unique:rainlab_blog_posts'],
+ 'content' => 'required',
+ 'powerseo_title' => 'required',
+ 'powerseo_description' => 'required',
+ 'powerseo_keywords' => 'required',
+ 'excerpt' => 'required'
+ ];
+
+ /**
+ * @var array Attributes that support translation, if available.
+ */
+ public $translatable = [
+ 'title',
+ 'content',
+ 'content_html',
+ 'excerpt',
+ 'metadata',
+ 'powerseo_title',
+ 'powerseo_description',
+ 'powerseo_keywords',
+ ['slug', 'index' => true]
+ ];
+
+ /**
+ * @var array Attributes to be stored as JSON
+ */
+ protected $jsonable = ['metadata'];
+
+ /**
+ * The attributes that should be mutated to dates.
+ * @var array
+ */
+ protected $dates = ['published_at'];
+
+ /**
+ * The attributes on which the post list can be ordered.
+ * @var array
+ */
+ public static $allowedSortingOptions = [
+ 'title asc' => 'rainlab.blog::lang.sorting.title_asc',
+ 'title desc' => 'rainlab.blog::lang.sorting.title_desc',
+ 'created_at asc' => 'rainlab.blog::lang.sorting.created_asc',
+ 'created_at desc' => 'rainlab.blog::lang.sorting.created_desc',
+ 'updated_at asc' => 'rainlab.blog::lang.sorting.updated_asc',
+ 'updated_at desc' => 'rainlab.blog::lang.sorting.updated_desc',
+ 'published_at asc' => 'rainlab.blog::lang.sorting.published_asc',
+ 'published_at desc' => 'rainlab.blog::lang.sorting.published_desc',
+ 'random' => 'rainlab.blog::lang.sorting.random'
+ ];
+
+ /*
+ * Relations
+ */
+ public $belongsTo = [
+ 'user' => BackendUser::class
+ ];
+
+ public $belongsToMany = [
+ 'categories' => [
+ Category::class,
+ 'table' => 'rainlab_blog_posts_categories',
+ 'order' => 'name'
+ ]
+ ];
+
+ public $attachMany = [
+ 'featured_images' => [\System\Models\File::class, 'order' => 'sort_order'],
+ 'content_images' => \System\Models\File::class
+ ];
+
+ /**
+ * @var array The accessors to append to the model's array form.
+ */
+ protected $appends = ['summary', 'has_summary'];
+
+ public $preview = null;
+
+ /**
+ * Limit visibility of the published-button
+ *
+ * @param $fields
+ * @param null $context
+ * @return void
+ */
+ public function filterFields($fields, $context = null)
+ {
+ if(isset($fields->type)){
+ // dd($fields->type->value);
+ if($fields->type->value == 'photo'){
+ $fields->video->hidden = true;
+ }else{
+ $fields->featured_images->hidden = true;
+ }
+ }
+
+ if (!isset($fields->published, $fields->published_at)) {
+ return;
+ }
+
+ $user = BackendAuth::getUser();
+
+ if (!$user->hasAnyAccess(['rainlab.blog.access_publish'])) {
+ $fields->published->hidden = true;
+ $fields->published_at->hidden = true;
+ }
+ else {
+ $fields->published->hidden = false;
+ $fields->published_at->hidden = false;
+ }
+ }
+
+ /**
+ * beforeValidate
+ */
+ public function beforeValidate()
+ {
+ if (empty($this->user)) {
+ $user = BackendAuth::getUser();
+ if (!is_null($user)) {
+ $this->user = $user->id;
+ }
+ }
+
+ $this->content_html = self::formatHtml($this->content);
+ }
+
+ /**
+ * afterValidate
+ */
+ public function afterValidate()
+ {
+ if ($this->published && !$this->published_at) {
+ throw new ValidationException([
+ 'published_at' => Lang::get('rainlab.blog::lang.post.published_validation')
+ ]);
+ }
+ }
+
+ /**
+ * getUserOptions
+ */
+ public function getUserOptions()
+ {
+ $options = [];
+
+ foreach (BackendUser::all() as $user) {
+ $options[$user->id] = $user->fullname . ' ('.$user->login.')';
+ }
+
+ return $options;
+ }
+
+ /**
+ * Sets the "url" attribute with a URL to this object.
+ * @param string $pageName
+ * @param Controller $controller
+ * @param array $params Override request URL parameters
+ *
+ * @return string
+ */
+ public function setUrl($pageName, $controller, $params = [])
+ {
+ $params = array_merge([
+ 'id' => $this->id,
+ 'slug' => $this->slug,
+ ], $params);
+
+ if (empty($params['category'])) {
+ $params['category'] = $this->categories->count() ? $this->categories->first()->slug : null;
+ }
+
+ // Expose published year, month and day as URL parameters.
+ if ($this->published) {
+ $params['year'] = $this->published_at->format('Y');
+ $params['month'] = $this->published_at->format('m');
+ $params['day'] = $this->published_at->format('d');
+ }
+
+ return $this->url = $controller->pageUrl($pageName, $params);
+ }
+
+ /**
+ * Used to test if a certain user has permission to edit post,
+ * returns TRUE if the user is the owner or has other posts access.
+ * @param BackendUser $user
+ * @return bool
+ */
+ public function canEdit(BackendUser $user)
+ {
+ return ($this->user_id == $user->id) || $user->hasAnyAccess(['rainlab.blog.access_other_posts']);
+ }
+
+ public static function formatHtml($input, $preview = false)
+ {
+ $result = Markdown::parse(trim($input));
+
+ // Check to see if the HTML should be cleaned from potential XSS
+ $user = BackendAuth::getUser();
+ if (!$user || !$user->hasAccess('backend.allow_unsafe_markdown')) {
+ $result = Html::clean($result);
+ }
+
+ if ($preview) {
+ $result = str_replace('', '', $result);
+ }
+
+ $result = TagProcessor::instance()->processTags($result, $preview);
+
+ return $result;
+ }
+
+ //
+ // Scopes
+ //
+
+ public function scopeIsPublished($query)
+ {
+ return $query
+ ->whereNotNull('published')
+ ->where('published', true)
+ ->whereNotNull('published_at')
+ ->where('published_at', '<', Carbon::now())
+ ;
+ }
+
+ /**
+ * Lists posts for the frontend
+ *
+ * @param $query
+ * @param array $options Display options
+ * @return Post
+ */
+ public function scopeListFrontEnd($query, $options)
+ {
+ /*
+ * Default options
+ */
+ extract(array_merge([
+ 'page' => 1,
+ 'perPage' => 30,
+ 'sort' => 'created_at',
+ 'categories' => null,
+ 'exceptCategories' => null,
+ 'category' => null,
+ 'search' => '',
+ 'published' => true,
+ 'featured' => null,
+ 'date' => '',
+ 'exceptPost' => null
+ ], $options));
+
+ $searchableFields = ['title', 'slug', 'excerpt', 'content'];
+
+ if ($published) {
+ $query->isPublished();
+ }
+
+
+ if (isset($featured)) {
+ $query->where('featured', $featured);
+ }
+
+ $date = trim($date);
+
+ if (strtotime($date)) {
+ $query->whereDate('published_at', '=', $date);
+ }
+
+ /*
+ * Except post(s)
+ */
+ if ($exceptPost) {
+ $exceptPosts = (is_array($exceptPost)) ? $exceptPost : [$exceptPost];
+ $exceptPostIds = [];
+ $exceptPostSlugs = [];
+
+ foreach ($exceptPosts as $exceptPost) {
+ $exceptPost = trim($exceptPost);
+
+ if (is_numeric($exceptPost)) {
+ $exceptPostIds[] = $exceptPost;
+ } else {
+ $exceptPostSlugs[] = $exceptPost;
+ }
+ }
+
+ if (count($exceptPostIds)) {
+ $query->whereNotIn('id', $exceptPostIds);
+ }
+ if (count($exceptPostSlugs)) {
+ $query->whereNotIn('slug', $exceptPostSlugs);
+ }
+ }
+
+ /*
+ * Sorting
+ */
+ if (in_array($sort, array_keys(static::$allowedSortingOptions))) {
+ if ($sort == 'random') {
+ $query->inRandomOrder();
+ } else {
+ @list($sortField, $sortDirection) = explode(' ', $sort);
+
+ if (is_null($sortDirection)) {
+ $sortDirection = "desc";
+ }
+
+ $query->orderBy($sortField, $sortDirection);
+ }
+ }
+
+ /*
+ * Search
+ */
+ $search = trim($search);
+ if (strlen($search)) {
+ $query->searchWhere($search, $searchableFields);
+ }
+
+ /*
+ * Categories
+ */
+ if ($categories !== null) {
+ $categories = is_array($categories) ? $categories : [$categories];
+ $query->whereHas('categories', function($q) use ($categories) {
+ $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
+ });
+ }
+
+ /*
+ * Except Categories
+ */
+ if (!empty($exceptCategories)) {
+ $exceptCategories = is_array($exceptCategories) ? $exceptCategories : [$exceptCategories];
+ array_walk($exceptCategories, 'trim');
+
+ $query->whereDoesntHave('categories', function ($q) use ($exceptCategories) {
+ $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('slug', $exceptCategories);
+ });
+ }
+
+ /*
+ * Category, including children
+ */
+ if ($category !== null) {
+ $category = Category::find($category);
+
+ $categories = $category->getAllChildrenAndSelf()->lists('id');
+ $query->whereHas('categories', function($q) use ($categories) {
+ $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
+ });
+ }
+
+ return $query->paginate($perPage, $page);
+ }
+
+ /**
+ * Allows filtering for specifc categories.
+ * @param Illuminate\Query\Builder $query QueryBuilder
+ * @param array $categories List of category ids
+ * @return Illuminate\Query\Builder QueryBuilder
+ */
+ public function scopeFilterCategories($query, $categories)
+ {
+ return $query->whereHas('categories', function($q) use ($categories) {
+ $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
+ });
+ }
+
+ //
+ // Summary / Excerpt
+ //
+
+ /**
+ * Used by "has_summary", returns true if this post uses a summary (more tag).
+ * @return boolean
+ */
+ public function getHasSummaryAttribute()
+ {
+ $more = Config::get('rainlab.blog::summary_separator', '');
+ $length = Config::get('rainlab.blog::summary_default_length', 600);
+
+ return (
+ !!strlen(trim($this->excerpt)) ||
+ strpos($this->content_html, $more) !== false ||
+ strlen(Html::strip($this->content_html)) > $length
+ );
+ }
+
+ /**
+ * Used by "summary", if no excerpt is provided, generate one from the content.
+ * Returns the HTML content before the tag or a limited 600
+ * character version.
+ *
+ * @return string
+ */
+ public function getSummaryAttribute()
+ {
+ $excerpt = $this->excerpt;
+ if (strlen(trim($excerpt))) {
+ return $excerpt;
+ }
+
+ $more = Config::get('rainlab.blog::summary_separator', '');
+
+ if (strpos($this->content_html, $more) !== false) {
+ $parts = explode($more, $this->content_html);
+
+ return array_get($parts, 0);
+ }
+
+ $length = Config::get('rainlab.blog::summary_default_length', 600);
+
+ return Html::limit($this->content_html, $length);
+ }
+
+ //
+ // Next / Previous
+ //
+
+ /**
+ * Apply a constraint to the query to find the nearest sibling
+ *
+ * // Get the next post
+ * Post::applySibling()->first();
+ *
+ * // Get the previous post
+ * Post::applySibling(-1)->first();
+ *
+ * // Get the previous post, ordered by the ID attribute instead
+ * Post::applySibling(['direction' => -1, 'attribute' => 'id'])->first();
+ *
+ * @param $query
+ * @param array $options
+ * @return
+ */
+ public function scopeApplySibling($query, $options = [])
+ {
+ if (!is_array($options)) {
+ $options = ['direction' => $options];
+ }
+
+ extract(array_merge([
+ 'direction' => 'next',
+ 'attribute' => 'published_at'
+ ], $options));
+
+ $isPrevious = in_array($direction, ['previous', -1]);
+ $directionOrder = $isPrevious ? 'asc' : 'desc';
+ $directionOperator = $isPrevious ? '>' : '<';
+
+ $query->where('id', '<>', $this->id);
+
+ if (!is_null($this->$attribute)) {
+ $query->where($attribute, $directionOperator, $this->$attribute);
+ }
+
+ return $query->orderBy($attribute, $directionOrder);
+ }
+
+ /**
+ * Returns the next post, if available.
+ * @return self
+ */
+ public function nextPost()
+ {
+ return self::isPublished()->applySibling()->first();
+ }
+
+ /**
+ * Returns the previous post, if available.
+ * @return self
+ */
+ public function previousPost()
+ {
+ return self::isPublished()->applySibling(-1)->first();
+ }
+
+ //
+ // Menu helpers
+ //
+
+ /**
+ * Handler for the pages.menuitem.getTypeInfo event.
+ * Returns a menu item type information. The type information is returned as array
+ * with the following elements:
+ * - references - a list of the item type reference options. The options are returned in the
+ * ["key"] => "title" format for options that don't have sub-options, and in the format
+ * ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional,
+ * required only if the menu item type requires references.
+ * - nesting - Boolean value indicating whether the item type supports nested items. Optional,
+ * false if omitted.
+ * - dynamicItems - Boolean value indicating whether the item type could generate new menu items.
+ * Optional, false if omitted.
+ * - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to
+ * resolve the item URL.
+ *
+ * @param string $type Specifies the menu item type
+ * @return array Returns an array
+ */
+ public static function getMenuTypeInfo($type)
+ {
+ $result = [];
+
+ if ($type == 'blog-post') {
+ $references = [];
+
+ $posts = self::orderBy('title')->get();
+ foreach ($posts as $post) {
+ $references[$post->id] = $post->title;
+ }
+
+ $result = [
+ 'references' => $references,
+ 'nesting' => false,
+ 'dynamicItems' => false
+ ];
+ }
+
+ if ($type == 'all-blog-posts') {
+ $result = [
+ 'dynamicItems' => true
+ ];
+ }
+
+ if ($type == 'category-blog-posts') {
+ $references = [];
+
+ $categories = Category::orderBy('name')->get();
+ foreach ($categories as $category) {
+ $references[$category->id] = $category->name;
+ }
+
+ $result = [
+ 'references' => $references,
+ 'dynamicItems' => true
+ ];
+ }
+
+ if ($result) {
+ $theme = Theme::getActiveTheme();
+
+ $pages = CmsPage::listInTheme($theme, true);
+ $cmsPages = [];
+
+ foreach ($pages as $page) {
+ if (!$page->hasComponent('blogPost')) {
+ continue;
+ }
+
+ /*
+ * Component must use a categoryPage filter with a routing parameter and post slug
+ * eg: categoryPage = "{{ :somevalue }}", slug = "{{ :somevalue }}"
+ */
+ $properties = $page->getComponentProperties('blogPost');
+ if (!isset($properties['categoryPage']) || !preg_match('/{{\s*:/', $properties['slug'])) {
+ continue;
+ }
+
+ $cmsPages[] = $page;
+ }
+
+ $result['cmsPages'] = $cmsPages;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Handler for the pages.menuitem.resolveItem event.
+ * Returns information about a menu item. The result is an array
+ * with the following keys:
+ * - url - the menu item URL. Not required for menu item types that return all available records.
+ * The URL should be returned relative to the website root and include the subdirectory, if any.
+ * Use the Url::to() helper to generate the URLs.
+ * - isActive - determines whether the menu item is active. Not required for menu item types that
+ * return all available records.
+ * - items - an array of arrays with the same keys (url, isActive, items) + the title key.
+ * The items array should be added only if the $item's $nesting property value is TRUE.
+ *
+ * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item.
+ * @param \Cms\Classes\Theme $theme Specifies the current theme.
+ * @param string $url Specifies the current page URL, normalized, in lower case
+ * The URL is specified relative to the website root, it includes the subdirectory name, if any.
+ * @return mixed Returns an array. Returns null if the item cannot be resolved.
+ */
+ public static function resolveMenuItem($item, $url, $theme)
+ {
+ $result = null;
+
+ if ($item->type == 'blog-post') {
+ if (!$item->reference || !$item->cmsPage) {
+ return;
+ }
+
+ $category = self::find($item->reference);
+ if (!$category) {
+ return;
+ }
+
+ $pageUrl = self::getPostPageUrl($item->cmsPage, $category, $theme);
+ if (!$pageUrl) {
+ return;
+ }
+
+ $pageUrl = Url::to($pageUrl);
+
+ $result = [];
+ $result['url'] = $pageUrl;
+ $result['isActive'] = $pageUrl == $url;
+ $result['mtime'] = $category->updated_at;
+ }
+ elseif ($item->type == 'all-blog-posts') {
+ $result = [
+ 'items' => []
+ ];
+
+ $posts = self::isPublished()
+ ->orderBy('title')
+ ->get()
+ ;
+
+ foreach ($posts as $post) {
+ $postItem = [
+ 'title' => $post->title,
+ 'url' => self::getPostPageUrl($item->cmsPage, $post, $theme),
+ 'mtime' => $post->updated_at
+ ];
+
+ $postItem['isActive'] = $postItem['url'] == $url;
+
+ $result['items'][] = $postItem;
+ }
+ }
+ elseif ($item->type == 'category-blog-posts') {
+ if (!$item->reference || !$item->cmsPage) {
+ return;
+ }
+
+ $category = Category::find($item->reference);
+ if (!$category) {
+ return;
+ }
+
+ $result = [
+ 'items' => []
+ ];
+
+ $query = self::isPublished()
+ ->orderBy('title');
+
+ $categories = $category->getAllChildrenAndSelf()->lists('id');
+ $query->whereHas('categories', function($q) use ($categories) {
+ $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
+ });
+
+ $posts = $query->get();
+
+ foreach ($posts as $post) {
+ $postItem = [
+ 'title' => $post->title,
+ 'url' => self::getPostPageUrl($item->cmsPage, $post, $theme),
+ 'mtime' => $post->updated_at
+ ];
+
+ $postItem['isActive'] = $postItem['url'] == $url;
+
+ $result['items'][] = $postItem;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns URL of a post page.
+ *
+ * @param $pageCode
+ * @param $category
+ * @param $theme
+ */
+ protected static function getPostPageUrl($pageCode, $category, $theme)
+ {
+ $page = CmsPage::loadCached($theme, $pageCode);
+ if (!$page) {
+ return;
+ }
+
+ $properties = $page->getComponentProperties('blogPost');
+ if (!isset($properties['slug'])) {
+ return;
+ }
+
+ /*
+ * Extract the routing parameter name from the category filter
+ * eg: {{ :someRouteParam }}
+ */
+ if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['slug'], $matches)) {
+ return;
+ }
+
+ $paramName = substr(trim($matches[1]), 1);
+ $params = [
+ $paramName => $category->slug,
+ 'year' => $category->published_at->format('Y'),
+ 'month' => $category->published_at->format('m'),
+ 'day' => $category->published_at->format('d')
+ ];
+ $url = CmsPage::url($page->getBaseFileName(), $params);
+
+ return $url;
+ }
+}
diff --git a/plugins/rainlab/blog/models/post/fields.yaml b/plugins/rainlab/blog/models/post/fields.yaml
index 29304d3..1ed19d1 100644
--- a/plugins/rainlab/blog/models/post/fields.yaml
+++ b/plugins/rainlab/blog/models/post/fields.yaml
@@ -1,73 +1,95 @@
-fields:
- title:
- label: 'rainlab.blog::lang.post.title'
- span: left
- placeholder: 'rainlab.blog::lang.post.title_placeholder'
- type: text
- slug:
- label: 'rainlab.blog::lang.post.slug'
- span: right
- placeholder: 'rainlab.blog::lang.post.slug_placeholder'
- preset:
- field: title
- type: slug
- type: text
- toolbar:
- type: partial
- path: post_toolbar
- cssClass: collapse-visible
-secondaryTabs:
- fields:
- content:
- tab: 'rainlab.blog::lang.post.tab_edit'
- type: RainLab\Blog\FormWidgets\BlogMarkdown
- cssClass: 'field-slim blog-post-preview'
- stretch: true
- mode: split
- categories:
- nameFrom: name
- descriptionFrom: description
- span: left
- type: relation
- commentAbove: 'rainlab.blog::lang.post.categories_comment'
- tab: 'rainlab.blog::lang.post.tab_categories'
- published:
- label: 'rainlab.blog::lang.post.published'
- span: left
- type: checkbox
- tab: 'rainlab.blog::lang.post.tab_manage'
- featured:
- label: 'показать в слайдере на главной странице'
- span: auto
- type: checkbox
- comment: 'не отмечать если не нужно выставлять на слайдере'
- tab: 'rainlab.blog::lang.post.tab_manage'
- user:
- tab: 'rainlab.blog::lang.post.tab_manage'
- label: 'rainlab.blog::lang.post.published_by'
- span: right
- type: dropdown
- emptyOption: 'rainlab.blog::lang.post.current_user'
- published_at:
- tab: 'rainlab.blog::lang.post.tab_manage'
- label: 'rainlab.blog::lang.post.published_on'
- span: left
- cssClass: checkbox-align
- type: datepicker
- mode: datetime
- trigger:
- action: enable
- field: published
- condition: checked
- excerpt:
- tab: 'rainlab.blog::lang.post.tab_manage'
- label: 'rainlab.blog::lang.post.excerpt'
- type: textarea
- size: small
- featured_images:
- tab: 'rainlab.blog::lang.post.tab_manage'
- label: 'rainlab.blog::lang.post.featured_images'
- type: fileupload
- mode: image
- imageWidth: 200
- imageHeight: 200
+fields:
+ title:
+ label: 'rainlab.blog::lang.post.title'
+ span: left
+ placeholder: 'rainlab.blog::lang.post.title_placeholder'
+ type: text
+ slug:
+ label: 'rainlab.blog::lang.post.slug'
+ span: right
+ placeholder: 'rainlab.blog::lang.post.slug_placeholder'
+ preset:
+ field: title
+ type: slug
+ type: text
+ toolbar:
+ type: partial
+ path: post_toolbar
+ cssClass: collapse-visible
+secondaryTabs:
+ fields:
+ content:
+ tab: 'rainlab.blog::lang.post.tab_edit'
+ type: RainLab\Blog\FormWidgets\BlogMarkdown
+ cssClass: 'field-slim blog-post-preview'
+ stretch: true
+ mode: split
+ categories:
+ nameFrom: name
+ descriptionFrom: description
+ span: left
+ type: relation
+ commentAbove: 'rainlab.blog::lang.post.categories_comment'
+ tab: 'rainlab.blog::lang.post.tab_categories'
+ published:
+ label: 'rainlab.blog::lang.post.published'
+ span: left
+ type: checkbox
+ tab: 'rainlab.blog::lang.post.tab_manage'
+ featured:
+ label: 'показать в слайдере на главной странице'
+ span: auto
+ type: checkbox
+ comment: 'не отмечать если не нужно выставлять на слайдере'
+ tab: 'rainlab.blog::lang.post.tab_manage'
+ user:
+ tab: 'rainlab.blog::lang.post.tab_manage'
+ label: 'rainlab.blog::lang.post.published_by'
+ span: right
+ type: dropdown
+ emptyOption: 'rainlab.blog::lang.post.current_user'
+ published_at:
+ tab: 'rainlab.blog::lang.post.tab_manage'
+ label: 'rainlab.blog::lang.post.published_on'
+ span: left
+ cssClass: checkbox-align
+ type: datepicker
+ mode: datetime
+ trigger:
+ action: enable
+ field: published
+ condition: checked
+ excerpt:
+ tab: 'rainlab.blog::lang.post.tab_manage'
+ label: 'rainlab.blog::lang.post.excerpt'
+ type: textarea
+ size: small
+ featured_images:
+ label: 'rainlab.blog::lang.post.featured_images'
+ mode: image
+ imageWidth: 200
+ imageHeight: 200
+ useCaption: true
+ thumbOptions:
+ mode: crop
+ extension: auto
+ span: left
+ type: fileupload
+ dependsOn: type
+ tab: 'rainlab.blog::lang.post.tab_manage'
+ type:
+ label: Type
+ options:
+ photo: Photo
+ video: Video
+ span: auto
+ default: photo
+ type: balloon-selector
+ tab: 'rainlab.blog::lang.post.tab_manage'
+ video:
+ label: Video
+ mode: file
+ span: left
+ type: mediafinder
+ dependsOn: type
+ tab: 'rainlab.blog::lang.post.tab_manage'
diff --git a/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_2.php b/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_2.php
new file mode 100644
index 0000000..14dbfe1
--- /dev/null
+++ b/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_2.php
@@ -0,0 +1,25 @@
+string('type')->nullable();
+ $table->string('video')->nullable();
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('rainlab_blog_posts', function($table)
+ {
+ $table->dropColumn('type');
+ $table->dropColumn('video');
+ });
+ }
+}
diff --git a/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_3.php b/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_3.php
new file mode 100644
index 0000000..2a45234
--- /dev/null
+++ b/plugins/rainlab/blog/updates/builder_table_update_rainlab_blog_posts_3.php
@@ -0,0 +1,23 @@
+string('type', 191)->default('photo')->change();
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('rainlab_blog_posts', function($table)
+ {
+ $table->string('type', 191)->default(null)->change();
+ });
+ }
+}
diff --git a/plugins/rainlab/blog/updates/version.yaml b/plugins/rainlab/blog/updates/version.yaml
index 70bbb90..2e58c67 100644
--- a/plugins/rainlab/blog/updates/version.yaml
+++ b/plugins/rainlab/blog/updates/version.yaml
@@ -70,3 +70,9 @@
1.6.3:
- 'Updated table rainlab_blog_posts'
- builder_table_update_rainlab_blog_posts.php
+1.6.4:
+ - 'Updated table rainlab_blog_posts'
+ - builder_table_update_rainlab_blog_posts_2.php
+1.6.5:
+ - 'Updated table rainlab_blog_posts'
+ - builder_table_update_rainlab_blog_posts_3.php
diff --git a/themes/hhm/pages/home.htm b/themes/hhm/pages/home.htm
new file mode 100644
index 0000000..c85b2a9
--- /dev/null
+++ b/themes/hhm/pages/home.htm
@@ -0,0 +1,6 @@
+title = "home"
+url = "/"
+is_hidden = 0
+robot_index = "index"
+robot_follow = "follow"
+==
\ No newline at end of file
diff --git a/themes/hhm/theme.yaml b/themes/hhm/theme.yaml
new file mode 100644
index 0000000..0865688
--- /dev/null
+++ b/themes/hhm/theme.yaml
@@ -0,0 +1,5 @@
+name: hhm
+description: ''
+author: ''
+homepage: ''
+code: ''