ContentsTable::operatorPromote()   C
last analyzed

Complexity

Conditions 7
Paths 24

Size

Total Lines 24
Code Lines 16

Duplication

Lines 9
Ratio 37.5 %

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 24
nop 2
dl 9
loc 24
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
namespace Content\Model\Table;
13
14
use Cake\Event\Event;
15
use Cake\ORM\Entity;
16
use Cake\ORM\Query;
17
use Cake\ORM\Table;
18
use Cake\ORM\TableRegistry;
19
use Cake\Validation\Validator;
20
use Search\Parser\TokenInterface;
21
use \ArrayObject;
22
23
/**
24
 * Represents "contents" database table.
25
 *
26
 * @property \Content\Model\Table\ContentTypesTable $ContentTypes
27
 * @property \Content\Model\Table\ContentsTable $TranslationOf
28
 * @property \User\Model\Table\RolesTable $Roles
29
 * @property \User\Model\Table\ContentRevisionsTable $ContentRevisions
30
 * @property \User\Model\Table\ContentsTable $Translations
31
 * @property \User\Model\Table\UsersTable $Author
32
 * @property \User\Model\Table\UsersTable $ModifiedBy
33
 * @method \Search\Engine\BaseEngine searchEngine(\Search\Engine\BaseEngine $engine = null)
34
 * @method \Cake\ORM\Query search(string $criteria, \Cake\ORM\Query|null $query = null)
35
 * @method \Cake\ORM\Query applySearchOperator(\Cake\ORM\Query $query, \Search\Parser\TokenInterface $token)
36
 * @method void addSearchOperator(string $name, mixed $handler, array $options = [])
37
 * @method void enableSearchOperator(string $name)
38
 * @method void disableSearchOperator(string $name)
39
 * @method void bindComments()
40
 * @method void unbindComments()
41
 * @method void fieldable()
42
 * @method \Cake\Datasource\EntityInterface attachFields(\Cake\Datasource\EntityInterface $entity)
43
 * @method \Cake\Datasource\ResultSetDecorator findComments(\Cake\ORM\Query $query, $options)
44
 */
45
class ContentsTable extends Table
46
{
47
48
    /**
49
     * List of implemented events.
50
     *
51
     * @return array
52
     */
53
    public function implementedEvents()
54
    {
55
        $events = [
56
            'Model.beforeSave' => [
57
                'callable' => 'beforeSave',
58
                'priority' => 16 // after Fieldable Behavior
59
            ]
60
        ];
61
62
        return $events;
63
    }
64
65
    /**
66
     * Initialize a table instance. Called after the constructor.
67
     *
68
     * @param array $config Configuration options passed to the constructor
69
     * @return void
70
     */
71
    public function initialize(array $config)
72
    {
73
        $this->belongsTo('ContentTypes', [
74
            'className' => 'Content.ContentTypes',
75
            'propertyName' => 'content_type',
76
            'fields' => ['slug', 'name', 'description'],
77
            'conditions' => ['Contents.content_type_slug = ContentTypes.slug']
78
        ]);
79
80
        $this->belongsTo('TranslationOf', [
81
            'className' => 'Content\Model\Table\ContentsTable',
82
            'foreignKey' => 'translation_for',
83
            'propertyName' => 'translation_of',
84
            'fields' => ['slug', 'title', 'description'],
85
        ]);
86
87
        $this->belongsToMany('Roles', [
88
            'className' => 'User.Roles',
89
            'propertyName' => 'roles',
90
            'through' => 'Content.ContentsRoles',
91
        ]);
92
93
        $this->hasMany('ContentRevisions', [
94
            'className' => 'Content.ContentRevisions',
95
            'dependent' => true,
96
        ]);
97
98
        $this->hasMany('Translations', [
99
            'className' => 'Content\Model\Table\ContentsTable',
100
            'foreignKey' => 'translation_for',
101
            'dependent' => true,
102
        ]);
103
104
        $this->addBehavior('Timestamp');
105
        $this->addBehavior('Comment.Commentable');
106
        $this->addBehavior('Sluggable');
107
        $this->addBehavior('User.WhoDidIt', [
108
            'idCallable' => function () {
109
                return user()->get('id');
110
            }
111
        ]);
112
        $this->addBehavior('Field.Fieldable', [
113
            'bundle' => function ($entity) {
114
                if ($entity->has('content_type_slug')) {
115
                    return $entity->content_type_slug;
116
                }
117
118
                if ($entity->has('id')) {
119
                    return $this
120
                        ->get($entity->id, [
121
                            'fields' => ['id', 'content_type_slug'],
122
                            'fieldable' => false,
123
                        ])
124
                        ->content_type_slug;
125
                }
126
127
                return '';
128
            }
129
        ]);
130
131
        $this->addBehavior('Search.Searchable', [
132
            'fields' => function ($content) {
133
                $words = '';
134
                if ($content->has('title')) {
135
                    $words .= " {$content->title}";
136
                }
137
138
                if ($content->has('description')) {
139
                    $words .= " {$content->description}";
140
                }
141
142
                if (!$content->has('_fields')) {
143
                    return $words;
144
                }
145
146
                foreach ($content->_fields as $virtualField) {
147
                    $words .= " {$virtualField}";
148
                }
149
150
                return $words;
151
            }
152
        ]);
153
154
        $this->addSearchOperator('promote', 'operatorPromote');
155
        $this->addSearchOperator('author', 'operatorAuthor');
156
        $this->addSearchOperator('limit', 'Search.Limit');
157
        $this->addSearchOperator('modified', 'Search.Date', ['field' => 'modified']);
158
        $this->addSearchOperator('created', 'Search.Date', ['field' => 'created']);
159
        $this->addSearchOperator('type', 'Search.Generic', ['field' => 'content_type_slug', 'conjunction' => 'auto']);
160
        $this->addSearchOperator('term', 'operatorTerm');
161
        $this->addSearchOperator('language', 'Search.Generic', ['field' => 'language', 'conjunction' => 'auto']);
162
        $this->addSearchOperator('order', 'Search.Order', ['fields' => ['slug', 'title', 'description', 'sticky', 'created', 'modified']]);
163
    }
164
165
    /**
166
     * Default validation rules set.
167
     *
168
     * @param \Cake\Validation\Validator $validator The validator object
169
     * @return \Cake\Validation\Validator
170
     */
171 View Code Duplication
    public function validationDefault(Validator $validator)
172
    {
173
        $validator
174
            ->add('title', [
175
                'notBlank' => [
176
                    'rule' => 'notBlank',
177
                    'message' => __d('content', 'You need to provide a title.'),
178
                ],
179
                'length' => [
180
                    'rule' => ['minLength', 3],
181
                    'message' => __d('content', 'Title need to be at least 3 characters long.'),
182
                ],
183
            ]);
184
185
        return $validator;
186
    }
187
188
    /**
189
     * This callback performs two action, saving revisions and checking publishing
190
     * constraints.
191
     *
192
     * - Saves a revision version of each content being saved if it has changed.
193
     *
194
     * - Verifies the publishing status and forces to be "false" if use has no
195
     *   publishing permissions for this content type.
196
     *
197
     * @param \Cake\Event\Event $event The event that was triggered
198
     * @param \Content\Model\Entity\Content $entity The entity being saved
199
     * @param \ArrayObject $options Array of options
200
     * @return bool True on success
201
     */
202
    public function beforeSave(Event $event, Entity $entity, ArrayObject $options = null)
203
    {
204
        $this->_saveRevision($entity);
205
        $this->_ensureStatus($entity);
206
207
        return true;
208
    }
209
210
    /**
211
     * Tries to create a revision for the given content.
212
     *
213
     * @param \Cake\ORM\Entity $entity The content
214
     * @return void
215
     */
216
    protected function _saveRevision(Entity $entity)
217
    {
218
        if ($entity->isNew()) {
219
            return;
220
        }
221
222
        try {
223
            $prev = TableRegistry::get('Content.Contents')->get($entity->id);
224
            $hash = $this->_calculateHash($prev);
225
            $exists = $this->ContentRevisions->exists([
226
                'ContentRevisions.content_id' => $entity->id,
227
                'ContentRevisions.hash' => $hash,
228
            ]);
229
230
            if (!$exists) {
231
                $revision = $this->ContentRevisions->newEntity([
232
                    'content_id' => $prev->id,
233
                    'summary' => $entity->get('edit_summary'),
234
                    'data' => $prev,
235
                    'hash' => $hash,
236
                ]);
237
238
                if (!$this->ContentRevisions->hasBehavior('Timestamp')) {
239
                    $this->ContentRevisions->addBehavior('Timestamp');
240
                }
241
                $this->ContentRevisions->save($revision);
242
            }
243
        } catch (\Exception $ex) {
244
            // unable to create content's review
245
        }
246
    }
247
248
    /**
249
     * Ensures that content content has the correct publishing status based in content
250
     * type restrictions.
251
     *
252
     * If it's a new content it will set the correct status. However if it's an
253
     * existing content and user has no publishing permissions this method will not
254
     * change content's status, so it will remain published if it was already
255
     * published by an administrator.
256
     *
257
     * @param \Cake\ORM\Entity $entity The content
258
     * @return void
259
     */
260
    protected function _ensureStatus(Entity $entity)
261
    {
262
        if (!$entity->has('status')) {
263
            return;
264
        }
265
266
        if (!$entity->has('content_type') &&
267
            ($entity->has('content_type_id') || $entity->has('content_type_slug'))
268
        ) {
269
            if ($entity->has('content_type_id')) {
270
                $type = $this->ContentTypes->get($entity->get('content_type_id'));
271
            } else {
272
                $type = $this->ContentTypes
273
                    ->find()
274
                    ->where(['content_type_slug' => $entity->get('content_type_id')])
275
                    ->limit(1)
276
                    ->first();
277
            }
278
        } else {
279
            $type = $entity->get('content_type');
280
        }
281
282
        if ($type && !$type->userAllowed('publish')) {
283
            if ($entity->isNew()) {
284
                $entity->set('status', false);
285
            } else {
286
                $entity->unsetProperty('status');
287
            }
288
        }
289
    }
290
291
    /**
292
     * Handles "promote" search operator.
293
     *
294
     *     promote:<true|false>
295
     *
296
     * @param \Cake\ORM\Query $query The query object
297
     * @param \Search\Parser\TokenInterface $token Operator token
298
     * @return \Cake\ORM\Query
299
     */
300
    public function operatorPromote(Query $query, TokenInterface $token)
301
    {
302
        $value = strtolower($token->value());
303
        $conjunction = $token->negated() ? '<>' : '';
304
        $conditions = [];
305
306
        if ($value === 'true') {
307
            $conditions = ["Contents.promote {$conjunction}" => 1];
308
        } elseif ($value === 'false') {
309
            $conditions = ['Contents.promote {$conjunction}' => 0];
310
        }
311
312 View Code Duplication
        if (!empty($conditions)) {
313
            if ($token->where() === 'or') {
314
                $query->orWhere($conditions);
315
            } elseif ($token->where() === 'and') {
316
                $query->andWhere($conditions);
317
            } else {
318
                $query->where($conditions);
319
            }
320
        }
321
322
        return $query;
323
    }
324
325
    /**
326
     * Handles "author" search operator.
327
     *
328
     *     author:<username1>,<username2>, ...
329
     *
330
     * @param \Cake\ORM\Query $query The query object
331
     * @param \Search\Parser\TokenInterface $token Operator token
332
     * @return \Cake\ORM\Query
333
     */
334
    public function operatorAuthor(Query $query, TokenInterface $token)
335
    {
336
        $value = explode(',', $token->value());
337
338
        if (!empty($value)) {
339
            $conjunction = $token->negated() ? 'NOT IN' : 'IN';
340
            $subQuery = TableRegistry::get('User.Users')->find()
341
                ->select(['id'])
342
                ->where(["Users.username {$conjunction}" => $value]);
343
344
            if ($token->where() === 'or') {
345
                $query->orWhere(['Contents.created_by IN' => $subQuery]);
346
            } elseif ($token->where() === 'and') {
347
                $query->andWhere(['Contents.created_by IN' => $subQuery]);
348
            } else {
349
                $query->where(['Contents.created_by IN' => $subQuery]);
350
            }
351
        }
352
353
        return $query;
354
    }
355
356
    /**
357
     * Handles "term" search operator.
358
     *
359
     *     term:term1-slug,term2-slug,...
360
     *
361
     * @param \Cake\ORM\Query $query The query object
362
     * @param \Search\Parser\TokenInterface $token Operator token
363
     * @return \Cake\ORM\Query
364
     */
365
    public function operatorterm(Query $query, TokenInterface $token)
366
    {
367
        $terms = explode(',', strtolower($token->value()));
368
        $conjunction = $token->negated() ? 'NOT IN' : 'IN';
369
370
        if (empty($terms)) {
371
            return $query;
372
        }
373
374
        $conditions = [
375
            "Contents.id {$conjunction}" => TableRegistry::get('Taxonomy.EntitiesTerms')
376
                ->find()
377
                ->select(['EntitiesTerms.entity_id'])
378
                ->where(['EntitiesTerms.table_alias' => $this->alias()])
379
                ->matching('Terms', function ($q) use ($terms) {
380
                    return $q->where(['Terms.slug IN' => $terms]);
381
                })
382
        ];
383
384 View Code Duplication
        if (!empty($conditions)) {
385
            if ($token->where() === 'or') {
386
                $query->orWhere($conditions);
387
            } elseif ($token->where() === 'and') {
388
                $query->andWhere($conditions);
389
            } else {
390
                $query->where($conditions);
391
            }
392
        }
393
394
        return $query;
395
    }
396
397
    /**
398
     * Generates a unique hash for the given entity.
399
     *
400
     * Used by revision system to detect if an entity has changed or not.
401
     *
402
     * @param \Cake\Datasource\EntityInterface $entity The entity for which calculate its hash
403
     * @return string MD5 hash for this particular entity
404
     */
405
    protected function _calculateHash($entity)
406
    {
407
        $hash = [];
408
        foreach ($entity->visibleProperties() as $property) {
409
            if (strpos($property, 'created') === false &&
410
                strpos($property, 'created_by') === false &&
411
                strpos($property, 'modified') === false &&
412
                strpos($property, 'modified_by') === false
413
            ) {
414
                if ($property == '_fields') {
415
                    foreach ($entity->get('_fields') as $field) {
416
                        if ($field instanceof \Field\Model\Entity\Field) {
417
                            $hash[] = is_object($field->value) || is_array($field->value) ? md5(serialize($field->value)) : md5($field->value);
418
                        }
419
                    }
420
                } else {
421
                    $hash[] = $entity->get($property);
422
                }
423
            }
424
        }
425
426
        return md5(serialize($hash));
427
    }
428
}
429