PluginProbe
Internal Link Juicer: SEO Auto Linker for WordPress / trunk
Internal Link Juicer: SEO Auto Linker for WordPress vtrunk
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.5.2 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.15.1 1.2.16 1.2.17 All 75 releases
internal-links / core / linkbuilder.php

linkbuilder.php in Internal Link Juicer: SEO Auto Linker for WordPress trunk, at core/linkbuilder.php

430 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ILJ\Core;
3
4 use ILJ\Core\Links\Text_To_Link_Converter_Interface;
5 use ILJ\Core\Links\Timeout_Monitor_Layer;
6 use ILJ\Core\Options;
7 use ILJ\Helper\Encoding;
8 use ILJ\Type\Ruleset;
9 use ILJ\Database\Linkindex;
10 use ILJ\Helper\Replacement;
11 use ILJ\Backend\Editor;
12 use ILJ\Database\LinkindexIndividualTemp;
13 use ILJ\Database\LinkindexTemp;
14 use ILJ\Helper\IndexAsset;
15 use ILJ\Helper\LinkBuilding;
16
17 /**
18 * The below line sets the `ticks` directive to prevent timeout occurring on the frontend.
19 * This is set for the complete file, @see https://www.php.net/manual/en/control-structures.declare.php
20 * This is needed for {@link Timeout_Monitor_Layer} to work, You might wonder why I didn't set this
21 * on {@link Timeout_Monitor_Layer}. The reason for this is that the ticks won't be propagated to code in other files.
22 * For example,
23 *
24 * `declare(ticks=1000) {
25 * $a = 20;
26 * $foo = new Foo()
27 * $foo->bar();
28 * }`
29 *
30 * You might expect all statements inside bar() method would be tickable, but it won't behave like that.
31 * You would need to set the ticks directive on the top of Foo.php in order to get
32 * the same behavior.
33 */
34 declare(ticks=1000);
35
36 /**
37 * The main LinkBuilder class
38 *
39 * Is responsible for editing a piece of content and setting links within by a given Ruleset
40 *
41 * @package ILJ\Core
42 * @since 1.0.0
43 */
44 class LinkBuilder implements Text_To_Link_Converter_Interface {
45
46 /**
47 * ID (post id / term id)
48 *
49 * @var int
50 * @since 1.0.0
51 */
52 private $id = null;
53
54 /**
55 * Type (post/term)
56 *
57 * @var string
58 * @since 1.0.1
59 */
60 private $type = null;
61
62 /**
63 * Link Ruleset
64 *
65 * @var Ruleset
66 * @since 1.0.0
67 */
68 private $link_ruleset = null;
69
70 /**
71 * Replace Ruleset
72 *
73 * @var Ruleset
74 * @since 1.0.0
75 */
76 private $replace_ruleset = null;
77
78 /**
79 * Content
80 *
81 * @var string
82 * @since 1.0.0
83 */
84 private $content = '';
85
86 /**
87 * Link page count
88 *
89 * @var int
90 */
91 private $link_page_count = 0;
92
93 /**
94 * Link target count
95 *
96 * @var array
97 */
98 private $link_target_count = array();
99
100 /**
101 * Used pattern
102 *
103 * @var array
104 */
105 private $used_pattern = array();
106
107 /**
108 * Link count per paragraph
109 *
110 * @var int
111 */
112 private $linkcount_per_paragraph = 0;
113
114 /**
115 * Meta Option. Determines if greedy mode is on/off. Enables linking as often as possible.
116 *
117 * @var bool
118 */
119 private $multi_keyword_mode = false;
120
121 /**
122 * Meta Option. Maximum number of outgoing links to be created in the current post/page. Zero means no limit
123 *
124 * @var int
125 */
126 private $links_per_page = 0;
127
128 /**
129 * Meta option. Maximum number of links to be created in the current target. Zero means no limit
130 *
131 * @var int
132 */
133 private $links_per_target = 0;
134
135 /**
136 * Meta option. Determine if it should go deeper into the paragraph or just continue
137 *
138 * @var int
139 */
140 private $links_per_paragraph_switch = 0;
141
142 /**
143 * Maximum number of links to be created in the current paragraph. Zero means no limit
144 *
145 * @var int
146 */
147 private $links_per_paragraph = 0;
148
149 /**
150 * The content type in which link replacements take place, defaults to html.
151 *
152 * @var boolean
153 */
154 private $content_type;
155
156 /**
157 * Constructor of ILJ_LinkBuilder
158 *
159 * @param int $id The ID of the current subject
160 * @param string $type The type of the current subject
161 * @param string $build_type
162 * @param boolean $content_type The content type in which link replacements take place, defaults to html.
163 *
164 * @return void
165 * @since 1.0.1
166 */
167 public function __construct($id, $type, $build_type = null, $content_type = 'html') {
168 $this->id = $id;
169 $this->type = $type;
170 $this->replace_ruleset = new Ruleset();
171 $this->multi_keyword_mode = (bool) Options::getOption(\ILJ\Core\Options\MultipleKeywords::getKey());
172 $this->links_per_page = (false === $this->multi_keyword_mode) ? Options::getOption(\ILJ\Core\Options\LinksPerPage::getKey()) : 0;
173 $this->links_per_target = (false === $this->multi_keyword_mode) ? Options::getOption(\ILJ\Core\Options\LinksPerTarget::getKey()) : 0;
174
175 $this->content_type = $content_type;
176
177 $this->setupInLinks($build_type);
178
179 }
180
181 /**
182 * Loads all ingoing links to current content from linkindex table and sets a new Ruleset type with it
183 *
184 * @since 1.0.0
185 * @param string $build_type
186 * @return void
187 */
188 private function setupInLinks($build_type = null) {
189 $this->link_ruleset = new Ruleset();
190 $this->link_page_count = 0;
191 $this->link_target_count = array();
192 $this->used_pattern = array();
193
194
195
196 if (is_null($build_type)) {
197 $post_rules = Linkindex::getRules($this->id, $this->type);
198 } elseif (IndexAsset::ILJ_FULL_BUILD == $build_type) {
199 $post_rules = LinkindexTemp::getRules($this->id, $this->type);
200 } elseif (IndexAsset::ILJ_INDIVIDUAL_BUILD == $build_type) {
201 $post_rules = LinkindexIndividualTemp::getRules($this->id, $this->type);
202 }
203
204 foreach ($post_rules as $post_rule) {
205 $this->link_ruleset->addRule($post_rule->anchor, $post_rule->link_to, $post_rule->type_to);
206 }
207
208 }
209
210 /**
211 * Return the linked content.
212 *
213 * @param mixed $content The content value passed to cache link value into
214 * @return mixed Mixed return value to cater different content compatibilities
215 */
216 public function link_content($content) {
217 return $this->linkContent($content);
218 }
219
220 /**
221 * Applies the link rules to a given piece of content
222 *
223 * @deprecated
224 * @since 1.0.0
225 * @param string $content The content of the post, where the rules get applied
226 * @return string
227 */
228 public function linkContent($content) {
229 if (!LinkBuilding::is_filter_needed($this->id, $this->type)) {
230 return $content;
231 }
232 $this->content = $content;
233 $this->replace_ruleset = Replacement::mask($this->content);
234 if ('' != $this->content) {
235 $this->maskLinkRules();
236 $this->applyReplaceRules();
237 }
238 return $this->content;
239 }
240
241 /**
242 * Applies the given LinkRuleset for the document through masking within the content.
243 *
244 * @since 1.0.0
245 * @return void
246 */
247 private function maskLinkRules() {
248 $link_page_count = 0;
249 $link_target_count = array();
250 $outgoing_links_limit = 0;
251 if ($this->links_per_page > 0) {
252 $outgoing_links_limit = $this->links_per_page;
253 }
254
255 {
256 $temp_values = $this->createLinkIndex($this->content, 0, $link_page_count, $link_target_count, 0, false, $outgoing_links_limit);
257 $full_content = $temp_values['content'];
258 $this->link_ruleset->reset();
259 }
260 $this->content = $full_content;
261 }
262
263 /**
264 * Runs Logical Checks For Linking Rules and settings
265 *
266 * @since 1.2.15
267 * @param mixed $content
268 * @param mixed $index
269 * @param mixed $link_page_count
270 * @param mixed $link_target_count
271 * @param mixed $linkcount_per_paragraph
272 * @param mixed $loop_thru_per_paragraph
273 * @param mixed $outgoing_links_limit
274 * @return array
275 */
276 protected function createLinkIndex($content, $index, $link_page_count, $link_target_count, $linkcount_per_paragraph, $loop_thru_per_paragraph, $outgoing_links_limit) {
277 while ($this->link_ruleset->hasRule()) {
278 $link_rule = $this->link_ruleset->getRule();
279
280 if (0 != $outgoing_links_limit && 0 < $outgoing_links_limit) {
281 if ($link_page_count == $outgoing_links_limit) {
282 break;
283 }
284 }
285
286 if (($this->links_per_target > 0 && array_key_exists($link_rule->value, $link_target_count) && $link_target_count[$link_rule->value] >= $this->links_per_target)
287 || (!$this->multi_keyword_mode && in_array($link_rule->pattern, $this->used_pattern))
288 ) {
289 $this->link_ruleset->nextRule();
290 continue;
291 }
292
293
294
295 $pattern = wptexturize($link_rule->pattern);
296 $pattern = Encoding::escape_ascii($pattern);
297 if (Options::getOption(Options\Case_Sensitive_Mode_Switch::getKey())) {
298 preg_match_all('/' . Encoding::mask_pattern($pattern) . '/u', $content, $rule_match);
299 } else {
300 preg_match_all('/' . Encoding::mask_pattern($pattern) . '/ui', $content, $rule_match);
301 }
302
303 if (!isset($rule_match['phrase']) || !count($rule_match['phrase'])) {
304 $this->link_ruleset->nextRule();
305 continue;
306 }
307 $phrases = $rule_match['phrase'];
308 foreach ($phrases as $rule) {
309 if (($this->links_per_target > 0
310 && array_key_exists($link_rule->value, $link_target_count)
311 && $link_target_count[$link_rule->value] == $this->links_per_target)
312 || (!$this->multi_keyword_mode && in_array($link_rule->pattern, $this->used_pattern))
313 ) {
314 $this->link_ruleset->nextRule();
315 continue 2;
316 }
317
318
319 $rule_id = 'ilj_' . uniqid('', true);
320 $link = $this->generateLink($link_rule, esc_html($rule));
321 if (!$link) {
322 $this->link_ruleset->nextRule();
323 continue;
324 }
325 $rule = Encoding::escape_ascii($rule);
326 if (Options::getOption(Options\Case_Sensitive_Mode_Switch::getKey())) {
327 $content = preg_replace('/' . Encoding::mask_pattern($rule) . '/u', $rule_id, $content, 1);
328 } else {
329 $content = preg_replace('/' . Encoding::mask_pattern($rule) . '/ui', $rule_id, $content, 1);
330 }
331 $this->replace_ruleset->addRule($rule_id, $link);
332
333 if (!array_key_exists($link_rule->value, $this->link_target_count)) {
334 $link_target_count[$link_rule->value] = 0;
335 }
336
337 $this->used_pattern[] = $link_rule->pattern;
338 $link_target_count[$link_rule->value]++;
339 $link_page_count++;
340
341 }
342 $this->link_ruleset->nextRule();
343 }
344 $obj = array(
345 'content' => $content,
346 'link_target_count' => $link_target_count,
347 'link_page_count' => $link_page_count,
348 );
349 return $obj;
350 }
351
352 /**
353 * Applies the configured masks and replaces the placeholders with the generated links.
354 *
355 * @since 1.0.0
356 * @return void
357 */
358 private function applyReplaceRules() {
359 while ($this->replace_ruleset->hasRule()) {
360 $replace_rule = $this->replace_ruleset->getRule();
361 $this->content = str_replace($replace_rule->pattern, $replace_rule->value, $this->content);
362 $this->replace_ruleset->nextRule();
363 }
364
365 if (preg_match('/ilj\_[a-z0-9]{14}\.[0-9]{8}/', $this->content)) {
366 $this->replace_ruleset->reset();
367 $this->applyReplaceRules();
368 }
369 }
370
371 /**
372 * Returns the template for link output
373 *
374 * @since 1.0.0
375 * @return string
376 */
377 private function getLinkTemplate() {
378 $default_template = \ILJ\Core\Options\LinkOutputInternal::getDefault();
379 $template = Options::getOption(\ILJ\Core\Options\LinkOutputInternal::getKey());
380
381 if ('' == $template) {
382 return $default_template;
383 }
384 return wp_specialchars_decode($template, \ENT_QUOTES);
385 }
386
387 /**
388 * Generates the link markup
389 *
390 * @since 1.0.0
391 * @param string $link_rule The post where the link should point to
392 * @param string $anchor The anchortext for the link
393 * @return bool|string
394 */
395 private function generateLink($link_rule, $anchor) {
396 $template = $this->getLinkTemplate();
397 $nofollow = (bool) Options::getOption(\ILJ\Core\Options\InternalNofollow::getKey());
398 $link_attrs = array();
399
400 if ('post' == $link_rule->type) {
401 if (get_post_status($link_rule->value) != 'publish') {
402 return false;
403 }
404 $url = get_the_permalink($link_rule->value);
405 }
406
407
408
409 $link = str_replace('{{url}}', (isset($url) ? $url : '#'), $template);
410 $link = str_replace('{{anchor}}', $anchor, $link);
411
412
413
414 $check_nofollow = true;
415
416
417
418 if ($check_nofollow && $nofollow) {
419 $link = str_replace('<a ', '<a rel="nofollow" ', $link);
420 }
421
422 if ('json' === $this->content_type) {
423 // If the content is json, the link should be escaped before replacement.
424 $link = trim(wp_json_encode($link), '"');
425 }
426
427 return $link;
428 }
429 }
430