Commit 477c06c4 authored by alexpott's avatar alexpott

Issue #1498674 by plach, das-peter, Schnitzel, dawehner, YesCT, attiks,...

Issue #1498674 by plach, das-peter, Schnitzel, dawehner, YesCT, attiks, Berdir, Gábor Hojtsy, Soul88, Carsten Müller: Refactor node properties to multilingual.
parent 1d266317
......@@ -34,20 +34,20 @@
* results that need to be presented on multiple pages, and the Tablesort
* Extender for generating appropriate queries for sortable tables.
*
* For example, one might wish to return a list of the most recent 10 nodes
* For example, one might wish to return a list of the most recent 10 rows
* authored by a given user. Instead of directly issuing the SQL query
* @code
* SELECT n.nid, n.title, n.created FROM node n WHERE n.uid = $uid LIMIT 0, 10;
* SELECT e.id, e.title, e.created FROM example e WHERE e.uid = $uid LIMIT 0, 10;
* @endcode
* one would instead call the Drupal functions:
* @code
* $result = db_query_range('SELECT n.nid, n.title, n.created
* FROM {node} n WHERE n.uid = :uid', 0, 10, array(':uid' => $uid));
* $result = db_query_range('SELECT e.id, e.title, e.created
* FROM {example} e WHERE e.uid = :uid', 0, 10, array(':uid' => $uid));
* foreach ($result as $record) {
* // Perform operations on $record->title, etc. here.
* }
* @endcode
* Curly braces are used around "node" to provide table prefixing via
* Curly braces are used around "example" to provide table prefixing via
* DatabaseConnection::prefixTables(). The explicit use of a user ID is pulled
* out into an argument passed to db_query() so that SQL injection attacks
* from user input can be caught and nullified. The LIMIT syntax varies between
......@@ -69,7 +69,7 @@
*
* Named placeholders begin with a colon followed by a unique string. Example:
* @code
* SELECT nid, title FROM {node} WHERE uid=:uid;
* SELECT id, title FROM {example} WHERE uid=:uid;
* @endcode
*
* ":uid" is a placeholder that will be replaced with a literal value when
......@@ -81,7 +81,7 @@
*
* Unnamed placeholders are simply a question mark. Example:
* @code
* SELECT nid, title FROM {node} WHERE uid=?;
* SELECT id, title FROM {example} WHERE uid=?;
* @endcode
*
* In this case, the array of arguments must be an indexed array of values to
......@@ -91,11 +91,11 @@
* running a LIKE query the SQL wildcard character, %, should be part of the
* value, not the query itself. Thus, the following is incorrect:
* @code
* SELECT nid, title FROM {node} WHERE title LIKE :title%;
* SELECT id, title FROM {example} WHERE title LIKE :title%;
* @endcode
* It should instead read:
* @code
* SELECT nid, title FROM {node} WHERE title LIKE :title;
* SELECT id, title FROM {example} WHERE title LIKE :title;
* @endcode
* and the value for :title should include a % as appropriate. Again, note the
* lack of quotation marks around :title. Because the value is not inserted
......@@ -109,7 +109,7 @@
* object-oriented API for defining a query structurally. For example, rather
* than:
* @code
* INSERT INTO node (nid, title, body) VALUES (1, 'my title', 'my body');
* INSERT INTO {example} (id, uid, path, name) VALUES (1, 2, 'home', 'Home path');
* @endcode
* one would instead write:
* @code
......
......@@ -4921,26 +4921,26 @@ function _form_set_attributes(&$element, $class = array()) {
* $context['message'] = check_plain($node->label());
* }
*
* // More advanced example: multi-step operation - load all nodes, five by five
* // A more advanced example is a multi-step operation that loads all rows,
* // five by five.
* function my_function_2(&$context) {
* if (empty($context['sandbox'])) {
* $context['sandbox']['progress'] = 0;
* $context['sandbox']['current_node'] = 0;
* $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField();
* $context['sandbox']['current_id'] = 0;
* $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT id) FROM {example}')->fetchField();
* }
* $limit = 5;
* $result = db_select('node')
* ->fields('node', array('nid'))
* ->condition('nid', $context['sandbox']['current_node'], '>')
* ->orderBy('nid')
* $result = db_select('example')
* ->fields('example', array('id'))
* ->condition('id', $context['sandbox']['current_id'], '>')
* ->orderBy('id')
* ->range(0, $limit)
* ->execute();
* foreach ($result as $row) {
* $node = node_load($row->nid, TRUE);
* $context['results'][] = $node->nid . ' : ' . check_plain($node->label());
* $context['results'][] = $row->id . ' : ' . check_plain($row->title);
* $context['sandbox']['progress']++;
* $context['sandbox']['current_node'] = $node->nid;
* $context['message'] = check_plain($node->label());
* $context['sandbox']['current_id'] = $row->id;
* $context['message'] = check_plain($row->title);
* }
* if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
* $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
......
......@@ -1468,9 +1468,12 @@ function menu_tree_collect_node_links(&$tree, &$node_links) {
function menu_tree_check_access(&$tree, $node_links = array()) {
if ($node_links) {
$nids = array_keys($node_links);
$select = db_select('node', 'n');
$select = db_select('node_field_data', 'n');
$select->addField('n', 'nid');
// @todo This should be actually filtering on the desired node status field
// language and just fall back to the default language.
$select->condition('n.status', 1);
$select->condition('n.nid', $nids, 'IN');
$select->addTag('node_access');
$nids = $select->execute()->fetchCol();
......
......@@ -433,20 +433,8 @@ function drupal_write_record($table, &$record, $primary_keys = array()) {
// Type cast to proper datatype, except when the value is NULL and the
// column allows this.
//
// MySQL PDO silently casts e.g. FALSE and '' to 0 when inserting the value
// into an integer column, but PostgreSQL PDO does not. Also type cast NULL
// when the column does not allow this.
if (isset($object->$field) || !empty($info['not null'])) {
if ($info['type'] == 'int' || $info['type'] == 'serial') {
$fields[$field] = (int) $fields[$field];
}
elseif ($info['type'] == 'float') {
$fields[$field] = (float) $fields[$field];
}
else {
$fields[$field] = (string) $fields[$field];
}
$fields[$field] = drupal_schema_get_field_value($info, $fields[$field]);
}
}
......@@ -521,6 +509,34 @@ function drupal_write_record($table, &$record, $primary_keys = array()) {
return $return;
}
/**
* Typecasts values to proper datatypes.
*
* MySQL PDO silently casts, e.g. FALSE and '' to 0, when inserting the value
* into an integer column, but PostgreSQL PDO does not. Look up the schema
* information and use that to correctly typecast the value.
*
* @param array $info
* An array describing the schema field info.
* @param mixed $value
* The value to be converted.
*
* @return mixed
* The converted value.
*/
function drupal_schema_get_field_value(array $info, $value) {
if ($info['type'] == 'int' || $info['type'] == 'serial') {
$value = (int) $value;
}
elseif ($info['type'] == 'float') {
$value = (float) $value;
}
else {
$value = (string) $value;
}
return $value;
}
/**
* @} End of "addtogroup schemaapi".
*/
......@@ -32,16 +32,16 @@ public function orderRandom() {
* yet selected.
*
* @code
* $query = db_select('node', 'n');
* $query->join('node_revision', 'nr', 'n.vid = nr.vid');
* $query = db_select('example', 'e');
* $query->join('example_revision', 'er', 'e.vid = er.vid');
* $query
* ->distinct()
* ->fields('n')
* ->fields('e')
* ->orderBy('timestamp');
* @endcode
*
* In this query, it is not possible (without relying on the schema) to know
* whether timestamp belongs to node_revisions and needs to be added or
* whether timestamp belongs to example_revision and needs to be added or
* belongs to node and is already selected. Queries like this will need to be
* corrected in the original query by adding an explicit call to
* SelectQuery::addField() or SelectQuery::fields().
......
......@@ -32,7 +32,7 @@
* The following keys are defined:
* - 'description': A string in non-markup plain text describing this table
* and its purpose. References to other tables should be enclosed in
* curly-brackets. For example, the node_revisions table
* curly-brackets. For example, the node_field_revision table
* description field might contain "Stores per-revision title and
* body data for each {node}."
* - 'fields': An associative array ('fieldname' => specification)
......@@ -42,7 +42,7 @@
* and its purpose. References to other tables should be enclosed in
* curly-brackets. For example, the node table vid field
* description might contain "Always holds the largest (most
* recent) {node_revision}.vid value for this nid."
* recent) {node_field_revision}.vid value for this nid."
* - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int',
* 'float', 'numeric', or 'serial'. Most types just map to the according
* database engine specific datatypes. Use 'serial' for auto incrementing
......@@ -150,7 +150,7 @@
* ),
* 'foreign keys' => array(
* 'node_revision' => array(
* 'table' => 'node_revision',
* 'table' => 'node_field_revision',
* 'columns' => array('vid' => 'vid'),
* ),
* 'node_author' => array(
......
......@@ -289,7 +289,7 @@ public function loadRevision($revision_id) {
// which attaches fields (if supported by the entity type) and calls the
// entity type specific load callback, for example hook_node_load().
if (!empty($queried_entities)) {
$this->attachLoad($queried_entities, TRUE);
$this->attachLoad($queried_entities, $revision_id);
}
return reset($queried_entities);
}
......
......@@ -328,6 +328,7 @@ public function getTranslation($langcode, $strict = TRUE) {
*/
public function getTranslationLanguages($include_default = TRUE) {
$translations = array();
$definitions = $this->getPropertyDefinitions();
// Build an array with the translation langcodes set as keys. Empty
// translations should not be included and must be skipped.
foreach ($this->getProperties() as $name => $property) {
......@@ -339,7 +340,7 @@ public function getTranslationLanguages($include_default = TRUE) {
foreach ($this->values[$name] as $langcode => $values) {
// If a value is there but the field object is empty, it has been
// unset, so we need to skip the field also.
if ($values && !(isset($this->fields[$name][$langcode]) && $this->fields[$name][$langcode]->isEmpty())) {
if ($values && !empty($definitions[$name]['translatable']) && !(isset($this->fields[$name][$langcode]) && $this->fields[$name][$langcode]->isEmpty())) {
$translations[$langcode] = TRUE;
}
}
......
......@@ -37,7 +37,14 @@ public static function getInfo() {
public function testBulkForm() {
$nodes = array();
for ($i = 0; $i < 10; $i++) {
$nodes[] = $this->drupalCreateNode(array('sticky' => FALSE));
// Ensure nodes are sorted in the same order they are inserted in the
// array.
$timestamp = REQUEST_TIME - $i;
$nodes[] = $this->drupalCreateNode(array(
'sticky' => FALSE,
'created' => $timestamp,
'changed' => $timestamp,
));
}
$this->drupalGet('test_bulk_form');
......
......@@ -56,7 +56,7 @@ display:
fields:
title:
id: title
table: node
table: node_field_data
field: title
label: ''
alter:
......@@ -124,7 +124,7 @@ display:
filters:
status:
value: '1'
table: node
table: node_field_data
field: status
id: status
expose:
......@@ -134,7 +134,7 @@ display:
sorts:
created:
id: created
table: node
table: node_field_data
field: created
order: DESC
plugin_id: date
......
......@@ -137,7 +137,7 @@ function getFeedEditObject($feed_url = NULL, array $values = array()) {
*/
function getDefaultFeedItemCount() {
// Our tests are based off of rss.xml, so let's find out how many elements should be related.
$feed_count = db_query_range('SELECT COUNT(*) FROM {node} n WHERE n.promote = 1 AND n.status = 1', 0, config('system.rss')->get('items.limit'))->fetchField();
$feed_count = db_query_range('SELECT COUNT(DISTINCT nid) FROM {node_field_data} n WHERE n.promote = 1 AND n.status = 1', 0, config('system.rss')->get('items.limit'))->fetchField();
return $feed_count > 10 ? 10 : $feed_count;
}
......
......@@ -35,14 +35,12 @@ protected function preSaveRevision(\stdClass $record, EntityInterface $entity) {
$record->log = '';
}
}
elseif (!isset($record->log) || $record->log === '') {
elseif (isset($entity->original) && (!isset($record->log) || $record->log === '')) {
// If we are updating an existing custom_block without adding a new
// revision, we need to make sure $entity->log is unset whenever it is
// empty. As long as $entity->log is unset, drupal_write_record() will not
// attempt to update the existing database column when re-saving the
// revision; therefore, this code allows us to avoid clobbering an
// existing log entry with an empty one.
unset($record->log);
// revision, we need to make sure $entity->log is reset whenever it is
// empty. Therefore, this code allows us to avoid clobbering an existing
// log entry with an empty one.
$record->log = $entity->original->log->value;
}
}
......
......@@ -268,35 +268,7 @@ function book_admin_paths() {
* An array of all books.
*/
function book_get_books() {
$all_books = &drupal_static(__FUNCTION__);
if (!isset($all_books)) {
$all_books = array();
$nids = db_query("SELECT DISTINCT(bid) FROM {book}")->fetchCol();
if ($nids) {
$query = db_select('book', 'b', array('fetch' => PDO::FETCH_ASSOC));
$query->join('node', 'n', 'b.nid = n.nid');
$query->join('menu_links', 'ml', 'b.mlid = ml.mlid');
$query->addField('n', 'type', 'type');
$query->addField('n', 'title', 'title');
$query->fields('b');
$query->fields('ml');
$query->condition('n.nid', $nids, 'IN');
$query->condition('n.status', 1);
$query->orderBy('ml.weight');
$query->orderBy('ml.link_title');
$query->addTag('node_access');
$result2 = $query->execute();
foreach ($result2 as $link) {
$link['href'] = $link['link_path'];
$link['options'] = unserialize($link['options']);
$all_books[$link['bid']] = $link;
}
}
}
return $all_books;
return Drupal::service('book.manager')->getAllBooks();
}
/**
......
services:
book.manager:
class: Drupal\book\BookManager
arguments: ['@database']
arguments: ['@database', '@plugin.manager.entity']
......@@ -6,8 +6,9 @@
namespace Drupal\book;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Book Manager Service.
......@@ -21,6 +22,13 @@ class BookManager {
*/
protected $database;
/**
* Entity manager Service Object.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* Books Array.
*
......@@ -31,8 +39,9 @@ class BookManager {
/**
* Constructs a BookManager object.
*/
public function __construct(Connection $database) {
public function __construct(Connection $database, EntityManager $entityManager) {
$this->database = $database;
$this->entityManager = $entityManager;
}
/**
......@@ -57,24 +66,30 @@ public function getAllBooks() {
protected function loadBooks() {
$this->books = array();
$nids = $this->database->query("SELECT DISTINCT(bid) FROM {book}")->fetchCol();
if ($nids) {
$query = $this->database->select('book', 'b', array('fetch' => \PDO::FETCH_ASSOC));
$query->join('node', 'n', 'b.nid = n.nid');
$query->join('menu_links', 'ml', 'b.mlid = ml.mlid');
$query->addField('n', 'type', 'type');
$query->addField('n', 'title', 'title');
$query->fields('b');
$query->fields('ml');
$query->condition('n.nid', $nids, 'IN');
$query->condition('n.status', 1);
$query->condition('b.nid', $nids);
$query->orderBy('ml.weight');
$query->orderBy('ml.link_title');
$query->addTag('node_access');
$query->addMetaData('base_table', 'book');
$book_links = $query->execute();
$nodes = $this->entityManager->getStorageController('node')->load($nids);
foreach ($book_links as $link) {
$link['href'] = $link['link_path'];
$link['options'] = unserialize($link['options']);
$this->books[$link['bid']] = $link;
$nid = $link['nid'];
if (isset($nodes[$nid]) && $nodes[$nid]->status) {
$link['href'] = $link['link_path'];
$link['options'] = unserialize($link['options']);
$link['title'] = $nodes[$nid]->label();
$link['type'] = $nodes[$nid]->bundle();
$this->books[$link['bid']] = $link;
}
}
}
}
......
......@@ -96,12 +96,12 @@ protected function blockBuild() {
elseif ($current_bid) {
// Only display this block when the user is browsing a book.
$select = db_select('node', 'n')
->fields('n', array('title'))
->fields('n', array('nid'))
->condition('n.nid', $node->book['bid'])
->addTag('node_access');
$title = $select->execute()->fetchField();
$nid = $select->execute()->fetchField();
// Only show the block if the user has view access for the top-level node.
if ($title) {
if ($nid) {
$tree = menu_tree_all_data($node->book['menu_name'], $node->book);
// There should only be one element at the top level.
$data = array_shift($tree);
......
......@@ -82,24 +82,27 @@ function comment_admin_overview($form, &$form_state, $arg) {
$query = db_select('comment', 'c')
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->extend('Drupal\Core\Database\Query\TableSortExtender');
$query->join('node', 'n', 'n.nid = c.nid');
$query->addField('n', 'title', 'node_title');
$query->join('node_field_data', 'n', 'n.nid = c.nid');
$query->addTag('node_access');
$result = $query
->fields('c', array('cid', 'subject', 'name', 'changed'))
->fields('c', array('cid', 'nid', 'subject', 'name', 'changed'))
->condition('c.status', $status)
->limit(50)
->orderByHeader($header)
->execute();
$nids = array();
$cids = array();
// We collect a sorted list of node_titles during the query to attach to the
// comments later.
foreach ($result as $row) {
$nids[] = $row->nid;
$cids[] = $row->cid;
$node_titles[] = $row->node_title;
}
// Ensure all nodes are statically cached so that we do not have to load them
// individually when getting their labels below.
node_load_multiple($nids);
$comments = comment_load_multiple($cids);
// Build a table listing the appropriate comments.
......@@ -109,7 +112,7 @@ function comment_admin_overview($form, &$form_state, $arg) {
foreach ($comments as $comment) {
// Remove the first node title from the node_titles array and attach to
// the comment.
$node_title = array_shift($node_titles);
$node_title = $comment->nid->entity->label();
$options[$comment->id()] = array(
'subject' => array(
'data' => array(
......
......@@ -37,8 +37,8 @@ function comment_uninstall() {
*/
function comment_enable() {
// Insert records into the node_comment_statistics for nodes that are missing.
$query = db_select('node', 'n');
$query->leftJoin('node_comment_statistics', 'ncs', 'ncs.nid = n.nid');
$query = db_select('node_field_data', 'n');
$query->leftJoin('node_comment_statistics', 'ncs', 'ncs.nid = n.nid AND n.default_langcode = 1');
$query->addField('n', 'created', 'last_comment_timestamp');
$query->addField('n', 'uid', 'last_comment_uid');
$query->addField('n', 'nid');
......
......@@ -452,13 +452,16 @@ function comment_permalink(Comment $comment) {
*/
function comment_get_recent($number = 10) {
$query = db_select('comment', 'c');
$query->innerJoin('node', 'n', 'n.nid = c.nid');
$query->innerJoin('node_field_data', 'n', 'n.nid = c.nid');
$query->addTag('node_access');
$query->addMetaData('base_table', 'comment');
$comments = $query
->fields('c')
->condition('c.status', COMMENT_PUBLISHED)
->condition('n.status', NODE_PUBLISHED)
// @todo This should be actually filtering on the desired node status field
// language and just fall back to the default language.
->condition('n.default_langcode', 1)
->orderBy('c.created', 'DESC')
// Additionally order by cid to ensure that comments with the same timestamp
// are returned in the exact order posted.
......
......@@ -503,7 +503,7 @@ function comment_views_data_alter(&$data) {
),
);
$data['node']['comment'] = array(
$data['node_field_data']['comment'] = array(
'title' => t('Comment status'),
'help' => t('Whether comments are enabled or disabled on the node.'),
'field' => array(
......@@ -517,7 +517,7 @@ function comment_views_data_alter(&$data) {
),
);
$data['node']['uid_touch'] = array(
$data['node_field_data']['uid_touch'] = array(
'title' => t('User posted or commented'),
'help' => t('Display nodes only if a user posted the node or commented on the node.'),
'argument' => array(
......
......@@ -232,7 +232,7 @@ protected function updateNodeStatistics($nid) {
}
else {
// Comments do not exist.
$node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
$node = db_query('SELECT uid, created FROM {node_field_data} WHERE nid = :nid LIMIT 1', array(':nid' => $nid))->fetchObject();
db_update('node_comment_statistics')
->fields(array(
'cid' => 0,
......
......@@ -51,7 +51,7 @@ public function entityQueryAlter(SelectInterface $query) {
// The Comment module doesn't implement any proper comment access,
// and as a consequence doesn't make sure that comments cannot be viewed
// when the user doesn't have access to the node.
$node_alias = $query->innerJoin('node', 'n', '%alias.nid = ' . $base_table . '.nid');
$node_alias = $query->innerJoin('node_field_data', 'n', '%alias.nid = ' . $base_table . '.nid');
// Pass the query to the node access control.
$this->reAlterQuery($query, 'node_access', $node_alias);
......
......@@ -58,7 +58,7 @@ class Comment extends WizardPluginBase {
),
'status_node' => array(
'value' => TRUE,
'table' => 'node',
'table' => 'node_field_data',
'field' => 'status',
'relationship' => 'nid'
)
......
......@@ -84,11 +84,19 @@ public function setUp() {
$comment->comment_body->value = 'Test body ' . $i;
$comment->comment_body->format = 'full_html';
// Ensure comments are sorted in ascending order.
$time = REQUEST_TIME + ($this->masterDisplayResults - $i);
$comment->created->value = $time;
$comment->changed->value = $time;
comment_save($comment);
}
// Store all the nodes just created to access their properties on the tests.
$this->commentsCreated = entity_load_multiple('comment');
// Sort created comments in ascending order.
ksort($this->commentsCreated, SORT_NUMERIC);
}
/**
......
......@@ -36,11 +36,11 @@ function testCommentUserUIDTest() {
$options = array(
'id' => 'uid_touch',
'table' => 'node',
'table' => 'node_field_data',
'field' => 'uid_touch',
'value' => array($this->loggedInUser->uid),
);
$view->addItem('default', 'filter', 'node', 'uid_touch', $options);
$view->addItem('default', 'filter', 'node_field_data', 'uid_touch', $options);
$this->executeView($view, array($this->account->uid));
$result_set = array(
array(
......
......@@ -80,7 +80,7 @@ public function testCommentWizard() {
$this->assertEqual($view->filter['status']->table, 'comment');
$this->assertEqual($view->filter['status']->field, 'status');
$this->assertTrue($view->filter['status']->value);
$this->assertEqual($view->filter['status_node']->table, 'node');
$this->assertEqual($view->filter['status_node']->table, 'node_field_data');
$this->assertEqual($view->filter['status_node']->field, 'status');
$this->assertTrue($view->filter['status_node']->value);
......
......@@ -18,7 +18,7 @@ display:
number_of_records: '0'
summary_options:
items_per_page: '25'
table: node
table: node_field_data
plugin_id: argument_comment_user_uid
cache:
type: none
......
......@@ -32,7 +32,7 @@ display:
fields:
title:
id: title
table: node
table: node_field_data
field: title
label: ''
alter:
......@@ -50,7 +50,7 @@ display:
filters:
status:
value: '1'
table: node
table: node_field_data
field: status
id: status
expose:
......@@ -59,7 +59,7 @@ display:
sorts: