Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
PFTree
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 11
1640
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 addChild
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTreeFromWikiText
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 configArray
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 setParentsId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 setChildren
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 getFromTopCategory
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 addSubCategories
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 getCurValues
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 populateChildren
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getSubcategories
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * A class that defines a tree - and can populate it based on either
4 * wikitext or a category structure.
5 *
6 * @ingroup PFFormInput
7 *
8 * @author Yaron Koren
9 * @author Amr El-Absy
10 */
11class PFTree {
12    public $title;
13    public $children;
14    public $depth;
15    public $top_category;
16    /**
17     * @var array[]
18     * @phan-var list<array>
19     */
20    public $tree_array;
21    public $current_values;
22
23    public function __construct( $depth, $cur_values ) {
24        $this->depth = $depth;
25        $this->current_values = $cur_values;
26        $this->title = "";
27        $this->children = [];
28    }
29
30    public function addChild( $child ) {
31        $this->children[] = $child;
32    }
33
34    /**
35     * This Function takes the wikitext-styled bullets as a parameter, and converts it into
36     * an array which is used within the class to modify the data passed to JS.
37     * @param string $wikitext
38     */
39    public function getTreeFromWikiText( $wikitext ) {
40        $lines = explode( "\n", $wikitext );
41        $full_tree = [];
42        $temporary_values = [];
43        foreach ( $lines as $line ) {
44            $numBullets = 0;
45            for ( $i = 0; $i < strlen( $line ) && $line[$i] == '*'; $i++ ) {
46                $numBullets++;
47            }
48            $lineText = trim( substr( $line, $numBullets ) );
49            $full_tree[] = [ 'level' => $numBullets, "text" => $lineText ];
50
51            if ( in_array( $lineText, $this->current_values ) && !in_array( $lineText, $temporary_values ) ) {
52                $temporary_values[] = $lineText;
53            }
54        }
55        $this->tree_array = $full_tree;
56        $this->current_values = $temporary_values;
57        $this->configArray();
58        $this->setParentsId();
59        $this->setChildren();
60        // Get rid of array keys, to make this a regular array again, as jsTree requires.
61        // @phan-suppress-next-line PhanRedundantArrayValuesCall False positive: array_values() renumbers here to 0, 1, 2,…
62        $this->tree_array = array_values( $this->tree_array );
63    }
64
65    /**
66     * This function sets an ID for each element to be used in the function setParentsId()
67     * so that every child can know its parent
68     * This function also determine whether or not the node will be opened, depending on
69     * the attribute $depth.
70     * This function also determine whether or not the node is selected.
71     * @suppress PhanTypeInvalidDimOffset TODO Document tree_array
72     */
73    private function configArray() {
74        for ( $i = 0; $i < count( $this->tree_array ); $i++ ) {
75            $this->tree_array[$i]['node_id'] = $i;
76            if ( $this->tree_array[$i]['level'] <= $this->depth ) {
77                $this->tree_array[$i]['state']['opened'] = true;
78            }
79            if ( in_array( $this->tree_array[$i]['text'], $this->current_values ) ) {
80                $this->tree_array[$i]['state']['selected'] = true;
81            }
82        }
83    }
84
85    /**
86     * For the tree array that was generated from wikitext, the node doesn't know its parent
87     * although it's easy to know for the human.
88     * This function searches for the nodes and get the closest node of the parent level, and
89     * sets it as a parent.
90     * The parent ID will be used in the function setChildren() that adds every child to its
91     * parent's attribute "children"
92     */
93    private function setParentsId() {
94        $numNodes = count( $this->tree_array );
95        for ( $i = $numNodes - 1; $i >= 0; $i-- ) {
96            for ( $j = $i; $j >= 0; $j-- ) {
97                if ( $this->tree_array[$i]['level'] - $this->tree_array[$j]['level'] == 1 ) {
98                    $this->tree_array[$i]['parent_id'] = $this->tree_array[$j]['node_id'];
99                    break;
100                }
101            }
102        }
103    }
104
105    /**
106     * This function convert the attribute $tree_array from its so-called flat structure
107     * into tree-like structure, as every node has an attribute called "children" that holds
108     * the children of this node.
109     * The attribute "children" is important because it is used in the library jsTree.
110     */
111    private function setChildren() {
112        for ( $i = count( $this->tree_array ) - 1; $i >= 0; $i-- ) {
113            for ( $j = $i; $j >= 0; $j-- ) {
114                if ( isset( $this->tree_array[$i]['parent_id'] ) ) {
115                    if ( $this->tree_array[$i]['parent_id'] == $this->tree_array[$j]['node_id'] ) {
116                        if ( isset( $this->tree_array[$j]['children'] ) ) {
117                            array_unshift( $this->tree_array[$j]['children'], $this->tree_array[$i] );
118                        } else {
119                            $this->tree_array[$j]['children'][] = $this->tree_array[$i];
120                        }
121                        unset( $this->tree_array[$i] );
122                    }
123                }
124            }
125        }
126    }
127
128    /**
129     * This Function takes the Top Category name as a parameter, and generate
130     * tree_array, which is used within the class to modify the data passed to JS.
131     * @param string $top_category
132     * @param bool $hideroot
133     */
134    public function getFromTopCategory( $top_category, $hideroot ) {
135        $this->top_category = $top_category;
136        $this->populateChildren();
137
138        $this->tree_array[0]['text'] = $top_category;
139        if ( in_array( $top_category, $this->current_values ) ) {
140            $this->tree_array[0]['state']['selected'] = true;
141        }
142        $children = $this->children;
143        $this->tree_array[0]['level'] = 1;
144        $this->tree_array[0]['state']['opened'] = true;
145
146        $children = self::addSubCategories( $children, 2, $this->depth, $this->current_values );
147
148        $this->tree_array[0]['children'] = $children;
149
150        $this->current_values = self::getCurValues( $this->tree_array );
151
152        if ( $hideroot ) {
153            $this->tree_array = $this->tree_array[0]['children'];
154        }
155    }
156
157    /**
158     * This function handles adding the children of the nodes in the Top Category tree.
159     * Also, it determines whether or not the node is selected depending on $cur_values
160     * @param array $children
161     * @param int $level
162     * @param int $depth
163     * @param array $cur_values
164     * @return array
165     */
166    public static function addSubCategories( $children, $level, $depth, $cur_values ) {
167        $newChildren = [];
168        foreach ( $children as $child ) {
169            $is_selected = false;
170            if ( $cur_values !== null ) {
171                if ( in_array( $child->title, $cur_values ) ) {
172                    $is_selected = true;
173                    unset( $cur_values[ array_search( $child->title, $cur_values ) ] );
174                }
175            }
176
177            $newChild = [
178                'text' => $child->title,
179                'level' => $level,
180                'children' => self::addSubCategories( $child->children, $level + 1, $depth, $cur_values )
181            ];
182            $newChild['state']['opened'] = $level <= $depth;
183            if ( $is_selected ) {
184                $newChild['state']['selected'] = true;
185            }
186            $newChildren[] = $newChild;
187        }
188        return $newChildren;
189    }
190
191    private static function getCurValues( $tree ) {
192        $cur_values = [];
193        foreach ( $tree as $node ) {
194            if ( isset( $node['state']['selected'] ) && $node['state']['selected'] ) {
195                $cur_values[] = $node['text'];
196            }
197            if ( isset( $node['children'] ) ) {
198                $children = self::getCurValues( $node['children'] );
199                $cur_values = array_merge( $cur_values, $children );
200            }
201        }
202        return $cur_values;
203    }
204
205    /**
206     * Recursive function to populate a tree based on category information.
207     */
208    private function populateChildren() {
209        $subcats = self::getSubcategories( $this->top_category );
210        foreach ( $subcats as $subcat ) {
211            $childTree = new PFTree( $this->depth, $this->current_values );
212            $childTree->top_category = $subcat;
213            $childTree->title = $subcat;
214            $childTree->populateChildren();
215            $this->addChild( $childTree );
216        }
217    }
218
219    /**
220     * Gets all the subcategories of the passed-in category.
221     *
222     * @todo This might not belong in this class.
223     *
224     * @param string $categoryName
225     * @return array
226     */
227    private static function getSubcategories( $categoryName ) {
228        $dbr = PFUtils::getReadDB();
229
230        $tables = [ 'page', 'categorylinks' ];
231        $fields = [ 'page_id', 'page_namespace', 'page_title',
232            'page_is_redirect', 'page_len', 'page_latest', 'cl_to',
233            'cl_from' ];
234        $where = [];
235        $joins = [];
236        $options = [ 'ORDER BY' => 'cl_type, cl_sortkey' ];
237
238        $joins['categorylinks'] = [ 'JOIN', 'cl_from = page_id' ];
239        $where['cl_to'] = str_replace( ' ', '_', $categoryName );
240        $options['USE INDEX']['categorylinks'] = 'cl_sortkey';
241
242        $tables = array_merge( $tables, [ 'category' ] );
243        $fields = array_merge( $fields, [ 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files' ] );
244        $joins['category'] = [ 'LEFT JOIN', [ 'cat_title = page_title', 'page_namespace' => NS_CATEGORY ] ];
245
246        $res = $dbr->select( $tables, $fields, $where, __METHOD__, $options, $joins );
247        $subcats = [];
248
249        foreach ( $res as $row ) {
250            $t = Title::newFromRow( $row );
251            if ( $t->getNamespace() == NS_CATEGORY ) {
252                $subcats[] = $t->getText();
253            }
254        }
255        return $subcats;
256    }
257}